summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml1
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.vue2
-rw-r--r--app/assets/javascripts/boards/index.js2
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js4
-rw-r--r--app/assets/javascripts/gl_dropdown.js4
-rw-r--r--app/assets/javascripts/ide/components/ide.vue40
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue19
-rw-r--r--app/assets/javascripts/ide/stores/actions.js52
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js118
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js18
-rw-r--r--app/assets/javascripts/ide/stores/getters.js3
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js11
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/ide/stores/mutations/branch.js6
-rw-r--r--app/assets/javascripts/ide/stores/mutations/project.js5
-rw-r--r--app/assets/javascripts/jobs/components/stages_dropdown.vue11
-rw-r--r--app/assets/javascripts/lib/graphql.js20
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue13
-rw-r--r--app/assets/javascripts/repository/components/breadcrumbs.vue61
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue2
-rw-r--r--app/assets/javascripts/repository/components/table/parent_row.vue4
-rw-r--r--app/assets/javascripts/repository/index.js34
-rw-r--r--app/assets/javascripts/repository/queries/getProjectShortPath.graphql3
-rw-r--r--app/assets/javascripts/repository/router.js9
-rw-r--r--app/assets/javascripts/repository/utils/title.js2
-rw-r--r--app/assets/javascripts/visual_review_toolbar/index.js2
-rw-r--r--app/assets/javascripts/visual_review_toolbar/styles/toolbar.css149
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue32
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue20
-rw-r--r--app/assets/stylesheets/components/avatar.scss240
-rw-r--r--app/assets/stylesheets/framework/awards.scss3
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss4
-rw-r--r--app/assets/stylesheets/pages/labels.scss3
-rw-r--r--app/assets/stylesheets/pages/notes.scss2
-rw-r--r--app/assets/stylesheets/pages/search.scss10
-rw-r--r--app/controllers/admin/projects_controller.rb11
-rw-r--r--app/controllers/graphql_controller.rb3
-rw-r--r--app/controllers/profiles/notifications_controller.rb4
-rw-r--r--app/helpers/environments_helper.rb3
-rw-r--r--app/helpers/labels_helper.rb56
-rw-r--r--app/helpers/projects_helper.rb8
-rw-r--r--app/helpers/storage_helper.rb3
-rw-r--r--app/models/ci/build.rb4
-rw-r--r--app/models/ci/job_artifact.rb13
-rw-r--r--app/models/namespace.rb1
-rw-r--r--app/models/project.rb1
-rw-r--r--app/models/project_services/pipelines_email_service.rb18
-rw-r--r--app/models/project_statistics.rb16
-rw-r--r--app/serializers/build_details_entity.rb5
-rw-r--r--app/serializers/job_artifact_report_entity.rb13
-rw-r--r--app/serializers/pipeline_entity.rb1
-rw-r--r--app/services/projects/update_statistics_service.rb2
-rw-r--r--app/views/admin/application_settings/_external_authorization_service_form.html.haml2
-rw-r--r--app/views/admin/labels/_form.html.haml5
-rw-r--r--app/views/admin/projects/_projects.html.haml4
-rw-r--r--app/views/admin/users/show.html.haml2
-rw-r--r--app/views/import/bitbucket_server/status.html.haml2
-rw-r--r--app/views/profiles/active_sessions/_active_session.html.haml11
-rw-r--r--app/views/profiles/active_sessions/index.html.haml4
-rw-r--r--app/views/profiles/chat_names/_chat_name.html.haml4
-rw-r--r--app/views/profiles/chat_names/index.html.haml16
-rw-r--r--app/views/profiles/notifications/show.html.haml16
-rw-r--r--app/views/projects/_files.html.haml7
-rw-r--r--app/views/projects/_home_panel.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml5
-rw-r--r--app/views/projects/commit/_commit_box.html.haml2
-rw-r--r--app/views/projects/jobs/_table.html.haml2
-rw-r--r--app/views/projects/settings/operations/_external_dashboard.html.haml2
-rw-r--r--app/views/projects/settings/repository/_protected_branches.html.haml2
-rw-r--r--app/views/projects/settings/repository/show.html.haml7
-rw-r--r--app/views/projects/tree/_readme.html.haml2
-rw-r--r--app/views/projects/tree/_tree_header.html.haml121
-rw-r--r--app/views/search/_form.html.haml1
-rw-r--r--app/views/shared/issuable/_label_page_create.html.haml4
-rw-r--r--app/views/shared/labels/_form.html.haml7
-rw-r--r--app/views/shared/notifications/_button.html.haml2
-rw-r--r--app/views/shared/notifications/_new_button.html.haml2
-rw-r--r--app/views/shared/notifications/_notification_dropdown.html.haml2
-rw-r--r--app/workers/post_receive.rb2
-rw-r--r--app/workers/project_cache_worker.rb4
-rw-r--r--changelogs/unreleased/11105-fix-cs-with-proxy.yml5
-rw-r--r--changelogs/unreleased/45687-web-ide-empty-state.yml5
-rw-r--r--changelogs/unreleased/57414-show-pipeline-iid.yml5
-rw-r--r--changelogs/unreleased/61024-update-resolved-icon.yml5
-rw-r--r--changelogs/unreleased/6104-ee-ce-difference.yml5
-rw-r--r--changelogs/unreleased/61639-flaky-spec-issue-boards-labels-creates-project-label-spec-features-boards-sidebar_spec-rb-350.yml5
-rw-r--r--changelogs/unreleased/61788-predefined-colours-dont-have-descriptive-labels.yml5
-rw-r--r--changelogs/unreleased/62227-webkit-icon-overlap.yml5
-rw-r--r--changelogs/unreleased/62487-external-policy-desc.yml5
-rw-r--r--changelogs/unreleased/add-wiki-size-to-statistics.yml5
-rw-r--r--changelogs/unreleased/auto-devops-kubernestes-bump1-11-10.yml5
-rw-r--r--changelogs/unreleased/ee-11040-added-conditional-rendering.yml5
-rw-r--r--changelogs/unreleased/i18n-active_sessions-in-user-profile.yml5
-rw-r--r--changelogs/unreleased/i18n-chat-of-user-profile.yml5
-rw-r--r--changelogs/unreleased/pipelines-email-default-branch-filter.yml5
-rw-r--r--changelogs/unreleased/referenced-labels.yml5
-rw-r--r--changelogs/unreleased/sh-add-header-to-jobs-admin-page.yml5
-rw-r--r--config/gitlab.yml.example2
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--config/initializers/7_prometheus_metrics.rb8
-rw-r--r--config/initializers/rack_timeout.rb23
-rw-r--r--config/routes/admin.rb2
-rw-r--r--config/webpack.config.review_toolbar.js58
-rw-r--r--db/migrate/20190527194830_add_wiki_size_to_statistics.rb9
-rw-r--r--db/post_migrate/20190527194900_schedule_calculate_wiki_sizes.rb32
-rw-r--r--db/schema.rb3
-rw-r--r--doc/administration/high_availability/nfs.md14
-rw-r--r--doc/administration/integration/plantuml.md2
-rw-r--r--doc/administration/job_artifacts.md3
-rw-r--r--doc/administration/monitoring/performance/grafana_configuration.md8
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md18
-rw-r--r--doc/api/boards.md179
-rw-r--r--doc/api/discussions.md216
-rw-r--r--doc/api/epic_issues.md1
-rw-r--r--doc/api/epic_links.md1
-rw-r--r--doc/api/groups.md1
-rw-r--r--doc/api/notes.md130
-rw-r--r--doc/api/projects.md5
-rw-r--r--doc/api/resource_label_events.md86
-rw-r--r--doc/api/scim.md2
-rw-r--r--doc/api/services.md70
-rw-r--r--doc/api/users.md6
-rw-r--r--doc/api/vulnerabilities.md2
-rw-r--r--doc/ci/enable_or_disable_ci.md4
-rw-r--r--doc/ci/git_submodules.md4
-rw-r--r--doc/ci/interactive_web_terminal/index.md4
-rw-r--r--doc/ci/junit_test_reports.md4
-rw-r--r--doc/ci/large_repositories/index.md4
-rw-r--r--doc/ci/merge_request_pipelines/index.md4
-rw-r--r--doc/ci/metrics_reports.md4
-rw-r--r--doc/ci/multi_project_pipelines.md32
-rw-r--r--doc/ci/pipelines.md4
-rw-r--r--doc/ci/quick_start/README.md12
-rw-r--r--doc/ci/review_apps/index.md8
-rw-r--r--doc/ci/runners/README.md4
-rw-r--r--doc/ci/services/README.md13
-rw-r--r--doc/ci/services/mysql.md4
-rw-r--r--doc/ci/services/postgres.md4
-rw-r--r--doc/ci/services/redis.md4
-rw-r--r--doc/ci/ssh_keys/README.md1
-rw-r--r--doc/ci/triggers/README.md4
-rw-r--r--doc/ci/variables/README.md13
-rw-r--r--doc/ci/yaml/README.md29
-rw-r--r--doc/development/architecture.md4
-rw-r--r--doc/development/geo.md78
-rw-r--r--doc/gitlab-basics/README.md2
-rw-r--r--doc/gitlab-basics/create-your-ssh-keys.md30
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.pngbin10534 -> 0 bytes
-rw-r--r--doc/ssh/README.md19
-rw-r--r--doc/topics/authentication/index.md10
-rw-r--r--doc/topics/autodevops/index.md26
-rw-r--r--doc/topics/autodevops/quick_start_guide.md2
-rw-r--r--doc/topics/git/index.md2
-rw-r--r--doc/university/glossary/README.md2
-rw-r--r--doc/user/admin_area/diff_limits.md41
-rw-r--r--doc/user/admin_area/index.md24
-rw-r--r--doc/user/application_security/security_dashboard/index.md1
-rw-r--r--doc/user/profile/personal_access_tokens.md1
-rw-r--r--doc/user/project/settings/index.md6
-rw-r--r--lib/api/entities.rb3
-rw-r--r--lib/api/helpers/issues_helpers.rb8
-rw-r--r--lib/api/helpers/protected_branches_helpers.rb13
-rw-r--r--lib/api/helpers/services_helpers.rb6
-rw-r--r--lib/api/issues.rb14
-rw-r--r--lib/api/protected_branches.rb26
-rw-r--r--lib/gitlab/background_migration/calculate_wiki_sizes.rb18
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml3
-rw-r--r--lib/gitlab/cluster/lifecycle_events.rb8
-rw-r--r--lib/gitlab/metrics/samplers/puma_sampler.rb92
-rw-r--r--lib/gitlab/path_regex.rb1
-rw-r--r--lib/gitlab/rack_timeout_observer.rb46
-rw-r--r--lib/tasks/gitlab/assets.rake6
-rw-r--r--locale/gitlab.pot155
-rw-r--r--package.json7
-rw-r--r--qa/README.md6
-rw-r--r--qa/docs/GUIDELINES.md46
-rw-r--r--qa/docs/WRITING_TESTS_FROM_SCRATCH.md482
-rw-r--r--qa/docs/best_practices.md (renamed from qa/docs/BEST_PRACTICES.md)6
-rw-r--r--qa/docs/guidelines.md97
-rw-r--r--qa/docs/writing_tests_from_scratch.md582
-rw-r--r--qa/qa.rb1
-rw-r--r--qa/qa/ce/strategy.rb1
-rw-r--r--qa/qa/page/base.rb5
-rw-r--r--qa/qa/page/element.rb25
-rw-r--r--qa/qa/page/main/login.rb16
-rw-r--r--qa/qa/page/main/menu.rb8
-rw-r--r--qa/qa/page/project/branches/show.rb8
-rw-r--r--qa/qa/page/validatable.rb22
-rw-r--r--qa/qa/page/view.rb4
-rw-r--r--qa/qa/resource/base.rb64
-rw-r--r--qa/qa/resource/file.rb31
-rw-r--r--qa/qa/runtime/browser.rb1
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb14
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb26
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb3
-rw-r--r--qa/qa/support/page/logging.rb7
-rw-r--r--qa/spec/page/element_spec.rb56
-rw-r--r--rubocop/cop/qa/element_with_pattern.rb20
-rw-r--r--spec/features/admin/admin_sees_project_statistics_spec.rb2
-rw-r--r--spec/features/boards/sidebar_spec.rb4
-rw-r--r--spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb10
-rw-r--r--spec/features/projects/commits/user_browses_commits_spec.rb2
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb1
-rw-r--r--spec/features/projects/files/user_creates_files_spec.rb1
-rw-r--r--spec/features/projects/jobs/user_browses_jobs_spec.rb6
-rw-r--r--spec/features/projects/jobs_spec.rb2
-rw-r--r--spec/features/projects/show/user_sees_collaboration_links_spec.rb1
-rw-r--r--spec/frontend/ide/stores/mutations/branch_spec.js35
-rw-r--r--spec/frontend/ide/stores/mutations/project_spec.js23
-rw-r--r--spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap2
-rw-r--r--spec/frontend/repository/components/breadcrumbs_spec.js44
-rw-r--r--spec/frontend/repository/components/table/parent_row_spec.js64
-rw-r--r--spec/helpers/environments_helper_spec.rb49
-rw-r--r--spec/helpers/projects_helper_spec.rb22
-rw-r--r--spec/helpers/storage_helper_spec.rb3
-rw-r--r--spec/javascripts/boards/boards_store_spec.js10
-rw-r--r--spec/javascripts/ide/components/ide_spec.js92
-rw-r--r--spec/javascripts/ide/components/ide_tree_list_spec.js59
-rw-r--r--spec/javascripts/ide/stores/actions/project_spec.js203
-rw-r--r--spec/javascripts/ide/stores/actions/tree_spec.js32
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js64
-rw-r--r--spec/javascripts/ide/stores/modules/commit/actions_spec.js59
-rw-r--r--spec/javascripts/jobs/components/stages_dropdown_spec.js21
-rw-r--r--spec/javascripts/jobs/mock_data.js1
-rw-r--r--spec/javascripts/notes/components/note_app_spec.js4
-rw-r--r--spec/javascripts/pipelines/mock_data.js1
-rw-r--r--spec/javascripts/pipelines/pipeline_url_spec.js5
-rw-r--r--spec/javascripts/test_bundle.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js28
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js5
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js2
-rw-r--r--spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb62
-rw-r--r--spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb96
-rw-r--r--spec/lib/gitlab/rack_timeout_observer_spec.rb58
-rw-r--r--spec/models/ci/build_spec.rb12
-rw-r--r--spec/models/ci/job_artifact_spec.rb15
-rw-r--r--spec/models/namespace_spec.rb6
-rw-r--r--spec/models/project_services/pipelines_email_service_spec.rb76
-rw-r--r--spec/models/project_statistics_spec.rb68
-rw-r--r--spec/requests/api/graphql/gitlab_schema_spec.rb29
-rw-r--r--spec/requests/api/groups_spec.rb3
-rw-r--r--spec/rubocop/cop/qa/element_with_pattern_spec.rb11
-rw-r--r--spec/serializers/build_details_entity_spec.rb9
-rw-r--r--spec/serializers/job_artifact_report_entity_spec.rb28
-rw-r--r--spec/services/projects/update_statistics_service_spec.rb12
-rw-r--r--spec/views/projects/commit/_commit_box.html.haml_spec.rb4
-rw-r--r--spec/views/projects/jobs/_build.html.haml_spec.rb10
-rw-r--r--spec/workers/project_cache_worker_spec.rb5
-rw-r--r--vendor/assets/javascripts/visual_review_toolbar.js (renamed from public/visual-review-toolbar.js)355
-rw-r--r--yarn.lock32
260 files changed, 4680 insertions, 1724 deletions
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index 986ba7558d5..d62b24ae97d 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -38,6 +38,7 @@ gitlab:assets:compile:
- bundle exec rake gitlab:assets:compile
- time scripts/build_assets_image
- scripts/clean-old-cached-assets
+ - rm -f /etc/apt/sources.list.d/google*.list # We don't need to update Chrome here
# Play dependent manual jobs
- install_api_client_dependencies_with_apt
- play_job "review-build-cng" || true # this job might not exist so ignore the failure if it cannot be played
diff --git a/Gemfile b/Gemfile
index f5f963bb2ff..66348414506 100644
--- a/Gemfile
+++ b/Gemfile
@@ -154,6 +154,7 @@ end
group :puma do
gem 'puma', '~> 3.12', require: false
gem 'puma_worker_killer', require: false
+ gem 'rack-timeout', require: false
end
# State machine
diff --git a/Gemfile.lock b/Gemfile.lock
index 9e922d8a3bb..3f85fa958bd 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -680,6 +680,7 @@ GEM
rack
rack-test (1.1.0)
rack (>= 1.0, < 3)
+ rack-timeout (0.5.1)
rails (5.1.7)
actioncable (= 5.1.7)
actionmailer (= 5.1.7)
@@ -1174,6 +1175,7 @@ DEPENDENCIES
rack-cors (~> 1.0.0)
rack-oauth2 (~> 1.9.3)
rack-proxy (~> 0.6.0)
+ rack-timeout
rails (= 5.1.7)
rails-controller-testing
rails-i18n (~> 5.1)
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
index a2b8a0af236..4ab2b17301f 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.vue
@@ -48,7 +48,7 @@ export default Vue.extend({
list.removeIssue(issue);
});
- boardsStore.detail.issue = {};
+ boardsStore.clearDetailIssue();
},
/**
* Build the default patch request.
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 4995a8d9367..25f686e4b28 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -167,7 +167,7 @@ export default () => {
boardsStore.detail.issue = newIssue;
},
clearDetailIssue() {
- boardsStore.detail.issue = {};
+ boardsStore.clearDetailIssue();
},
toggleSubscription(id) {
const { issue } = boardsStore.detail;
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 70861fbf2b3..fef16c11c0d 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -188,6 +188,10 @@ const boardsStore = {
updateFiltersUrl() {
window.history.pushState(null, null, `?${this.filter.path}`);
},
+
+ clearDetailIssue() {
+ this.detail.issue = {};
+ },
};
BoardsStoreEE.initEESpecific(boardsStore);
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 6a4c1aab308..a143d79097b 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -335,6 +335,10 @@ GitLabDropdown = (function() {
_this.fullData = data;
_this.parseData(_this.fullData);
_this.focusTextInput();
+
+ // Update dropdown position since remote data may have changed dropdown size
+ _this.dropdown.find('.dropdown-menu-toggle').dropdown('update');
+
if (
_this.options.filterable &&
_this.filter &&
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 9894ebb0624..e41b1530226 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,6 +1,7 @@
<script>
import Vue from 'vue';
import { mapActions, mapState, mapGetters } from 'vuex';
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import FindFile from '~/vue_shared/components/file_finder/index.vue';
import NewModal from './new_dropdown/modal.vue';
@@ -22,6 +23,8 @@ export default {
FindFile,
ErrorMessage,
CommitEditorHeader,
+ GlButton,
+ GlLoadingIcon,
},
props: {
rightPaneComponent: {
@@ -47,13 +50,15 @@ export default {
'someUncommittedChanges',
'isCommitModeActive',
'allBlobs',
+ 'emptyRepo',
+ 'currentTree',
]),
},
mounted() {
window.onbeforeunload = e => this.onBeforeUnload(e);
},
methods: {
- ...mapActions(['toggleFileFinder']),
+ ...mapActions(['toggleFileFinder', 'openNewEntryModal']),
onBeforeUnload(e = {}) {
const returnValue = __('Are you sure you want to lose unsaved changes?');
@@ -98,17 +103,40 @@ export default {
<repo-editor :file="activeFile" class="multi-file-edit-pane-content" />
</template>
<template v-else>
- <div v-once class="ide-empty-state">
+ <div class="ide-empty-state">
<div class="row js-empty-state">
<div class="col-12">
<div class="svg-content svg-250"><img :src="emptyStateSvgPath" /></div>
</div>
<div class="col-12">
<div class="text-content text-center">
- <h4>Welcome to the GitLab IDE</h4>
- <p>
- Select a file from the left sidebar to begin editing. Afterwards, you'll be able
- to commit your changes.
+ <h4>
+ {{ __('Make and review changes in the browser with the Web IDE') }}
+ </h4>
+ <template v-if="emptyRepo">
+ <p>
+ {{
+ __(
+ "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes.",
+ )
+ }}
+ </p>
+ <gl-button
+ variant="success"
+ :title="__('New file')"
+ :aria-label="__('New file')"
+ @click="openNewEntryModal({ type: 'blob' })"
+ >
+ {{ __('New file') }}
+ </gl-button>
+ </template>
+ <gl-loading-icon v-else-if="!currentTree || currentTree.loading" size="md" />
+ <p v-else>
+ {{
+ __(
+ "Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes.",
+ )
+ }}
</p>
</div>
</div>
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index 81374f26645..95782b2c88a 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -54,14 +54,17 @@ export default {
<slot name="header"></slot>
</header>
<div class="ide-tree-body h-100">
- <file-row
- v-for="file in currentTree.tree"
- :key="file.key"
- :file="file"
- :level="0"
- :extra-component="$options.FileRowExtra"
- @toggleTreeOpen="toggleTreeOpen"
- />
+ <template v-if="currentTree.tree.length">
+ <file-row
+ v-for="file in currentTree.tree"
+ :key="file.key"
+ :file="file"
+ :level="0"
+ :extra-component="$options.FileRowExtra"
+ @toggleTreeOpen="toggleTreeOpen"
+ />
+ </template>
+ <div v-else class="file-row">{{ __('No files') }}</div>
</div>
</template>
</div>
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index fd678e6e10c..dc8ca732879 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -1,12 +1,15 @@
import $ from 'jquery';
import Vue from 'vue';
+import { __, sprintf } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import flash from '~/flash';
+import _ from 'underscore';
import * as types from './mutation_types';
import { decorateFiles } from '../lib/files';
import { stageKeys } from '../constants';
+import service from '../services';
-export const redirectToUrl = (_, url) => visitUrl(url);
+export const redirectToUrl = (self, url) => visitUrl(url);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
@@ -239,6 +242,53 @@ export const renameEntry = (
}
};
+export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) =>
+ new Promise((resolve, reject) => {
+ const currentProject = state.projects[projectId];
+ if (!currentProject || !currentProject.branches[branchId] || force) {
+ service
+ .getBranchData(projectId, branchId)
+ .then(({ data }) => {
+ const { id } = data.commit;
+ commit(types.SET_BRANCH, {
+ projectPath: projectId,
+ branchName: branchId,
+ branch: data,
+ });
+ commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
+ resolve(data);
+ })
+ .catch(e => {
+ if (e.response.status === 404) {
+ reject(e);
+ } else {
+ flash(
+ __('Error loading branch data. Please try again.'),
+ 'alert',
+ document,
+ null,
+ false,
+ true,
+ );
+
+ reject(
+ new Error(
+ sprintf(
+ __('Branch not loaded - %{branchId}'),
+ {
+ branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`,
+ },
+ false,
+ ),
+ ),
+ );
+ }
+ });
+ } else {
+ resolve(currentProject.branches[branchId]);
+ }
+ });
+
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index 4b10d148ebf..dd8f17e4f3a 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -35,48 +35,6 @@ export const getProjectData = ({ commit, state }, { namespace, projectId, force
}
});
-export const getBranchData = (
- { commit, dispatch, state },
- { projectId, branchId, force = false } = {},
-) =>
- new Promise((resolve, reject) => {
- if (
- typeof state.projects[`${projectId}`] === 'undefined' ||
- !state.projects[`${projectId}`].branches[branchId] ||
- force
- ) {
- service
- .getBranchData(`${projectId}`, branchId)
- .then(({ data }) => {
- const { id } = data.commit;
- commit(types.SET_BRANCH, {
- projectPath: `${projectId}`,
- branchName: branchId,
- branch: data,
- });
- commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
- resolve(data);
- })
- .catch(e => {
- if (e.response.status === 404) {
- dispatch('showBranchNotFoundError', branchId);
- } else {
- flash(
- __('Error loading branch data. Please try again.'),
- 'alert',
- document,
- null,
- false,
- true,
- );
- }
- reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
- });
- } else {
- resolve(state.projects[`${projectId}`].branches[branchId]);
- }
- });
-
export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) =>
service
.getBranchData(projectId, branchId)
@@ -125,40 +83,66 @@ export const showBranchNotFoundError = ({ dispatch }, branchId) => {
});
};
-export const openBranch = ({ dispatch, state }, { projectId, branchId, basePath }) => {
- dispatch('setCurrentBranchId', branchId);
-
- dispatch('getBranchData', {
- projectId,
- branchId,
+export const showEmptyState = ({ commit, state }, { projectId, branchId }) => {
+ const treePath = `${projectId}/${branchId}`;
+ commit(types.CREATE_TREE, { treePath });
+ commit(types.TOGGLE_LOADING, {
+ entry: state.trees[treePath],
+ forceValue: false,
});
+};
- return dispatch('getFiles', {
+export const openBranch = ({ dispatch, state, getters }, { projectId, branchId, basePath }) => {
+ dispatch('setCurrentBranchId', branchId);
+
+ if (getters.emptyRepo) {
+ return dispatch('showEmptyState', { projectId, branchId });
+ }
+ return dispatch('getBranchData', {
projectId,
branchId,
})
.then(() => {
- if (basePath) {
- const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
- const treeEntryKey = Object.keys(state.entries).find(
- key => key === path && !state.entries[key].pending,
- );
- const treeEntry = state.entries[treeEntryKey];
-
- if (treeEntry) {
- dispatch('handleTreeEntryAction', treeEntry);
- } else {
- dispatch('createTempEntry', {
- name: path,
- type: 'blob',
- });
- }
- }
- })
- .then(() => {
dispatch('getMergeRequestsForBranch', {
projectId,
branchId,
});
+ dispatch('getFiles', {
+ projectId,
+ branchId,
+ })
+ .then(() => {
+ if (basePath) {
+ const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath;
+ const treeEntryKey = Object.keys(state.entries).find(
+ key => key === path && !state.entries[key].pending,
+ );
+ const treeEntry = state.entries[treeEntryKey];
+
+ if (treeEntry) {
+ dispatch('handleTreeEntryAction', treeEntry);
+ } else {
+ dispatch('createTempEntry', {
+ name: path,
+ type: 'blob',
+ });
+ }
+ }
+ })
+ .catch(
+ () =>
+ new Error(
+ sprintf(
+ __('An error occurred whilst getting files for - %{branchId}'),
+ {
+ branchId: `<strong>${_.escape(projectId)}/${_.escape(branchId)}</strong>`,
+ },
+ false,
+ ),
+ ),
+ );
+ })
+ .catch(() => {
+ dispatch('showBranchNotFoundError', branchId);
});
};
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 3d83e4a9ba5..75511574d3e 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -74,17 +74,13 @@ export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } =
resolve();
})
.catch(e => {
- if (e.response.status === 404) {
- dispatch('showBranchNotFoundError', branchId);
- } else {
- dispatch('setErrorMessage', {
- text: __('An error occurred whilst loading all the files.'),
- action: payload =>
- dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)),
- actionText: __('Please try again'),
- actionPayload: { projectId, branchId },
- });
- }
+ dispatch('setErrorMessage', {
+ text: __('An error occurred whilst loading all the files.'),
+ action: payload =>
+ dispatch('getFiles', payload).then(() => dispatch('setErrorMessage', null)),
+ actionText: __('Please try again'),
+ actionPayload: { projectId, branchId },
+ });
reject(e);
});
} else {
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 490658a4543..7a88ac5b116 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -36,6 +36,9 @@ export const currentMergeRequest = state => {
export const currentProject = state => state.projects[state.currentProjectId];
+export const emptyRepo = state =>
+ state.projects[state.currentProjectId] && state.projects[state.currentProjectId].empty_repo;
+
export const currentTree = state =>
state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index c2760eb1554..f81bdb8a30e 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -135,6 +135,17 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
return null;
}
+ if (!data.parent_ids.length) {
+ commit(
+ rootTypes.TOGGLE_EMPTY_STATE,
+ {
+ projectPath: rootState.currentProjectId,
+ value: false,
+ },
+ { root: true },
+ );
+ }
+
dispatch('setLastCommitMessage', data);
dispatch('updateCommitMessage', '');
return dispatch('updateFilesAfterCommit', {
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index a5f8098dc17..86ab76136df 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -12,6 +12,7 @@ export const SET_LINKS = 'SET_LINKS';
export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
+export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE';
// Merge Request Mutation Types
export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST';
diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js
index e09f88878f4..6afd8de2aa4 100644
--- a/app/assets/javascripts/ide/stores/mutations/branch.js
+++ b/app/assets/javascripts/ide/stores/mutations/branch.js
@@ -19,6 +19,12 @@ export default {
});
},
[types.SET_BRANCH_WORKING_REFERENCE](state, { projectId, branchId, reference }) {
+ if (!state.projects[projectId].branches[branchId]) {
+ Object.assign(state.projects[projectId].branches, {
+ [branchId]: {},
+ });
+ }
+
Object.assign(state.projects[projectId].branches[branchId], {
workingReference: reference,
});
diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js
index 284b39a2c72..9230f3839c1 100644
--- a/app/assets/javascripts/ide/stores/mutations/project.js
+++ b/app/assets/javascripts/ide/stores/mutations/project.js
@@ -21,4 +21,9 @@ export default {
}),
});
},
+ [types.TOGGLE_EMPTY_STATE](state, { projectPath, value }) {
+ Object.assign(state.projects[projectPath], {
+ empty_repo: value,
+ });
+ },
};
diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue
index 6e92b599b0a..cb073a9b04d 100644
--- a/app/assets/javascripts/jobs/components/stages_dropdown.vue
+++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue
@@ -2,6 +2,7 @@
import _ from 'underscore';
import { GlLink } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue';
import Icon from '~/vue_shared/components/icon.vue';
export default {
@@ -9,6 +10,7 @@ export default {
CiIcon,
Icon,
GlLink,
+ PipelineLink,
},
props: {
pipeline: {
@@ -48,9 +50,12 @@ export default {
<ci-icon :status="pipeline.details.status" class="vertical-align-middle" />
<span class="font-weight-bold">{{ s__('Job|Pipeline') }}</span>
- <gl-link :href="pipeline.path" class="js-pipeline-path link-commit qa-pipeline-path"
- >#{{ pipeline.id }}</gl-link
- >
+ <pipeline-link
+ :href="pipeline.path"
+ :pipeline-id="pipeline.id"
+ :pipeline-iid="pipeline.iid"
+ class="js-pipeline-path link-commit qa-pipeline-path"
+ />
<template v-if="hasRef">
{{ s__('Job|for') }}
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 47e91dedd5a..5857f9e22ae 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -1,6 +1,8 @@
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { createUploadLink } from 'apollo-upload-client';
+import { ApolloLink } from 'apollo-link';
+import { BatchHttpLink } from 'apollo-link-batch-http';
import csrf from '~/lib/utils/csrf';
export default (resolvers = {}, config = {}) => {
@@ -11,13 +13,19 @@ export default (resolvers = {}, config = {}) => {
uri = `${config.baseUrl}${uri}`.replace(/\/{3,}/g, '/');
}
+ const httpOptions = {
+ uri,
+ headers: {
+ [csrf.headerKey]: csrf.token,
+ },
+ };
+
return new ApolloClient({
- link: createUploadLink({
- uri,
- headers: {
- [csrf.headerKey]: csrf.token,
- },
- }),
+ link: ApolloLink.split(
+ operation => operation.getContext().hasUpload,
+ createUploadLink(httpOptions),
+ new BatchHttpLink(httpOptions),
+ ),
cache: new InMemoryCache(config.cacheConfig),
resolvers,
});
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index c7cfc0f0f3b..307e56708e0 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -57,7 +57,7 @@ export default {
class="line-resolve-btn is-disabled"
type="button"
>
- <icon name="check-circle" />
+ <icon :name="allResolved ? 'check-circle-filled' : 'check-circle'" />
</span>
<span class="line-resolve-text">
{{ resolvedDiscussionsCount }}/{{ resolvableDiscussionsCount }}
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 5a4ff15d198..78f397ccc12 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -135,7 +135,7 @@ export default {
@click="onResolve"
>
<template v-if="!isResolving">
- <icon name="check-circle" />
+ <icon :name="isResolved ? 'check-circle-filled' : 'check-circle'" />
</template>
<gl-loading-icon v-else inline />
</button>
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index b2e365e5cde..f3a71ee434c 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -83,6 +83,8 @@ export default {
v-if="shouldRenderContent"
:status="status"
:item-id="pipeline.id"
+ :item-iid="pipeline.iid"
+ :item-id-tooltip="__('Pipeline ID (IID)')"
:time="pipeline.created_at"
:user="pipeline.user"
:actions="actions"
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index c41ecab1294..00c02e15562 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -2,6 +2,7 @@
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import _ from 'underscore';
import { __, sprintf } from '~/locale';
+import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import popover from '~/vue_shared/directives/popover';
@@ -19,6 +20,7 @@ export default {
components: {
UserAvatarLink,
GlLink,
+ PipelineLink,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -59,10 +61,13 @@ export default {
};
</script>
<template>
- <div class="table-section section-10 d-none d-sm-none d-md-block pipeline-tags">
- <gl-link :href="pipeline.path" class="js-pipeline-url-link">
- <span class="pipeline-id">#{{ pipeline.id }}</span>
- </gl-link>
+ <div class="table-section section-10 d-none d-sm-none d-md-block pipeline-tags section-wrap">
+ <pipeline-link
+ :href="pipeline.path"
+ :pipeline-id="pipeline.id"
+ :pipeline-iid="pipeline.iid"
+ class="js-pipeline-url-link"
+ />
<div class="label-container">
<span
v-if="pipeline.flags.latest"
diff --git a/app/assets/javascripts/repository/components/breadcrumbs.vue b/app/assets/javascripts/repository/components/breadcrumbs.vue
new file mode 100644
index 00000000000..6eca015036f
--- /dev/null
+++ b/app/assets/javascripts/repository/components/breadcrumbs.vue
@@ -0,0 +1,61 @@
+<script>
+import getRefMixin from '../mixins/get_ref';
+import getProjectShortPath from '../queries/getProjectShortPath.graphql';
+
+export default {
+ apollo: {
+ projectShortPath: {
+ query: getProjectShortPath,
+ },
+ },
+ mixins: [getRefMixin],
+ props: {
+ currentPath: {
+ type: String,
+ required: false,
+ default: '/',
+ },
+ },
+ data() {
+ return {
+ projectShortPath: '',
+ };
+ },
+ computed: {
+ pathLinks() {
+ return this.currentPath
+ .split('/')
+ .filter(p => p !== '')
+ .reduce(
+ (acc, name, i) => {
+ const path = `${i > 0 ? acc[i].path : ''}/${name}`;
+
+ return acc.concat({
+ name,
+ path,
+ to: `/tree/${this.ref}${path}`,
+ });
+ },
+ [{ name: this.projectShortPath, path: '/', to: `/tree/${this.ref}` }],
+ );
+ },
+ },
+ methods: {
+ isLast(i) {
+ return i === this.pathLinks.length - 1;
+ },
+ },
+};
+</script>
+
+<template>
+ <nav :aria-label="__('Files breadcrumb')">
+ <ol class="breadcrumb repo-breadcrumb">
+ <li v-for="(link, i) in pathLinks" :key="i" class="breadcrumb-item">
+ <router-link :to="link.to" :aria-current="isLast(i) ? 'page' : null">
+ {{ link.name }}
+ </router-link>
+ </li>
+ </ol>
+ </nav>
+</template>
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index 2b0a4644bf6..f4df98ac2ff 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -50,7 +50,7 @@ export default {
);
},
showParentRow() {
- return !this.isLoadingFiles && this.path !== '';
+ return !this.isLoadingFiles && ['', '/'].indexOf(this.path) === -1;
},
},
watch: {
diff --git a/app/assets/javascripts/repository/components/table/parent_row.vue b/app/assets/javascripts/repository/components/table/parent_row.vue
index b4433f00d8a..3c39f404226 100644
--- a/app/assets/javascripts/repository/components/table/parent_row.vue
+++ b/app/assets/javascripts/repository/components/table/parent_row.vue
@@ -27,8 +27,8 @@ export default {
</script>
<template>
- <tr v-once @click="clickRow">
- <td colspan="3" class="tree-item-file-name">
+ <tr class="tree-item">
+ <td colspan="3" class="tree-item-file-name" @click.self="clickRow">
<router-link :to="parentRoute" :aria-label="__('Go to parent')">
..
</router-link>
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index a5c125c2ff7..52f53be045b 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -1,22 +1,52 @@
import Vue from 'vue';
import createRouter from './router';
import App from './components/app.vue';
+import Breadcrumbs from './components/breadcrumbs.vue';
import apolloProvider from './graphql';
import { setTitle } from './utils/title';
export default function setupVueRepositoryList() {
const el = document.getElementById('js-tree-list');
- const { projectPath, ref, fullName } = el.dataset;
+ const { projectPath, projectShortPath, ref, fullName } = el.dataset;
const router = createRouter(projectPath, ref);
apolloProvider.clients.defaultClient.cache.writeData({
data: {
projectPath,
+ projectShortPath,
ref,
},
});
- router.afterEach(({ params: { pathMatch } }) => setTitle(pathMatch, ref, fullName));
+ router.afterEach(({ params: { pathMatch } }) => {
+ const isRoot = pathMatch === undefined || pathMatch === '/';
+
+ setTitle(pathMatch, ref, fullName);
+
+ if (!isRoot) {
+ document
+ .querySelectorAll('.js-keep-hidden-on-navigation')
+ .forEach(elem => elem.classList.add('hidden'));
+ }
+
+ document
+ .querySelectorAll('.js-hide-on-navigation')
+ .forEach(elem => elem.classList.toggle('hidden', !isRoot));
+ });
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: document.getElementById('js-repo-breadcrumb'),
+ router,
+ apolloProvider,
+ render(h) {
+ return h(Breadcrumbs, {
+ props: {
+ currentPath: this.$route.params.pathMatch,
+ },
+ });
+ },
+ });
return new Vue({
el,
diff --git a/app/assets/javascripts/repository/queries/getProjectShortPath.graphql b/app/assets/javascripts/repository/queries/getProjectShortPath.graphql
new file mode 100644
index 00000000000..34eb26598c2
--- /dev/null
+++ b/app/assets/javascripts/repository/queries/getProjectShortPath.graphql
@@ -0,0 +1,3 @@
+query getProjectShortPath {
+ projectShortPath @client
+}
diff --git a/app/assets/javascripts/repository/router.js b/app/assets/javascripts/repository/router.js
index f7132b99d9e..9322c81ab97 100644
--- a/app/assets/javascripts/repository/router.js
+++ b/app/assets/javascripts/repository/router.js
@@ -16,15 +16,8 @@ export default function createRouter(base, baseRef) {
name: 'treePath',
component: TreePage,
props: route => ({
- path: route.params.pathMatch.replace(/^\//, ''),
+ path: route.params.pathMatch && route.params.pathMatch.replace(/^\//, ''),
}),
- beforeEnter(to, from, next) {
- document
- .querySelectorAll('.js-hide-on-navigation')
- .forEach(el => el.classList.add('hidden'));
-
- next();
- },
},
{
path: '/',
diff --git a/app/assets/javascripts/repository/utils/title.js b/app/assets/javascripts/repository/utils/title.js
index 3c3e918c0a8..4e194640e92 100644
--- a/app/assets/javascripts/repository/utils/title.js
+++ b/app/assets/javascripts/repository/utils/title.js
@@ -1,5 +1,7 @@
// eslint-disable-next-line import/prefer-default-export
export const setTitle = (pathMatch, ref, project) => {
+ if (!pathMatch) return;
+
const path = pathMatch.replace(/^\//, '');
const isEmpty = path === '';
diff --git a/app/assets/javascripts/visual_review_toolbar/index.js b/app/assets/javascripts/visual_review_toolbar/index.js
new file mode 100644
index 00000000000..91d0382feac
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/index.js
@@ -0,0 +1,2 @@
+import './styles/toolbar.css';
+import 'vendor/visual_review_toolbar';
diff --git a/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css b/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css
new file mode 100644
index 00000000000..342b3599a44
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css
@@ -0,0 +1,149 @@
+/*
+ As a standalone script, the toolbar has its own css
+ */
+
+#gitlab-collapse > * {
+ pointer-events: none;
+}
+
+#gitlab-form-wrapper {
+ display: flex;
+ flex-direction: column;
+ width: 100%
+}
+
+#gitlab-review-container {
+ max-width: 22rem;
+ max-height: 22rem;
+ overflow: scroll;
+ position: fixed;
+ bottom: 1rem;
+ right: 1rem;
+ display: flex;
+ flex-direction: row-reverse;
+ padding: 1rem;
+ background-color: #fff;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
+ 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
+ 'Noto Color Emoji';
+ font-size: .8rem;
+ font-weight: 400;
+ color: #2e2e2e;
+}
+
+.gitlab-open-wrapper {
+ max-width: 22rem;
+ max-height: 22rem;
+}
+
+.gitlab-closed-wrapper {
+ max-width: 3.4rem;
+ max-height: 3.4rem;
+}
+
+.gitlab-button {
+ cursor: pointer;
+ transition: background-color 100ms linear, border-color 100ms linear, color 100ms linear, box-shadow 100ms linear;
+}
+
+.gitlab-button-secondary {
+ background: none #fff;
+ margin: 0 .5rem;
+ border: 1px solid #e3e3e3;
+}
+
+.gitlab-button-secondary:hover {
+ background-color: #f0f0f0;
+ border-color: #e3e3e3;
+ color: #2e2e2e;
+}
+
+.gitlab-button-secondary:active {
+ color: #2e2e2e;
+ background-color: #e1e1e1;
+ border-color: #dadada;
+}
+
+.gitlab-button-success:hover {
+ color: #fff;
+ background-color: #137e3f;
+ border-color: #127339;
+}
+
+.gitlab-button-success:active {
+ background-color: #168f48;
+ border-color: #12753a;
+ color: #fff;
+}
+
+.gitlab-button-success {
+ background-color: #1aaa55;
+ border: 1px solid #168f48;
+ color: #fff;
+}
+
+.gitlab-button-wide {
+ width: 100%;
+}
+
+.gitlab-button-wrapper {
+ margin-top: 1rem;
+ display: flex;
+ align-items: baseline;
+ justify-content: flex-end;
+}
+
+.gitlab-collapse {
+ width: 2.4rem;
+ height: 2.2rem;
+ margin-left: 1rem;
+ padding: .5rem;
+}
+
+.gitlab-collapse-closed {
+ align-self: center;
+}
+
+.gitlab-checkbox-label {
+ padding: 0 .2rem;
+}
+
+.gitlab-checkbox-wrapper {
+ display: flex;
+ align-items: baseline;
+}
+
+.gitlab-label {
+ font-weight: 600;
+ display: inline-block;
+ width: 100%;
+}
+
+.gitlab-link {
+ color: #1b69b6;
+ text-decoration: none;
+ background-color: transparent;
+ background-image: none;
+}
+
+.gitlab-message {
+ padding: .25rem 0;
+ margin: 0;
+ line-height: 1.2rem;
+}
+
+.gitlab-metadata-note {
+ font-size: .7rem;
+ line-height: 1rem;
+ color: #666;
+ margin-bottom: 0;
+}
+
+.gitlab-input {
+ width: 100%;
+ border: 1px solid #dfdfdf;
+ border-radius: 4px;
+ padding: .1rem .2rem;
+ min-height: 2rem;
+ max-width: 17rem;
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index f5fa68308bc..c377c16fb13 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -5,6 +5,7 @@ import { sprintf, __ } from '~/locale';
import PipelineStage from '~/pipelines/components/stage.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
+import PipelineLink from '~/vue_shared/components/ci_pipeline_link.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import mrWidgetPipelineMixin from 'ee_else_ce/vue_merge_request_widget/mixins/mr_widget_pipeline';
@@ -16,6 +17,7 @@ export default {
Icon,
TooltipOnTruncate,
GlLink,
+ PipelineLink,
LinkedPipelinesMiniList: () =>
import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'),
},
@@ -112,9 +114,12 @@ export default {
<div class="media-body">
<div class="font-weight-bold js-pipeline-info-container">
{{ s__('Pipeline|Pipeline') }}
- <gl-link :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number"
- >#{{ pipeline.id }}</gl-link
- >
+ <pipeline-link
+ :href="pipeline.path"
+ :pipeline-id="pipeline.id"
+ :pipeline-iid="pipeline.iid"
+ class="pipeline-id pipeline-iid font-weight-normal"
+ />
{{ pipeline.details.status.label }}
<template v-if="hasCommitInfo">
{{ s__('Pipeline|for') }}
diff --git a/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue b/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue
new file mode 100644
index 00000000000..eae4c06467c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/ci_pipeline_link.vue
@@ -0,0 +1,32 @@
+<script>
+import { GlLink, GlTooltipDirective } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ href: {
+ type: String,
+ required: true,
+ },
+ pipelineId: {
+ type: Number,
+ required: true,
+ },
+ pipelineIid: {
+ type: Number,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <gl-link v-gl-tooltip :href="href" :title="__('Pipeline ID (IID)')">
+ <span class="pipeline-id">#{{ pipelineId }}</span>
+ <span class="pipeline-iid">(#{{ pipelineIid }})</span>
+ </gl-link>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index 3f45dc7853b..0bac63b1062 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -37,6 +37,16 @@ export default {
type: Number,
required: true,
},
+ itemIid: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ itemIdTooltip: {
+ type: String,
+ required: false,
+ default: '',
+ },
time: {
type: String,
required: true,
@@ -85,7 +95,12 @@ export default {
<section class="header-main-content">
<ci-icon-badge :status="status" />
- <strong> {{ itemName }} #{{ itemId }} </strong>
+ <strong v-gl-tooltip :title="itemIdTooltip">
+ {{ itemName }} #{{ itemId }}
+ <template v-if="itemIid"
+ >(#{{ itemIid }})</template
+ >
+ </strong>
<template v-if="shouldRenderTriggeredLabel">
triggered
@@ -96,9 +111,8 @@ export default {
<timeago-tooltip :time="time" />
- by
-
<template v-if="user">
+ by
<gl-link
v-gl-tooltip
:href="user.path"
diff --git a/app/assets/stylesheets/components/avatar.scss b/app/assets/stylesheets/components/avatar.scss
index 4ab197b935b..25ee3ca944d 100644
--- a/app/assets/stylesheets/components/avatar.scss
+++ b/app/assets/stylesheets/components/avatar.scss
@@ -1,28 +1,111 @@
+$avatar-sizes: (
+ 16: (
+ font-size: 10px,
+ line-height: 16px,
+ border-radius: $border-radius-small
+ ),
+ 18: (
+ border-radius: $border-radius-small
+ ),
+ 19: (
+ border-radius: $border-radius-small
+ ),
+ 20: (
+ border-radius: $border-radius-small
+ ),
+ 24: (
+ font-size: 12px,
+ line-height: 24px,
+ border-radius: $border-radius-default
+ ),
+ 26: (
+ font-size: 20px,
+ line-height: 1.33,
+ border-radius: $border-radius-default
+ ),
+ 32: (
+ font-size: 14px,
+ line-height: 32px,
+ border-radius: $border-radius-default
+ ),
+ 36: (
+ border-radius: $border-radius-default
+ ),
+ 40: (
+ font-size: 16px,
+ line-height: 38px,
+ border-radius: $border-radius-default
+ ),
+ 46: (
+ border-radius: $border-radius-default
+ ),
+ 48: (
+ font-size: 20px,
+ line-height: 48px,
+ border-radius: $border-radius-large
+ ),
+ 60: (
+ font-size: 32px,
+ line-height: 58px,
+ border-radius: $border-radius-large
+ ),
+ 64: (
+ font-size: 28px,
+ line-height: 64px,
+ border-radius: $border-radius-large
+ ),
+ 70: (
+ font-size: 34px,
+ line-height: 70px,
+ border-radius: $border-radius-large
+ ),
+ 90: (
+ font-size: 36px,
+ line-height: 88px,
+ border-radius: $border-radius-large
+ ),
+ 96: (
+ font-size: 48px,
+ line-height: 96px,
+ border-radius: $border-radius-large
+ ),
+ 100: (
+ font-size: 36px,
+ line-height: 98px,
+ border-radius: $border-radius-large
+ ),
+ 110: (
+ font-size: 40px,
+ line-height: 108px,
+ font-weight: $gl-font-weight-normal,
+ border-radius: $border-radius-large
+ ),
+ 140: (
+ font-size: 72px,
+ line-height: 138px,
+ border-radius: $border-radius-large
+ ),
+ 160: (
+ font-size: 96px,
+ line-height: 158px,
+ border-radius: $border-radius-large
+ )
+);
+
+$identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $identicon-blue, $identicon-teal,
+ $identicon-orange, $gray-darker;
+
.avatar-circle {
float: left;
margin-right: 15px;
border-radius: $avatar-radius;
border: 1px solid $gray-normal;
- &.s16 { @include avatar-size(16px, 8px); }
- &.s18 { @include avatar-size(18px, 8px); }
- &.s19 { @include avatar-size(19px, 8px); }
- &.s20 { @include avatar-size(20px, 8px); }
- &.s24 { @include avatar-size(24px, 8px); }
- &.s26 { @include avatar-size(26px, 8px); }
- &.s32 { @include avatar-size(32px, 8px); }
- &.s36 { @include avatar-size(36px, 16px); }
- &.s40 { @include avatar-size(40px, 16px); }
- &.s46 { @include avatar-size(46px, 16px); }
- &.s48 { @include avatar-size(48px, 16px); }
- &.s60 { @include avatar-size(60px, 16px); }
- &.s64 { @include avatar-size(64px, 16px); }
- &.s70 { @include avatar-size(70px, 16px); }
- &.s90 { @include avatar-size(90px, 16px); }
- &.s96 { @include avatar-size(96px, 16px); }
- &.s100 { @include avatar-size(100px, 16px); }
- &.s110 { @include avatar-size(110px, 16px); }
- &.s140 { @include avatar-size(140px, 16px); }
- &.s160 { @include avatar-size(160px, 16px); }
+
+ @each $size, $size-config in $avatar-sizes {
+ &.s#{$size} {
+ @include avatar-size(#{$size}px, if($size < 36, 8px, 16px));
+ }
+ }
}
.avatar {
@@ -42,8 +125,13 @@
margin-left: 2px;
flex-shrink: 0;
- &.s16 { margin-right: 4px; }
- &.s24 { margin-right: 4px; }
+ &.s16 {
+ margin-right: 4px;
+ }
+
+ &.s24 {
+ margin-right: 4px;
+ }
}
&.center {
@@ -69,60 +157,25 @@
background-color: $gray-darker;
// Sizes
- &.s16 { font-size: 10px;
- line-height: 16px; }
-
- &.s24 { font-size: 12px;
- line-height: 24px; }
-
- &.s26 { font-size: 20px;
- line-height: 1.33; }
-
- &.s32 { font-size: 14px;
- line-height: 32px; }
-
- &.s40 { font-size: 16px;
- line-height: 38px; }
-
- &.s48 { font-size: 20px;
- line-height: 48px; }
-
- &.s60 { font-size: 32px;
- line-height: 58px; }
-
- &.s64 { font-size: 28px;
- line-height: 64px; }
-
- &.s70 { font-size: 34px;
- line-height: 70px; }
-
- &.s90 { font-size: 36px;
- line-height: 88px; }
-
- &.s96 { font-size: 48px;
- line-height: 96px; }
-
- &.s100 { font-size: 36px;
- line-height: 98px; }
-
- &.s110 { font-size: 40px;
- line-height: 108px;
- font-weight: $gl-font-weight-normal; }
-
- &.s140 { font-size: 72px;
- line-height: 138px; }
-
- &.s160 { font-size: 96px;
- line-height: 158px; }
+ @each $size, $size-config in $avatar-sizes {
+ $keys: map-keys($size-config);
+
+ &.s#{$size} {
+ @each $key in $keys {
+ // We don't want `border-radius` to be included here.
+ @if ($key != 'border-radius') {
+ #{$key}: map-get($size-config, #{$key});
+ }
+ }
+ }
+ }
// Background colors
- &.bg1 { background-color: $identicon-red; }
- &.bg2 { background-color: $identicon-purple; }
- &.bg3 { background-color: $identicon-indigo; }
- &.bg4 { background-color: $identicon-blue; }
- &.bg5 { background-color: $identicon-teal; }
- &.bg6 { background-color: $identicon-orange; }
- &.bg7 { background-color: $gray-darker; }
+ @for $i from 1 through length($identicon-backgrounds) {
+ &.bg#{$i} {
+ background-color: nth($identicon-backgrounds, $i);
+ }
+ }
}
.avatar-container {
@@ -139,41 +192,32 @@
.avatar {
border-radius: 0;
+ border: 0;
height: auto;
width: 100%;
margin: 0;
align-self: center;
}
- &.s40 { min-width: 40px;
- min-height: 40px; }
+ &.s40 {
+ min-width: 40px;
+ min-height: 40px;
+ }
- &.s64 { min-width: 64px;
- min-height: 64px; }
+ &.s64 {
+ min-width: 64px;
+ min-height: 64px;
+ }
}
.rect-avatar {
border-radius: $border-radius-small;
- &.s16 { border-radius: $border-radius-small; }
- &.s18 { border-radius: $border-radius-small; }
- &.s19 { border-radius: $border-radius-small; }
- &.s20 { border-radius: $border-radius-small; }
- &.s24 { border-radius: $border-radius-default; }
- &.s26 { border-radius: $border-radius-default; }
- &.s32 { border-radius: $border-radius-default; }
- &.s36 { border-radius: $border-radius-default; }
- &.s40 { border-radius: $border-radius-default; }
- &.s46 { border-radius: $border-radius-default; }
- &.s48 { border-radius: $border-radius-large; }
- &.s60 { border-radius: $border-radius-large; }
- &.s64 { border-radius: $border-radius-large; }
- &.s70 { border-radius: $border-radius-large; }
- &.s90 { border-radius: $border-radius-large; }
- &.s96 { border-radius: $border-radius-large; }
- &.s100 { border-radius: $border-radius-large; }
- &.s110 { border-radius: $border-radius-large; }
- &.s140 { border-radius: $border-radius-large; }
- &.s160 { border-radius: $border-radius-large; }
+
+ @each $size, $size-config in $avatar-sizes {
+ &.s#{$size} {
+ border-radius: map-get($size-config, 'border-radius');
+ }
+ }
}
.avatar-counter {
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index a8df7e1bfad..7760c48cb92 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -232,9 +232,6 @@
height: $default-icon-size;
width: $default-icon-size;
border-radius: 50%;
- }
-
- path {
fill: $gray-700;
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 8fb4027bf97..cd951f67293 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -570,10 +570,10 @@
}
.dropdown-menu-close {
- right: 5px;
+ top: $gl-padding-4;
+ right: $gl-padding-8;
width: 20px;
height: 20px;
- top: -1px;
}
.dropdown-menu-close-icon {
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 13288d8bad1..11e8a32389f 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -456,8 +456,9 @@
// Don't hide the overflow in system messages
.system-note-message,
-.issuable-detail,
+.issuable-details,
.md-preview-holder,
+.referenced-commands,
.note-body {
.scoped-label-wrapper {
.badge {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 50c87e55f56..170432a9e62 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -822,6 +822,7 @@ $note-form-margin-left: 72px;
.line-resolve-btn {
margin-right: 5px;
+ color: $gray-darkest;
svg {
vertical-align: middle;
@@ -836,7 +837,6 @@ $note-form-margin-left: 72px;
background-color: transparent;
border: 0;
outline: 0;
- color: $gray-darkest;
transition: color $general-hover-transition-duration $general-hover-transition-curve;
&.is-disabled {
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 37071a57bb3..dbf600df9d6 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -261,3 +261,13 @@ input[type='checkbox']:hover {
color: $blue-600;
}
}
+
+// Disable webkit input icons, link to solution: https://stackoverflow.com/questions/9421551/how-do-i-remove-all-default-webkit-search-field-styling
+/* stylelint-disable property-no-vendor-prefix */
+input[type='search']::-webkit-search-decoration,
+input[type='search']::-webkit-search-cancel-button,
+input[type='search']::-webkit-search-results-button,
+input[type='search']::-webkit-search-results-decoration {
+ -webkit-appearance: none;
+}
+/* stylelint-enable */
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index aeff0c96b64..70db15916b9 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -3,7 +3,7 @@
class Admin::ProjectsController < Admin::ApplicationController
include MembersPresentation
- before_action :project, only: [:show, :transfer, :repository_check]
+ before_action :project, only: [:show, :transfer, :repository_check, :destroy]
before_action :group, only: [:show, :transfer]
def index
@@ -35,6 +35,15 @@ class Admin::ProjectsController < Admin::ApplicationController
end
# rubocop: enable CodeReuse/ActiveRecord
+ def destroy
+ ::Projects::DestroyService.new(@project, current_user, {}).async_execute
+ flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.full_name }
+
+ redirect_to admin_projects_path, status: :found
+ rescue Projects::DestroyService::DestroyError => ex
+ redirect_to admin_projects_path, status: 302, alert: ex.message
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def transfer
namespace = Namespace.find_by(id: params[:new_namespace_id])
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index e8f38899647..1ce0afac83b 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -53,7 +53,8 @@ class GraphqlController < ApplicationController
{
query: single_query_info[:query],
variables: build_variables(single_query_info[:variables]),
- operation_name: single_query_info[:operationName]
+ operation_name: single_query_info[:operationName],
+ context: context
}
end
end
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index b719b70c56e..617e5bb7cb3 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -14,9 +14,9 @@ class Profiles::NotificationsController < Profiles::ApplicationController
result = Users::UpdateService.new(current_user, user_params.merge(user: current_user)).execute
if result[:status] == :success
- flash[:notice] = "Notification settings saved"
+ flash[:notice] = _("Notification settings saved")
else
- flash[:alert] = "Failed to save new settings"
+ flash[:alert] = _("Failed to save new settings")
end
redirect_back_or_default(default: profile_notifications_path)
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 365b94f5a3e..8002eb08ada 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -30,7 +30,8 @@ module EnvironmentsHelper
"environments-endpoint": project_environments_path(project, format: :json),
"project-path" => project_path(project),
"tags-path" => project_tags_path(project),
- "has-metrics" => "#{environment.has_metrics?}"
+ "has-metrics" => "#{environment.has_metrics?}",
+ "external-dashboard-url" => project.metrics_setting_external_dashboard_url
}
end
end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index acc8aeae282..a6b3b79737b 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -76,29 +76,39 @@ module LabelsHelper
end
def suggested_colors
- [
- '#0033CC',
- '#428BCA',
- '#44AD8E',
- '#A8D695',
- '#5CB85C',
- '#69D100',
- '#004E00',
- '#34495E',
- '#7F8C8D',
- '#A295D6',
- '#5843AD',
- '#8E44AD',
- '#FFECDB',
- '#AD4363',
- '#D10069',
- '#CC0033',
- '#FF0000',
- '#D9534F',
- '#D1D100',
- '#F0AD4E',
- '#AD8D43'
- ]
+ {
+ '#0033CC' => s_('SuggestedColors|UA blue'),
+ '#428BCA' => s_('SuggestedColors|Moderate blue'),
+ '#44AD8E' => s_('SuggestedColors|lime green'),
+ '#A8D695' => s_('SuggestedColors|Feijoa'),
+ '#5CB85C' => s_('SuggestedColors|Slightly desaturated green'),
+ '#69D100' => s_('SuggestedColors|Bright Green'),
+ '#004E00' => s_('SuggestedColors|Very dark lime green'),
+ '#34495E' => s_('SuggestedColors|Very dark desaturated blue'),
+ '#7F8C8D' => s_('SuggestedColors|Dark grayish cyan'),
+ '#A295D6' => s_('SuggestedColors|Slightly desaturated blue'),
+ '#5843AD' => s_('SuggestedColors|Dark moderate blue'),
+ '#8E44AD' => s_('SuggestedColors|Dark moderate violet'),
+ '#FFECDB' => s_('SuggestedColors|Very pale orange'),
+ '#AD4363' => s_('SuggestedColors|Dark moderate pink'),
+ '#D10069' => s_('SuggestedColors|Strong pink'),
+ '#CC0033' => s_('SuggestedColors|Strong red'),
+ '#FF0000' => s_('SuggestedColors|Pure red'),
+ '#D9534F' => s_('SuggestedColors|Soft red'),
+ '#D1D100' => s_('SuggestedColors|Strong yellow'),
+ '#F0AD4E' => s_('SuggestedColors|Soft orange'),
+ '#AD8D43' => s_('SuggestedColors|Dark moderate orange')
+ }
+ end
+
+ def render_suggested_colors
+ colors_html = suggested_colors.map do |color_hex_value, color_name|
+ link_to('', '#', class: "has-tooltip", style: "background-color: #{color_hex_value}", data: { color: color_hex_value }, title: color_name)
+ end
+
+ content_tag(:div, class: 'suggest-colors') do
+ colors_html.join.html_safe
+ end
end
def text_color_for_bg(bg_color)
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index f798bfbf703..e587cf4045d 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -343,6 +343,10 @@ module ProjectsHelper
description.html_safe % { project_name: project.name }
end
+ def metrics_external_dashboard_url
+ @project.metrics_setting_external_dashboard_url
+ end
+
private
def get_project_nav_tabs(project, current_user)
@@ -655,4 +659,8 @@ module ProjectsHelper
project.builds_enabled? &&
!project.repository.gitlab_ci_yml
end
+
+ def vue_file_list_enabled?
+ Gitlab::Graphql.enabled? && Feature.enabled?(:vue_file_list, @project)
+ end
end
diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb
index e80b3f2b54a..ecf37bae6b3 100644
--- a/app/helpers/storage_helper.rb
+++ b/app/helpers/storage_helper.rb
@@ -12,10 +12,11 @@ module StorageHelper
def storage_counters_details(statistics)
counters = {
counter_repositories: storage_counter(statistics.repository_size),
+ counter_wikis: storage_counter(statistics.wiki_size),
counter_build_artifacts: storage_counter(statistics.build_artifacts_size),
counter_lfs_objects: storage_counter(statistics.lfs_objects_size)
}
- _("%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS") % counters
+ _("%{counter_repositories} repositories, %{counter_wikis} wikis, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS") % counters
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 1b67a7272bc..56786fae6ea 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -765,6 +765,10 @@ module Ci
end
end
+ def report_artifacts
+ job_artifacts.with_reports
+ end
+
# Virtual deployment status depending on the environment status.
def deployment_status
return unless starts_environment?
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 3beb76ffc2b..0dbeab30498 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -26,10 +26,13 @@ module Ci
metrics: 'metrics.txt'
}.freeze
- TYPE_AND_FORMAT_PAIRS = {
+ INTERNAL_TYPES = {
archive: :zip,
metadata: :gzip,
- trace: :raw,
+ trace: :raw
+ }.freeze
+
+ REPORT_TYPES = {
junit: :gzip,
metrics: :gzip,
@@ -45,6 +48,8 @@ module Ci
performance: :raw
}.freeze
+ TYPE_AND_FORMAT_PAIRS = INTERNAL_TYPES.merge(REPORT_TYPES).freeze
+
belongs_to :project
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
@@ -66,6 +71,10 @@ module Ci
where(file_type: types)
end
+ scope :with_reports, -> do
+ with_file_types(REPORT_TYPES.keys.map(&:to_s))
+ end
+
scope :test_reports, -> do
with_file_types(TEST_REPORT_FILE_TYPES)
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 7393ef4b05c..f7c31890198 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -76,6 +76,7 @@ class Namespace < ApplicationRecord
'namespaces.*',
'COALESCE(SUM(ps.storage_size), 0) AS storage_size',
'COALESCE(SUM(ps.repository_size), 0) AS repository_size',
+ 'COALESCE(SUM(ps.wiki_size), 0) AS wiki_size',
'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size',
'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size',
'COALESCE(SUM(ps.packages_size), 0) AS packages_size'
diff --git a/app/models/project.rb b/app/models/project.rb
index ab4da61dcf8..20895923d3b 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -309,6 +309,7 @@ class Project < ApplicationRecord
delegate :group_clusters_enabled?, to: :group, allow_nil: true
delegate :root_ancestor, to: :namespace, allow_nil: true
delegate :last_pipeline, to: :commit, allow_nil: true
+ delegate :external_dashboard_url, to: :metrics_setting, allow_nil: true, prefix: true
# Validations
validates :creator, presence: true, on: :create
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index 7ba69370f14..ae5d5038099 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -2,11 +2,11 @@
class PipelinesEmailService < Service
prop_accessor :recipients
- boolean_accessor :notify_only_broken_pipelines
+ boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
validates :recipients, presence: true, if: :valid_recipients?
def initialize_properties
- self.properties ||= { notify_only_broken_pipelines: true }
+ self.properties ||= { notify_only_broken_pipelines: true, notify_only_default_branch: false }
end
def title
@@ -54,7 +54,9 @@ class PipelinesEmailService < Service
placeholder: _('Emails separated by comma'),
required: true },
{ type: 'checkbox',
- name: 'notify_only_broken_pipelines' }
+ name: 'notify_only_broken_pipelines' },
+ { type: 'checkbox',
+ name: 'notify_only_default_branch' }
]
end
@@ -67,6 +69,16 @@ class PipelinesEmailService < Service
end
def should_pipeline_be_notified?(data)
+ notify_for_pipeline_branch?(data) && notify_for_pipeline?(data)
+ end
+
+ def notify_for_pipeline_branch?(data)
+ return true unless notify_only_default_branch?
+
+ data[:object_attributes][:ref] == data[:project][:default_branch]
+ end
+
+ def notify_for_pipeline?(data)
case data[:object_attributes][:status]
when 'success'
!notify_only_broken_pipelines?
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 6fe8cb40d25..832c8417b5b 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -4,9 +4,16 @@ class ProjectStatistics < ApplicationRecord
belongs_to :project
belongs_to :namespace
+ default_value_for :wiki_size, 0
+
+ # older migrations fail due to non-existent attribute without this
+ def wiki_size
+ has_attribute?(:wiki_size) ? super : 0
+ end
+
before_save :update_storage_size
- COLUMNS_TO_REFRESH = [:repository_size, :lfs_objects_size, :commit_count].freeze
+ COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count].freeze
INCREMENTABLE_COLUMNS = { build_artifacts_size: %i[storage_size], packages_size: %i[storage_size] }.freeze
def total_repository_size
@@ -27,11 +34,14 @@ class ProjectStatistics < ApplicationRecord
self.commit_count = project.repository.commit_count
end
- # Repository#size needs to be converted from MB to Byte.
def update_repository_size
self.repository_size = project.repository.size * 1.megabyte
end
+ def update_wiki_size
+ self.wiki_size = project.wiki.repository.size * 1.megabyte
+ end
+
def update_lfs_objects_size
self.lfs_objects_size = project.lfs_objects.sum(:size)
end
@@ -42,7 +52,7 @@ class ProjectStatistics < ApplicationRecord
end
def update_storage_size
- self.storage_size = repository_size + lfs_objects_size + build_artifacts_size + packages_size
+ self.storage_size = repository_size + wiki_size + lfs_objects_size + build_artifacts_size + packages_size
end
# Since this incremental update method does not call update_storage_size above,
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 6928968edc0..67e44ee9d10 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -42,6 +42,11 @@ class BuildDetailsEntity < JobEntity
end
end
+ expose :report_artifacts,
+ as: :reports,
+ using: JobArtifactReportEntity,
+ if: -> (*) { can?(current_user, :read_build, build) }
+
expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build|
erase_project_job_path(project, build)
diff --git a/app/serializers/job_artifact_report_entity.rb b/app/serializers/job_artifact_report_entity.rb
new file mode 100644
index 00000000000..4280351a6b0
--- /dev/null
+++ b/app/serializers/job_artifact_report_entity.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class JobArtifactReportEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :file_type
+ expose :file_format
+ expose :size
+
+ expose :download_path do |artifact|
+ download_project_job_artifacts_path(artifact.job.project, artifact.job, file_type: artifact.file_format)
+ end
+end
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index 9ef93b2387f..ec2698ecbe3 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -4,6 +4,7 @@ class PipelineEntity < Grape::Entity
include RequestAwareEntity
expose :id
+ expose :iid
expose :user, using: UserEntity
expose :active?, as: :active
diff --git a/app/services/projects/update_statistics_service.rb b/app/services/projects/update_statistics_service.rb
index f32a779fab0..28677a398f3 100644
--- a/app/services/projects/update_statistics_service.rb
+++ b/app/services/projects/update_statistics_service.rb
@@ -3,7 +3,7 @@
module Projects
class UpdateStatisticsService < BaseService
def execute
- return unless project && project.repository.exists?
+ return unless project
Rails.logger.info("Updating statistics for project #{project.id}")
diff --git a/app/views/admin/application_settings/_external_authorization_service_form.html.haml b/app/views/admin/application_settings/_external_authorization_service_form.html.haml
index 01f6c7afe61..7587ecbf9d3 100644
--- a/app/views/admin/application_settings/_external_authorization_service_form.html.haml
+++ b/app/views/admin/application_settings/_external_authorization_service_form.html.haml
@@ -5,7 +5,7 @@
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
- = _('External Classification Policy Authorization')
+ = _('External Classification Policy Authorization')
.settings-content
= form_for @application_setting, url: admin_application_settings_path(anchor: 'js-external-auth-settings'), html: { class: 'fieldset-form' } do |f|
diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml
index 49aa62a5408..299d0a12e6c 100644
--- a/app/views/admin/labels/_form.html.haml
+++ b/app/views/admin/labels/_form.html.haml
@@ -24,10 +24,7 @@
%br
= _("Or you can choose one of the suggested colors below")
- .suggest-colors
- - suggested_colors.each do |color|
- = link_to '#', style: "background-color: #{color}", data: { color: color } do
- &nbsp;
+ = render_suggested_colors
.form-actions
= f.submit _('Save'), class: 'btn btn-success js-save-button'
diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml
index 9117f63f939..2f7ad35eb3e 100644
--- a/app/views/admin/projects/_projects.html.haml
+++ b/app/views/admin/projects/_projects.html.haml
@@ -7,7 +7,7 @@
= link_to 'Edit', edit_project_path(project), id: "edit_#{dom_id(project)}", class: "btn"
%button.delete-project-button.btn.btn-danger{ data: { toggle: 'modal',
target: '#delete-project-modal',
- delete_project_url: project_path(project),
+ delete_project_url: admin_project_path(project),
project_name: project.name }, type: 'button' }
= s_('AdminProjects|Delete')
@@ -17,7 +17,7 @@
- if project.archived
%span.badge.badge-warning archived
.title
- = link_to(admin_namespace_project_path(project.namespace, project)) do
+ = link_to(admin_project_path(project)) do
.dash-project-avatar
.avatar-container.rect-avatar.s40
= project_icon(project, alt: '', class: 'avatar project-avatar s40', width: 40, height: 40)
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index c4178296e67..dcd6f7c8078 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -124,6 +124,8 @@
%strong
= Gitlab::Access.human_access_with_none(@user.highest_role)
+ = render_if_exists 'admin/users/using_license_seat', user: @user
+
- if @user.ldap_user?
%li
%span.light LDAP uid:
diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml
index 9280f12e187..40609fddbde 100644
--- a/app/views/import/bitbucket_server/status.html.haml
+++ b/app/views/import/bitbucket_server/status.html.haml
@@ -29,7 +29,7 @@
%tr
%th= _('From Bitbucket Server')
%th= _('To GitLab')
- %th= _(' Status')
+ %th= _('Status')
%tbody
- @already_added_projects.each do |project|
%tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
diff --git a/app/views/profiles/active_sessions/_active_session.html.haml b/app/views/profiles/active_sessions/_active_session.html.haml
index 2bf514d72a5..bb31049111c 100644
--- a/app/views/profiles/active_sessions/_active_session.html.haml
+++ b/app/views/profiles/active_sessions/_active_session.html.haml
@@ -8,18 +8,19 @@
%div
%strong= active_session.ip_address
- if is_current_session
- %div This is your current session
+ %div
+ = _('This is your current session')
- else
%div
- Last accessed on
+ = _('Last accessed on')
= l(active_session.updated_at, format: :short)
%div
%strong= active_session.browser
- on
+ = s_('ProfileSession|on')
%strong= active_session.os
%div
- %strong Signed in
- on
+ %strong= _('Signed in')
+ = s_('ProfileSession|on')
= l(active_session.created_at, format: :short)
diff --git a/app/views/profiles/active_sessions/index.html.haml b/app/views/profiles/active_sessions/index.html.haml
index 8688a52843d..d651319fc3f 100644
--- a/app/views/profiles/active_sessions/index.html.haml
+++ b/app/views/profiles/active_sessions/index.html.haml
@@ -1,4 +1,4 @@
-- page_title 'Active Sessions'
+- page_title _('Active Sessions')
- @content_class = "limit-container-width" unless fluid_layout
.row.prepend-top-default
@@ -6,7 +6,7 @@
%h4.prepend-top-0
= page_title
%p
- This is a list of devices that have logged into your account. Revoke any sessions that you do not recognize.
+ = _('This is a list of devices that have logged into your account. Revoke any sessions that you do not recognize.')
.col-lg-8
.append-bottom-default
diff --git a/app/views/profiles/chat_names/_chat_name.html.haml b/app/views/profiles/chat_names/_chat_name.html.haml
index 9e82e47c1e1..ff67f92ad07 100644
--- a/app/views/profiles/chat_names/_chat_name.html.haml
+++ b/app/views/profiles/chat_names/_chat_name.html.haml
@@ -21,7 +21,7 @@
- if chat_name.last_used_at
= time_ago_with_tooltip(chat_name.last_used_at)
- else
- Never
+ = _('Never')
%td
- = link_to 'Remove', profile_chat_name_path(chat_name), method: :delete, class: 'btn btn-danger float-right', data: { confirm: 'Are you sure you want to revoke this nickname?' }
+ = link_to _('Remove'), profile_chat_name_path(chat_name), method: :delete, class: 'btn btn-danger float-right', data: { confirm: _('Are you sure you want to revoke this nickname?') }
diff --git a/app/views/profiles/chat_names/index.html.haml b/app/views/profiles/chat_names/index.html.haml
index 4b6e419af50..0c8098a97d5 100644
--- a/app/views/profiles/chat_names/index.html.haml
+++ b/app/views/profiles/chat_names/index.html.haml
@@ -1,4 +1,4 @@
-- page_title 'Chat'
+- page_title _('Chat')
- @content_class = "limit-container-width" unless fluid_layout
.row.prepend-top-default
@@ -6,7 +6,7 @@
%h4.prepend-top-0
= page_title
%p
- You can see your Chat accounts.
+ = _('You can see your chat accounts.')
.col-lg-8
%h5 Active chat names (#{@chat_names.size})
@@ -16,15 +16,15 @@
%table.table.chat-names
%thead
%tr
- %th Project
- %th Service
- %th Team domain
- %th Nickname
- %th Last used
+ %th= _('Project')
+ %th= _('Service')
+ %th= _('Team domain')
+ %th= _('Nickname')
+ %th= _('Last used')
%th
%tbody
= render @chat_names
- else
.settings-message.text-center
- You don't have any active chat names.
+ = _("You don't have any active chat names.")
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index e616e5546b3..fa35fbd3961 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -1,4 +1,4 @@
-- page_title "Notifications"
+- page_title _('Notifications')
- @content_class = "limit-container-width" unless fluid_layout
%div
@@ -14,12 +14,12 @@
%h4.prepend-top-0
= page_title
%p
- You can specify notification level per group or per project.
+ = _('You can specify notification level per group or per project.')
%p
- By default, all projects and groups will use the global notifications setting.
+ = _('By default, all projects and groups will use the global notifications setting.')
.col-lg-8
%h5.prepend-top-0
- Global notification settings
+ = _('Global notification settings')
= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f|
= render_if_exists 'profiles/notifications/email_settings', form: f
@@ -35,19 +35,19 @@
= form_for @user, url: profile_notifications_path, method: :put do |f|
%label{ for: 'user_notified_of_own_activity' }
= f.check_box :notified_of_own_activity
- %span Receive notifications about your own activity
+ %span= _('Receive notifications about your own activity')
%hr
%h5
- Groups (#{@group_notifications.count})
+ = _('Groups (%{count})') % { count: @group_notifications.count }
%div
%ul.bordered-list
- @group_notifications.each do |setting|
= render 'group_settings', setting: setting, group: setting.source
%h5
- Projects (#{@project_notifications.count})
+ = _('Projects (%{count})') % { count: @project_notifications.count }
%p.account-well
- To specify the notification level per project of a group you belong to, you need to visit project page and change notification level there.
+ = _('To specify the notification level per project of a group you belong to, you need to visit project page and change notification level there.')
.append-bottom-default
%ul.bordered-list
- @project_notifications.each do |setting|
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index bb46b440c18..2b0c3985755 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -4,7 +4,6 @@
- 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)
-- vue_file_list = Feature.enabled?(:vue_file_list, @project)
#tree-holder.tree-holder.clearfix
.nav-block
@@ -14,11 +13,11 @@
= render 'shared/commit_well', commit: commit, ref: ref, project: project
- if is_project_overview
- .project-buttons.append-bottom-default{ class: ("js-hide-on-navigation" if vue_file_list) }
+ .project-buttons.append-bottom-default{ class: ("js-keep-hidden-on-navigation" if vue_file_list_enabled?) }
= render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout)
- - if vue_file_list
- #js-tree-list{ data: { project_path: @project.full_path, full_name: @project.name_with_namespace, ref: ref } }
+ - if vue_file_list_enabled?
+ #js-tree-list{ data: { project_path: @project.full_path, project_short_path: @project.path, ref: ref, full_name: @project.name_with_namespace } }
- if @tree.readme
= render "projects/tree/readme", readme: @tree.readme
- else
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index a97322dace4..9f5241344a7 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,7 +1,7 @@
- empty_repo = @project.empty_repo?
- show_auto_devops_callout = show_auto_devops_callout?(@project)
- max_project_topic_length = 15
-.project-home-panel{ class: [("empty-project" if empty_repo), ("js-hide-on-navigation" if Feature.enabled?(:vue_file_list, @project))] }
+.project-home-panel{ class: [("empty-project" if empty_repo), ("js-keep-hidden-on-navigation" if vue_file_list_enabled?)] }
.row.append-bottom-8
.home-panel-title-row.col-md-12.col-lg-6.d-flex
.avatar-container.rect-avatar.s64.home-panel-avatar.append-right-default.float-none
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index f4560404c03..bdf7b933ab8 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -53,9 +53,10 @@
%span.badge.badge-info= _('manual')
- if pipeline_link
- %td
- = link_to pipeline_path(pipeline) do
+ %td.pipeline-link
+ = link_to pipeline_path(pipeline), class: 'has-tooltip', title: _('Pipeline ID (IID)') do
%span.pipeline-id ##{pipeline.id}
+ %span.pipeline-iid (##{pipeline.iid})
%span by
- if pipeline.user
= user_avatar(user: pipeline.user, size: 20)
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index a0db48bf8ff..ef2777e6601 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -81,7 +81,7 @@
= link_to project_pipeline_path(@project, last_pipeline.id), class: "ci-status-icon-#{last_pipeline.status}" do
= ci_icon_for_status(last_pipeline.status)
#{ _('Pipeline') }
- = link_to "##{last_pipeline.id}", project_pipeline_path(@project, last_pipeline.id)
+ = link_to "##{last_pipeline.id} (##{last_pipeline.iid})", project_pipeline_path(@project, last_pipeline.id), class: "has-tooltip", title: _('Pipeline ID (IID)')
= ci_label_for_status(last_pipeline.status)
- if last_pipeline.stages_count.nonzero?
#{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), last_pipeline.stages_count) }
diff --git a/app/views/projects/jobs/_table.html.haml b/app/views/projects/jobs/_table.html.haml
index d124d3ebfc1..b08223546f7 100644
--- a/app/views/projects/jobs/_table.html.haml
+++ b/app/views/projects/jobs/_table.html.haml
@@ -16,7 +16,7 @@
%th Runner
%th Stage
%th Name
- %th
+ %th Timing
%th Coverage
%th
diff --git a/app/views/projects/settings/operations/_external_dashboard.html.haml b/app/views/projects/settings/operations/_external_dashboard.html.haml
index 2fbb9195a04..f049c35b38d 100644
--- a/app/views/projects/settings/operations/_external_dashboard.html.haml
+++ b/app/views/projects/settings/operations/_external_dashboard.html.haml
@@ -1,2 +1,2 @@
-.js-operation-settings{ data: { external_dashboard: { path: '',
+.js-operation-settings{ data: { external_dashboard: { path: metrics_external_dashboard_url,
help_page_path: help_page_path('user/project/operations/link_to_external_dashboard') } } }
diff --git a/app/views/projects/settings/repository/_protected_branches.html.haml b/app/views/projects/settings/repository/_protected_branches.html.haml
new file mode 100644
index 00000000000..31630828571
--- /dev/null
+++ b/app/views/projects/settings/repository/_protected_branches.html.haml
@@ -0,0 +1,2 @@
+= render "projects/protected_branches/index"
+= render "projects/protected_tags/index"
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index cb3a035c49e..ff30cc4f6db 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -3,14 +3,17 @@
- @content_class = "limit-container-width" unless fluid_layout
= render "projects/default_branch/show"
+= render_if_exists "projects/push_rules/index"
= render "projects/mirrors/mirror_repos"
-# Protected branches & tags use a lot of nested partials.
-# The shared parts of the views can be found in the `shared` directory.
-# Those are used throughout the actual views. These `shared` views are then
-# reused in EE.
-= render "projects/protected_branches/index"
-= render "projects/protected_tags/index"
+= render "projects/settings/repository/protected_branches"
+
= render @deploy_keys
= render "projects/deploy_tokens/index"
= render "projects/cleanup/show"
+
+= render_if_exists 'shared/promotions/promote_repository_features'
diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml
index e935af23659..4f6c7e1f9a6 100644
--- a/app/views/projects/tree/_readme.html.haml
+++ b/app/views/projects/tree/_readme.html.haml
@@ -1,5 +1,5 @@
- if readme.rich_viewer
- %article.file-holder.readme-holder{ id: 'readme', class: [("limited-width-container" unless fluid_layout), ("js-hide-on-navigation" if Feature.enabled?(:vue_file_list, @project))] }
+ %article.file-holder.readme-holder{ id: 'readme', class: [("limited-width-container" unless fluid_layout), ("js-hide-on-navigation" if vue_file_list_enabled?)] }
.js-file-title.file-title
= blob_icon readme.mode, readme.name
= link_to project_blob_path(@project, tree_join(@ref, readme.path)) do
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index ec8e5234bd4..ea6349f2f57 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -6,71 +6,74 @@
= render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
- if on_top_of_branch?
- - addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown', 'data-boundary': 'window' }
+ - addtotree_toggle_attributes = { 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown', 'data-boundary': 'window' }
- else
- addtotree_toggle_attributes = { title: _("You can only add files when you are on a branch"), data: { container: 'body' }, class: 'disabled has-tooltip' }
- %ul.breadcrumb.repo-breadcrumb
- %li.breadcrumb-item
- = link_to project_tree_path(@project, @ref) do
- = @project.path
- - path_breadcrumbs do |title, path|
+ - if vue_file_list_enabled?
+ #js-repo-breadcrumb
+ - else
+ %ul.breadcrumb.repo-breadcrumb
%li.breadcrumb-item
- = link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path))
+ = link_to project_tree_path(@project, @ref) do
+ = @project.path
+ - path_breadcrumbs do |title, path|
+ %li.breadcrumb-item
+ = link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path))
- - if can_collaborate || can_create_mr_from_fork
- %li.breadcrumb-item
- %a.btn.add-to-tree.qa-add-to-tree{ addtotree_toggle_attributes }
- = sprite_icon('plus', size: 16, css_class: 'float-left')
- = sprite_icon('arrow-down', size: 16, css_class: 'float-left')
- - if on_top_of_branch?
- .add-to-tree-dropdown
- %ul.dropdown-menu
- - if can_edit_tree?
- %li.dropdown-header
- #{ _('This directory') }
- %li
- = link_to project_new_blob_path(@project, @id), class: 'qa-new-file-option' do
- #{ _('New file') }
- %li
- = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do
- #{ _('Upload file') }
- %li
- = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
- #{ _('New directory') }
- - elsif can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project)
- %li
- - continue_params = { to: project_new_blob_path(@project, @id),
- notice: edit_in_new_fork_notice,
- notice_now: edit_in_new_fork_notice_now }
- - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params)
- = link_to fork_path, method: :post do
- #{ _('New file') }
- %li
- - continue_params = { to: request.fullpath,
- notice: edit_in_new_fork_notice + " Try to upload a file again.",
- notice_now: edit_in_new_fork_notice_now }
- - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params)
- = link_to fork_path, method: :post do
- #{ _('Upload file') }
- %li
- - continue_params = { to: request.fullpath,
- notice: edit_in_new_fork_notice + " Try to create a new directory again.",
- notice_now: edit_in_new_fork_notice_now }
- - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params)
- = link_to fork_path, method: :post do
- #{ _('New directory') }
+ - if can_collaborate || can_create_mr_from_fork
+ %li.breadcrumb-item
+ %button.btn.add-to-tree.qa-add-to-tree{ addtotree_toggle_attributes, type: 'button' }
+ = sprite_icon('plus', size: 16, css_class: 'float-left')
+ = sprite_icon('arrow-down', size: 16, css_class: 'float-left')
+ - if on_top_of_branch?
+ .add-to-tree-dropdown
+ %ul.dropdown-menu
+ - if can_edit_tree?
+ %li.dropdown-header
+ #{ _('This directory') }
+ %li
+ = link_to project_new_blob_path(@project, @id), class: 'qa-new-file-option' do
+ #{ _('New file') }
+ %li
+ = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do
+ #{ _('Upload file') }
+ %li
+ = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
+ #{ _('New directory') }
+ - elsif can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project)
+ %li
+ - continue_params = { to: project_new_blob_path(@project, @id),
+ notice: edit_in_new_fork_notice,
+ notice_now: edit_in_new_fork_notice_now }
+ - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params)
+ = link_to fork_path, method: :post do
+ #{ _('New file') }
+ %li
+ - continue_params = { to: request.fullpath,
+ notice: edit_in_new_fork_notice + " Try to upload a file again.",
+ notice_now: edit_in_new_fork_notice_now }
+ - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params)
+ = link_to fork_path, method: :post do
+ #{ _('Upload file') }
+ %li
+ - continue_params = { to: request.fullpath,
+ notice: edit_in_new_fork_notice + " Try to create a new directory again.",
+ notice_now: edit_in_new_fork_notice_now }
+ - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params)
+ = link_to fork_path, method: :post do
+ #{ _('New directory') }
- - if can?(current_user, :push_code, @project)
- %li.divider
- %li.dropdown-header
- #{ _('This repository') }
- %li
- = link_to new_project_branch_path(@project) do
- #{ _('New branch') }
- %li
- = link_to new_project_tag_path(@project) do
- #{ _('New tag') }
+ - if can?(current_user, :push_code, @project)
+ %li.divider
+ %li.dropdown-header
+ #{ _('This repository') }
+ %li
+ = link_to new_project_branch_path(@project) do
+ #{ _('New branch') }
+ %li
+ = link_to new_project_tag_path(@project) do
+ #{ _('New tag') }
.tree-controls
= link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml
index 4af0c6bf84a..db0dcc8adfb 100644
--- a/app/views/search/_form.html.haml
+++ b/app/views/search/_form.html.haml
@@ -13,3 +13,4 @@
- unless params[:snippets].eql? 'true'
= render 'filter'
= button_tag _("Search"), class: "btn btn-success btn-search"
+ = render_if_exists 'search/form_elasticsearch'
diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml
index d173e3c0192..a0d3bc64f1f 100644
--- a/app/views/shared/issuable/_label_page_create.html.haml
+++ b/app/views/shared/issuable/_label_page_create.html.haml
@@ -9,9 +9,7 @@
.dropdown-labels-error.js-label-error
%input#new_label_name.default-dropdown-input{ type: "text", placeholder: _('Name new label') }
.suggest-colors.suggest-colors-dropdown
- - suggested_colors.each do |color|
- = link_to '#', style: "background-color: #{color}", data: { color: color } do
- &nbsp
+ = render_suggested_colors
.dropdown-label-color-input
.dropdown-label-color-preview.js-dropdown-label-color-preview
%input#new_label_color.default-dropdown-input{ type: "text", placeholder: _('Assign custom color like #FF0000') }
diff --git a/app/views/shared/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml
index 4b88aff3313..78ff225daad 100644
--- a/app/views/shared/labels/_form.html.haml
+++ b/app/views/shared/labels/_form.html.haml
@@ -25,12 +25,7 @@
Choose any color.
%br
Or you can choose one of the suggested colors below
-
- .suggest-colors
- - suggested_colors.each do |color|
- = link_to '#', style: "background-color: #{color}", data: { color: color } do
- &nbsp;
-
+ = render_suggested_colors
.form-actions
- if @label.persisted?
= f.submit 'Save changes', class: 'btn btn-success js-save-button'
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index 2ece7b7f701..31cc0c091dd 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -15,7 +15,7 @@
= 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", 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" } }
+ %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: "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/views/shared/notifications/_new_button.html.haml b/app/views/shared/notifications/_new_button.html.haml
index af8ab992f0e..052e6da5bae 100644
--- a/app/views/shared/notifications/_new_button.html.haml
+++ b/app/views/shared/notifications/_new_button.html.haml
@@ -16,7 +16,7 @@
= sprite_icon("arrow-down", css_class: "icon mr-0")
.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", placement: 'top', 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 - %{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", placement: 'top', toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } }
= notification_setting_icon(notification_setting)
%span.js-notification-loading.fa.hidden
= sprite_icon("arrow-down", css_class: "icon")
diff --git a/app/views/shared/notifications/_notification_dropdown.html.haml b/app/views/shared/notifications/_notification_dropdown.html.haml
index 85ad74f9a39..a6ef2d51171 100644
--- a/app/views/shared/notifications/_notification_dropdown.html.haml
+++ b/app/views/shared/notifications/_notification_dropdown.html.haml
@@ -8,5 +8,5 @@
%li.divider
%li
%a.update-notification{ href: "#", role: "button", class: ("is-active" if notification_setting.custom?), data: { toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), notification_level: "custom", notification_title: "Custom" } }
- %strong.dropdown-menu-inner-title Custom
+ %strong.dropdown-menu-inner-title= s_('NotificationSetting|Custom')
%span.dropdown-menu-inner-content= notification_description("custom")
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 9a9c0c9d803..3f1639ec2ed 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -74,6 +74,8 @@ class PostReceive
def process_wiki_changes(post_received)
post_received.project.touch(:last_activity_at, :last_repository_updated_at)
+ post_received.project.wiki.repository.expire_statistics_caches
+ ProjectCacheWorker.perform_async(post_received.project.id, [], [:wiki_size])
end
def log(message)
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index b2e0701008a..4e8ea903139 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -16,10 +16,12 @@ class ProjectCacheWorker
def perform(project_id, files = [], statistics = [])
project = Project.find_by(id: project_id)
- return unless project && project.repository.exists?
+ return unless project
update_statistics(project, statistics)
+ return unless project.repository.exists?
+
project.repository.refresh_method_caches(files.map(&:to_sym))
project.cleanup
diff --git a/changelogs/unreleased/11105-fix-cs-with-proxy.yml b/changelogs/unreleased/11105-fix-cs-with-proxy.yml
new file mode 100644
index 00000000000..ee32427d20e
--- /dev/null
+++ b/changelogs/unreleased/11105-fix-cs-with-proxy.yml
@@ -0,0 +1,5 @@
+---
+title: Fix proxy support in Container Scanning
+merge_request: 27246
+author:
+type: fixed
diff --git a/changelogs/unreleased/45687-web-ide-empty-state.yml b/changelogs/unreleased/45687-web-ide-empty-state.yml
new file mode 100644
index 00000000000..9ef148275ab
--- /dev/null
+++ b/changelogs/unreleased/45687-web-ide-empty-state.yml
@@ -0,0 +1,5 @@
+---
+title: Empty project state for Web IDE
+merge_request: 26556
+author:
+type: added
diff --git a/changelogs/unreleased/57414-show-pipeline-iid.yml b/changelogs/unreleased/57414-show-pipeline-iid.yml
new file mode 100644
index 00000000000..596ae00e5a3
--- /dev/null
+++ b/changelogs/unreleased/57414-show-pipeline-iid.yml
@@ -0,0 +1,5 @@
+---
+title: Show Pipeline IID everywhere Pipeline ID is shown
+merge_request: 57414
+author: Mike Scott
+type: added
diff --git a/changelogs/unreleased/61024-update-resolved-icon.yml b/changelogs/unreleased/61024-update-resolved-icon.yml
new file mode 100644
index 00000000000..4a4de9eb13a
--- /dev/null
+++ b/changelogs/unreleased/61024-update-resolved-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Add check circle filled icon for resolved comments
+merge_request: 28663
+author:
+type: changed
diff --git a/changelogs/unreleased/6104-ee-ce-difference.yml b/changelogs/unreleased/6104-ee-ce-difference.yml
new file mode 100644
index 00000000000..59d31daf0eb
--- /dev/null
+++ b/changelogs/unreleased/6104-ee-ce-difference.yml
@@ -0,0 +1,5 @@
+---
+title: Unified EE/CS differences in repository/show.html
+merge_request: 13562
+author:
+type: other
diff --git a/changelogs/unreleased/61639-flaky-spec-issue-boards-labels-creates-project-label-spec-features-boards-sidebar_spec-rb-350.yml b/changelogs/unreleased/61639-flaky-spec-issue-boards-labels-creates-project-label-spec-features-boards-sidebar_spec-rb-350.yml
new file mode 100644
index 00000000000..9b4f13353f5
--- /dev/null
+++ b/changelogs/unreleased/61639-flaky-spec-issue-boards-labels-creates-project-label-spec-features-boards-sidebar_spec-rb-350.yml
@@ -0,0 +1,5 @@
+---
+title: Fix dropdown position when loading remote data
+merge_request: 28526
+author:
+type: fixed
diff --git a/changelogs/unreleased/61788-predefined-colours-dont-have-descriptive-labels.yml b/changelogs/unreleased/61788-predefined-colours-dont-have-descriptive-labels.yml
new file mode 100644
index 00000000000..25c83d24007
--- /dev/null
+++ b/changelogs/unreleased/61788-predefined-colours-dont-have-descriptive-labels.yml
@@ -0,0 +1,5 @@
+---
+title: Adds a text label to color pickers to improve accessibility.
+merge_request: 28343
+author: Chris Toynbee
+type: changed
diff --git a/changelogs/unreleased/62227-webkit-icon-overlap.yml b/changelogs/unreleased/62227-webkit-icon-overlap.yml
new file mode 100644
index 00000000000..47d7583f4c2
--- /dev/null
+++ b/changelogs/unreleased/62227-webkit-icon-overlap.yml
@@ -0,0 +1,5 @@
+---
+title: Add style to disable webkit icons for search inputs
+merge_request: 28833
+author: Jarek Ostrowski @jareko
+type: fixed
diff --git a/changelogs/unreleased/62487-external-policy-desc.yml b/changelogs/unreleased/62487-external-policy-desc.yml
new file mode 100644
index 00000000000..2e787b89db1
--- /dev/null
+++ b/changelogs/unreleased/62487-external-policy-desc.yml
@@ -0,0 +1,5 @@
+---
+title: Move text under p tag
+merge_request: 28901
+author:
+type: fixed
diff --git a/changelogs/unreleased/add-wiki-size-to-statistics.yml b/changelogs/unreleased/add-wiki-size-to-statistics.yml
new file mode 100644
index 00000000000..85b6d7a1774
--- /dev/null
+++ b/changelogs/unreleased/add-wiki-size-to-statistics.yml
@@ -0,0 +1,5 @@
+---
+title: Add wiki size to project statistics
+merge_request: 25321
+author: Peter Marko
+type: added
diff --git a/changelogs/unreleased/auto-devops-kubernestes-bump1-11-10.yml b/changelogs/unreleased/auto-devops-kubernestes-bump1-11-10.yml
new file mode 100644
index 00000000000..9ba55719bdf
--- /dev/null
+++ b/changelogs/unreleased/auto-devops-kubernestes-bump1-11-10.yml
@@ -0,0 +1,5 @@
+---
+title: Bumps Kubernetes in Auto DevOps to 1.11.10
+merge_request: 28525
+author:
+type: other
diff --git a/changelogs/unreleased/ee-11040-added-conditional-rendering.yml b/changelogs/unreleased/ee-11040-added-conditional-rendering.yml
new file mode 100644
index 00000000000..7b06e43830f
--- /dev/null
+++ b/changelogs/unreleased/ee-11040-added-conditional-rendering.yml
@@ -0,0 +1,5 @@
+---
+title: "Added conditional rendering to `app/views/search/_form.html.haml` for CE/EE code base consistency"
+merge_request: 28883
+author: Michel Engelen
+type: other
diff --git a/changelogs/unreleased/i18n-active_sessions-in-user-profile.yml b/changelogs/unreleased/i18n-active_sessions-in-user-profile.yml
new file mode 100644
index 00000000000..fe6eb3a2bf7
--- /dev/null
+++ b/changelogs/unreleased/i18n-active_sessions-in-user-profile.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings of active sessions page in user profile
+merge_request: 28590
+author: antony liu
+type: other
diff --git a/changelogs/unreleased/i18n-chat-of-user-profile.yml b/changelogs/unreleased/i18n-chat-of-user-profile.yml
new file mode 100644
index 00000000000..663b4ffc1a1
--- /dev/null
+++ b/changelogs/unreleased/i18n-chat-of-user-profile.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings of chat page in user profile
+merge_request: 28632
+author:
+type: other
diff --git a/changelogs/unreleased/pipelines-email-default-branch-filter.yml b/changelogs/unreleased/pipelines-email-default-branch-filter.yml
new file mode 100644
index 00000000000..4c2a54af0bf
--- /dev/null
+++ b/changelogs/unreleased/pipelines-email-default-branch-filter.yml
@@ -0,0 +1,5 @@
+---
+title: Add notify_only_default_branch option to PipelinesEmailService
+merge_request: 28271
+author: Peter Marko
+type: added
diff --git a/changelogs/unreleased/referenced-labels.yml b/changelogs/unreleased/referenced-labels.yml
new file mode 100644
index 00000000000..c39ef4c2478
--- /dev/null
+++ b/changelogs/unreleased/referenced-labels.yml
@@ -0,0 +1,5 @@
+---
+title: Add referenced-commands in no overflow list
+merge_request: 28858
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-add-header-to-jobs-admin-page.yml b/changelogs/unreleased/sh-add-header-to-jobs-admin-page.yml
new file mode 100644
index 00000000000..b089e6e4f37
--- /dev/null
+++ b/changelogs/unreleased/sh-add-header-to-jobs-admin-page.yml
@@ -0,0 +1,5 @@
+---
+title: Add a column header to admin/jobs page
+merge_request: 28837
+author:
+type: other
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 23377b43f78..c83f569d885 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -752,6 +752,8 @@ production: &base
monitoring:
# Time between sampling of unicorn socket metrics, in seconds
# unicorn_sampler_interval: 10
+ # Time between sampling of Puma metrics, in seconds
+ # puma_sampler_interval: 5
# IP whitelist to access monitoring endpoints
ip_whitelist:
- 127.0.0.0/8
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index d56bd7654af..0c8d94ccaed 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -491,6 +491,7 @@ Settings.webpack.dev_server['port'] ||= 3808
Settings['monitoring'] ||= Settingslogic.new({})
Settings.monitoring['ip_whitelist'] ||= ['127.0.0.1/8']
Settings.monitoring['unicorn_sampler_interval'] ||= 10
+Settings.monitoring['puma_sampler_interval'] ||= 5
Settings.monitoring['ruby_sampler_interval'] ||= 60
Settings.monitoring['sidekiq_exporter'] ||= Settingslogic.new({})
Settings.monitoring.sidekiq_exporter['enabled'] ||= false
diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb
index 8052880cc3d..68f8487d377 100644
--- a/config/initializers/7_prometheus_metrics.rb
+++ b/config/initializers/7_prometheus_metrics.rb
@@ -29,12 +29,18 @@ if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled?
Gitlab::Cluster::LifecycleEvents.on_worker_start do
defined?(::Prometheus::Client.reinitialize_on_pid_change) && Prometheus::Client.reinitialize_on_pid_change
- unless Sidekiq.server?
+ if defined?(::Unicorn)
Gitlab::Metrics::Samplers::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start
end
Gitlab::Metrics::Samplers::RubySampler.initialize_instance(Settings.monitoring.ruby_sampler_interval).start
end
+
+ if defined?(::Puma)
+ Gitlab::Cluster::LifecycleEvents.on_master_start do
+ Gitlab::Metrics::Samplers::PumaSampler.initialize_instance(Settings.monitoring.puma_sampler_interval).start
+ end
+ end
end
Gitlab::Cluster::LifecycleEvents.on_master_restart do
diff --git a/config/initializers/rack_timeout.rb b/config/initializers/rack_timeout.rb
new file mode 100644
index 00000000000..5c4f2dd708c
--- /dev/null
+++ b/config/initializers/rack_timeout.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+# Unicorn terminates any request which runs longer than 60 seconds.
+# Puma doesn't have any timeout mechanism for terminating long-running
+# requests, to make sure that server is not paralyzed by long-running
+# or stuck queries, we add a request timeout which terminates the
+# request after 60 seconds. This may be dangerous in some situations
+# (https://github.com/heroku/rack-timeout/blob/master/doc/exceptions.md)
+# and it's used only as the last resort. In such case this termination is
+# logged and we should fix the potential timeout issue in the code itself.
+
+if defined?(::Puma) && !Rails.env.test?
+ require 'rack/timeout/base'
+
+ Gitlab::Application.configure do |config|
+ config.middleware.insert_before(Rack::Runtime, Rack::Timeout,
+ service_timeout: 60,
+ wait_timeout: 90)
+ end
+
+ observer = Gitlab::RackTimeoutObserver.new
+ Rack::Timeout.register_state_change_observer(:gitlab_rack_timeout, &observer.callback)
+end
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index bc19219a0b8..ae79beb1dba 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -83,7 +83,7 @@ namespace :admin do
resources(:projects,
path: '/',
constraints: { id: Gitlab::PathRegex.project_route_regex },
- only: [:show]) do
+ only: [:show, :destroy]) do
member do
put :transfer
diff --git a/config/webpack.config.review_toolbar.js b/config/webpack.config.review_toolbar.js
new file mode 100644
index 00000000000..baaba7ed387
--- /dev/null
+++ b/config/webpack.config.review_toolbar.js
@@ -0,0 +1,58 @@
+const path = require('path');
+const CompressionPlugin = require('compression-webpack-plugin');
+
+const ROOT_PATH = path.resolve(__dirname, '..');
+const CACHE_PATH = process.env.WEBPACK_CACHE_PATH || path.join(ROOT_PATH, 'tmp/cache');
+const NO_SOURCEMAPS = process.env.NO_SOURCEMAPS;
+const IS_PRODUCTION = process.env.NODE_ENV === 'production';
+
+const devtool = IS_PRODUCTION ? 'source-map' : 'cheap-module-eval-source-map';
+
+const alias = {
+ vendor: path.join(ROOT_PATH, 'vendor/assets/javascripts'),
+ spec: path.join(ROOT_PATH, 'spec/javascripts'),
+};
+
+module.exports = {
+ mode: IS_PRODUCTION ? 'production' : 'development',
+
+ context: path.join(ROOT_PATH, 'app/assets/javascripts'),
+
+ name: 'visual_review_toolbar',
+
+ entry: './visual_review_toolbar',
+
+ output: {
+ path: path.join(ROOT_PATH, 'public/assets/webpack'),
+ filename: 'visual_review_toolbar.js',
+ library: 'VisualReviewToolbar',
+ libraryTarget: 'var',
+ },
+
+ resolve: {
+ alias,
+ },
+
+ module: {
+ rules: [
+ {
+ test: /\.js$/,
+ loader: 'babel-loader',
+ options: {
+ cacheDirectory: path.join(CACHE_PATH, 'babel-loader'),
+ },
+ },
+ {
+ test: /\.css$/,
+ use: ['style-loader', 'css-loader'],
+ },
+ ],
+ },
+
+ plugins: [
+ // compression can require a lot of compute time and is disabled in CI
+ new CompressionPlugin(),
+ ].filter(Boolean),
+
+ devtool: NO_SOURCEMAPS ? false : devtool,
+};
diff --git a/db/migrate/20190527194830_add_wiki_size_to_statistics.rb b/db/migrate/20190527194830_add_wiki_size_to_statistics.rb
new file mode 100644
index 00000000000..d4f16cdec18
--- /dev/null
+++ b/db/migrate/20190527194830_add_wiki_size_to_statistics.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddWikiSizeToStatistics < ActiveRecord::Migration[5.0]
+ DOWNTIME = false
+
+ def change
+ add_column :project_statistics, :wiki_size, :bigint
+ end
+end
diff --git a/db/post_migrate/20190527194900_schedule_calculate_wiki_sizes.rb b/db/post_migrate/20190527194900_schedule_calculate_wiki_sizes.rb
new file mode 100644
index 00000000000..04cf5906b61
--- /dev/null
+++ b/db/post_migrate/20190527194900_schedule_calculate_wiki_sizes.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class ScheduleCalculateWikiSizes < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ MIGRATION = 'CalculateWikiSizes'
+ BATCH_SIZE = 100000
+ BATCH_TIME = 5.minutes
+
+ class ProjectStatistics < ActiveRecord::Base
+ self.table_name = 'project_statistics'
+
+ scope :without_wiki_size, -> { where(wiki_size: nil) }
+
+ include ::EachBatch
+ end
+
+ disable_ddl_transaction!
+
+ def up
+ queue_background_migration_jobs_by_range_at_intervals(
+ ::ScheduleCalculateWikiSizes::ProjectStatistics.without_wiki_size,
+ MIGRATION,
+ BATCH_TIME,
+ batch_size: BATCH_SIZE)
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index bb59af540fe..89140048ad3 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20190524062810) do
+ActiveRecord::Schema.define(version: 20190527194900) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -1745,6 +1745,7 @@ ActiveRecord::Schema.define(version: 20190524062810) do
t.bigint "lfs_objects_size", default: 0, null: false
t.bigint "build_artifacts_size", default: 0, null: false
t.bigint "packages_size"
+ t.bigint "wiki_size"
t.index ["namespace_id"], name: "index_project_statistics_on_namespace_id", using: :btree
t.index ["project_id"], name: "index_project_statistics_on_project_id", unique: true, using: :btree
end
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index caadec3ac4e..d1233d815ed 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -39,13 +39,8 @@ options:
### Improving NFS performance with GitLab
-NOTE: **Note:**
-This is only available starting in certain versions of GitLab:
- * 11.5.11
- * 11.6.11
- * 11.7.12
- * 11.8.8
- * 11.9.0 and up (e.g. 11.10, 11.11, etc.)
+NOTE: **Note:** This is only available starting in certain versions of GitLab: 11.5.11,
+11.6.11, 11.7.12, 11.8.8, 11.9.0 and up (e.g. 11.10, 11.11, etc.)
If you are using NFS to share Git data, we recommend that you enable a
number of feature flags that will allow GitLab application processes to
@@ -107,6 +102,11 @@ stored on a local volume.
For more details on another person's experience with EFS, see
[Amazon's Elastic File System: Burst Credits](https://rawkode.com/2017/04/16/amazons-elastic-file-system-burst-credits/)
+## Avoid using CephFS and GlusterFS
+
+GitLab strongly recommends against using CephFS and GlusterFS.
+These distributed file systems are not well-suited for GitLab's input/output access patterns because git uses many small files and access times and file locking times to propagate will make git activity very slow.
+
## Avoid using PostgreSQL with NFS
GitLab strongly recommends against running your PostgreSQL database
diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md
index b7b820abb40..82e0c14ffc2 100644
--- a/doc/administration/integration/plantuml.md
+++ b/doc/administration/integration/plantuml.md
@@ -27,7 +27,7 @@ own PlantUML server is easy in Debian/Ubuntu distributions using Tomcat.
First you need to create a `plantuml.war` file from the source code:
```
-sudo apt-get install graphviz openjdk-7-jdk git-core maven
+sudo apt-get install graphviz openjdk-8-jdk git-core maven
git clone https://github.com/plantuml/plantuml-server.git
cd plantuml-server
mvn package
diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md
index e7792106f81..ef370573a98 100644
--- a/doc/administration/job_artifacts.md
+++ b/doc/administration/job_artifacts.md
@@ -100,6 +100,9 @@ artifacts, you can use an object storage like AWS S3 instead.
This configuration relies on valid AWS credentials to be configured already.
Use an object storage option like AWS S3 to store job artifacts.
+DANGER: **Danger:**
+If you're enabling S3 in [GitLab HA](high_availability/README.md), you will need to have an [NFS mount set up for CI traces and artifacts](high_availability/nfs.md#a-single-nfs-mount) or enable [live tracing](job_traces.md#new-live-trace-architecture). If these settings are not set, you will risk job traces disappearing or not being saved.
+
### Object Storage Settings
For source installations the following settings are nested under `artifacts:` and then `object_store:`. On omnibus installs they are prefixed by `artifacts_object_store_`.
diff --git a/doc/administration/monitoring/performance/grafana_configuration.md b/doc/administration/monitoring/performance/grafana_configuration.md
index ab43ec2cc4f..187fb2f73a1 100644
--- a/doc/administration/monitoring/performance/grafana_configuration.md
+++ b/doc/administration/monitoring/performance/grafana_configuration.md
@@ -3,7 +3,7 @@
[Grafana](http://grafana.org/) is a tool that allows you to visualize time
series metrics through graphs and dashboards. It supports several backend
data stores, including InfluxDB. GitLab writes performance data to InfluxDB
-and Grafana will allow you to query InfluxDB to display useful graphs.
+and Grafana will allow you to query to display useful graphs.
For the easiest installation and configuration, install Grafana on the same
server as InfluxDB. For larger installations, you may want to split out these
@@ -11,11 +11,13 @@ services.
## Installation
-Grafana supplies package repositories (Yum/Apt) for easy installation.
+[GitLab Omnibus can help you install Grafana (recommended)](https://docs.gitlab.com/omnibus/settings/grafana.html)
+or Grafana supplies package repositories (Yum/Apt) for easy installation.
See [Grafana installation documentation](http://docs.grafana.org/installation/)
for detailed steps.
-> **Note**: Before starting Grafana for the first time, set the admin user
+NOTE: **Note:**
+Before starting Grafana for the first time, set the admin user
and password in `/etc/grafana/grafana.ini`. Otherwise, the default password
will be `admin`.
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index c243dd9edbb..4529bd3bee3 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -103,6 +103,24 @@ Some basic Ruby runtime metrics are available:
[GC.stat]: https://ruby-doc.org/core-2.3.0/GC.html#method-c-stat
+## Puma Metrics **[EXPERIMENTAL]**
+
+When Puma is used instead of Unicorn, following metrics are available:
+
+| Metric | Type | Since | Description |
+|:-------------------------------------------- |:------- |:----- |:----------- |
+| puma_workers | Gauge | 12.0 | Total number of workers |
+| puma_running_workers | Gauge | 12.0 | Number of booted workers |
+| puma_stale_workers | Gauge | 12.0 | Number of old workers |
+| puma_phase | Gauge | 12.0 | Phase number (increased during phased restarts) |
+| puma_running | Gauge | 12.0 | Number of running threads |
+| puma_queued_connections | Gauge | 12.0 | Number of connections in that worker's "todo" set waiting for a worker thread |
+| puma_active_connections | Gauge | 12.0 | Number of threads processing a request |
+| puma_pool_capacity | Gauge | 12.0 | Number of requests the worker is capable of taking right now |
+| puma_max_threads | Gauge | 12.0 | Maximum number of worker threads |
+| puma_idle_threads | Gauge | 12.0 | Number of spawned threads which are not processing a request |
+| rack_state_total | Gauge | 12.0 | Number of requests in a given rack state |
+
## Metrics shared directory
GitLab's Prometheus client requires a directory to store metrics data shared between multi-process services.
diff --git a/doc/api/boards.md b/doc/api/boards.md
index 28c73db6b98..a96206f5df3 100644
--- a/doc/api/boards.md
+++ b/doc/api/boards.md
@@ -141,6 +141,173 @@ Example response:
}
```
+## Create a board **[STARTER]**
+
+Creates a board.
+
+```
+POST /projects/:id/boards
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `name` | string | yes | The name of the new board |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/boards?name=newboard
+```
+
+Example response:
+
+```json
+ {
+ "id": 1,
+ "project": {
+ "id": 5,
+ "name": "Diaspora Project Site",
+ "name_with_namespace": "Diaspora / Diaspora Project Site",
+ "path": "diaspora-project-site",
+ "path_with_namespace": "diaspora/diaspora-project-site",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
+ "web_url": "http://example.com/diaspora/diaspora-project-site"
+ },
+ "name": "newboard",
+ "milestone": {
+ "id": 12
+ "title": "10.0"
+ },
+ "lists" : [
+ {
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+ },
+ {
+ "id" : 2,
+ "label" : {
+ "name" : "Ready",
+ "color" : "#FF0000",
+ "description" : null
+ },
+ "position" : 2
+ },
+ {
+ "id" : 3,
+ "label" : {
+ "name" : "Production",
+ "color" : "#FF5F00",
+ "description" : null
+ },
+ "position" : 3
+ }
+ ]
+ }
+```
+
+## Update a board **[STARTER]**
+
+> [Introduced][ee-5954] in [GitLab Starter](https://about.gitlab.com/pricing/) 11.1.
+
+Updates a board.
+
+```
+PUT /projects/:id/boards/:board_id
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `board_id` | integer | yes | The ID of a board |
+| `name` | string | no | The new name of the board |
+| `assignee_id` | integer | no | The assignee the board should be scoped to |
+| `milestone_id` | integer | no | The milestone the board should be scoped to |
+| `labels` | string | no | Comma-separated list of label names which the board should be scoped to |
+| `weight` | integer | no | The weight range from 0 to 9, to which the board should be scoped to |
+
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/boards/1?name=new_name&milestone_id=43&assignee_id=1&labels=Doing&weight=4
+```
+
+Example response:
+
+```json
+ {
+ "id": 1,
+ "project": {
+ "id": 5,
+ "name": "Diaspora Project Site",
+ "name_with_namespace": "Diaspora / Diaspora Project Site",
+ "path": "diaspora-project-site",
+ "path_with_namespace": "diaspora/diaspora-project-site",
+ "created_at": "2018-07-03T05:48:49.982Z",
+ "default_branch": null,
+ "tag_list": [],
+ "ssh_url_to_repo": "ssh://user@example.com/diaspora/diaspora-project-site.git",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
+ "web_url": "http://example.com/diaspora/diaspora-project-site",
+ "readme_url": null,
+ "avatar_url": null,
+ "star_count": 0,
+ "forks_count": 0,
+ "last_activity_at": "2018-07-03T05:48:49.982Z"
+ },
+ "lists": [],
+ "name": "new_name",
+ "group": null,
+ "milestone": {
+ "id": 43,
+ "iid": 1,
+ "project_id": 15,
+ "title": "Milestone 1",
+ "description": "Milestone 1 desc",
+ "state": "active",
+ "created_at": "2018-07-03T06:36:42.618Z",
+ "updated_at": "2018-07-03T06:36:42.618Z",
+ "due_date": null,
+ "start_date": null,
+ "web_url": "http://example.com/root/board1/milestones/1"
+ },
+ "assignee": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://example.com/root"
+ },
+ "labels": [{
+ "id": 10,
+ "name": "Doing",
+ "color": "#5CB85C",
+ "description": null
+ }],
+ "weight": 4
+ }
+```
+
+## Delete a board **[STARTER]**
+
+Deletes a board.
+
+```
+DELETE /projects/:id/boards/:board_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `board_id` | integer | yes | The ID of a board |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/boards/1
+```
+
## List board lists
Get a list of the board's lists.
@@ -237,7 +404,15 @@ POST /projects/:id/boards/:board_id/lists
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
-| `label_id` | integer | yes | The ID of a label |
+| `label_id` | integer | no | The ID of a label |
+| `assignee_id` **[PREMIUM]** | integer | no | The ID of a user |
+| `milestone_id` **[PREMIUM]** | integer | no | The ID of a milestone |
+
+NOTE: **Note**:
+Label, assignee and milestone arguments are mutually exclusive,
+that is, only one of them are accepted in a request.
+Check the [Issue Board docs](../user/project/issue_board.md#summary-of-features-per-tier)
+for more information regarding the required license for each list type.
```bash
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/boards/1/lists?label_id=5
@@ -307,3 +482,5 @@ DELETE /projects/:id/boards/:board_id/lists/:list_id
```bash
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1
```
+
+[ee-5954]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/5954
diff --git a/doc/api/discussions.md b/doc/api/discussions.md
index 07a6201b10b..9defef4fd53 100644
--- a/doc/api/discussions.md
+++ b/doc/api/discussions.md
@@ -1,6 +1,12 @@
# Discussions API
-Discussions are set of related notes on snippets, issues, merge requests or commits.
+Discussions are a set of related notes on:
+
+- Snippets
+- Issues
+- Epics **[ULTIMATE]**
+- Merge requests
+- Commits
This includes system notes, which are notes about changes to the object (for example, when a milestone changes, there will be a corresponding system note). Label notes are not part of this API, but recorded as separate events in [resource label events](resource_label_events.md).
@@ -424,6 +430,214 @@ Parameters:
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippets/11/discussions/636
```
+## Epics **[ULTIMATE]**
+
+### List group epic discussions
+
+Gets a list of all discussions for a single epic.
+
+```
+GET /groups/:id/epics/:epic_id/discussions
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ------------ |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+
+```json
+[
+ {
+ "id": "6a9c1750b37d513a43987b574953fceb50b03ce7",
+ "individual_note": false,
+ "notes": [
+ {
+ "id": 1126,
+ "type": "DiscussionNote",
+ "body": "discussion text",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-03T21:54:39.668Z",
+ "updated_at": "2018-03-03T21:54:39.668Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Epic",
+ "noteable_id": null,
+ "resolvable": false
+ },
+ {
+ "id": 1129,
+ "type": "DiscussionNote",
+ "body": "reply to the discussion",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-04T13:38:02.127Z",
+ "updated_at": "2018-03-04T13:38:02.127Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Epic",
+ "noteable_id": null,
+ "resolvable": false
+ }
+ ]
+ },
+ {
+ "id": "87805b7c09016a7058e91bdbe7b29d1f284a39e6",
+ "individual_note": true,
+ "notes": [
+ {
+ "id": 1128,
+ "type": null,
+ "body": "a single comment",
+ "attachment": null,
+ "author": {
+ "id": 1,
+ "name": "root",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/00afb8fb6ab07c3ee3e9c1f38777e2f4?s=80&d=identicon",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2018-03-04T09:17:22.520Z",
+ "updated_at": "2018-03-04T09:17:22.520Z",
+ "system": false,
+ "noteable_id": 3,
+ "noteable_type": "Epic",
+ "noteable_id": null,
+ "resolvable": false
+ }
+ ]
+ }
+]
+```
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/discussions
+```
+
+### Get single epic discussion
+
+Returns a single discussion for a specific group epic
+
+```
+GET /groups/:id/epics/:epic_id/discussions/:discussion_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `discussion_id` | integer | yes | The ID of a discussion |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7
+```
+
+### Create new epic discussion
+
+Creates a new discussion to a single group epic. This is similar to creating
+a note but but another comments (replies) can be added to it later.
+
+```
+POST /groups/:id/epics/:epic_id/discussions
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `body` | string | yes | The content of a discussion |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights) |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/discussions?body=comment
+```
+
+### Add note to existing epic discussion
+
+Adds a new note to the discussion. This can also
+[create a discussion from a single comment](../user/discussions/#start-a-discussion-by-replying-to-a-standard-comment).
+
+```
+POST /groups/:id/epics/:epic_id/discussions/:discussion_id/notes
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+| `body` | string | yes | The content of a discussion |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z (requires admin or project/group owner rights) |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes?body=comment
+```
+
+### Modify existing epic discussion note
+
+Modify existing discussion note of an epic.
+
+```
+PUT /groups/:id/epics/:epic_id/discussions/:discussion_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+| `body` | string | yes | The content of a discussion |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/6a9c1750b37d513a43987b574953fceb50b03ce7/notes/1108?body=comment
+```
+
+### Delete an epic discussion note
+
+Deletes an existing discussion note of an epic.
+
+```
+DELETE /groups/:id/epics/:epic_id/discussions/:discussion_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `discussion_id` | integer | yes | The ID of a discussion |
+| `note_id` | integer | yes | The ID of a discussion note |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/discussions/636
+```
+
## Merge requests
### List project merge request discussions
diff --git a/doc/api/epic_issues.md b/doc/api/epic_issues.md
index 438a3361dcc..ec59ea7068e 100644
--- a/doc/api/epic_issues.md
+++ b/doc/api/epic_issues.md
@@ -7,6 +7,7 @@ If a user is not a member of a group and the group is private, a `GET` request o
Epics are available only in Ultimate. If epics feature is not available a `403` status code will be returned.
## List issues for an epic
+
Gets all issues that are assigned to an epic and the authenticated user has access to.
```
diff --git a/doc/api/epic_links.md b/doc/api/epic_links.md
index d6e43ae7074..9ad90a6d0f1 100644
--- a/doc/api/epic_links.md
+++ b/doc/api/epic_links.md
@@ -12,6 +12,7 @@ If a user is not a member of a group and the group is private, a `GET` request o
Epics are available only in the [Ultimate/Gold tier](https://about.gitlab.com/pricing/). If the epics feature is not available, a `403` status code will be returned.
## List epics related to a given epic
+
Gets all child epics of an epic.
```
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 907b443d355..20789a1d4a4 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -68,6 +68,7 @@ GET /groups?statistics=true
"statistics": {
"storage_size" : 212,
"repository_size" : 33,
+ "wiki_size" : 100,
"lfs_objects_size" : 123,
"job_artifacts_size" : 57
diff --git a/doc/api/notes.md b/doc/api/notes.md
index dfde80c6441..c09129c22d4 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -1,6 +1,11 @@
# Notes API
-Notes are comments on snippets, issues or merge requests.
+Notes are comments on:
+
+- Snippets
+- Issues
+- Merge requests
+- Epics **[ULTIMATE]**
This includes system notes, which are notes about changes to the object (for example, when a milestone changes, there will be a corresponding system note). Label notes are not part of this API, but recorded as separate events in [resource label events](resource_label_events.md).
@@ -390,3 +395,126 @@ Parameters:
```bash
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/merge_requests/7/notes/1602
```
+
+## Epics **[ULTIMATE]**
+
+### List all epic notes
+
+Gets a list of all notes for a single epic. Epic notes are comments users can post to an epic.
+
+```
+GET /groups/:id/epics/:epic_id/notes
+GET /groups/:id/epics/:epic_id/notes?sort=asc&order_by=updated_at
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of a group epic |
+| `sort` | string | no | Return epic notes sorted in `asc` or `desc` order. Default is `desc` |
+| `order_by` | string | no | Return epic notes ordered by `created_at` or `updated_at` fields. Default is `created_at` |
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/notes
+```
+
+### Get single epic note
+
+Returns a single note for a given epic.
+
+```
+GET /groups/:id/epics/:epic_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `note_id` | integer | yes | The ID of a note |
+
+```json
+{
+ "id": 52,
+ "title": "Epic",
+ "file_name": "epic.rb",
+ "author": {
+ "id": 1,
+ "username": "pipin",
+ "email": "admin@example.com",
+ "name": "Pip",
+ "state": "active",
+ "created_at": "2013-09-30T13:46:01Z"
+ },
+ "expires_at": null,
+ "updated_at": "2013-10-02T07:34:20Z",
+ "created_at": "2013-10-02T07:34:20Z"
+}
+```
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/11/notes/1
+```
+
+### Create new epic note
+
+Creates a new note for a single epic. Epic notes are comments users can post to an epic.
+If you create a note where the body only contains an Award Emoji, you'll receive this object back.
+
+```
+POST /groups/:id/epics/:epic_id/notes
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `body` | string | yes | The content of a note |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippet/11/notes?body=note
+```
+
+### Modify existing epic note
+
+Modify existing note of an epic.
+
+```
+PUT /groups/:id/epics/:epic_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `note_id` | integer | yes | The ID of a note |
+| `body` | string | yes | The content of a note |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/snippet/11/notes?body=note
+```
+
+### Delete an epic note
+
+Deletes an existing note of an epic.
+
+```
+DELETE /groups/:id/epics/:epic_id/notes/:note_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `note_id` | integer | yes | The ID of a note |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/5/epics/52/notes/1659
+```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 951961e45ff..75669d85803 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -153,6 +153,7 @@ When the user is authenticated and `simple` is not set this returns something li
"commit_count": 37,
"storage_size": 1038090,
"repository_size": 1038090,
+ "wiki_size" : 0,
"lfs_objects_size": 0,
"job_artifacts_size": 0
},
@@ -234,6 +235,7 @@ When the user is authenticated and `simple` is not set this returns something li
"commit_count": 12,
"storage_size": 2066080,
"repository_size": 2066080,
+ "wiki_size" : 0,
"lfs_objects_size": 0,
"job_artifacts_size": 0
},
@@ -342,6 +344,7 @@ GET /users/:user_id/projects
"commit_count": 37,
"storage_size": 1038090,
"repository_size": 1038090,
+ "wiki_size" : 0,
"lfs_objects_size": 0,
"job_artifacts_size": 0
},
@@ -423,6 +426,7 @@ GET /users/:user_id/projects
"commit_count": 12,
"storage_size": 2066080,
"repository_size": 2066080,
+ "wiki_size" : 0,
"lfs_objects_size": 0,
"job_artifacts_size": 0
},
@@ -548,6 +552,7 @@ GET /projects/:id
"commit_count": 37,
"storage_size": 1038090,
"repository_size": 1038090,
+ "wiki_size" : 0,
"lfs_objects_size": 0,
"job_artifacts_size": 0
},
diff --git a/doc/api/resource_label_events.md b/doc/api/resource_label_events.md
index e1f9ffa9472..f0a7ac4e41d 100644
--- a/doc/api/resource_label_events.md
+++ b/doc/api/resource_label_events.md
@@ -88,6 +88,92 @@ Parameters:
curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/issues/11/resource_label_events/1
```
+## Epics **[ULTIMATE]**
+
+### List group epic label events
+
+Gets a list of all label events for a single epic.
+
+```
+GET /groups/:id/epics/:epic_id/resource_label_events
+```
+
+| Attribute | Type | Required | Description |
+| ------------------- | ---------------- | ---------- | ------------ |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+
+```json
+[
+ {
+ "id": 106,
+ "user": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/root"
+ },
+ "created_at": "2018-08-19T11:43:01.746Z",
+ "resource_type": "Epic",
+ "resource_id": 33,
+ "label": {
+ "id": 73,
+ "name": "a1",
+ "color": "#34495E",
+ "description": ""
+ },
+ "action": "add"
+ },
+ {
+ "id": 107,
+ "user": {
+ "id": 1,
+ "name": "Administrator",
+ "username": "root",
+ "state": "active",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "http://gitlab.example.com/root"
+ },
+ "created_at": "2018-08-19T11:43:01.746Z",
+ "resource_type": "Epic",
+ "resource_id": 33,
+ "label": {
+ "id": 37,
+ "name": "glabel2",
+ "color": "#A8D695",
+ "description": ""
+ },
+ "action": "add"
+ }
+]
+```
+
+```bash
+curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/11/resource_label_events
+```
+
+### Get single epic label event
+
+Returns a single label event for a specific group epic
+
+```
+GET /groups/:id/epics/:epic_id/resource_label_events/:resource_label_event_id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------------- | -------------- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
+| `epic_id` | integer | yes | The ID of an epic |
+| `resource_label_event_id` | integer | yes | The ID of a label event |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/1/epics/11/resource_label_events/107
+```
+
## Merge requests
### List project merge request label events
diff --git a/doc/api/scim.md b/doc/api/scim.md
index 4595c6f2ed3..3870ea788e7 100644
--- a/doc/api/scim.md
+++ b/doc/api/scim.md
@@ -1,4 +1,4 @@
-# SCIM API
+# SCIM API **[SILVER ONLY]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/9388) in [GitLab Silver](https://about.gitlab.com/pricing/) 11.10.
diff --git a/doc/api/services.md b/doc/api/services.md
index 742abccb69e..898cfad7254 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -754,6 +754,7 @@ Parameters:
| `recipients` | string | yes | Comma-separated list of recipient email addresses |
| `add_pusher` | boolean | no | Add pusher to recipients list |
| `notify_only_broken_pipelines` | boolean | no | Notify only broken pipelines |
+| `notify_only_default_branch` | boolean | no | Send notifications only for the default branch ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/28271)) |
### Delete Pipeline-Emails service
@@ -1100,6 +1101,75 @@ Get JetBrains TeamCity CI service settings for a project.
GET /projects/:id/services/teamcity
```
+## Jenkins CI **[STARTER]**
+
+A continuous integration and build server
+
+### Create/Edit Jenkins CI service
+
+Set Jenkins CI service for a project.
+
+```
+PUT /projects/:id/services/jenkins
+```
+
+Parameters:
+
+- `jenkins_url` (**required**) - Jenkins URL like http://jenkins.example.com
+- `project_name` (**required**) - The URL-friendly project name. Example: my_project_name
+- `username` (optional) - A user with access to the Jenkins server, if applicable
+- `password` (optional) - The password of the user
+
+### Delete Jenkins CI service
+
+Delete Jenkins CI service for a project.
+
+```
+DELETE /projects/:id/services/jenkins
+```
+
+### Get Jenkins CI service settings
+
+Get Jenkins CI service settings for a project.
+
+```
+GET /projects/:id/services/jenkins
+```
+
+## Jenkins CI (Deprecated) Service
+
+A continuous integration and build server
+
+### Create/Edit Jenkins CI (Deprecated) service
+
+Set Jenkins CI (Deprecated) service for a project.
+
+```
+PUT /projects/:id/services/jenkins-deprecated
+```
+
+Parameters:
+
+- `project_url` (**required**) - Jenkins project URL like http://jenkins.example.com/job/my-project/
+- `multiproject_enabled` (optional) - Multi-project mode is configured in Jenkins GitLab Hook plugin
+- `pass_unstable` (optional) - Unstable builds will be treated as passing
+
+### Delete Jenkins CI (Deprecated) service
+
+Delete Jenkins CI (Deprecated) service for a project.
+
+```
+DELETE /projects/:id/services/jenkins-deprecated
+```
+
+### Get Jenkins CI (Deprecated) service settings
+
+Get Jenkins CI (Deprecated) service settings for a project.
+
+```
+GET /projects/:id/services/jenkins-deprecated
+```
+
[jira-doc]: ../user/project/integrations/jira.md
[old-jira-api]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/api/services.md#jira
diff --git a/doc/api/users.md b/doc/api/users.md
index d3e67d3d510..47028c679b8 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -140,8 +140,7 @@ GET /users
"can_create_project": true,
"two_factor_enabled": true,
"external": false,
- "private_profile": false,
- "highest_role":10
+ "private_profile": false
}
]
```
@@ -257,7 +256,8 @@ Parameters:
"can_create_project": true,
"two_factor_enabled": true,
"external": false,
- "private_profile": false
+ "private_profile": false,
+ "highest_role":10
}
```
diff --git a/doc/api/vulnerabilities.md b/doc/api/vulnerabilities.md
index 87f77613ad3..390d0966244 100644
--- a/doc/api/vulnerabilities.md
+++ b/doc/api/vulnerabilities.md
@@ -1,4 +1,4 @@
-# Vulnerabilities API
+# Vulnerabilities API **[ULTIMATE]**
Every API call to vulnerabilities must be authenticated.
diff --git a/doc/ci/enable_or_disable_ci.md b/doc/ci/enable_or_disable_ci.md
index 7aa7de97c43..9934d543991 100644
--- a/doc/ci/enable_or_disable_ci.md
+++ b/doc/ci/enable_or_disable_ci.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# How to enable or disable GitLab CI/CD
To effectively use GitLab CI/CD, you need a valid [`.gitlab-ci.yml`](yaml/README.md)
diff --git a/doc/ci/git_submodules.md b/doc/ci/git_submodules.md
index 37078230b34..551044dd76f 100644
--- a/doc/ci/git_submodules.md
+++ b/doc/ci/git_submodules.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Using Git submodules with GitLab CI
> **Notes:**
diff --git a/doc/ci/interactive_web_terminal/index.md b/doc/ci/interactive_web_terminal/index.md
index 7109b2ec583..1387d4df500 100644
--- a/doc/ci/interactive_web_terminal/index.md
+++ b/doc/ci/interactive_web_terminal/index.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Interactive Web Terminals
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/50144) in GitLab 11.3.
diff --git a/doc/ci/junit_test_reports.md b/doc/ci/junit_test_reports.md
index 799217c9a08..fa78f53f563 100644
--- a/doc/ci/junit_test_reports.md
+++ b/doc/ci/junit_test_reports.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# JUnit test reports
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/45318) in GitLab 11.2.
diff --git a/doc/ci/large_repositories/index.md b/doc/ci/large_repositories/index.md
index 244ccbb92b0..29d649ad717 100644
--- a/doc/ci/large_repositories/index.md
+++ b/doc/ci/large_repositories/index.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Optimizing GitLab for large repositories
Large repositories consisting of more than 50k files in a worktree
diff --git a/doc/ci/merge_request_pipelines/index.md b/doc/ci/merge_request_pipelines/index.md
index b3ff55daea2..fe2fc790505 100644
--- a/doc/ci/merge_request_pipelines/index.md
+++ b/doc/ci/merge_request_pipelines/index.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Pipelines for merge requests
NOTE: **Note**:
diff --git a/doc/ci/metrics_reports.md b/doc/ci/metrics_reports.md
index 83a7094faaa..b7824402d45 100644
--- a/doc/ci/metrics_reports.md
+++ b/doc/ci/metrics_reports.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Metrics Reports **[PREMIUM]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/9788) in [GitLab Premium](https://about.gitlab.com/pricing) 11.10.
diff --git a/doc/ci/multi_project_pipelines.md b/doc/ci/multi_project_pipelines.md
index 556059c01b6..bcd92243d97 100644
--- a/doc/ci/multi_project_pipelines.md
+++ b/doc/ci/multi_project_pipelines.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Multi-project pipelines **[PREMIUM]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/2121) in
@@ -134,6 +138,34 @@ staging:
The `ENVIRONMENT` variable will be passed to every job defined in a downstream
pipeline. It will be available as an environment variable when GitLab Runner picks a job.
+In the following configuration, the `MY_VARIABLE` variable will be passed
+downstream, because jobs inherit variables declared in top-level `variables`:
+
+```yaml
+variables:
+ MY_VARIABLE: my-value
+
+my-pipeline:
+ variables:
+ ENVIRONMENT: something
+ trigger: my/project
+```
+
+You might want to pass some information about the upstream pipeline using, for
+example, predefined variables. In order to do that, you can use interpolation
+to pass any variable. For example:
+
+```yaml
+my-pipeline:
+ variables:
+ UPSTREAM_BRANCH: $CI_COMMIT_REF_NAME
+ trigger: my/project
+```
+
+In this scenario, the `UPSTREAM_BRANCH` variable with a value related to the
+upstream pipeline will be passed to a `downstream` job, and will be available
+within the context of all downstream builds.
+
### Limitations
Because bridge jobs are a little different to regular jobs, it is not
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index 4dbe1a85588..8b0634ed268 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Creating and using CI/CD pipelines
> Introduced in GitLab 8.8.
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 015f1c0dc0f..02370bead00 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Getting started with GitLab CI/CD
>**Note:** Starting from version 8.0, GitLab [Continuous Integration][ci] (CI)
@@ -35,11 +39,11 @@ project's **Pipelines** page.
---
-This guide assumes that you:
+This guide assumes that you have:
-- have a working GitLab instance of version 8.0+r or are using
- [GitLab.com](https://gitlab.com)
-- have a project in GitLab that you would like to use CI for
+- A working GitLab instance of version 8.0+r or are using
+ [GitLab.com](https://gitlab.com).
+- A project in GitLab that you would like to use CI for.
Let's break it down to pieces and work on solving the GitLab CI puzzle.
diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md
index 1a71c5fd258..7b039fe6654 100644
--- a/doc/ci/review_apps/index.md
+++ b/doc/ci/review_apps/index.md
@@ -1,9 +1,13 @@
+---
+type: reference
+---
+
# Review Apps
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/21971) in GitLab 8.12. Further additions were made in GitLab 8.13 and 8.14.
> - Inspired by [Heroku's Review Apps](https://devcenter.heroku.com/articles/github-integration-review-apps), which itself was inspired by [Fourchette](https://github.com/rainforestapp/fourchette).
-Review Apps are a collaboration tool that takes the hard work out of providing an environment to showcase product changes.
+Review Apps is a collaboration tool that takes the hard work out of providing an environment to showcase product changes.
## Introduction
@@ -18,7 +22,7 @@ Review Apps:
In the above example:
-- A Review App is built every time a commit is pushed to`topic branch`.
+- A Review App is built every time a commit is pushed to `topic branch`.
- The reviewer fails two reviews before passing the third review.
- Once the review as passed, `topic branch` is merged into `master` where it's deploy to staging.
- After been approved in staging, the changes that were merged into `master` are deployed in to production.
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index ce55b231666..b089229ab58 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Configuring GitLab Runners
In GitLab CI, Runners run the code defined in [`.gitlab-ci.yml`](../yaml/README.md).
diff --git a/doc/ci/services/README.md b/doc/ci/services/README.md
index 2eda5d23976..7fe12eb53e7 100644
--- a/doc/ci/services/README.md
+++ b/doc/ci/services/README.md
@@ -1,13 +1,18 @@
---
comments: false
+type: index
---
-# GitLab CI Services
+# GitLab CI services examples
-GitLab CI uses the `services` keyword to define what docker containers should
-be linked with your base image. Below is a list of examples you may use.
+The [`services`](../docker/using_docker_images.md#what-is-a-service)
+keyword defines a Docker image that runs during a `job` linked to the
+Docker image that the image keyword defines. This allows you to access
+the service image during build time.
+
+The service image can run any application, but the most common use
+case is to run a database container, for example:
- [Using MySQL](mysql.md)
- [Using PostgreSQL](postgres.md)
- [Using Redis](redis.md)
-- [Using Other Services](../docker/using_docker_images.md#what-is-a-service)
diff --git a/doc/ci/services/mysql.md b/doc/ci/services/mysql.md
index 5fa378fc4c2..697452cee83 100644
--- a/doc/ci/services/mysql.md
+++ b/doc/ci/services/mysql.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Using MySQL
As many applications depend on MySQL as their database, you will eventually
diff --git a/doc/ci/services/postgres.md b/doc/ci/services/postgres.md
index 2e6d7ae94d2..211eea26eb0 100644
--- a/doc/ci/services/postgres.md
+++ b/doc/ci/services/postgres.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Using PostgreSQL
As many applications depend on PostgreSQL as their database, you will
diff --git a/doc/ci/services/redis.md b/doc/ci/services/redis.md
index 36f71427ae7..8b227154b06 100644
--- a/doc/ci/services/redis.md
+++ b/doc/ci/services/redis.md
@@ -1,3 +1,7 @@
+---
+type: reference
+---
+
# Using Redis
As many applications depend on Redis as their key-value store, you will
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index 9ed1ec5aa5c..69591ed605c 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -1,5 +1,6 @@
---
last_updated: 2017-12-13
+type: tutorial
---
# Using SSH keys with GitLab CI/CD
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index ad80b5d8818..04c541fefe7 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -1,3 +1,7 @@
+---
+type: tutorial
+---
+
# Triggering pipelines through the API
> **Notes**:
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 67e1d316f02..2157a6dc097 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -91,10 +91,9 @@ This means that the value of the variable will be hidden in job logs,
though it must match certain requirements to do so:
- The value must be in a single line.
-- The value must not have escape characters.
-- The value must not use variables.
-- The value must not have any whitespace.
+- The value must contain only letters, numbers, or underscores.
- The value must be at least 8 characters long.
+- The value must not use variables.
If the value does not meet the requirements above, then the CI variable will fail to save.
In order to save, either alter the value to meet the masking requirements
@@ -612,8 +611,8 @@ $'\''git'\'' "checkout" "-f" "-q" "dd648b2e48ce6518303b0bb580b2ee32fadaf045"
Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-machine-1480971377-317a7d0f-digital-ocean-4gb...
++ export CI=true
++ CI=true
-++ export CI_API_V4_API_URL=https://example.com:3000/api/v4
-++ CI_API_V4_API_URL=https://example.com:3000/api/v4
+++ export CI_API_V4_URL=https://example.com:3000/api/v4
+++ CI_API_V4_URL=https://example.com:3000/api/v4
++ export CI_DEBUG_TRACE=false
++ CI_DEBUG_TRACE=false
++ export CI_COMMIT_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
@@ -652,8 +651,8 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach
++ GITLAB_CI=true
++ export CI=true
++ CI=true
-++ export CI_API_V4_API_URL=https://example.com:3000/api/v4
-++ CI_API_V4_API_URL=https://example.com:3000/api/v4
+++ export CI_API_V4_URL=https://example.com:3000/api/v4
+++ CI_API_V4_URL=https://example.com:3000/api/v4
++ export GITLAB_CI=true
++ GITLAB_CI=true
++ export CI_JOB_ID=7046507
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 8667eacd3d5..18c85618b1b 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -386,17 +386,12 @@ job:
- branches@gitlab-org/gitlab-ce
except:
- master@gitlab-org/gitlab-ce
- - release/.*@gitlab-org/gitlab-ce
+ - /^release/.*$/@gitlab-org/gitlab-ce
```
The above example will run `job` for all branches on `gitlab-org/gitlab-ce`,
except `master` and those with names prefixed with `release/`.
-NOTE: **Note:**
-Because `@` is used to denote the beginning of a ref's repository path,
-matching a ref name containing the `@` character in a regular expression
-requires the use of the hex character code match `\x40`.
-
If a job does not have an `only` rule, `only: ['branches', 'tags']` is set by
default. If it doesn't have an `except` rule, it is empty.
@@ -415,6 +410,28 @@ job:
only: ['branches', 'tags']
```
+#### Regular expressions
+
+Because `@` is used to denote the beginning of a ref's repository path,
+matching a ref name containing the `@` character in a regular expression
+requires the use of the hex character code match `\x40`.
+
+Only the tag or branch name can be matched by a regular expression.
+The repository path, if given, is always matched literally.
+
+If a regular expression shall be used to match the tag or branch name,
+the entire ref name part of the pattern has to be a regular expression,
+and must be surrounded by `/`.
+(With regular expression flags appended after the closing `/`.)
+So `issue-/.*/` won't work to match all tag names or branch names
+that begin with `issue-`.
+
+TIP: **Tip**
+Use anchors `^` and `$` to avoid the regular expression
+matching only a substring of the tag name or branch name.
+For example, `/^issue-.*$/` is equivalent to `/^issue-/`,
+while just `/issue/` would also match a branch called `severe-issues`.
+
### Supported `only`/`except` regexp syntax
CAUTION: **Warning:**
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index 4b76d5f9c5b..22a52a313b3 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -142,7 +142,7 @@ Component statuses are linked to configuration documentation for each component.
| [ElasticSearch](#elasticsearch) | Improved search within GitLab | [⤓][elasticsearch-omnibus] | [⤓][elasticsearch-charts] | [⤓][elasticsearch-charts] | [❌](https://gitlab.com/groups/gitlab-org/-/epics/153) | [⤓][elasticsearch-source] | [⤓][elasticsearch-gdk] | EE Only |
| [Sentry integration](#sentry) | Error tracking for deployed apps | [⤓][sentry-integration] | [⤓][sentry-integration] | [⤓][sentry-integration] | [⤓][sentry-integration] | [⤓][sentry-integration] | [⤓][sentry-integration] | CE & EE |
| [Jaeger integration](#jaeger) | Distributed tracing for deployed apps | [⤓][jaeger-integration] | [⤓][jaeger-integration] | [⤓][jaeger-integration] | [⤓][jaeger-integration] | [⤓][jaeger-integration] | [⤓][jaeger-integration] | EE Only |
-| [Kubernetes cluster apps](#kubernetes-cluster-apps) | Deploy [Helm](https://docs.helm.sh/), [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/), [Cert-Manager](https://docs.cert-manager.io/en/latest/), [Prometheus](https://prometheus.io/docs/introduction/overview/), a [Runner](https://docs.gitlab.com/runner/), [JupyterHub](http://jupyter.org/), [Knative](https://cloud.google.com/knative) to a cluster | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | CE & EE |
+| [GitLab Managed Apps](#gitlab-managed-apps) | Deploy [Helm](https://docs.helm.sh/), [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/), [Cert-Manager](https://docs.cert-manager.io/en/latest/), [Prometheus](https://prometheus.io/docs/introduction/overview/), a [Runner](https://docs.gitlab.com/runner/), [JupyterHub](http://jupyter.org/), [Knative](https://cloud.google.com/knative) to a cluster | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | [⤓][managed-k8s-apps] | CE & EE |
### Component details
@@ -435,7 +435,7 @@ Sidekiq is a Ruby background job processor that pulls jobs from the redis queue
- Configuration: [Omnibus][inbound-email-omnibus], [Charts][inbound-email-charts], [Source][gitlab-yml], [GDK][gitlab-yml]
- Layer: Core Service (Processor)
-#### Kubernetes Cluster Apps
+#### GitLab Managed Apps
- Configuration: [Omnibus][managed-k8s-apps], [Charts][managed-k8s-apps], [Source][managed-k8s-apps], [GDK][managed-k8s-apps]
- Layer: Core Service (Processor)
diff --git a/doc/development/geo.md b/doc/development/geo.md
index 87ec34ec5c4..6e59fab34c7 100644
--- a/doc/development/geo.md
+++ b/doc/development/geo.md
@@ -10,6 +10,7 @@ the diagram below and are described in more detail within this document.
## Replication layer
Geo handles replication for different components:
+
- [Database](#database-replication): includes the entire application, except cache and jobs.
- [Git repositories](#repository-replication): includes both projects and wikis.
- [Uploaded blobs](#uploads-replication): includes anything from images attached on issues
@@ -209,20 +210,38 @@ bundle exec rake geo:db:migrate
### Foreign Data Wrapper
-The use of [FDW](#fdw) was introduced in GitLab 10.1.
+> Introduced in GitLab 10.1.
+
+Foreign Data Wrapper ([FDW](#fdw)) is used by the [Geo Log Cursor](#geo-log-cursor) and improves
+the performance of many synchronization operations.
-This is useful for the [Geo Log Cursor](#geo-log-cursor) and improves
-the performance of some synchronization operations.
+FDW is a PostgreSQL extension ([`postgres_fdw`](https://www.postgresql.org/docs/current/postgres-fdw.html)) that is enabled within
+the Geo Tracking Database (on a **secondary** node), which allows it
+to connect to the readonly database replica and perform queries and filter
+data from both instances.
While FDW is available in older versions of PostgreSQL, we needed to
raise the minimum required version to 9.6 as this includes many
performance improvements to the FDW implementation.
+This persistent connection is configured as an FDW server
+named `gitlab_secondary`. This configuration exists within the database's user
+context only. To access the `gitlab_secondary`, GitLab needs to use the
+same database user that had previously been configured.
+
+The Geo Tracking Database accesses the readonly database replica via FDW as a regular user,
+limited by its own restrictions. The credentials are configured as a
+`USER MAPPING` associated with the `SERVER` mapped previously
+(`gitlab_secondary`).
+
+FDW configuration and credentials definition are managed automatically by the
+Omnibus GitLab `gitlab-ctl reconfigure` command.
+
#### Refeshing the Foreign Tables
-Whenever the database schema changes on the **primary** node, the
-**secondary** node will need to refresh its foreign tables by running
-the following:
+Whenever a new Geo node is configured or the database schema changes on the
+**primary** node, you must refresh the foreign tables on the **secondary** node
+by running the following:
```sh
bundle exec rake geo:db:refresh_foreign_tables
@@ -243,6 +262,53 @@ STATEMENT: SELECT a.attname, format_type(a.atttypid, a.atttypmod)
ORDER BY a.attnum
```
+#### Accessing data from a Foreign Table
+
+At the SQL level, all you have to do is `SELECT` data from `gitlab_secondary.*`.
+
+Here's an example of how to access all projects from the Geo Tracking Database's FDW:
+
+```sql
+SELECT * FROM gitlab_secondary.projects;
+```
+
+As a more real-world example, this is how you filter for unarchived projects
+on the Tracking Database:
+
+```sql
+SELECT project_registry.*
+ FROM project_registry
+ JOIN gitlab_secondary.projects
+ ON (project_registry.project_id = gitlab_secondary.projects.id
+ AND gitlab_secondary.projects.archived IS FALSE)
+```
+
+At the ActiveRecord level, we have additional Models that represent the
+foreign tables. They must be mapped in a slightly different way, and they are read-only.
+
+Check the existing FDW models in `ee/app/models/geo/fdw` for reference.
+
+From a developer's perspective, it's no different than creating a model that
+represents a Database View.
+
+With the examples above, you can access the projects with:
+
+```ruby
+Geo::Fdw::Project.all
+```
+
+and to access the `ProjectRegistry` filtering by unarchived projects:
+
+```ruby
+# We have to use Arel here:
+project_registry_table = Geo::ProjectRegistry.arel_table
+fdw_project_table = Geo::Fdw::Project.arel_table
+
+project_registry_table.join(fdw_project_table)
+ .on(project_registry_table[:project_id].eq(fdw_project_table[:id]))
+ .where((fdw_project_table[:archived]).eq(true)) # if you append `.to_sql` you can check generated query
+```
+
## Finders
Geo uses [Finders](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/app/finders),
diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md
index 28ba20bec09..00ac6d6b650 100644
--- a/doc/gitlab-basics/README.md
+++ b/doc/gitlab-basics/README.md
@@ -15,7 +15,7 @@ This documentation is split into the following groups:
The following are guides to basic GitLab functionality:
-- [Create and add your SSH Keys](create-your-ssh-keys.md), for enabling Git over SSH.
+- [Create and add your SSH public key](create-your-ssh-keys.md), for enabling Git over SSH.
- [Create a project](create-project.md), to start using GitLab.
- [Create a group](../user/group/index.md#create-a-new-group), to combine and administer projects together.
- [Create a branch](create-branch.md), to make changes to files stored in a project's repository.
diff --git a/doc/gitlab-basics/create-your-ssh-keys.md b/doc/gitlab-basics/create-your-ssh-keys.md
index 8fecdc6948e..aac73d4c9c5 100644
--- a/doc/gitlab-basics/create-your-ssh-keys.md
+++ b/doc/gitlab-basics/create-your-ssh-keys.md
@@ -1,22 +1,22 @@
-# How to create your SSH keys
+# Create and add your SSH public key
-This topic describes how to create SSH keys. You do this to use Git over SSH instead of Git over HTTP.
+This topic describes how to:
-## Creating your SSH keys
+- Create an SSH key pair to use with GitLab.
+- Add the SSH public key to your GitLab account.
-1. Go to your [command line](start-using-git.md) and follow the [instructions](../ssh/README.md) to generate your SSH key pair.
-1. Log in to GitLab.
-1. In the upper-right corner, click your avatar and select **Settings**.
-1. On the **User Settings** menu, select **SSH keys**.
-1. Paste the **public** key generated in the first step in the **Key**
- text field.
-1. Optionally, give it a descriptive title so that you can recognize it in the
- event you add multiple keys.
-1. Finally, click the **Add key** button to add it to GitLab. You will be able to see
- its fingerprint, title, and creation date.
+You do this to use [Git over SSH instead of Git over HTTP](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols).
- ![SSH key single page](img/profile_settings_ssh_keys_single_key.png)
+## Creating your SSH key pair
+
+1. Go to your [command line](start-using-git.md).
+1. Follow the [instructions](../ssh/README.md#generating-a-new-ssh-key-pair) to generate your SSH key pair.
+
+## Adding your SSH public key to GitLab
+
+To add the SSH public key to GitLab,
+see [Adding an SSH key to your GitLab account](../ssh/README.md#adding-an-ssh-key-to-your-gitlab-account).
NOTE: **Note:**
Once you add a key, you cannot edit it. If the paste
-didn't work, you need to remove the offending key and re-add it.
+[didn't work](../ssh/README.md#testing-that-everything-is-set-up-correctly), you need to remove the key and re-add it.
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png
deleted file mode 100644
index 8014f1d5301..00000000000
--- a/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png
+++ /dev/null
Binary files differ
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index 9c4a391e8da..3bfebfc5d9b 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -55,7 +55,7 @@ As an admin, you can restrict
By default, all keys are permitted, which is also the case for
[GitLab.com](../user/gitlab_com/index.md#ssh-host-keys-fingerprints).
-## ED25519 SSH keys
+### ED25519 SSH keys
Following [best practices](https://linux-audit.com/using-ed25519-openssh-keys-instead-of-dsa-rsa-ecdsa/),
you should always favor [ED25519](https://ed25519.cr.yp.to/) SSH keys, since they
@@ -65,7 +65,7 @@ They were introduced in OpenSSH 6.5, so any modern OS should include the
option to create them. If for any reason your OS or the GitLab instance you
interact with doesn't support this, you can fallback to RSA.
-## RSA SSH keys
+### RSA SSH keys
RSA keys are the most common ones and therefore the most compatible with
servers that may have an old OpenSSH version. Use them if the GitLab server
@@ -166,12 +166,13 @@ Now, it's time to add the newly created public key to your GitLab account.
NOTE: **Note:**
If you opted to create an RSA key, the name might differ.
-1. Add your public SSH key to your GitLab account by clicking your avatar
- in the upper right corner and selecting **Settings**. From there on,
- navigate to **SSH Keys** and paste your public key in the "Key" section.
- If you created the key with a comment, this will appear under "Title".
- If not, give your key an identifiable title like _Work Laptop_ or
- _Home Workstation_, and click **Add key**.
+1. Add your **public** SSH key to your GitLab account by:
+ 1. Clicking your avatar in the upper right corner and selecting **Settings**.
+ 1. Navigating to **SSH Keys** and pasting your **public** key in the **Key** field. If you:
+
+ - Created the key with a comment, this will appear in the **Title** field.
+ - Created the key without a comment, give your key an identifiable title like _Work Laptop_ or _Home Workstation_.
+ 1. Click the **Add key** button.
NOTE: **Note:**
If you manually copied your public SSH key make sure you copied the entire
@@ -305,7 +306,7 @@ who needs to know and configure the private key.
GitLab administrators set up Global Deploy keys in the Admin area under the
section **Deploy Keys**. Ensure keys have a meaningful title as that will be
the primary way for project maintainers and owners to identify the correct Global
-Deploy key to add. For instance, if the key gives access to a SaaS CI instance,
+Deploy key to add. For instance, if the key gives access to a SaaS CI instance,
use the name of that service in the key name if that is all it is used for.
When creating Global Shared Deploy keys, give some thought to the granularity
of keys - they could be of very narrow usage such as just a specific service or
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index 00b6c1dfdc2..228da2d1f57 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -17,11 +17,11 @@ This page gathers all the resources for the topic **Authentication** within GitL
## GitLab administrators
- [LDAP (Community Edition)](../../administration/auth/ldap.md)
-- [LDAP (Enterprise Edition)](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html) **[STARTER]**
+- [LDAP (Enterprise Edition)](../../administration/auth/ldap-ee.md) **[STARTER]**
- [Enforce Two-factor Authentication (2FA)](../../security/two_factor_authentication.md#enforce-two-factor-authentication-2fa)
- **Articles:**
- [How to Configure LDAP with GitLab CE](../../administration/auth/how_to_configure_ldap_gitlab_ce/index.md)
- - [How to Configure LDAP with GitLab EE](https://docs.gitlab.com/ee/administration/auth/how_to_configure_ldap_gitlab_ee/index.html) **[STARTER]**
+ - [How to Configure LDAP with GitLab EE](../../administration/auth/how_to_configure_ldap_gitlab_ee/index.md) **[STARTER]**
- [Feature Highlight: LDAP Integration](https://about.gitlab.com/2014/07/10/feature-highlight-ldap-sync/)
- [Debugging LDAP](https://about.gitlab.com/handbook/support/workflows/support-engineering/ldap/debugging_ldap.html)
- **Integrations:**
@@ -30,10 +30,10 @@ This page gathers all the resources for the topic **Authentication** within GitL
- [Atlassian Crowd OmniAuth Provider](../../administration/auth/crowd.md)
- [CAS OmniAuth Provider](../../integration/cas.md)
- [SAML OmniAuth Provider](../../integration/saml.md)
- - [SAML for GitLab.com Groups](https://docs.gitlab.com/ee/user/group/saml_sso/index.html) **[SILVER ONLY]**
- - [SCIM user provisioning for GitLab.com Groups](https://docs.gitlab.com/ee/user/group/saml_sso/scim_setup.html) **[SILVER ONLY]**
+ - [SAML for GitLab.com Groups](../../user/group/saml_sso/index.md) **[SILVER ONLY]**
+ - [SCIM user provisioning for GitLab.com Groups](../../user/group/saml_sso/scim_setup.md) **[SILVER ONLY]**
- [Okta SSO provider](../../administration/auth/okta.md)
- - [Kerberos integration (GitLab EE)](https://docs.gitlab.com/ee/integration/kerberos.html) **[STARTER]**
+ - [Kerberos integration (GitLab EE)](../../integration/kerberos.md) **[STARTER]**
## API
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 4ceccaabf86..04938080539 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -13,7 +13,7 @@ Starting with GitLab 11.3, the Auto DevOps pipeline is enabled by default for al
projects. If it has not been explicitly enabled for the project, Auto DevOps will be automatically
disabled on the first pipeline failure. Your project will continue to use an alternative
[CI/CD configuration file](../../ci/yaml/README.md) if one is found. A GitLab
-administrator can [change this setting](../../user/admin_area/settings/continuous_integration.html#auto-devops-core-only)
+administrator can [change this setting](../../user/admin_area/settings/continuous_integration.md#auto-devops-core-only)
in the admin area.
With Auto DevOps, the software development process becomes easier to set up
@@ -181,7 +181,7 @@ Those environments are tied to jobs that use [Auto Deploy](#auto-deploy), so
except for the environment scope, they would also need to have a different
domain they would be deployed to. This is why you need to define a separate
`KUBE_INGRESS_BASE_DOMAIN` variable for all the above
-[based on the environment](https://docs.gitlab.com/ee/ci/variables/#limiting-environment-scopes-of-environment-variables-premium).
+[based on the environment](../../ci/variables/README.md#limiting-environment-scopes-of-environment-variables-premium).
The following table is an example of how the three different clusters would
be configured.
@@ -363,7 +363,7 @@ created, and is uploaded as an artifact which you can later download and check
out.
Any differences between the source and target branches are also
-[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html).
+[shown in the merge request widget](../../user/project/merge_requests/code_quality.md).
### Auto SAST **[ULTIMATE]**
@@ -376,7 +376,7 @@ report is created, it's uploaded as an artifact which you can later download and
check out.
Any security warnings are also shown in the merge request widget. Read more how
-[SAST works](https://docs.gitlab.com/ee/user/application_security/sast/index.html).
+[SAST works](../../user/application_security/sast/index.md).
NOTE: **Note:**
The Auto SAST stage will be skipped on licenses other than Ultimate.
@@ -395,7 +395,7 @@ report is created, it's uploaded as an artifact which you can later download and
check out.
Any security warnings are also shown in the merge request widget. Read more about
-[Dependency Scanning](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index.html).
+[Dependency Scanning](../../user/application_security/dependency_scanning/index.md).
NOTE: **Note:**
The Auto Dependency Scanning stage will be skipped on licenses other than Ultimate.
@@ -414,7 +414,7 @@ report is created, it's uploaded as an artifact which you can later download and
check out.
Any licenses are also shown in the merge request widget. Read more how
-[License Management works](https://docs.gitlab.com/ee/user/application_security/license_management/index.html).
+[License Management works](../../user/application_security/license_management/index.md).
NOTE: **Note:**
The Auto License Management stage will be skipped on licenses other than Ultimate.
@@ -430,7 +430,7 @@ created, it's uploaded as an artifact which you can later download and
check out.
Any security warnings are also shown in the merge request widget. Read more how
-[Container Scanning works](https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html).
+[Container Scanning works](../../user/application_security/container_scanning/index.md).
NOTE: **Note:**
The Auto Container Scanning stage will be skipped on licenses other than Ultimate.
@@ -486,7 +486,7 @@ issues. Once the report is created, it's uploaded as an artifact which you can
later download and check out.
Any security warnings are also shown in the merge request widget. Read how
-[DAST works](https://docs.gitlab.com/ee/user/application_security/dast/index.html).
+[DAST works](../../user/application_security/dast/index.md).
NOTE: **Note:**
The Auto DAST stage will be skipped on licenses other than Ultimate.
@@ -504,7 +504,7 @@ Auto Browser Performance Testing utilizes the [Sitespeed.io container](https://h
```
Any performance differences between the source and target branches are also
-[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/browser_performance_testing.html).
+[shown in the merge request widget](../../user/project/merge_requests/browser_performance_testing.md).
### Auto Deploy
@@ -673,7 +673,7 @@ repo or by specifying a project variable:
### Custom Helm chart per environment **[PREMIUM]**
You can specify the use of a custom Helm chart per environment by scoping the environment variable
-to the desired environment. See [Limiting environment scopes of variables](https://docs.gitlab.com/ee/ci/variables/#limiting-environment-scopes-of-variables-premium).
+to the desired environment. See [Limiting environment scopes of variables](../../ci/variables/README.md#limiting-environment-scopes-of-environment-variables-premium).
### Customizing `.gitlab-ci.yml`
@@ -739,8 +739,8 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac
| `AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD` | From Gitlab 11.11, this variable can be used to set a password to connect to the helm repository. Defaults to no credentials. (Also set AUTO_DEVOPS_CHART_REPOSITORY_USERNAME) |
| `REPLICAS` | The number of replicas to deploy; defaults to 1. |
| `PRODUCTION_REPLICAS` | The number of replicas to deploy in the production environment. This takes precedence over `REPLICAS`; defaults to 1. |
-| `CANARY_REPLICAS` | The number of canary replicas to deploy for [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html); defaults to 1 |
-| `CANARY_PRODUCTION_REPLICAS` | The number of canary replicas to deploy for [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) in the production environment. This takes precedence over `CANARY_REPLICAS`; defaults to 1 |
+| `CANARY_REPLICAS` | The number of canary replicas to deploy for [Canary Deployments](../../user/project/canary_deployments.md); defaults to 1 |
+| `CANARY_PRODUCTION_REPLICAS` | The number of canary replicas to deploy for [Canary Deployments](../../user/project/canary_deployments.md) in the production environment. This takes precedence over `CANARY_REPLICAS`; defaults to 1 |
| `ADDITIONAL_HOSTS` | Fully qualified domain names specified as a comma-separated list that are added to the ingress hosts. |
| `<ENVIRONMENT>_ADDITIONAL_HOSTS` | For a specific environment, the fully qualified domain names specified as a comma-separated list that are added to the ingress hosts. This takes precedence over `ADDITIONAL_HOSTS`. |
| `POSTGRES_ENABLED` | Whether PostgreSQL is enabled; defaults to `"true"`. Set to `false` to disable the automatic deployment of PostgreSQL. |
@@ -917,7 +917,7 @@ you when you're ready to manually deploy to production.
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ci-yml/merge_requests/171)
in GitLab 11.0.
-A [canary environment](https://docs.gitlab.com/ee/user/project/canary_deployments.html) can be used
+A [canary environment](../../user/project/canary_deployments.md) can be used
before any changes are deployed to production.
If `CANARY_ENABLED` is defined in your project (e.g., set `CANARY_ENABLED` to
diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
index 367e192b85a..cc83d20d65a 100644
--- a/doc/topics/autodevops/quick_start_guide.md
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -208,7 +208,7 @@ applications. In the rightmost column for the production environment, you can ma
application is running.
Right below, there is the
-[Deploy Board](https://docs.gitlab.com/ee/user/project/deploy_boards.html).
+[Deploy Board](../../user/project/deploy_boards.md).
The squares represent pods in your Kubernetes cluster that are associated with
the given environment. Hovering above each square you can see the state of a
deployment and clicking a square will take you to the pod's logs page.
diff --git a/doc/topics/git/index.md b/doc/topics/git/index.md
index 7707d56764e..db6d1a62f59 100644
--- a/doc/topics/git/index.md
+++ b/doc/topics/git/index.md
@@ -70,5 +70,5 @@ We've gathered some resources to help you to get the best from Git with GitLab.
- [Getting Started with Git LFS](https://about.gitlab.com/2017/01/30/getting-started-with-git-lfs-tutorial/)
- [GitLab Git LFS documentation](../../workflow/lfs/manage_large_binaries_with_git_lfs.md)
-- [Git-Annex to Git-LFS migration guide](https://docs.gitlab.com/ee/workflow/lfs/migrate_from_git_annex_to_git_lfs.html)
+- [Git-Annex to Git-LFS migration guide](../../workflow/lfs/migrate_from_git_annex_to_git_lfs.md)
- Article (2015-08-13): [Towards a production quality open source Git LFS server](https://about.gitlab.com/2015/08/13/towards-a-production-quality-open-source-git-lfs-server/)
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index 647ad4b2e24..f15b0107de5 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -381,7 +381,7 @@ Takes changes from one branch, and [applies them](https://git-scm.com/docs/git-m
[Arises](https://about.gitlab.com/2016/09/06/resolving-merge-conflicts-from-the-gitlab-ui/) when a merge can't be performed cleanly between two versions of the same file.
-#### Merge Request
+#### Merge Request (MR)
[Takes changes](../../gitlab-basics/add-merge-request.md) from one branch, and applies them into another branch.
diff --git a/doc/user/admin_area/diff_limits.md b/doc/user/admin_area/diff_limits.md
index 9205860ef1f..4063c40a751 100644
--- a/doc/user/admin_area/diff_limits.md
+++ b/doc/user/admin_area/diff_limits.md
@@ -1,21 +1,40 @@
+---
+type: reference
+---
+
# Diff limits administration
+You can set a maximum size for display of diff files (patches).
+
+## Maximum diff patch size
+
+Diff files which exceed this value will be presented as 'too large' and won't
+be expandable. Instead of an expandable view, a link to the blob view will be
+shown.
+
+Patches greater than 10% of this size will be automatically collapsed, and a
+link to expand the diff will be presented.
+
NOTE: **Note:**
Merge requests and branch comparison views will be affected.
CAUTION: **Caution:**
-These settings are currently under experimental state. They'll
-increase the resource consumption of your instance and should
-be edited mindfully.
+This setting is experimental. An increased maximum will increase resource
+consumption of your instance. Keep this in mind when adjusting the maximum.
-1. Access **Admin area > Settings > General**
-1. Expand **Diff limits**
+1. Go to **Admin area > Settings > General**.
+1. Expand **Diff limits**.
+1. Enter a value for **Maximum diff patch size**, measured in bytes.
+1. Click on **Save changes**.
-### Maximum diff patch size
+<!-- ## Troubleshooting
-This is the content size each diff file (patch) is allowed to reach before
-it's collapsed, without the possibility of being expanded. A link redirecting
-to the blob view will be presented for the patches that surpass this limit.
+Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+one might have when setting this up, or when something is changed, or on upgrading, it's
+important to describe those, too. Think of things that may go wrong and include them here.
+This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask.
-Patches surpassing 10% of this content size will be automatically collapsed,
-but expandable (a link to expand the diff will be presented).
+Each scenario can be a third-level heading, e.g. `### Getting error message X`.
+If you have none to add when creating a doc, leave this section in place
+but commented out to help encourage others to add to it in the future. -->
diff --git a/doc/user/admin_area/index.md b/doc/user/admin_area/index.md
index 4ed1287abbc..db5da26ca0a 100644
--- a/doc/user/admin_area/index.md
+++ b/doc/user/admin_area/index.md
@@ -114,3 +114,27 @@ To change the sort order, click the sort dropdown and select the desired order.
To search for users, enter your criteria in the search field. The user search is case
insensitive, and applies partial matching to name and username. To search for an email address,
you must provide the complete email address.
+
+## Administer Jobs
+
+You can administer all jobs in the GitLab instance from the Admin Area's Jobs page.
+
+To access the Jobs page, go to **Admin Area > Overview > Jobs**.
+
+All jobs are listed, in reverse order of their job ID.
+
+Click the **All** tab to list all jobs. Click the **Pending**, **Running**, or **Finished** tab to list only jobs of that status.
+
+For each job, the following details are listed:
+
+| Field | Description |
+|--------- | ----------- |
+| Status | Job status, either **passed**, **skipped**, or **failed**. |
+| Job | Includes links to the job, branch, and the commit that started the job. |
+| Pipeline | Includes a link to the specific pipeline. |
+| Project | Name of the project, and organization, to which the job belongs. |
+| Runner | Name of the CI runner assigned to execute the job. |
+| Stage | Stage that the job is declared in a `.gitlab-ci.yml` file. |
+| Name | Name of the job specified in a `.gitlab-ci.yml` file. |
+| Timing | Duration of the job, and how long ago the job completed. |
+| Coverage | Percentage of tests coverage. |
diff --git a/doc/user/application_security/security_dashboard/index.md b/doc/user/application_security/security_dashboard/index.md
index ca31e15c65f..19eeb06a259 100644
--- a/doc/user/application_security/security_dashboard/index.md
+++ b/doc/user/application_security/security_dashboard/index.md
@@ -54,6 +54,7 @@ First, navigate to the Security Dashboard found under your group's
Once you're on the dashboard, at the top you should see a series of filters for:
- Severity
+- Confidence
- Report type
- Project
diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md
index 4085f3b678c..5166524d13b 100644
--- a/doc/user/profile/personal_access_tokens.md
+++ b/doc/user/profile/personal_access_tokens.md
@@ -49,6 +49,7 @@ the following table.
[2fa]: ../account/two_factor_authentication.md
[api]: ../../api/README.md
[ce-3749]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3749
+[ce-5951]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951
[ce-14838]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14838
[container registry]: ../project/container_registry.md
[users]: ../../api/users.md
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index 99dd018a3ba..737dea1de6c 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -100,9 +100,9 @@ Only project Owners and Admin users have the [permissions] to transfer a project
You can transfer an existing project into a [group](../../group/index.md) if:
-1. you have at least **Maintainer** [permissions] to that group
-1. you are an **Owner** of the project.
-
+1. You have at least **Maintainer** [permissions] to that group.
+1. The project is in a subgroup you own.
+1. You are at least a **Maintainer** of the project under your personal namespace.
Similarly, if you are an owner of a group, you can transfer any of its projects
under your own user.
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 96a1ccefbe5..4e99fec02dd 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -239,6 +239,7 @@ module API
end
end
+ expose :empty_repo?, as: :empty_repo
expose :archived?, as: :archived
expose :visibility
expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group }
@@ -302,6 +303,7 @@ module API
expose :commit_count
expose :storage_size
expose :repository_size
+ expose :wiki_size
expose :lfs_objects_size
expose :build_artifacts_size, as: :job_artifacts_size
end
@@ -354,6 +356,7 @@ module API
with_options format_with: -> (value) { value.to_i } do
expose :storage_size
expose :repository_size
+ expose :wiki_size
expose :lfs_objects_size
expose :build_artifacts_size, as: :job_artifacts_size
end
diff --git a/lib/api/helpers/issues_helpers.rb b/lib/api/helpers/issues_helpers.rb
index fc66cec5341..5b7199fddb0 100644
--- a/lib/api/helpers/issues_helpers.rb
+++ b/lib/api/helpers/issues_helpers.rb
@@ -3,6 +3,14 @@
module API
module Helpers
module IssuesHelpers
+ extend Grape::API::Helpers
+
+ params :optional_issue_params_ee do
+ end
+
+ params :optional_issues_params_ee do
+ end
+
def self.update_params_at_least_one_of
[
:assignee_id,
diff --git a/lib/api/helpers/protected_branches_helpers.rb b/lib/api/helpers/protected_branches_helpers.rb
new file mode 100644
index 00000000000..0fc6841d79a
--- /dev/null
+++ b/lib/api/helpers/protected_branches_helpers.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module ProtectedBranchesHelpers
+ extend ActiveSupport::Concern
+ extend Grape::API::Helpers
+
+ params :optional_params_ee do
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb
index 953be7f3798..44c577204b8 100644
--- a/lib/api/helpers/services_helpers.rb
+++ b/lib/api/helpers/services_helpers.rb
@@ -563,6 +563,12 @@ module API
name: :notify_only_broken_pipelines,
type: Boolean,
desc: 'Notify only broken pipelines'
+ },
+ {
+ required: false,
+ name: :notify_only_default_branch,
+ type: Boolean,
+ desc: 'Send notifications only for the default branch'
}
],
'pivotaltracker' => [
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 0b4da01f3c8..56960a2eb64 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -9,16 +9,6 @@ module API
before { authenticate_non_get! }
helpers do
- if Gitlab.ee?
- params :issues_params_ee do
- optional :weight, types: [Integer, String], integer_none_any: true, desc: 'The weight of the issue'
- end
-
- params :issue_params_ee do
- optional :weight, type: Integer, desc: 'The weight of the issue'
- end
- end
-
params :issues_stats_params do
optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
optional :milestone, type: String, desc: 'Milestone title'
@@ -47,7 +37,7 @@ module API
optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
optional :confidential, type: Boolean, desc: 'Filter confidential or public issues'
- use :issues_params_ee if Gitlab.ee?
+ use :optional_issues_params_ee
end
params :issues_params do
@@ -73,7 +63,7 @@ module API
optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
optional :discussion_locked, type: Boolean, desc: " Boolean parameter indicating if the issue's discussion is locked"
- use :issue_params_ee if Gitlab.ee?
+ use :optional_issue_params_ee
end
end
diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb
index f8cce1ed784..33dea25289a 100644
--- a/lib/api/protected_branches.rb
+++ b/lib/api/protected_branches.rb
@@ -8,6 +8,8 @@ module API
before { authorize_admin_project }
+ helpers Helpers::ProtectedBranchesHelpers
+
params do
requires :id, type: String, desc: 'The ID of a project'
end
@@ -52,29 +54,7 @@ module API
values: ProtectedBranch::MergeAccessLevel.allowed_access_levels,
desc: 'Access levels allowed to merge (defaults: `40`, maintainer access level)'
- if Gitlab.ee?
- optional :unprotect_access_level, type: Integer,
- values: ProtectedBranch::UnprotectAccessLevel.allowed_access_levels,
- desc: 'Access levels allowed to unprotect (defaults: `40`, maintainer access level)'
-
- optional :allowed_to_push, type: Array, desc: 'An array of users/groups allowed to push' do
- optional :access_level, type: Integer, values: ProtectedBranch::PushAccessLevel.allowed_access_levels
- optional :user_id, type: Integer
- optional :group_id, type: Integer
- end
-
- optional :allowed_to_merge, type: Array, desc: 'An array of users/groups allowed to merge' do
- optional :access_level, type: Integer, values: ProtectedBranch::MergeAccessLevel.allowed_access_levels
- optional :user_id, type: Integer
- optional :group_id, type: Integer
- end
-
- optional :allowed_to_unprotect, type: Array, desc: 'An array of users/groups allowed to unprotect' do
- optional :access_level, type: Integer, values: ProtectedBranch::UnprotectAccessLevel.allowed_access_levels
- optional :user_id, type: Integer
- optional :group_id, type: Integer
- end
- end
+ use :optional_params_ee
end
# rubocop: disable CodeReuse/ActiveRecord
post ':id/protected_branches' do
diff --git a/lib/gitlab/background_migration/calculate_wiki_sizes.rb b/lib/gitlab/background_migration/calculate_wiki_sizes.rb
new file mode 100644
index 00000000000..886c41a2b9d
--- /dev/null
+++ b/lib/gitlab/background_migration/calculate_wiki_sizes.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class CalculateWikiSizes
+ def perform(start_id, stop_id)
+ ::ProjectStatistics.where(wiki_size: nil)
+ .where(id: start_id..stop_id)
+ .includes(project: [:route, :group, namespace: [:owner]]).find_each do |statistics|
+ statistics.refresh!(only: [:wiki_size])
+ rescue => e
+ Rails.logger.error "Failed to update wiki statistics. id: #{statistics.id} message: #{e.message}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index 939112e6e41..87cd62ef2d4 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -50,7 +50,7 @@ variables:
POSTGRES_DB: $CI_ENVIRONMENT_SLUG
POSTGRES_VERSION: 9.6.2
- KUBERNETES_VERSION: 1.11.9
+ KUBERNETES_VERSION: 1.11.10
HELM_VERSION: 2.13.1
DOCKER_DRIVER: overlay2
diff --git a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
index 324e39c7747..5372ec6cceb 100644
--- a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
@@ -23,6 +23,9 @@ container_scanning:
DOCKER_HOST: tcp://${DOCKER_SERVICE}:2375/
# https://hub.docker.com/r/arminc/clair-local-scan/tags
CLAIR_LOCAL_SCAN_VERSION: v2.0.8_fe9b059d930314b54c78f75afe265955faf4fdc1
+ ## Disable the proxy for clair-local-scan, otherwise Container Scanning will
+ ## fail when a proxy is used.
+ NO_PROXY: ${DOCKER_SERVICE},localhost
allow_failure: true
services:
- docker:stable-dind
diff --git a/lib/gitlab/cluster/lifecycle_events.rb b/lib/gitlab/cluster/lifecycle_events.rb
index b05dca409d1..e0f9eb59924 100644
--- a/lib/gitlab/cluster/lifecycle_events.rb
+++ b/lib/gitlab/cluster/lifecycle_events.rb
@@ -44,6 +44,14 @@ module Gitlab
(@master_restart_hooks ||= []) << block
end
+ def on_master_start(&block)
+ if in_clustered_environment?
+ on_before_fork(&block)
+ else
+ on_worker_start(&block)
+ end
+ end
+
#
# Lifecycle integration methods (called from unicorn.rb, puma.rb, etc.)
#
diff --git a/lib/gitlab/metrics/samplers/puma_sampler.rb b/lib/gitlab/metrics/samplers/puma_sampler.rb
new file mode 100644
index 00000000000..87669b253bc
--- /dev/null
+++ b/lib/gitlab/metrics/samplers/puma_sampler.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'puma/state_file'
+
+module Gitlab
+ module Metrics
+ module Samplers
+ class PumaSampler < BaseSampler
+ def metrics
+ @metrics ||= init_metrics
+ end
+
+ def init_metrics
+ {
+ puma_workers: ::Gitlab::Metrics.gauge(:puma_workers, 'Total number of workers'),
+ puma_running_workers: ::Gitlab::Metrics.gauge(:puma_running_workers, 'Number of active workers'),
+ puma_stale_workers: ::Gitlab::Metrics.gauge(:puma_stale_workers, 'Number of stale workers'),
+ puma_phase: ::Gitlab::Metrics.gauge(:puma_phase, 'Phase number (increased during phased restarts)'),
+ puma_running: ::Gitlab::Metrics.gauge(:puma_running, 'Number of running threads'),
+ puma_queued_connections: ::Gitlab::Metrics.gauge(:puma_queued_connections, 'Number of connections in that worker\'s "todo" set waiting for a worker thread'),
+ puma_active_connections: ::Gitlab::Metrics.gauge(:puma_active_connections, 'Number of threads processing a request'),
+ puma_pool_capacity: ::Gitlab::Metrics.gauge(:puma_pool_capacity, 'Number of requests the worker is capable of taking right now'),
+ puma_max_threads: ::Gitlab::Metrics.gauge(:puma_max_threads, 'Maximum number of worker threads'),
+ puma_idle_threads: ::Gitlab::Metrics.gauge(:puma_idle_threads, 'Number of spawned threads which are not processing a request')
+ }
+ end
+
+ def sample
+ json_stats = puma_stats
+ return unless json_stats
+
+ stats = JSON.parse(json_stats)
+
+ if cluster?(stats)
+ sample_cluster(stats)
+ else
+ sample_single_worker(stats)
+ end
+ end
+
+ private
+
+ def puma_stats
+ Puma.stats
+ rescue NoMethodError
+ Rails.logger.info "PumaSampler: stats are not available yet, waiting for Puma to boot"
+ nil
+ end
+
+ def sample_cluster(stats)
+ set_master_metrics(stats)
+
+ stats['worker_status'].each do |worker|
+ labels = { worker: "worker_#{worker['index']}" }
+
+ metrics[:puma_phase].set(labels, worker['phase'])
+ set_worker_metrics(worker['last_status'], labels)
+ end
+ end
+
+ def sample_single_worker(stats)
+ metrics[:puma_workers].set({}, 1)
+ metrics[:puma_running_workers].set({}, 1)
+
+ set_worker_metrics(stats)
+ end
+
+ def cluster?(stats)
+ stats['worker_status'].present?
+ end
+
+ def set_master_metrics(stats)
+ labels = { worker: "master" }
+
+ metrics[:puma_workers].set(labels, stats['workers'])
+ metrics[:puma_running_workers].set(labels, stats['booted_workers'])
+ metrics[:puma_stale_workers].set(labels, stats['old_workers'])
+ metrics[:puma_phase].set(labels, stats['phase'])
+ end
+
+ def set_worker_metrics(stats, labels = {})
+ metrics[:puma_running].set(labels, stats['running'])
+ metrics[:puma_queued_connections].set(labels, stats['backlog'])
+ metrics[:puma_active_connections].set(labels, stats['max_threads'] - stats['pool_capacity'])
+ metrics[:puma_pool_capacity].set(labels, stats['pool_capacity'])
+ metrics[:puma_max_threads].set(labels, stats['max_threads'])
+ metrics[:puma_idle_threads].set(labels, stats['running'] + stats['pool_capacity'] - stats['max_threads'])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index aa2c1ac9cef..a07b1246bee 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -58,7 +58,6 @@ module Gitlab
uploads
users
v2
- visual-review-toolbar.js
].freeze
# This list should contain all words following `/*namespace_id/:project_id` in
diff --git a/lib/gitlab/rack_timeout_observer.rb b/lib/gitlab/rack_timeout_observer.rb
new file mode 100644
index 00000000000..80d3f7dea60
--- /dev/null
+++ b/lib/gitlab/rack_timeout_observer.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class RackTimeoutObserver
+ def initialize
+ @counter = Gitlab::Metrics.counter(:rack_state_total, 'Number of requests in a given rack state')
+ end
+
+ # returns the Proc to be used as the observer callback block
+ def callback
+ method(:log_timeout_exception)
+ end
+
+ private
+
+ def log_timeout_exception(env)
+ info = env[::Rack::Timeout::ENV_INFO_KEY]
+ return unless info
+
+ @counter.increment(labels(info, env))
+ end
+
+ def labels(info, env)
+ params = controller_params(env) || grape_params(env) || {}
+
+ {
+ controller: params['controller'],
+ action: params['action'],
+ route: params['route'],
+ state: info.state
+ }
+ end
+
+ def controller_params(env)
+ env['action_dispatch.request.parameters']
+ end
+
+ def grape_params(env)
+ endpoint = env[Grape::Env::API_ENDPOINT]
+ route = endpoint&.route&.pattern&.origin
+ return unless route
+
+ { 'route' => route }
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake
index 7a42e4e92a0..a07ae3a418a 100644
--- a/lib/tasks/gitlab/assets.rake
+++ b/lib/tasks/gitlab/assets.rake
@@ -10,9 +10,15 @@ namespace :gitlab do
rake:assets:precompile
webpack:compile
gitlab:assets:fix_urls
+ gitlab:assets:compile_vrt
].each(&Gitlab::TaskHelpers.method(:invoke_and_time_task))
end
+ desc 'GitLab | Assets | Compile visual review toolbar'
+ task :compile_vrt do
+ system 'yarn', 'webpack-vrt'
+ end
+
desc 'GitLab | Assets | Clean up old compiled frontend assets'
task clean: ['rake:assets:clean']
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c0b3eaf1e8b..f7352843b5b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -19,9 +19,6 @@ msgstr ""
msgid " Please sign in."
msgstr ""
-msgid " Status"
-msgstr ""
-
msgid " Try to %{action} this file again."
msgstr ""
@@ -110,7 +107,7 @@ msgstr ""
msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr ""
-msgid "%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS"
+msgid "%{counter_repositories} repositories, %{counter_wikis} wikis, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS"
msgstr ""
msgid "%{count} more"
@@ -955,6 +952,9 @@ msgstr ""
msgid "An error occurred whilst fetching the latest pipeline."
msgstr ""
+msgid "An error occurred whilst getting files for - %{branchId}"
+msgstr ""
+
msgid "An error occurred whilst loading all the files."
msgstr ""
@@ -1126,6 +1126,9 @@ msgstr ""
msgid "Are you sure you want to reset the health check token?"
msgstr ""
+msgid "Are you sure you want to revoke this nickname?"
+msgstr ""
+
msgid "Are you sure you want to stop this environment?"
msgstr ""
@@ -1482,6 +1485,9 @@ msgstr ""
msgid "Branch name"
msgstr ""
+msgid "Branch not loaded - %{branchId}"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr ""
@@ -1629,6 +1635,9 @@ msgstr ""
msgid "By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format."
msgstr ""
+msgid "By default, all projects and groups will use the global notifications setting."
+msgstr ""
+
msgid "ByAuthor|by"
msgstr ""
@@ -2990,6 +2999,9 @@ msgstr ""
msgid "Create a new branch"
msgstr ""
+msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes."
+msgstr ""
+
msgid "Create a new issue"
msgstr ""
@@ -4383,6 +4395,9 @@ msgstr ""
msgid "Files"
msgstr ""
+msgid "Files breadcrumb"
+msgstr ""
+
msgid "Files, directories, and submodules in the path %{path} for commit reference %{ref}"
msgstr ""
@@ -4653,6 +4668,9 @@ msgstr ""
msgid "Given access %{time_ago}"
msgstr ""
+msgid "Global notification settings"
+msgstr ""
+
msgid "Go Back"
msgstr ""
@@ -4809,6 +4827,9 @@ msgstr ""
msgid "Groups"
msgstr ""
+msgid "Groups (%{count})"
+msgstr ""
+
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
@@ -5614,6 +5635,9 @@ msgstr[1] ""
msgid "Last Pipeline"
msgstr ""
+msgid "Last accessed on"
+msgstr ""
+
msgid "Last activity"
msgstr ""
@@ -5641,6 +5665,9 @@ msgstr ""
msgid "Last updated"
msgstr ""
+msgid "Last used"
+msgstr ""
+
msgid "LastPushEvent|You pushed to"
msgstr ""
@@ -5781,6 +5808,9 @@ msgstr ""
msgid "MRDiff|Show full file"
msgstr ""
+msgid "Make and review changes in the browser with the Web IDE"
+msgstr ""
+
msgid "Make issue confidential."
msgstr ""
@@ -6377,6 +6407,9 @@ msgstr ""
msgid "Next"
msgstr ""
+msgid "Nickname"
+msgstr ""
+
msgid "No"
msgstr ""
@@ -6434,6 +6467,9 @@ msgstr ""
msgid "No file selected"
msgstr ""
+msgid "No files"
+msgstr ""
+
msgid "No files found."
msgstr ""
@@ -6557,6 +6593,9 @@ msgstr ""
msgid "Notification setting - %{notification_title}"
msgstr ""
+msgid "Notification settings saved"
+msgstr ""
+
msgid "NotificationEvent|Close issue"
msgstr ""
@@ -6608,6 +6647,9 @@ msgstr ""
msgid "NotificationLevel|Watch"
msgstr ""
+msgid "NotificationSetting|Custom"
+msgstr ""
+
msgid "Notifications"
msgstr ""
@@ -6847,6 +6889,9 @@ msgstr ""
msgid "Pipeline"
msgstr ""
+msgid "Pipeline ID (IID)"
+msgstr ""
+
msgid "Pipeline Schedule"
msgstr ""
@@ -7222,6 +7267,9 @@ msgstr ""
msgid "Profile Settings"
msgstr ""
+msgid "ProfileSession|on"
+msgstr ""
+
msgid "Profiles| You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account. Once you confirm %{deleteAccount}, it cannot be undone or recovered."
msgstr ""
@@ -7771,6 +7819,9 @@ msgstr ""
msgid "Projects"
msgstr ""
+msgid "Projects (%{count})"
+msgstr ""
+
msgid "Projects Successfully Retrieved"
msgstr ""
@@ -7999,6 +8050,9 @@ msgstr ""
msgid "Real-time features"
msgstr ""
+msgid "Receive notifications about your own activity"
+msgstr ""
+
msgid "Recent Project Activity"
msgstr ""
@@ -8585,6 +8639,9 @@ msgstr ""
msgid "Select Archive Format"
msgstr ""
+msgid "Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes."
+msgstr ""
+
msgid "Select a group to invite"
msgstr ""
@@ -8714,6 +8771,9 @@ msgstr ""
msgid "Serverless|There is currently no function data available from Knative. This could be for a variety of reasons including:"
msgstr ""
+msgid "Service"
+msgstr ""
+
msgid "Service Templates"
msgstr ""
@@ -8890,6 +8950,9 @@ msgstr ""
msgid "Sign-up restrictions"
msgstr ""
+msgid "Signed in"
+msgstr ""
+
msgid "Signed in with %{authentication} authentication"
msgstr ""
@@ -9352,6 +9415,69 @@ msgstr ""
msgid "Suggested change"
msgstr ""
+msgid "SuggestedColors|Bright Green"
+msgstr ""
+
+msgid "SuggestedColors|Dark grayish cyan"
+msgstr ""
+
+msgid "SuggestedColors|Dark moderate blue"
+msgstr ""
+
+msgid "SuggestedColors|Dark moderate orange"
+msgstr ""
+
+msgid "SuggestedColors|Dark moderate pink"
+msgstr ""
+
+msgid "SuggestedColors|Dark moderate violet"
+msgstr ""
+
+msgid "SuggestedColors|Feijoa"
+msgstr ""
+
+msgid "SuggestedColors|Moderate blue"
+msgstr ""
+
+msgid "SuggestedColors|Pure red"
+msgstr ""
+
+msgid "SuggestedColors|Slightly desaturated blue"
+msgstr ""
+
+msgid "SuggestedColors|Slightly desaturated green"
+msgstr ""
+
+msgid "SuggestedColors|Soft orange"
+msgstr ""
+
+msgid "SuggestedColors|Soft red"
+msgstr ""
+
+msgid "SuggestedColors|Strong pink"
+msgstr ""
+
+msgid "SuggestedColors|Strong red"
+msgstr ""
+
+msgid "SuggestedColors|Strong yellow"
+msgstr ""
+
+msgid "SuggestedColors|UA blue"
+msgstr ""
+
+msgid "SuggestedColors|Very dark desaturated blue"
+msgstr ""
+
+msgid "SuggestedColors|Very dark lime green"
+msgstr ""
+
+msgid "SuggestedColors|Very pale orange"
+msgstr ""
+
+msgid "SuggestedColors|lime green"
+msgstr ""
+
msgid "Sunday"
msgstr ""
@@ -9502,6 +9628,9 @@ msgstr ""
msgid "Team"
msgstr ""
+msgid "Team domain"
+msgstr ""
+
msgid "Template"
msgstr ""
@@ -9933,12 +10062,18 @@ msgstr ""
msgid "This is a delayed job to run in %{remainingTime}"
msgstr ""
+msgid "This is a list of devices that have logged into your account. Revoke any sessions that you do not recognize."
+msgstr ""
+
msgid "This is a security log of important events involving your account."
msgstr ""
msgid "This is the author's first Merge Request to this project."
msgstr ""
+msgid "This is your current session"
+msgstr ""
+
msgid "This issue is confidential"
msgstr ""
@@ -10327,6 +10462,9 @@ msgstr ""
msgid "To preserve performance only <strong>%{display_size} of %{real_size}</strong> files are displayed."
msgstr ""
+msgid "To specify the notification level per project of a group you belong to, you need to visit project page and change notification level there."
+msgstr ""
+
msgid "To start serving your jobs you can add Runners to your group"
msgstr ""
@@ -11313,9 +11451,15 @@ msgstr ""
msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr ""
+msgid "You can see your chat accounts."
+msgstr ""
+
msgid "You can set up jobs to only use Runners with specific tags. Separate tags with commas."
msgstr ""
+msgid "You can specify notification level per group or per project."
+msgstr ""
+
msgid "You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}."
msgstr ""
@@ -11346,6 +11490,9 @@ msgstr ""
msgid "You do not have permission to leave this %{namespaceType}."
msgstr ""
+msgid "You don't have any active chat names."
+msgstr ""
+
msgid "You don't have any applications"
msgstr ""
diff --git a/package.json b/package.json
index a4ec2dce0df..2b45f25e9c3 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,8 @@
"stylelint-create-utility-map": "node scripts/frontend/stylelint/stylelint-utility-map.js",
"test": "node scripts/frontend/test",
"webpack": "NODE_OPTIONS=\"--max-old-space-size=3584\" webpack --config config/webpack.config.js",
- "webpack-prod": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.js"
+ "webpack-prod": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.js",
+ "webpack-vrt": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.review_toolbar.js"
},
"dependencies": {
"@babel/core": "^7.4.4",
@@ -37,9 +38,11 @@
"@babel/preset-env": "^7.4.4",
"@gitlab/csslab": "^1.9.0",
"@gitlab/svgs": "^1.63.0",
- "@gitlab/ui": "^3.10.0",
+ "@gitlab/ui": "^3.10.1",
"apollo-cache-inmemory": "^1.5.1",
"apollo-client": "^2.5.1",
+ "apollo-link": "^1.2.11",
+ "apollo-link-batch-http": "^1.2.11",
"apollo-upload-client": "^10.0.0",
"at.js": "^1.5.4",
"autosize": "^4.0.0",
diff --git a/qa/README.md b/qa/README.md
index 002ad4c65f5..f75205133e6 100644
--- a/qa/README.md
+++ b/qa/README.md
@@ -49,10 +49,10 @@ will need to [modify your GDK setup](https://gitlab.com/gitlab-org/gitlab-qa/blo
### Writing tests
-- [Writing tests from scratch tutorial](docs/WRITING_TESTS_FROM_SCRATCH.md)
- - [Best practices](docs/BEST_PRACTICES.md)
+- [Writing tests from scratch tutorial](docs/writing_tests_from_scratch.md)
+ - [Best practices](docs/best_practices.md)
- [Using page objects](qa/page/README.md)
- - [Guidelines](docs/GUIDELINES.md)
+ - [Guidelines](docs/guidelines.md)
### Running specific tests
diff --git a/qa/docs/GUIDELINES.md b/qa/docs/GUIDELINES.md
deleted file mode 100644
index 9db52cd07e6..00000000000
--- a/qa/docs/GUIDELINES.md
+++ /dev/null
@@ -1,46 +0,0 @@
-# Style guide for writing GUI tests
-
-This document describes the conventions used at GitLab for writing GUI tests using the GitLab QA project.
-
-## `click_` versus `go_to_`
-
-### When to use `click_`?
-
-When clicking in a single link to navigate, use `click_`.
-
-E.g.:
-
-```ruby
-def click_ci_cd_pipelines
- within_sidebar do
- click_element :link_pipelines
- end
-end
-```
-
-From a testing perspective, if we want to check that clicking a link, or a button (a single interaction) is working as intended, we would want the test to read as:
-
-- Click a certain element
-- Verify the action took place
-
-### When to use `go_to_`?
-
-When interacting with multiple elements to go to a page, use `go_to_`.
-
-E.g.:
-
-```ruby
-def go_to_operations_environments
- hover_operations do
- within_submenu do
- click_element(:operations_environments_link)
- end
- end
-end
-```
-
-`go_to_` fits the definition of interacting with multiple elements very well given it's more of a meta-navigation action that includes multiple interactions.
-
-Notice that in the above example, before clicking the `:operations_environments_link`, another element is hovered over.
-
-> We can create these methods as helpers to abstract multi-step navigation.
diff --git a/qa/docs/WRITING_TESTS_FROM_SCRATCH.md b/qa/docs/WRITING_TESTS_FROM_SCRATCH.md
deleted file mode 100644
index 309fcc4064c..00000000000
--- a/qa/docs/WRITING_TESTS_FROM_SCRATCH.md
+++ /dev/null
@@ -1,482 +0,0 @@
-# Writing end-to-end tests step-by-step
-
-In this tutorial, you will find different examples, and the steps involved, in the creation of end-to-end (_e2e_) tests for GitLab CE and GitLab EE, using GitLab QA.
-
-> When referring to end-to-end tests in this document, this means testing a specific feature end-to-end, such as a user logging in, the creation of a project, the management of labels, breaking down epics into sub-epics and issues, etc.
-
-## Important information before we start writing tests
-
-It's important to understand that end-to-end tests of isolated features, such as the ones described in the above note, doesn't mean that everything needs to happen through the GUI.
-
-If you don't exactly understand what we mean by **not everything needs to happen through the GUI,** please make sure you've read the [best practices](./BEST_PRACTICES.md) before moving on.
-
-## This document covers the following items:
-
-0. Identifying if end-to-end tests are really needed
-1. Identifying the [DevOps stage](https://about.gitlab.com/stages-devops-lifecycle/) of the feature that you are going to cover with end-to-end tests
-2. Creating the skeleton of the test file (`*_spec.rb`)
-3. The [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc) of the test cases logic
-4. Extracting duplicated code into methods
-5. Tests' pre-conditions (`before :all` and `before`) using resources and [Page Objects](./qa/page/README.md)
-6. Optimizing the test suite
-7. Using and implementing resources
-8. Moving elements definitions and its methods to [Page Objects](./qa/page/README.md)
- - Adding testability to the application
-
-### 0. Are end-to-end tests needed?
-
-At GitLab we respect the [test pyramid](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/testing_guide/testing_levels.md), and so, we recommend to check the code coverage of a specific feature before writing end-to-end tests.
-
-Sometimes you may notice that there is already a good coverage in other test levels, and we can stay confident that if we break a feature, we will still have quick feedback about it, even without having end-to-end tests.
-
-If after this analysis you still think that end-to-end tests are needed, keep reading.
-
-### 1. Identifying the DevOps stage
-
-The GitLab QA end-to-end tests are organized by the different [stages in the DevOps lifecycle](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa/specs/features/browser_ui), and so, if you are creating tests for issue creation, for instance, you would locate the spec files under the `qa/qa/specs/features/browser_ui/2_plan/` directory since issue creation is part of the Plan stage.
-
- In another case of a test for listing merged merge requests (MRs), the test should go under the `qa/qa/specs/features/browser_ui/3_create/` directory since merge request is a feature from the Create stage.
-
-> There may be sub-directories inside the stages directories, for different features. For example: `.../browser_ui/2_plan/ee_epics/` and `.../browser_ui/2_plan/issues/`.
-
-Now, let's say we want to create tests for the [scoped labels](https://about.gitlab.com/2019/04/22/gitlab-11-10-released/#scoped-labels) feature, available on GitLab EE Premium (this feature is part of the Plan stage.)
-
-> Because these tests are for a feature available only on GitLab EE, we need to create them in the [EE repository](https://gitlab.com/gitlab-org/gitlab-ee).
-
-Since [there is no specific directory for this feature](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/qa/qa/specs/features/browser_ui/2_plan), we should create a sub-directory for it.
-
-Under `.../browser_ui/2_plan/`, let's create a sub-directory called `ee_scoped_labels/`.
-
-> Notice that since this feature is only available for GitLab EE we prefix the sub-directory with `ee_`.
-
-### 2. Test skeleton
-
-Inside the newly created sub-directory, let's create a file describing the test suite (e.g. `editing_scoped_labels_spec.rb`.)
-
-#### The `context` and `describe` blocks
-
-Specs have an outer `context` that indicates the DevOps stage. The next level is the `describe` block, that briefly states the subject of the test suite. See the following example:
-
-```ruby
-module QA
- context 'Plan' do
- describe 'Editing scoped labels properties on issues' do
- end
- end
-end
-```
-
-#### The `it` blocks
-
-Every test suite is composed by at least one `it` block, and a good way to start writing end-to-end tests is by typing test cases descriptions as `it` blocks. Take a look at the following example:
-
-```ruby
-module QA
- context 'Plan' do
- describe 'Editing scoped labels properties on issues' do
- it 'replaces an existing label if it has the same key' do
- end
-
- it 'keeps both scoped labels when adding a label with a different key' do
- end
- end
- end
-end
-```
-
-### 3. Test cases MVC
-
-For the [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc) of our test cases, let's say that we already have the application in the state needed for the tests, and then let's focus on the logic of the test cases only.
-
-To evolve the test cases drafted on step 2, let's imagine that the user is already logged in a GitLab EE instance, they already have at least a Premium license in use, there is already a project created, there is already an issue opened in the project, the issue already has a scoped label (e.g. `foo::bar`), there are other scoped labels (for the same scope and for a different scope, e.g. `foo::baz` and `bar::bah`), and finally, the user is already on the issue's page. Let's also suppose that for every test case the application is in a clean state, meaning that one test case won't affect another.
-
-> Note: there are different approaches to create an application state for end-to-end tests. Some of them are very time consuming and subject to failures, such as when using the GUI for all the pre-conditions of the tests. On the other hand, other approaches are more efficient, such as using the public APIs. The latter is more efficient since it doesn't depend on the GUI. We won't focus on this part yet, but it's good to keep it in mind.
-
-Let's now focus on the first test case.
-
-```ruby
-it 'keeps the latest scoped label when adding a label with the same key of an existing one, but with a different value' do
- # This implementation is only for tutorial purposes. We normally encapsulate elements in Page Objects.
- page.find('.block.labels .edit-link').click
- page.find('.dropdown-menu-labels .dropdown-input-field').send_keys ['foo::baz', :enter]
- page.find('#content-body').click
- page.refresh
-
- scoped_label = page.find('.qa-labels-block .scoped-label-wrapper')
-
- expect(scoped_label).to have_content('foo::baz')
- expect(scoped_label).not_to have_content('foo::bar')
- expect(page).to have_content('added foo::baz label and removed foo::bar')
-end
-```
-
-> Notice that the test itself is simple. The most challenging part is the creation of the application state, which will be covered later.
-
-> The exemplified test cases' MVC is not enough for the change to be submitted in an MR, but they help on building up the test logic. The reason is that we do not want to use locators directly in the tests, and tests **must** use [Page Objects](./qa/page/README.md) before they can be merged.
-
-Below are the steps that the test covers:
-
-1. The test finds the 'Edit' link for the labels and clicks on it
-2. Then it fills in the 'Assign labels' input field with the value 'foo::baz' and press enter
-3. Then it clicks in the content body to apply the label and refreshes the page
-4. Finally the expectation that the previous scoped label was removed and that the new one was added happens
-
-Let's now see how the second test case would look like.
-
-```ruby
-it 'keeps both scoped labels when adding a label with a different key' do
- # This implementation is only for tutorial purposes. We normally encapsulate elements in Page Objects.
- page.find('.block.labels .edit-link').click
- page.find('.dropdown-menu-labels .dropdown-input-field').send_keys ['bar::bah', :enter]
- page.find('#content-body').click
- page.refresh
-
- scoped_labels = page.all('.qa-labels-block .scoped-label-wrapper')
-
- expect(scoped_labels.first).to have_content('bar::bah')
- expect(scoped_labels.last).to have_content('foo::ba')
- expect(page).to have_content('added bar::bah')
- expect(page).to have_content('added foo::ba')
-end
-```
-
-> Note that elements are always located using CSS selectors, and a good practice is to add test specific attribute:value for elements (this is called adding testability to the application and we will talk more about it later.)
-
-Below are the steps that the test covers:
-
-1. The test finds the 'Edit' link for the labels and clicks on it
-2. Then it fills in the 'Assign labels' input field with the value 'bar::bah' and press enter
-3. Then it clicks in the content body to apply the label and refreshes the page
-4. Finally the expectation that the both scoped labels are present happens
-
-> Similar to the previous test, this one is also very straight forward, but there is some code duplication. Let's address it.
-
-### 4. Extracting duplicated code
-
-If we refactor the tests created on step 3 we could come up with something like this:
-
-```ruby
-it 'keeps the latest scoped label when adding a label with the same key of an existing one, but with a different value' do
- select_label_and_refresh 'foo::baz'
-
- expect(page).to have_content('added foo::baz')
- expect(page).to have_content('and removed foo::bar')
-
- scoped_label = page.find('.qa-labels-block .scoped-label-wrapper')
-
- expect(scoped_label).to have_content('foo::baz')
- expect(scoped_label).not_to have_content('foo::bar')
-end
-
-it 'keeps both scoped label when adding a label with a different key' do
- select_label_and_refresh 'bar::bah'
-
- expect(page).to have_content('added bar::bah')
- expect(page).to have_content('added foo::ba')
-
- scoped_labels = page.all('.qa-labels-block .scoped-label-wrapper')
-
- expect(scoped_labels.first).to have_content('bar::bah')
- expect(scoped_labels.last).to have_content('foo::ba')
-end
-
-def select_label_and_refresh(label)
- page.find('.block.labels .edit-link').click
- page.find('.dropdown-menu-labels .dropdown-input-field').send_keys [label, :enter]
- page.find('#content-body').click
- page.refresh
-end
-```
-
-By creating a reusable `select_label_and_refresh` method, we remove the code duplication, and later we can move this method to a Page Object class that will be created for easier maintenance purposes.
-
-> Notice that the reusable method is created in the bottom of the file. The reason for that is that reading the code should be similar to reading a newspaper, where high-level information is at the top, like the title and summary of the news, while low level, or more specific information, is at the bottom.
-
-### 5. Tests' pre-conditions using resources and Page Objects
-
-In this section, we will address the previously mentioned subject of creating the application state for the tests, using the `before :all` and `before` blocks, together with resources and Page Objects.
-
-#### `before :all`
-
-A pre-condition for the entire test suite is defined in the `before :all` block.
-
-For our test suite example, some things that could happen before the entire test suite starts are:
-
-- The user logging in;
-- A premium license already being set up;
-- A project being created with an issue and labels already setup.
-
-> In case of a test suite with only one `it` block it's ok to use only the `before` block (see below) with all the test's pre-conditions.
-
-#### `before`
-
-A pre-condition for each test case is defined in the `before` block.
-
-For our test cases samples, what we need is that for every test the issue page is opened, and there is only one scoped label applied to it.
-
-#### Implementation
-
-In the following code we will focus on the test suite and the test cases' pre-conditions only:
-
-```ruby
-module QA
- context 'Plan' do
- describe 'Editing scoped labels properties on issues' do
- before :all do
- project = Resource::Project.fabricate_via_api! do |resource|
- resource.name = 'scoped-labels-project'
- end
-
- @foo_bar_scoped_label = 'foo::bar'
-
- @issue = Resource::Issue.fabricate_via_api! do |issue|
- issue.project = project
- issue.title = 'Issue to test the scoped labels'
- issue.labels = @foo_bar_scoped_label
- end
-
- @labels = ['foo::baz', 'bar::bah']
- @labels.each do |label|
- Resource::Label.fabricate_via_api! do |l|
- l.project = project.id
- l.title = label
- end
- end
-
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
- end
-
- before do
- Page::Project::Issue::Show.perform do |issue_page|
- @issue.visit!
- end
- end
-
- it 'keeps the latest scoped label when adding a label with the same key of an existing one, but with a different value' do
- ...
- end
-
- it 'keeps both scoped labels when adding a label with a different key' do
- ...
- end
-
- def select_label_and_refresh(label)
- ...
- end
- end
- end
-end
-```
-
-In the `before :all` block we create all the application state needed for the tests to run. We do that by fabricating resources via APIs (`project`, `@issue`, and `@labels`), by using the `Runtime::Browser.visit` method to go to the login page, and by performing a `sign_in_using_credentials` from the `Login` Page Object.
-
-> When creating the resources, notice that when calling the `fabricate_via_api` method, we pass some attribute:values, like `name` for the `project` resource; `project`, `title`, and `labels` for the `issue` resource; and `project`, and `title` for `label` resources.
-
-> What's important to understand here is that by creating the application state mostly using the public APIs we save a lot of time in the test suite setup stage.
-
-> Soon we will cover the use of the already existing resources' methods and the creation of your own `fabricate_via_api` methods for resources where this is still not available, but first, let's optimize our implementation.
-
-### 6. Optimization
-
-As already mentioned in the [best practices](./BEST_PRACTICES.md) document, end-to-end tests are very costly in terms of execution time, and it's our responsibility as software engineers to ensure that we optimize them as much as possible.
-
-> Differently than unit tests, that exercise every little piece of the application in isolation, usually having only one assertion per test, and being very fast to run, end-to-end tests can have more actions and assertions in a single test to help on speeding up the test's feedback since they are much slower when comparing to unit tests.
-
-Some improvements that we could make in our test suite to optimize its time to run are:
-
-1. Having a single test case (an `it` block) that exercise both scenarios to avoid "wasting" time in the tests' pre-conditions, instead of having two different test cases.
-2. Moving all the pre-conditions to the `before` block since there will be only one `it` block.
-3. Making the selection of labels more performant by allowing for the selection of more than one label in the same reusable method.
-
-Let's look at a suggestion that addresses the above points, one by one:
-
-```ruby
-module QA
- context 'Plan' do
- describe 'Editing scoped labels properties on issues' do
- before do
- project = Resource::Project.fabricate_via_api! do |resource|
- resource.name = 'scoped-labels-project'
- end
-
- @foo_bar_scoped_label = 'foo::bar'
-
- @issue = Resource::Issue.fabricate_via_api! do |issue|
- issue.project = project
- issue.title = 'Issue to test the scoped labels'
- issue.labels = @foo_bar_scoped_label
- end
-
- @labels = ['foo::baz', 'bar::bah']
- @labels.each do |label|
- Resource::Label.fabricate_via_api! do |l|
- l.project = project.id
- l.title = label
- end
- end
-
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_credentials)
- Page::Project::Issue::Show.perform do |issue_page|
- @issue.visit!
- end
- end
-
- it 'correctly applies the scoped labels depending if they are from the same or a different scope' do
- select_labels_and_refresh @labels
-
- scoped_labels = page.all('.qa-labels-block .scoped-label-wrapper')
-
- expect(page).to have_content("added #{@foo_bar_scoped_label}")
- expect(page).to have_content("added #{@labels[1]} #{@labels[0]} labels and removed #{@foo_bar_scoped_label}")
- expect(scoped_labels.count).to eq(2)
- expect(scoped_labels.first).to have_content(@labels[1])
- expect(scoped_labels.last).to have_content(@labels[0])
- end
-
- def select_labels_and_refresh(labels)
- find('.block.labels .edit-link').click
- labels.each do |label|
- find('.dropdown-menu-labels .dropdown-input-field').send_keys [label, :enter]
- end
- find('#content-body').click
- refresh
- end
- end
- end
- end
-```
-
-As you can see, now all the pre-conditions from the `before :all` block were moved to the `before` block, addressing point 2.
-
-To address point 1, we changed the test implementation from two `it` blocks into a single one that exercises both scenarios. Now the new test description is: `'correctly applies the scoped labels depending if they are from the same or a different scope'`. It's a long description, but it describes well what the test does.
-
-> Notice that the implementation of the new and unique `it` block had to change a little bit. Below we describe in details what it does.
-
-1. At the same time, it selects two scoped labels, one from the same scope of the one already applied in the issue during the setup phase (in the `before` block), and another one from a different scope.
-2. It runs the assertions that the labels where correctly added and removed; that only two labels are applied; and that those are the correct ones, and that they are shown in the right order.
-
-Finally, the `select_label_and_refresh` method is changed to `select_labels_and_refresh`, which accepts an array of labels instead of a single label, and it iterates on them for faster label selection (this is what is used in step 1 explained above.)
-
-### 7. Resources
-
-You can think of resources as anything that can be created on GitLab CE or EE, either through the GUI, the API, or the CLI.
-
-With that in mind, resources can be a project, an epic, an issue, a label, a commit, etc.
-
-As you saw in the tests' pre-conditions and the optimization sections, we're already creating some of these resources, and we are doing that by calling the `fabricate_via_api!` method.
-
-> We could be using the `fabricate!` method instead, which would use the `fabricate_via_api!` method if it exists, and fallback to GUI fabrication otherwise, but we recommend being explicit to make it clear what the test does. Also, we recommend fabricating resources via API since this makes tests faster and more reliable, unless the test is focusing on the GUI itself, or there's no GUI coverage for that specific part in any other test.
-
-For our test suite example, the [project resource](https://gitlab.com/gitlab-org/gitlab-ee/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/project.rb#L55) already had a `fabricate_via_api!` method available, while other resources don't have it, so we will have to create them, like for the issue and label resources. Also, we will have to make a small change in the project resource to expose its `id` attribute so that we can refer to it when fabricating the issue.
-
-#### Implementation
-
-Following we describe the changes needed in every of the before-mentioned resource files.
-
-**Project resource**
-
-Let's start with the smallest change.
-
-In the [project resource](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/project.rb), let's expose its `id` attribute.
-
-Add the following `attribute :id` right below the [`attribute :description`](https://gitlab.com/gitlab-org/gitlab-ee/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/project.rb#L11).
-
-> This line is needed to allow for issues and labels to be automatically added to a project when fabricating them via API.
-
-**Issue resource**
-
-Now, let's make it possible to create an issue resource through the API.
-
-First, in the [issue resource](https://gitlab.com/gitlab-org/gitlab-ee/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/issue.rb), let's expose its labels attribute.
-
-Add the following `attribute :labels` right below the [`attribute :title`](https://gitlab.com/gitlab-org/gitlab-ee/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/issue.rb#L15).
-
-> This line is needed to allow for labels to be automatically added to an issue when fabricating it via API.
-
-Next, add the following code right below the [`fabricate!`](https://gitlab.com/gitlab-org/gitlab-ee/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/issue.rb#L27) method.
-
-```ruby
-def api_get_path
- "/projects/#{project.id}/issues/#{id}"
-end
-
-def api_post_path
- "/projects/#{project.id}/issues"
-end
-
-def api_post_body
- {
- title: title,
- labels: [labels]
- }
-end
-```
-
-By defining the `api_get_path` method, we allow the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to get a single issue.
-
-> This `GET` path can be found in the [public API documentation](https://docs.gitlab.com/ee/api/issues.html#single-issue).
-
-By defining the `api_post_path` method, we allow the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to create a new issue in a specific project.
-
-> This `POST` path can be found in the [public API documentation](https://docs.gitlab.com/ee/api/issues.html#new-issue).
-
-By defining the `api_post_body` method, we allow the [`ApiFabricator.api_post`](https://gitlab.com/gitlab-org/gitlab-ee/blob/a9177ca1812bac57e2b2fa4560e1d5dd8ffac38b/qa/qa/resource/api_fabricator.rb#L68) method to know which data to send when making the `POST` request.
-
-> Notice that we pass both `title` and `labels` attributes in the `api_post_body`, where `labels` receives an array of labels, and [`title` is required](https://docs.gitlab.com/ee/api/issues.html#new-issue).
-
-**Label resource**
-
-Finally, let's make it possible to create label resources through the API.
-
-Add the following code right below the [`fabricate!`](https://gitlab.com/gitlab-org/gitlab-ee/blob/a9177ca1812bac57e2b2fa4560e1d5dd8ffac38b/qa/qa/resource/label.rb#L36) method.
-
-```ruby
-def resource_web_url(resource)
- super
-rescue ResourceURLMissingError
- # this particular resource does not expose a web_url property
-end
-
-def api_get_path
- raise NotImplementedError, "The Labels API doesn't expose a single-resource endpoint so this method cannot be properly implemented."
-end
-
-def api_post_path
- "/projects/#{project}/labels"
-end
-
-def api_post_body
- {
- name: @title,
- color: @color
- }
-end
-```
-
-By defining the `resource_web_url(resource)` method, we override the one from the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb#L44) module. We do that to avoid failing the test due to this particular resource not exposing a `web_url` property.
-
-By defining the `api_get_path` method, we **would** allow for the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to get a single label, but since there's no path available for that in the publich API, we raise a `NotImplementedError` instead.
-
-By defining the `api_post_path` method, we allow for the [`ApiFabricator `](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to create a new label in a specific project.
-
-By defining the `api_post_body` method, we we allow for the [`ApiFabricator.api_post`](https://gitlab.com/gitlab-org/gitlab-ee/blob/a9177ca1812bac57e2b2fa4560e1d5dd8ffac38b/qa/qa/resource/api_fabricator.rb#L68) method to know which data to send when making the `POST` request.
-
-> Notice that we pass both `name` and `color` attributes in the `api_post_body` since [those are required](https://docs.gitlab.com/ee/api/labels.html#create-a-new-label).
-
-### 8. Page Objects
-
-> Page Objects are auto-loaded in the `qa/qa.rb` file and available in all the test files (`*_spec.rb`).
-
-Page Objects are used in end-to-end tests for maintenance reasons, where page's elements and methods are defined to be reused in any test.
-
-Take a look at [this document that specifically details the usage of Page Objects](./qa/page/README.md).
-
-Now, let's go back to our examples.
-
-...
-
-#### Adding testability
-
-TBD.
diff --git a/qa/docs/BEST_PRACTICES.md b/qa/docs/best_practices.md
index 3a2640607e4..62a90563bf0 100644
--- a/qa/docs/BEST_PRACTICES.md
+++ b/qa/docs/best_practices.md
@@ -4,7 +4,7 @@ The majority of the end-to-end tests require some state to be built in the appli
A good example is a user being logged in as a pre-condition for testing the feature.
-But if the login feature is already covered with end-to-end tests through the GUI, there is no reason to perform such an expensive task to test the functionality of creating a project, or importing a repo, even if this features depend on a user being logged in. Let's see an example to make things clear.
+But if the login feature is already covered with end-to-end tests through the GUI, there is no reason to perform such an expensive task to test the functionality of creating a project, or importing a repo, even if these features depend on a user being logged in. Let's see an example to make things clear.
Let's say that, on average, the process to perform a successful login through the GUI takes 2 seconds.
@@ -33,6 +33,6 @@ Finally, interacting with the application only by its GUI generates a higher rat
**The takeaways here are:**
- Building state through the GUI is time consuming and it's not sustainable as the test suite grows.
-- When depending only on the GUI to create the application's state and tests fail due to front-end issues, we can't rely on the test failures rate, and we generates a higher rate of test flakiness.
+- When depending only on the GUI to create the application's state and tests fail due to front-end issues, we can't rely on the test failures rate, and we generate a higher rate of test flakiness.
-Now that we are aware of all of it, [let's go create some tests](./WRITING_TESTS_FROM_SCRATCH.md).
+Now that we are aware of all of it, [let's go create some tests](writing_tests_from_scratch.md#this-document-covers-the-following-items).
diff --git a/qa/docs/guidelines.md b/qa/docs/guidelines.md
new file mode 100644
index 00000000000..cd4b939fd71
--- /dev/null
+++ b/qa/docs/guidelines.md
@@ -0,0 +1,97 @@
+# Style guide for writing E2E tests
+
+This document describes the conventions used at GitLab for writing E2E tests using the GitLab QA project.
+
+## `click_` versus `go_to_`
+
+### When to use `click_`?
+
+When clicking in a single link to navigate, use `click_`.
+
+E.g.:
+
+```ruby
+def click_ci_cd_pipelines
+ within_sidebar do
+ click_element :link_pipelines
+ end
+end
+```
+
+From a testing perspective, if we want to check that clicking a link, or a button (a single interaction) is working as intended, we would want the test to read as:
+
+- Click a certain element
+- Verify the action took place
+
+### When to use `go_to_`?
+
+When interacting with multiple elements to go to a page, use `go_to_`.
+
+E.g.:
+
+```ruby
+def go_to_operations_environments
+ hover_operations do
+ within_submenu do
+ click_element(:operations_environments_link)
+ end
+ end
+end
+```
+
+`go_to_` fits the definition of interacting with multiple elements very well given it's more of a meta-navigation action that includes multiple interactions.
+
+Notice that in the above example, before clicking the `:operations_environments_link`, another element is hovered over.
+
+> We can create these methods as helpers to abstract multi-step navigation.
+
+### Element Naming Convention
+
+When adding new elements to a page, it's important that we have a uniform element naming convention.
+
+We follow a simple formula roughly based on hungarian notation.
+
+*Formula*: `element :<descriptor>_<type>`
+
+- `descriptor`: The natural-language description of what the element is. On the login page, this could be `username`, or `password`.
+- `type`: A physical control on the page that can be seen by a user.
+ - `_button`
+ - `_link`
+ - `_tab`
+ - `_dropdown`
+ - `_field`
+ - `_checkbox`
+ - `_radio`
+ - `_content`
+
+*Note: This list is a work in progress. This list will eventually be the end-all enumeration of all available types.
+ I.e., any element that does not end with something in this list is bad form.*
+
+#### Examples
+
+**Good**
+```ruby
+view '...' do
+ element :edit_button
+ element :notes_tab
+ element :squash_checkbox
+ element :username_field
+ element :issue_title_content
+end
+```
+
+**Bad**
+```ruby
+view '...' do
+ # `_confirmation` should be `_field`. what sort of confirmation? a checkbox confirmation? no real way to disambiguate.
+ # an appropriate replacement would be `element :password_confirmation_field`
+ element :password_confirmation
+
+ # `clone_options` is too vague. If it's a dropdown menu, it should be `clone_dropdown`.
+ # If it's a checkbox, it should be `clone_checkbox`
+ element :clone_options
+
+ # how is this url being displayed? is it a textbox? a simple span?
+ element :ssh_clone_url
+end
+```
diff --git a/qa/docs/writing_tests_from_scratch.md b/qa/docs/writing_tests_from_scratch.md
new file mode 100644
index 00000000000..06b60a1ef45
--- /dev/null
+++ b/qa/docs/writing_tests_from_scratch.md
@@ -0,0 +1,582 @@
+# Writing end-to-end tests step-by-step
+
+In this tutorial, you will find different examples, and the steps involved, in the creation of end-to-end (_e2e_) tests for GitLab CE and GitLab EE, using GitLab QA.
+
+> When referring to end-to-end tests in this document, this means testing a specific feature end-to-end, such as a user logging in, the creation of a project, the management of labels, breaking down epics into sub-epics and issues, etc.
+
+## Important information before we start writing tests
+
+It's important to understand that end-to-end tests of isolated features, such as the ones described in the above note, doesn't mean that everything needs to happen through the GUI.
+
+If you don't exactly understand what we mean by **not everything needs to happen through the GUI,** please make sure you've read the [best practices](best_practices.md) before moving on.
+
+## This document covers the following items:
+
+- [0.](#0-are-end-to-end-tests-needed) Identifying if end-to-end tests are really needed
+- [1.](#1-identifying-the-devops-stage) Identifying the [DevOps stage](https://about.gitlab.com/stages-devops-lifecycle/) of the feature that you are going to cover with end-to-end tests
+- [2.](#2-test-skeleton) Creating the skeleton of the test file (`*_spec.rb`)
+- [3.](#3-test-cases-mvc) The [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc) of the test cases' logic
+- [4.](#4-extracting-duplicated-code) Extracting duplicated code into methods
+- [5.](#5-tests-pre-conditions-using-resources-and-page-objects) Tests' pre-conditions (`before :context` and `before`) using resources and [Page Objects](./qa/qa/page/README.md)
+- [6.](#6-optimization) Optimizing the test suite
+- [7.](#7-resources) Using and implementing resources
+- [8.](#8-page-objects) Moving element definitions and methods to [Page Objects](./qa/qa/page/README.md)
+
+### 0. Are end-to-end tests needed?
+
+At GitLab we respect the [test pyramid](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/testing_guide/testing_levels.md), and so, we recommend you check the code coverage of a specific feature before writing end-to-end tests, for both [CE](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby/#_AllFiles) and [EE](https://gitlab-org.gitlab.io/gitlab-ee/coverage-ruby/#_AllFiles) projects.
+
+Sometimes you may notice that there is already good coverage in other test levels, and we can stay confident that if we break a feature, we will still have quick feedback about it, even without having end-to-end tests.
+
+If after this analysis you still think that end-to-end tests are needed, keep reading.
+
+### 1. Identifying the DevOps stage
+
+The GitLab QA end-to-end tests are organized by the different [stages in the DevOps lifecycle](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa/specs/features/browser_ui), and so, if you are creating tests for issue creation, for instance, you would locate the spec files under the `qa/qa/specs/features/browser_ui/2_plan/` directory since issue creation is part of the Plan stage.
+
+ In another case of a test for listing merged merge requests (MRs), the test should go under the `qa/qa/specs/features/browser_ui/3_create/` directory since merge requests are a feature from the Create stage.
+
+> There may be sub-directories inside the stages directories, for different features. For example: `.../browser_ui/2_plan/ee_epics/` and `.../browser_ui/2_plan/issues/`.
+
+Now, let's say we want to create tests for the [scoped labels](https://about.gitlab.com/2019/04/22/gitlab-11-10-released/#scoped-labels) feature, available on GitLab EE Premium (this feature is part of the Plan stage.)
+
+> Because these tests are for a feature available only on GitLab EE, we need to create them in the [EE repository](https://gitlab.com/gitlab-org/gitlab-ee).
+
+Since [there is no specific directory for this feature](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/qa/qa/specs/features/browser_ui/2_plan), we should create a sub-directory for it.
+
+Under `.../browser_ui/2_plan/`, let's create a sub-directory called `ee_scoped_labels/`.
+
+> Notice that since this feature is only available for GitLab EE we prefix the sub-directory with `ee_`.
+
+### 2. Test skeleton
+
+Inside the newly created sub-directory, let's create a file describing the test suite (e.g. `editing_scoped_labels_spec.rb`.)
+
+#### The `context` and `describe` blocks
+
+Specs have an outer `context` that indicates the DevOps stage. The next level is the `describe` block, that briefly states the subject of the test suite. See the following example:
+
+```ruby
+module QA
+ context 'Plan' do
+ describe 'Editing scoped labels on issues' do
+ end
+ end
+end
+```
+
+#### The `it` blocks
+
+Every test suite is composed of at least one `it` block, and a good way to start writing end-to-end tests is by writing test cases descriptions as `it` blocks. These might help you to think of different test scenarios. Take a look at the following example:
+
+```ruby
+module QA
+ context 'Plan' do
+ describe 'Editing scoped labels on issues' do
+ it 'replaces an existing label if it has the same key' do
+ end
+
+ it 'keeps both scoped labels when adding a label with a different key' do
+ end
+ end
+ end
+end
+```
+
+### 3. Test cases MVC
+
+For the [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc) of our test cases, let's say that we already have the application in the state needed for the tests, and then let's focus on the logic of the test cases only.
+
+To evolve the test cases drafted on step 2, let's imagine that the user is already logged into a GitLab EE instance, they already have at least a Premium license in use, there is already a project created, there is already an issue opened in the project, the issue already has a scoped label (e.g. `animal::fox`), there are other scoped labels (for the same scope and for a different scope (e.g. `animal::dolphin` and `plant::orchid`), and finally, the user is already on the issue's page. Let's also suppose that for every test case the application is in a clean state, meaning that one test case won't affect another.
+
+> Note: there are different approaches to creating an application state for end-to-end tests. Some of them are very time consuming and subject to failures, such as when using the GUI for all the pre-conditions of the tests. On the other hand, other approaches are more efficient, such as using the public APIs. The latter is more efficient since it doesn't depend on the GUI. We won't focus on this part yet, but it's good to keep it in mind.
+
+Let's now focus on the first test case.
+
+```ruby
+it 'replaces an existing label if it has the same key' do
+ # This implementation is only for tutorial purposes. We normally encapsulate elements in Page Objects (which we cover on section 8).
+ page.find('.block.labels .edit-link').click
+ page.find('.dropdown-menu-labels .dropdown-input-field').send_keys ['animal::dolphin', :enter]
+ page.find('#content-body').click
+ page.refresh
+
+ labels_block = page.find('.qa-labels-block')
+
+ expect(labels_block).to have_content('animal::dolphin')
+ expect(labels_block).not_to have_content('animal::fox')
+ expect(page).to have_content('added animal::dolphin label and removed animal::fox')
+end
+```
+
+> Notice that the test itself is simple. The most challenging part is the creation of the application state, which will be covered later.
+
+> The exemplified test case's MVC is not enough for the change to be merged, but it helps to build up the test logic. The reason is that we do not want to use locators directly in the tests, and tests **must** use [Page Objects](./qa/qa/page/README.md) before they can be merged. This way we better separate the responsibilities, where the Page Objects encapsulate elements and methods that allow us to interact with pages, while the spec files describe the test cases in more business-related language.
+
+Below are the steps that the test covers:
+
+1. The test finds the 'Edit' link for the labels and clicks on it.
+2. Then it fills in the 'Assign labels' input field with the value 'animal::dolphin' and press enters.
+3. Then it clicks in the content body to apply the label and refreshes the page.
+4. Finally, the expectations check that the previous scoped label was removed and that the new one was added.
+
+Let's now see how the second test case would look.
+
+```ruby
+it 'keeps both scoped labels when adding a label with a different key' do
+ # This implementation is only for tutorial purposes. We normally encapsulate elements in Page Objects (which we cover on section 8).
+ page.find('.block.labels .edit-link').click
+ page.find('.dropdown-menu-labels .dropdown-input-field').send_keys ['plant::orchid', :enter]
+ page.find('#content-body').click
+ page.refresh
+
+ labels_block = page.find('.qa-labels-block')
+
+ expect(labels_block).to have_content('animal::fox')
+ expect(labels_block).to have_content('plant::orchid')
+ expect(page).to have_content('added animal::fox')
+ expect(page).to have_content('added plant::orchid')
+end
+```
+
+> Note that elements are always located using CSS selectors, and a good practice is to add test-specific selectors (this is called adding testability to the application and we will talk more about it later.) For example, the `labels_block` element uses the selector `.qa-labels-block`, which was added specifically for testing purposes.
+
+Below are the steps that the test covers:
+
+1. The test finds the 'Edit' link for the labels and clicks on it.
+2. Then it fills in the 'Assign labels' input field with the value 'plant::orchid' and press enters.
+3. Then it clicks in the content body to apply the label and refreshes the page.
+4. Finally, the expectations check that both scoped labels are present.
+
+> Similar to the previous test, this one is also very straightforward, but there is some code duplication. Let's address it.
+
+### 4. Extracting duplicated code
+
+If we refactor the tests created on step 3 we could come up with something like this:
+
+```ruby
+before do
+ ...
+
+ @initial_label = 'animal::fox'
+ @new_label_same_scope = 'animal::dolphin'
+ @new_label_different_scope = 'plant::orchid'
+
+ ...
+end
+
+it 'replaces an existing label if it has the same key' do
+ select_label_and_refresh @new_label_same_scope
+
+ labels_block = page.find('.qa-labels-block')
+
+ expect(labels_block).to have_content(@new_label_same_scope)
+ expect(labels_block).not_to have_content(@initial_label)
+ expect(page).to have_content("added #{@new_label_same_scope}")
+ expect(page).to have_content("and removed #{@initial_label}")
+end
+
+it 'keeps both scoped label when adding a label with a different key' do
+ select_label_and_refresh @new_label_different_scope
+
+ labels_block = page.find('.qa-labels-block')
+
+ expect(labels_blocks).to have_content(@new_label_different_scope)
+ expect(labels_blocks).to have_content(@initial_label)
+ expect(page).to have_content("added #{@new_label_different_scope}")
+ expect(page).to have_content("added #{@initial_label}")
+end
+
+def select_label_and_refresh(label)
+ page.find('.block.labels .edit-link').click
+ page.find('.dropdown-menu-labels .dropdown-input-field').send_keys [label, :enter]
+ page.find('#content-body').click
+ page.refresh
+end
+```
+
+First, we remove the duplication of strings by defining the global variables `@initial_label`, `@new_label_same_scope` and `@new_label_different_scope` in the `before` block, and by using them in the expectations.
+
+Then, by creating a reusable `select_label_and_refresh` method, we remove the code duplication of this action, and later we can move this method to a Page Object class that will be created for easier maintenance purposes.
+
+> Notice that the reusable method is created at the bottom of the file. The reason for that is that reading the code should be similar to reading a newspaper, where high-level information is at the top, like the title and summary of the news, while low level, or more specific information, is at the bottom (this helps readability).
+
+### 5. Tests' pre-conditions using resources and Page Objects
+
+In this section, we will address the previously mentioned subject of creating the application state for the tests, using the `before :context` and `before` blocks, together with resources and Page Objects.
+
+#### `before :context`
+
+A pre-condition for the entire test suite is defined in the `before :context` block.
+
+> For our test suite, due to the need of the tests being completely independent of each other, we won't use the `before :context` block. The `before :context` block would make the tests dependent on each other because the first test changes the label of the issue, and the second one depends on the `'animal::fox'` label being set.
+
+> **Tip:** In case of a test suite with only one `it` block it's ok to use only the `before` block (see below) with all the test's pre-conditions.
+
+#### `before`
+
+As the pre-conditions for our test suite, the things that needs to happen before each test starts are:
+
+- The user logging in;
+- A premium license already being set;
+- A project being created with an issue and labels already set;
+- The issue page being opened with only one scoped label applied to the it.
+
+> When running end-to-end tests as part of the GitLab's continuous integration process [a license is already set as an environment variable](https://gitlab.com/gitlab-org/gitlab-ee/blob/1a60d926740db10e3b5724713285780a4f470531/qa/qa/ee/strategy.rb#L20). For running tests locally you can set up such license by following the document [what tests can be run?](https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/what_tests_can_be_run.md#supported-remote-grid-environment-variables), based on the [supported GitLab environment variables](https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/what_tests_can_be_run.md#supported-gitlab-environment-variables).
+
+#### Implementation
+
+In the following code we will focus only on the test suite's pre-conditions:
+
+```ruby
+module QA
+ context 'Plan' do
+ describe 'Editing scoped labels on issues' do
+ before do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+
+ @initial_label = 'animal::fox'
+ @new_label_same_scope = 'animal::dolphin'
+ @new_label_different_scope = 'plant::orchid'
+
+ issue = Resource::Issue.fabricate_via_api! do |issue|
+ issue.title = 'Issue to test the scoped labels'
+ issue.labels = @initial_label
+ end
+
+ [@new_label_same_scope, @new_label_different_scope].each do |label|
+ Resource::Label.fabricate_via_api! do |l|
+ l.project = issue.project.id
+ l.title = label
+ end
+ end
+
+ issue.visit!
+ end
+
+ it 'replaces an existing label if it has the same key' do
+ ...
+ end
+
+ it 'keeps both scoped labels when adding a label with a different key' do
+ ...
+ end
+
+ def select_label_and_refresh(label)
+ ...
+ end
+ end
+ end
+end
+```
+
+In the `before` block we create all the application state needed for the tests to run. We do that by using the `Runtime::Browser.visit` method to go to the login page, by performing a `sign_in_using_credentials` from the `Login` Page Object, by fabricating resources via APIs (`issue`, and `Resource::Label`), and by using the `issue.visit!` to visit the issue page.
+
+> A project is created in the background by creating the `issue` resource.
+
+> When [creating the resources](./qa/qa/resource/README.md), notice that when calling the `fabricate_via_api` method, we pass some attribute:values, like `title`, and `labels` for the `issue` resource; and `project` and `title` for the `label` resource.
+
+> What's important to understand here is that by creating the application state mostly using the public APIs we save a lot of time in the test suite setup stage.
+
+> Soon we will cover the use of the already existing resources' methods and the creation of your own `fabricate_via_api` methods for resources where this is still not available, but first, let's optimize our implementation.
+
+### 6. Optimization
+
+As already mentioned in the [best practices](./BEST_PRACTICES.md) document, end-to-end tests are very costly in terms of execution time, and it's our responsibility as software engineers to ensure that we optimize them as much as possible.
+
+> Note that end-to-end tests are slow to run and so they can have several actions and assertions in a single test, which helps us get feedback from the tests sooner. In comparison, unit tests are much faster to run and can exercise every little piece of the application in isolation, and so they usually have only one assertion per test.
+
+Some improvements that we could make in our test suite to optimize its time to run are:
+
+1. Having a single test case (an `it` block) that exercises both scenarios to avoid "wasting" time in the tests' pre-conditions, instead of having two different test cases.
+2. Making the selection of labels more performant by allowing for the selection of more than one label in the same reusable method.
+
+Let's look at a suggestion that addresses the above points, one by one:
+
+```ruby
+module QA
+ context 'Plan' do
+ describe 'Editing scoped labels on issues' do
+ before do
+ ...
+ end
+
+ it 'correctly applies scoped labels depending on if they are from the same or a different scope' do
+ select_labels_and_refresh [@new_label_same_scope, @new_label_different_scope]
+
+ labels_block = page.all('.qa-labels-block')
+
+ expect(labels_block).to have_content(@new_label_same_scope)
+ expect(labels_block).to have_content(@new_label_different_scope)
+ expect(labels_block).not_to have_content(@initial_label)
+ expect(page).to have_content("added #{@initial_label}")
+ expect(page).to have_content("added #{@new_label_same_scope} #{@new_label_different_scope} labels and removed #{@initial_label}")
+ end
+
+ def select_labels_and_refresh(labels)
+ find('.block.labels .edit-link').click
+ labels.each do |label|
+ find('.dropdown-menu-labels .dropdown-input-field').send_keys [label, :enter]
+ end
+ find('#content-body').click
+ refresh
+ end
+ end
+ end
+ end
+```
+
+To address point 1, we changed the test implementation from two `it` blocks into a single one that exercises both scenarios. Now the new test description is: `'correctly applies the scoped labels depending if they are from the same or a different scope'`. It's a long description, but it describes well what the test does.
+
+> Notice that the implementation of the new and unique `it` block had to change a little bit. Below we describe in details what it does.
+
+1. It selects two scoped labels simultaneously, one from the same scope of the one already applied in the issue during the setup phase (in the `before` block), and another one from a different scope.
+2. It asserts that the correct labels are visible in the `labels_block`, and that the labels were correctly added and removed;
+3. Finally, the `select_label_and_refresh` method is changed to `select_labels_and_refresh`, which accepts an array of labels instead of a single label, and it iterates on them for faster label selection (this is what is used in step 1 explained above.)
+
+### 7. Resources
+
+**Note:** When writing this document, some code that is now merged to master was not implemented yet, but we left them here for the readers to understand the whole process of end-to-end test creation.
+
+You can think of [resources](qa/qa/resource/README.md) as anything that can be created on GitLab CE or EE, either through the GUI, the API, or the CLI.
+
+With that in mind, resources can be a project, an epic, an issue, a label, a commit, etc.
+
+As you saw in the tests' pre-conditions and the optimization sections, we're already creating some of these resources, and we are doing that by calling the `fabricate_via_api!` method.
+
+> We could be using the `fabricate!` method instead, which would use the `fabricate_via_api!` method if it exists, and fallback to GUI fabrication otherwise, but we recommend being explicit to make it clear what the test does. Also, we always recommend fabricating resources via API since this makes tests faster and more reliable.
+
+For our test suite example, the resources that we need to create don't have the necessary code for the `fabricate_via_api!` method to correctly work (e.g., the issue and label resources), so we will have to create them.
+
+#### Implementation
+
+In the following we describe the changes needed in each of the resource files mentioned above.
+
+**Issue resource**
+
+Now, let's make it possible to create an issue resource through the API.
+
+First, in the [issue resource](https://gitlab.com/gitlab-org/gitlab-ee/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/issue.rb), let's expose its labels attribute.
+
+Add the following `attribute :labels` right below the [`attribute :title`](https://gitlab.com/gitlab-org/gitlab-ee/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/issue.rb#L15).
+
+> This line is needed to allow for labels to be automatically added to an issue when fabricating it via API.
+
+Next, add the following code right below the [`fabricate!`](https://gitlab.com/gitlab-org/gitlab-ee/blob/d3584e80b4236acdf393d815d604801573af72cc/qa/qa/resource/issue.rb#L27) method.
+
+```ruby
+def api_get_path
+ "/projects/#{project.id}/issues/#{id}"
+end
+
+def api_post_path
+ "/projects/#{project.id}/issues"
+end
+
+def api_post_body
+ {
+ title: title,
+ labels: [labels]
+ }
+end
+```
+
+By defining the `api_get_path` method, we allow the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to get a single issue.
+
+> This `GET` path can be found in the [public API documentation](https://docs.gitlab.com/ee/api/issues.html#single-issue).
+
+By defining the `api_post_path` method, we allow the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to create a new issue in a specific project.
+
+> This `POST` path can be found in the [public API documentation](https://docs.gitlab.com/ee/api/issues.html#new-issue).
+
+By defining the `api_post_body` method, we allow the [`ApiFabricator.api_post`](https://gitlab.com/gitlab-org/gitlab-ee/blob/a9177ca1812bac57e2b2fa4560e1d5dd8ffac38b/qa/qa/resource/api_fabricator.rb#L68) method to know which data to send when making the `POST` request.
+
+> Notice that we pass both `title` and `labels` attributes in the `api_post_body`, where `labels` receives an array of labels, and [`title` is required](https://docs.gitlab.com/ee/api/issues.html#new-issue).
+
+**Label resource**
+
+Finally, let's make it possible to create label resources through the API.
+
+Add the following code right below the [`fabricate!`](https://gitlab.com/gitlab-org/gitlab-ee/blob/a9177ca1812bac57e2b2fa4560e1d5dd8ffac38b/qa/qa/resource/label.rb#L36) method.
+
+```ruby
+def resource_web_url(resource)
+ super
+rescue ResourceURLMissingError
+ # this particular resource does not expose a web_url property
+end
+
+def api_get_path
+ raise NotImplementedError, "The Labels API doesn't expose a single-resource endpoint so this method cannot be properly implemented."
+end
+
+def api_post_path
+ "/projects/#{project}/labels"
+end
+
+def api_post_body
+ {
+ name: @title,
+ color: @color
+ }
+end
+```
+
+By defining the `resource_web_url(resource)` method, we override the one from the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb#L44) module. We do that to avoid failing the test due to this particular resource not exposing a `web_url` property.
+
+By defining the `api_get_path` method, we **would** allow for the [`ApiFabricator`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to get a single label, but since there's no path available for that in the publich API, we raise a `NotImplementedError` instead.
+
+By defining the `api_post_path` method, we allow for the [`ApiFabricator `](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/resource/api_fabricator.rb) module to know which path to use to create a new label in a specific project.
+
+By defining the `api_post_body` method, we we allow for the [`ApiFabricator.api_post`](https://gitlab.com/gitlab-org/gitlab-ee/blob/a9177ca1812bac57e2b2fa4560e1d5dd8ffac38b/qa/qa/resource/api_fabricator.rb#L68) method to know which data to send when making the `POST` request.
+
+> Notice that we pass both `name` and `color` attributes in the `api_post_body` since [those are required](https://docs.gitlab.com/ee/api/labels.html#create-a-new-label).
+
+### 8. Page Objects
+
+Page Objects are used in end-to-end tests for maintenance reasons, where a page's elements and methods are defined to be reused in any test.
+
+> Page Objects are auto-loaded in the `qa/qa.rb` file and available in all the test files (`*_spec.rb`).
+
+Take a look at [this document for a detailed explanation about Page Objects](./qa/page/README.md).
+
+Now, let's go back to our example.
+
+As you may have noticed, we are defining elements with CSS selectors and the `select_labels_and_refresh` method directly in the test file, and this is an anti-pattern since we need to better separate the responsibilities.
+
+To address this issue, we will move the implementation to Page Objects, and the test suite will only focus on the business rules that we are testing.
+
+#### Updates in the test file
+
+As in a test-driven development approach, let's start changing the test file even before the Page Object implementation is in place.
+
+Replace the code of the `it` block in the test file by the following:
+
+```ruby
+module QA
+ context 'Plan' do
+ describe 'Editing scoped labels on issues' do
+ before do
+ ...
+ end
+
+ it 'correctly applies scoped labels depending on if they are from the same or a different scope' do
+ Page::Project::Issue::Show.perform do |issue_page|
+ issue_page.select_labels_and_refresh [@new_label_same_scope, @new_label_different_scope]
+
+ expect(page).to have_content("added #{@initial_label}")
+ expect(page).to have_content("added #{@new_label_same_scope} #{@new_label_different_scope} labels and removed #{@initial_label}")
+ expect(issue_page.text_of_labels_block).to have_content(@new_label_same_scope)
+ expect(issue_page.text_of_labels_block).to have_content(@new_label_different_scope)
+ expect(issue_page.text_of_labels_block).not_to have_content(@initial_label)
+ end
+ end
+ end
+ end
+end
+```
+
+Notice that `select_labels_and_refresh` is now a method from the issue Page Object (which is not yet implemented), and that we verify the labels' text by using `text_of_labels_block`, instead of via the `labels_block` element. The `text_of_labels_block` method will also be implemented in the issue Page Object.
+
+Let's now update the Issue Page Object.
+
+#### Updates in the Issue Page Object
+
+> Page Objects are located in the `qa/qa/page/` directory, and its sub-directories.
+
+The file we will have to change is the [Issue Page Object](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/qa/qa/page/project/issue/show.rb).
+
+First, add the following code right below the definition of an already implemented view:
+
+```ruby
+view 'app/views/shared/issuable/_sidebar.html.haml' do
+ element :labels_block
+ element :edit_link_labels
+ element :dropdown_menu_labels
+end
+
+view 'app/helpers/dropdowns_helper.rb' do
+ element :dropdown_input_field
+end
+```
+
+Similarly to what we did before, let's first change the Page Object even without the elements being defined in the view (`_sidebar.html.haml`) and the `dropdowns_helper.rb` files, and later we will update them by adding the appropriate CSS selectors.
+
+Now, let's implement the methods `select_labels_and_refresh` and `text_of_labels_block`.
+
+Somewhere between the definition of the views and the private methods, add the following snippet of code:
+
+```ruby
+def select_labels_and_refresh(labels)
+ click_element(:edit_link_labels)
+ labels.each do |label|
+ within_element(:dropdown_menu_labels, text: label) do
+ send_keys_to_element(:dropdown_input_field, [label, :enter])
+ end
+ end
+ click_body
+ labels.each do |label|
+ has_element?(:labels_block, text: label)
+ end
+ refresh
+end
+
+def text_of_labels_block
+ find_element(:labels_block)
+end
+```
+
+##### Details of `select_labels_and_refresh`
+
+Notice that we have not only moved the `select_labels_and_refresh` method, but we have also changed its implementation to:
+1. Click the `:edit_link_labels` element previously defined, instead of using `find('.block.labels .edit-link').click`
+2. Use `within_element(:dropdown_menu_labels, text: label)`, and inside of it, we call `send_keys_to_element(:dropdown_input_field, [label, :enter])`, which is a method that we will implement in the `QA::Page::Base` class to replace `find('.dropdown-menu-labels .dropdown-input-field').send_keys [label, :enter]`
+3. Use `click_body` after iterating on each label, instead of using `find('#content-body').click`
+4. Iterate on every label again, and then we use `has_element?(:labels_block, text: label)` after clicking the page body (which applies the labels), and before refreshing the page, to avoid test flakiness due to refreshing too fast.
+
+##### Details of `text_of_labels_block`
+
+The `text_of_labels_block` method is a simple method that returns the `:labels_block` element (`find_element(:labels_block)`).
+
+#### Updates in the view (*.html.haml) and `dropdowns_helper.rb` files
+
+Now let's change the view and the `dropdowns_helper` files to add the selectors that relate to the Page Object.
+
+In the [app/views/shared/issuable/_sidebar.html.haml](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/app/views/shared/issuable/_sidebar.html.haml) file, on [line 105 ](https://gitlab.com/gitlab-org/gitlab-ee/blob/84043fa72ca7f83ae9cde48ad670e6d5d16501a3/app/views/shared/issuable/_sidebar.html.haml#L105), add an extra class `qa-edit-link-labels`.
+
+The code should look like this: `= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link qa-edit-link-labels float-right'`.
+
+In the same file, on [line 121](https://gitlab.com/gitlab-org/gitlab-ee/blob/84043fa72ca7f83ae9cde48ad670e6d5d16501a3/app/views/shared/issuable/_sidebar.html.haml#L121), add an extra class `.qa-dropdown-menu-labels`.
+
+The code should look like this: `.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.qa-dropdown-menu-labels.dropdown-menu-selectable`.
+
+In the [`dropdowns_helper.rb`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/app/helpers/dropdowns_helper.rb) file, on [line 94](https://gitlab.com/gitlab-org/gitlab-ee/blob/99e51a374f2c20bee0989cac802e4b5621f72714/app/helpers/dropdowns_helper.rb#L94), add an extra class `qa-dropdown-input-field`.
+
+The code should look like this: `filter_output = search_field_tag search_id, nil, class: "dropdown-input-field qa-dropdown-input-field", placeholder: placeholder, autocomplete: 'off'`.
+
+> Classes starting with `qa-` are used for testing purposes only, and by defining such classes in the elements we add **testability** in the application.
+
+> When defining a class like `qa-labels-block`, it is transformed into `:labels_block` for usage in the Page Objects. So, `qa-edit-link-labels` is tranformed into `:edit_link_labels`, `qa-dropdown-menu-labels` is transformed into `:dropdown_menu_labels`, and `qa-dropdown-input-field` is transformed into `:dropdown_input_field`. Also, we use a [sanity test](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa/page#how-did-we-solve-fragile-tests-problem) to check that defined elements have their respective `qa-` selectors in the specified views.
+
+> We did not define the `qa-labels-block` class in the `app/views/shared/issuable/_sidebar.html.haml` file because it was already there to be used.
+
+#### Updates in the `QA::Page::Base` class
+
+The last thing that we have to do is to update `QA::Page::Base` class to add the `send_keys_to_element` method on it.
+
+Add the following snippet of code somewhere where class methods are defined:
+
+```ruby
+def send_keys_to_element(name, keys)
+ find_element(name).send_keys(keys)
+end
+```
+
+This method receives an element (`name`) and the `keys` that it will send to that element, and the keys are an array that can receive strings, or "special" keys, like `:enter`.
+
+As you might remember, in the Issue Page Object we call this method like this: `send_keys_to_element(:dropdown_input_field, [label, :enter])`.
+
+___
+
+With that, you should be able to start writing end-to-end tests yourself. *Congratulations!*
diff --git a/qa/qa.rb b/qa/qa.rb
index f580691f952..944dcc31917 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -130,6 +130,7 @@ module QA
autoload :View, 'qa/page/view'
autoload :Element, 'qa/page/element'
autoload :Validator, 'qa/page/validator'
+ autoload :Validatable, 'qa/page/validatable'
module Main
autoload :Login, 'qa/page/main/login'
diff --git a/qa/qa/ce/strategy.rb b/qa/qa/ce/strategy.rb
index d7748a976f0..7e2d02424fe 100644
--- a/qa/qa/ce/strategy.rb
+++ b/qa/qa/ce/strategy.rb
@@ -13,7 +13,6 @@ module QA
# The login page could take some time to load the first time it is visited.
# We visit the login page and wait for it to properly load only once before the tests.
QA::Runtime::Browser.visit(:gitlab, QA::Page::Main::Login)
- QA::Page::Main::Login.perform(&:assert_page_loaded)
end
end
end
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index c395e5f6011..389f4e0032e 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -8,6 +8,7 @@ module QA
prepend Support::Page::Logging if Runtime::Env.debug?
include Capybara::DSL
include Scenario::Actable
+ extend Validatable
extend SingleForwardable
ElementNotFound = Class.new(RuntimeError)
@@ -93,8 +94,10 @@ module QA
find_element(name).set(false)
end
- def click_element(name)
+ # replace with (..., page = self.class)
+ def click_element(name, page = nil)
find_element(name).click
+ page.validate_elements_present! if page
end
def fill_element(name, content)
diff --git a/qa/qa/page/element.rb b/qa/qa/page/element.rb
index d92e71467fe..7a01320901d 100644
--- a/qa/qa/page/element.rb
+++ b/qa/qa/page/element.rb
@@ -1,28 +1,41 @@
# frozen_string_literal: true
+require 'active_support/core_ext/array/extract_options'
+
module QA
module Page
class Element
- attr_reader :name
+ attr_reader :name, :attributes
- def initialize(name, pattern = nil)
+ def initialize(name, *options)
@name = name
- @pattern = pattern || selector
+ @attributes = options.extract_options!
+ @attributes[:pattern] ||= selector
+
+ options.each do |option|
+ if option.is_a?(String) || option.is_a?(Regexp)
+ @attributes[:pattern] = option
+ end
+ end
end
def selector
"qa-#{@name.to_s.tr('_', '-')}"
end
+ def required?
+ !!@attributes[:required]
+ end
+
def selector_css
".#{selector}"
end
def expression
- if @pattern.is_a?(String)
- @_regexp ||= Regexp.new(Regexp.escape(@pattern))
+ if @attributes[:pattern].is_a?(String)
+ @_regexp ||= Regexp.new(Regexp.escape(@attributes[:pattern]))
else
- @pattern
+ @attributes[:pattern]
end
end
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index 99b3d1b83d3..8970eeb6678 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -39,19 +39,7 @@ module QA
end
view 'app/views/layouts/devise.html.haml' do
- element :login_page
- end
-
- def assert_page_loaded
- unless page_loaded?
- raise QA::Runtime::Browser::NotRespondingError, "Login page did not load at #{QA::Page::Main::Login.perform(&:current_url)}"
- end
- end
-
- def page_loaded?
- wait(max: 60) do
- has_element?(:login_page)
- end
+ element :login_page, required: true
end
def sign_in_using_credentials(user = nil)
@@ -159,7 +147,7 @@ module QA
fill_element :login_field, user.username
fill_element :password_field, user.password
- click_element :sign_in_button
+ click_element :sign_in_button, Page::Main::Menu
end
def set_initial_password_if_present
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
index e98d531c86e..5eb24d2d2ba 100644
--- a/qa/qa/page/main/menu.rb
+++ b/qa/qa/page/main/menu.rb
@@ -10,15 +10,15 @@ module QA
end
view 'app/views/layouts/header/_default.html.haml' do
- element :navbar
- element :user_avatar
+ element :navbar, required: true
+ element :user_avatar, required: true
element :user_menu, '.dropdown-menu' # rubocop:disable QA/ElementWithPattern
end
view 'app/views/layouts/nav/_dashboard.html.haml' do
element :admin_area_link
- element :projects_dropdown
- element :groups_dropdown
+ element :projects_dropdown, required: true
+ element :groups_dropdown, required: true
element :snippets_link
end
diff --git a/qa/qa/page/project/branches/show.rb b/qa/qa/page/project/branches/show.rb
index 762eacdab15..480fc7d78cb 100644
--- a/qa/qa/page/project/branches/show.rb
+++ b/qa/qa/page/project/branches/show.rb
@@ -28,9 +28,11 @@ module QA
finished_loading?
end
- def has_no_branch?(branch_name)
- within_element(:all_branches) do
- has_no_element?(:branch_name, text: branch_name, wait: Support::Waiter::DEFAULT_MAX_WAIT_TIME)
+ def has_no_branch?(branch_name, reload: false)
+ wait(reload: reload) do
+ within_element(:all_branches) do
+ has_no_element?(:branch_name, text: branch_name)
+ end
end
end
diff --git a/qa/qa/page/validatable.rb b/qa/qa/page/validatable.rb
new file mode 100644
index 00000000000..8467d261285
--- /dev/null
+++ b/qa/qa/page/validatable.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Validatable
+ PageValidationError = Class.new(StandardError)
+
+ def validate_elements_present!
+ base_page = self.new
+
+ elements.each do |element|
+ next unless element.required?
+
+ # TODO: this wait needs to be replaced by the wait class
+ unless base_page.has_element?(element.name, wait: 60)
+ raise Validatable::PageValidationError, "#{element.name} did not appear on #{self.name} as expected"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/view.rb b/qa/qa/page/view.rb
index 96f3917a8ab..613059b2d32 100644
--- a/qa/qa/page/view.rb
+++ b/qa/qa/page/view.rb
@@ -50,8 +50,8 @@ module QA
@elements = []
end
- def element(name, pattern = nil)
- @elements.push(Page::Element.new(name, pattern))
+ def element(name, *args)
+ @elements.push(Page::Element.new(name, *args))
end
end
end
diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb
index 523d92c7ef3..283fc6cdbcb 100644
--- a/qa/qa/resource/base.rb
+++ b/qa/qa/resource/base.rb
@@ -15,6 +15,38 @@ module QA
def_delegators :evaluator, :attribute
+ def self.fabricate!(*args, &prepare_block)
+ fabricate_via_api!(*args, &prepare_block)
+ rescue NotImplementedError
+ fabricate_via_browser_ui!(*args, &prepare_block)
+ end
+
+ def self.fabricate_via_browser_ui!(*args, &prepare_block)
+ options = args.extract_options!
+ resource = options.fetch(:resource) { new }
+ parents = options.fetch(:parents) { [] }
+
+ do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
+ log_fabrication(:browser_ui, resource, parents, args) { resource.fabricate!(*args) }
+
+ current_url
+ end
+ end
+
+ def self.fabricate_via_api!(*args, &prepare_block)
+ options = args.extract_options!
+ resource = options.fetch(:resource) { new }
+ parents = options.fetch(:parents) { [] }
+
+ raise NotImplementedError unless resource.api_support?
+
+ resource.eager_load_api_client!
+
+ do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
+ log_fabrication(:api, resource, parents, args) { resource.fabricate_via_api! }
+ end
+ end
+
def fabricate!(*_args)
raise NotImplementedError
end
@@ -55,38 +87,6 @@ module QA
QA::Runtime::Logger.info "<#{self.class}> Attribute #{name.inspect} has both API response `#{api_value}` and a block. API response will be picked. Block will be ignored."
end
- def self.fabricate!(*args, &prepare_block)
- fabricate_via_api!(*args, &prepare_block)
- rescue NotImplementedError
- fabricate_via_browser_ui!(*args, &prepare_block)
- end
-
- def self.fabricate_via_browser_ui!(*args, &prepare_block)
- options = args.extract_options!
- resource = options.fetch(:resource) { new }
- parents = options.fetch(:parents) { [] }
-
- do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
- log_fabrication(:browser_ui, resource, parents, args) { resource.fabricate!(*args) }
-
- current_url
- end
- end
-
- def self.fabricate_via_api!(*args, &prepare_block)
- options = args.extract_options!
- resource = options.fetch(:resource) { new }
- parents = options.fetch(:parents) { [] }
-
- raise NotImplementedError unless resource.api_support?
-
- resource.eager_load_api_client!
-
- do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do
- log_fabrication(:api, resource, parents, args) { resource.fabricate_via_api! }
- end
- end
-
def self.do_fabricate!(resource:, prepare_block:, parents: [])
prepare_block.call(resource) if prepare_block
diff --git a/qa/qa/resource/file.rb b/qa/qa/resource/file.rb
index 57e82ac19ad..ca74654bf90 100644
--- a/qa/qa/resource/file.rb
+++ b/qa/qa/resource/file.rb
@@ -3,9 +3,12 @@
module QA
module Resource
class File < Base
- attr_accessor :name,
+ attr_accessor :author_email,
+ :author_name,
+ :branch,
:content,
- :commit_message
+ :commit_message,
+ :name
attribute :project do
Project.fabricate! do |resource|
@@ -31,6 +34,30 @@ module QA
page.commit_changes
end
end
+
+ def resource_web_url(resource)
+ super
+ rescue ResourceURLMissingError
+ # this particular resource does not expose a web_url property
+ end
+
+ def api_get_path
+ "/projects/#{CGI.escape(project.path_with_namespace)}/repository/files/#{CGI.escape(@name)}"
+ end
+
+ def api_post_path
+ api_get_path
+ end
+
+ def api_post_body
+ {
+ branch: @branch || "master",
+ author_email: @author_email || Runtime::User.default_email,
+ author_name: @author_name || Runtime::User.username,
+ content: content,
+ commit_message: commit_message
+ }
+ end
end
end
end
diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb
index 174a52bd376..3bf4b3bbbfb 100644
--- a/qa/qa/runtime/browser.rb
+++ b/qa/qa/runtime/browser.rb
@@ -33,6 +33,7 @@ module QA
def self.visit(address, page = nil, &block)
new.visit(address, page, &block)
+ page.validate_elements_present!
end
def self.configure!
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb
index b1d641b507f..67610b62ed7 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb
@@ -4,16 +4,14 @@ module QA
context 'Manage', :orchestrated, :mattermost do
describe 'Mattermost login' do
it 'user logs into Mattermost using GitLab OAuth' do
- Runtime::Browser.visit(:gitlab, Page::Main::Login) do
- Page::Main::Login.act { sign_in_using_credentials }
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
- Runtime::Browser.visit(:mattermost, Page::Mattermost::Login) do
- Page::Mattermost::Login.act { sign_in_using_oauth }
+ Runtime::Browser.visit(:mattermost, Page::Mattermost::Login)
+ Page::Mattermost::Login.perform(&:sign_in_using_oauth)
- Page::Mattermost::Main.perform do |page|
- expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/)
- end
- end
+ Page::Mattermost::Main.perform do |page|
+ expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/)
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
index cf6c24fa873..37a784248d4 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
@@ -84,7 +84,7 @@ module QA
page.refresh
Page::Project::Branches::Show.perform do |branches_view|
- expect(branches_view).to have_no_branch(second_branch)
+ expect(branches_view).to have_no_branch(second_branch, reload: true)
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb
index 46346d1b984..d345fbfe995 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb
@@ -12,7 +12,7 @@ module QA
file_content = 'QA Test - File content'
commit_message_for_create = 'QA Test - Create new file'
- Resource::File.fabricate! do |file|
+ Resource::File.fabricate_via_browser_ui! do |file|
file.name = file_name
file.content = file_content
file.commit_message = commit_message_for_create
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb
index b7400cdca97..680c5e21fa4 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb
@@ -3,13 +3,16 @@
module QA
context 'Create' do
# failure reported: https://gitlab.com/gitlab-org/quality/nightly/issues/42
- # also failing in staging until the fix is picked into the next release:
- # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24533
describe 'Commit data', :quarantine do
before(:context) do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
+ # Get the user's details to confirm they're included in the email patch
+ @user = Resource::User.fabricate_via_api! do |user|
+ user.username = Runtime::User.username
+ end
+
project_push = Resource::Repository::ProjectPush.fabricate! do |push|
push.file_name = 'README.md'
push.file_content = '# This is a test project'
@@ -21,12 +24,13 @@ module QA
# add second file to repo to enable diff from initial commit
@commit_message = 'Add second file'
- Page::Project::Show.perform(&:create_new_file!)
- Page::File::Form.perform do |f|
- f.add_name('second')
- f.add_content('second file content')
- f.add_commit_message(@commit_message)
- f.commit_changes
+ Resource::File.fabricate_via_api! do |file|
+ file.project = @project
+ file.name = 'second'
+ file.content = 'second file content'
+ file.commit_message = @commit_message
+ file.author_name = @user.name
+ file.author_email = @user.public_email
end
end
@@ -42,15 +46,11 @@ module QA
end
it 'user views raw email patch' do
- user = Resource::User.fabricate_via_api! do |user|
- user.username = Runtime::User.username
- end
-
view_commit
Page::Project::Commit::Show.perform(&:select_email_patches)
- expect(page).to have_content("From: #{user.name} <#{user.public_email}>")
+ expect(page).to have_content("From: #{@user.name} <#{@user.public_email}>")
expect(page).to have_content('Subject: [PATCH] Add second file')
expect(page).to have_content('diff --git a/second b/second')
end
diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
index 9201a05337f..86ba5e819ba 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
@@ -9,8 +9,7 @@ module QA
Page::Main::Login.perform(&:sign_in_using_credentials)
end
- # Transient failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/68
- describe 'Auto DevOps support', :orchestrated, :kubernetes, :quarantine do
+ describe 'Auto DevOps support', :orchestrated, :kubernetes do
context 'when rbac is enabled' do
before(:all) do
@cluster = Service::KubernetesCluster.new.create!
diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb
index ff505fdbddd..3fe567d7757 100644
--- a/qa/qa/support/page/logging.rb
+++ b/qa/qa/support/page/logging.rb
@@ -56,8 +56,11 @@ module QA
elements
end
- def click_element(name)
- log("clicking :#{name}")
+ def click_element(name, page = nil)
+ msg = ["clicking :#{name}"]
+ msg << ", expecting to be at #{page.class}" if page
+
+ log(msg.compact.join(' '))
super
end
diff --git a/qa/spec/page/element_spec.rb b/qa/spec/page/element_spec.rb
index d5d6dff69da..f746fe06e40 100644
--- a/qa/spec/page/element_spec.rb
+++ b/qa/spec/page/element_spec.rb
@@ -50,4 +50,60 @@ describe QA::Page::Element do
expect(subject.matches?('some_name selector')).to be false
end
end
+
+ describe 'attributes' do
+ context 'element with no args' do
+ subject { described_class.new(:something) }
+
+ it 'defaults pattern to #selector' do
+ expect(subject.attributes[:pattern]).to eq 'qa-something'
+ expect(subject.attributes[:pattern]).to eq subject.selector
+ end
+
+ it 'is not required by default' do
+ expect(subject.required?).to be false
+ end
+ end
+
+ context 'element with a pattern' do
+ subject { described_class.new(:something, /link_to 'something'/) }
+
+ it 'has an attribute[pattern] of the pattern' do
+ expect(subject.attributes[:pattern]).to eq /link_to 'something'/
+ end
+
+ it 'is not required by default' do
+ expect(subject.required?).to be false
+ end
+ end
+
+ context 'element with requirement; no pattern' do
+ subject { described_class.new(:something, required: true) }
+
+ it 'has an attribute[pattern] of the selector' do
+ expect(subject.attributes[:pattern]).to eq 'qa-something'
+ expect(subject.attributes[:pattern]).to eq subject.selector
+ end
+
+ it 'is required' do
+ expect(subject.required?).to be true
+ end
+ end
+
+ context 'element with requirement and pattern' do
+ subject { described_class.new(:something, /link_to 'something_else_entirely'/, required: true) }
+
+ it 'has an attribute[pattern] of the passed pattern' do
+ expect(subject.attributes[:pattern]).to eq /link_to 'something_else_entirely'/
+ end
+
+ it 'is required' do
+ expect(subject.required?).to be true
+ end
+
+ it 'has a selector of the name' do
+ expect(subject.selector).to eq 'qa-something'
+ end
+ end
+ end
end
diff --git a/rubocop/cop/qa/element_with_pattern.rb b/rubocop/cop/qa/element_with_pattern.rb
index 9d80946f1ba..d14eeaaeaf3 100644
--- a/rubocop/cop/qa/element_with_pattern.rb
+++ b/rubocop/cop/qa/element_with_pattern.rb
@@ -1,18 +1,21 @@
+# frozen_string_literal: true
+
require_relative '../../qa_helpers'
module RuboCop
module Cop
module QA
- # This cop checks for the usage of factories in migration specs
+ # This cop checks for the usage of patterns in QA elements
#
# @example
#
# # bad
- # let(:user) { create(:user) }
+ # element :some_element, "link_to 'something'"
+ # element :some_element, /link_to 'something'/
#
# # good
- # let(:users) { table(:users) }
- # let(:user) { users.create!(name: 'User 1', username: 'user1') }
+ # element :some_element
+ # element :some_element, required: true
class ElementWithPattern < RuboCop::Cop::Cop
include QAHelpers
@@ -22,10 +25,13 @@ module RuboCop
return unless in_qa_file?(node)
return unless method_name(node).to_s == 'element'
- element_name, pattern = node.arguments
- return unless pattern
+ element_name, *args = node.arguments
+
+ return if args.first.nil?
- add_offense(node, location: pattern.source_range, message: MESSAGE % "qa-#{element_name.value.to_s.tr('_', '-')}")
+ args.first.each_node(:str) do |arg|
+ add_offense(arg, message: MESSAGE % "qa-#{element_name.value.to_s.tr('_', '-')}")
+ end
end
private
diff --git a/spec/features/admin/admin_sees_project_statistics_spec.rb b/spec/features/admin/admin_sees_project_statistics_spec.rb
index 95d1fc5b57a..b5323a1c76d 100644
--- a/spec/features/admin/admin_sees_project_statistics_spec.rb
+++ b/spec/features/admin/admin_sees_project_statistics_spec.rb
@@ -15,7 +15,7 @@ describe "Admin > Admin sees project statistics" do
let(:project) { create(:project, :repository) }
it "shows project statistics" do
- expect(page).to have_content("Storage: 0 Bytes (0 Bytes repositories, 0 Bytes build artifacts, 0 Bytes LFS)")
+ expect(page).to have_content("Storage: 0 Bytes (0 Bytes repositories, 0 Bytes wikis, 0 Bytes build artifacts, 0 Bytes LFS)")
end
end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 87c0dc40e5c..b1798c11361 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -352,6 +352,8 @@ describe 'Issue Boards', :js do
page.within('.labels') do
click_link 'Edit'
+ wait_for_requests
+
click_link 'Create project label'
fill_in 'new_label_name', with: 'test label'
first('.suggest-colors-dropdown a').click
@@ -368,6 +370,8 @@ describe 'Issue Boards', :js do
page.within('.labels') do
click_link 'Edit'
+ wait_for_requests
+
click_link 'Create project label'
fill_in 'new_label_name', with: 'test label'
first('.suggest-colors-dropdown a').click
diff --git a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
index 6e54aa6006b..d4ad11b3585 100644
--- a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
@@ -52,7 +52,7 @@ describe 'Merge request > User merges when pipeline succeeds', :js do
# so we have to wait for asynchronous call to reload it
# and have_content expectation handles that.
#
- expect(page).to have_content "Pipeline ##{pipeline.id} running"
+ expect(page).to have_content "Pipeline ##{pipeline.id} (##{pipeline.iid}) running"
end
it_behaves_like 'Merge when pipeline succeeds activator'
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index a32c6bdcf8f..93ddde623fe 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -160,7 +160,7 @@ describe 'Merge request > User sees merge widget', :js do
it 'shows head pipeline information' do
within '.ci-widget-content' do
- expect(page).to have_content("Pipeline ##{pipeline.id} pending " \
+ expect(page).to have_content("Pipeline ##{pipeline.id} (##{pipeline.iid}) pending " \
"for #{pipeline.short_sha} " \
"on #{pipeline.ref}")
end
@@ -189,7 +189,7 @@ describe 'Merge request > User sees merge widget', :js do
it 'shows head pipeline information' do
within '.ci-widget-content' do
- expect(page).to have_content("Pipeline ##{pipeline.id} pending " \
+ expect(page).to have_content("Pipeline ##{pipeline.id} (##{pipeline.iid}) pending " \
"for #{pipeline.short_sha} " \
"on #{merge_request.to_reference} " \
"with #{merge_request.source_branch}")
@@ -201,7 +201,7 @@ describe 'Merge request > User sees merge widget', :js do
it 'shows head pipeline information' do
within '.ci-widget-content' do
- expect(page).to have_content("Pipeline ##{pipeline.id} pending " \
+ expect(page).to have_content("Pipeline ##{pipeline.id} (##{pipeline.iid}) pending " \
"for #{pipeline.short_sha} " \
"on #{merge_request.to_reference} " \
"with #{merge_request.source_branch}")
@@ -234,7 +234,7 @@ describe 'Merge request > User sees merge widget', :js do
it 'shows head pipeline information' do
within '.ci-widget-content' do
- expect(page).to have_content("Pipeline ##{pipeline.id} pending " \
+ expect(page).to have_content("Pipeline ##{pipeline.id} (##{pipeline.iid}) pending " \
"for #{pipeline.short_sha} " \
"on #{merge_request.to_reference} " \
"with #{merge_request.source_branch} " \
@@ -248,7 +248,7 @@ describe 'Merge request > User sees merge widget', :js do
it 'shows head pipeline information' do
within '.ci-widget-content' do
- expect(page).to have_content("Pipeline ##{pipeline.id} pending " \
+ expect(page).to have_content("Pipeline ##{pipeline.id} (##{pipeline.iid}) pending " \
"for #{pipeline.short_sha} " \
"on #{merge_request.to_reference} " \
"with #{merge_request.source_branch} " \
diff --git a/spec/features/projects/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb
index 574a8aefd63..953517cdff9 100644
--- a/spec/features/projects/commits/user_browses_commits_spec.rb
+++ b/spec/features/projects/commits/user_browses_commits_spec.rb
@@ -61,7 +61,7 @@ describe 'User browses commits' do
it 'renders commit ci info' do
visit project_commit_path(project, sample_commit.id)
- expect(page).to have_content "Pipeline ##{pipeline.id} pending"
+ expect(page).to have_content "Pipeline ##{pipeline.id} (##{pipeline.iid}) pending"
end
end
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index 6762460971f..44715261b8b 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -5,6 +5,7 @@ describe 'Projects > Files > Project owner creates a license file', :js do
let(:project_maintainer) { project.owner }
before do
+ stub_feature_flags(vue_file_list: false)
project.repository.delete_file(project_maintainer, 'LICENSE',
message: 'Remove LICENSE', branch_name: 'master')
sign_in(project_maintainer)
diff --git a/spec/features/projects/files/user_creates_files_spec.rb b/spec/features/projects/files/user_creates_files_spec.rb
index dd2964c2186..69f8bd4d319 100644
--- a/spec/features/projects/files/user_creates_files_spec.rb
+++ b/spec/features/projects/files/user_creates_files_spec.rb
@@ -12,6 +12,7 @@ describe 'Projects > Files > User creates files' do
let(:user) { create(:user) }
before do
+ stub_feature_flags(vue_file_list: false)
stub_feature_flags(web_ide_default: false)
project.add_maintainer(user)
diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb
index ebc20d15d67..bd6c73f4b85 100644
--- a/spec/features/projects/jobs/user_browses_jobs_spec.rb
+++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb
@@ -16,6 +16,12 @@ describe 'User browses jobs' do
visit(project_jobs_path(project))
end
+ it 'shows pipeline id and IID' do
+ page.within('td.pipeline-link') do
+ expect(page).to have_content("##{pipeline.id} (##{pipeline.iid})")
+ end
+ end
+
it 'shows the coverage' do
page.within('td.coverage') do
expect(page).to have_content('99.9%')
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 9cf04fe13b4..77ea613b282 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -129,7 +129,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
visit project_job_path(project, job)
within '.js-pipeline-info' do
- expect(page).to have_content("Pipeline ##{pipeline.id} for #{pipeline.ref}")
+ expect(page).to have_content("Pipeline ##{pipeline.id} (##{pipeline.iid}) for #{pipeline.ref}")
end
end
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 24777788248..46586b891e7 100644
--- a/spec/features/projects/show/user_sees_collaboration_links_spec.rb
+++ b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
@@ -5,6 +5,7 @@ describe 'Projects > Show > Collaboration links' do
let(:user) { create(:user) }
before do
+ stub_feature_flags(vue_file_list: false)
project.add_developer(user)
sign_in(user)
end
diff --git a/spec/frontend/ide/stores/mutations/branch_spec.js b/spec/frontend/ide/stores/mutations/branch_spec.js
index 29eb859ddaf..0900b25d5d3 100644
--- a/spec/frontend/ide/stores/mutations/branch_spec.js
+++ b/spec/frontend/ide/stores/mutations/branch_spec.js
@@ -37,4 +37,39 @@ describe('Multi-file store branch mutations', () => {
expect(localState.projects.Example.branches.master.commit.title).toBe('Example commit');
});
});
+
+ describe('SET_BRANCH_WORKING_REFERENCE', () => {
+ beforeEach(() => {
+ localState.projects = {
+ Foo: {
+ branches: {
+ bar: {},
+ },
+ },
+ };
+ });
+
+ it('sets workingReference for existing branch', () => {
+ mutations.SET_BRANCH_WORKING_REFERENCE(localState, {
+ projectId: 'Foo',
+ branchId: 'bar',
+ reference: 'foo-bar-ref',
+ });
+
+ expect(localState.projects.Foo.branches.bar.workingReference).toBe('foo-bar-ref');
+ });
+
+ it('does not fail on non-existent just yet branch', () => {
+ expect(localState.projects.Foo.branches.unknown).toBeUndefined();
+
+ mutations.SET_BRANCH_WORKING_REFERENCE(localState, {
+ projectId: 'Foo',
+ branchId: 'unknown',
+ reference: 'fun-fun-ref',
+ });
+
+ expect(localState.projects.Foo.branches.unknown).not.toBeUndefined();
+ expect(localState.projects.Foo.branches.unknown.workingReference).toBe('fun-fun-ref');
+ });
+ });
});
diff --git a/spec/frontend/ide/stores/mutations/project_spec.js b/spec/frontend/ide/stores/mutations/project_spec.js
new file mode 100644
index 00000000000..b3ce39c33d2
--- /dev/null
+++ b/spec/frontend/ide/stores/mutations/project_spec.js
@@ -0,0 +1,23 @@
+import mutations from '~/ide/stores/mutations/project';
+import state from '~/ide/stores/state';
+
+describe('Multi-file store branch mutations', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ localState.projects = { abcproject: { empty_repo: true } };
+ });
+
+ describe('TOGGLE_EMPTY_STATE', () => {
+ it('sets empty_repo for project to passed value', () => {
+ mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: 'abcproject', value: false });
+
+ expect(localState.projects.abcproject.empty_repo).toBe(false);
+
+ mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: 'abcproject', value: true });
+
+ expect(localState.projects.abcproject.empty_repo).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap b/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap
index 5f9f13d591d..a2a7d0ee91e 100644
--- a/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap
+++ b/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap
@@ -3,6 +3,7 @@
exports[`MR Popover loaded state matches the snapshot 1`] = `
<glpopover-stub
boundary="viewport"
+ cssclasses=""
placement="top"
show=""
target=""
@@ -61,6 +62,7 @@ exports[`MR Popover loaded state matches the snapshot 1`] = `
exports[`MR Popover shows skeleton-loader while apollo is loading 1`] = `
<glpopover-stub
boundary="viewport"
+ cssclasses=""
placement="top"
show=""
target=""
diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js
new file mode 100644
index 00000000000..068fa317a87
--- /dev/null
+++ b/spec/frontend/repository/components/breadcrumbs_spec.js
@@ -0,0 +1,44 @@
+import { shallowMount, RouterLinkStub } from '@vue/test-utils';
+import Breadcrumbs from '~/repository/components/breadcrumbs.vue';
+
+let vm;
+
+function factory(currentPath) {
+ vm = shallowMount(Breadcrumbs, {
+ propsData: {
+ currentPath,
+ },
+ stubs: {
+ RouterLink: RouterLinkStub,
+ },
+ });
+}
+
+describe('Repository breadcrumbs component', () => {
+ afterEach(() => {
+ vm.destroy();
+ });
+
+ it.each`
+ path | linkCount
+ ${'/'} | ${1}
+ ${'app'} | ${2}
+ ${'app/assets'} | ${3}
+ ${'app/assets/javascripts'} | ${4}
+ `('renders $linkCount links for path $path', ({ path, linkCount }) => {
+ factory(path);
+
+ expect(vm.findAll(RouterLinkStub).length).toEqual(linkCount);
+ });
+
+ it('renders last link as active', () => {
+ factory('app/assets');
+
+ expect(
+ vm
+ .findAll(RouterLinkStub)
+ .at(2)
+ .attributes('aria-current'),
+ ).toEqual('page');
+ });
+});
diff --git a/spec/frontend/repository/components/table/parent_row_spec.js b/spec/frontend/repository/components/table/parent_row_spec.js
new file mode 100644
index 00000000000..7020055271f
--- /dev/null
+++ b/spec/frontend/repository/components/table/parent_row_spec.js
@@ -0,0 +1,64 @@
+import { shallowMount, RouterLinkStub } from '@vue/test-utils';
+import ParentRow from '~/repository/components/table/parent_row.vue';
+
+let vm;
+let $router;
+
+function factory(path) {
+ $router = {
+ push: jest.fn(),
+ };
+
+ vm = shallowMount(ParentRow, {
+ propsData: {
+ commitRef: 'master',
+ path,
+ },
+ stubs: {
+ RouterLink: RouterLinkStub,
+ },
+ mocks: {
+ $router,
+ },
+ });
+}
+
+describe('Repository parent row component', () => {
+ afterEach(() => {
+ vm.destroy();
+ });
+
+ it.each`
+ path | to
+ ${'app'} | ${'/tree/master/'}
+ ${'app/assets'} | ${'/tree/master/app'}
+ `('renders link in $path to $to', ({ path, to }) => {
+ factory(path);
+
+ expect(vm.find(RouterLinkStub).props().to).toEqual({
+ path: to,
+ });
+ });
+
+ it('pushes new router when clicking row', () => {
+ factory('app/assets');
+
+ vm.find('td').trigger('click');
+
+ expect($router.push).toHaveBeenCalledWith({
+ path: '/tree/master/app',
+ });
+ });
+
+ // We test that it does not get called when clicking any internal
+ // links as this was causing multipe routes to get pushed
+ it('does not trigger router.push when clicking link', () => {
+ factory('app/assets');
+
+ vm.find('a').trigger('click');
+
+ expect($router.push).not.toHaveBeenCalledWith({
+ path: '/tree/master/app',
+ });
+ });
+});
diff --git a/spec/helpers/environments_helper_spec.rb b/spec/helpers/environments_helper_spec.rb
new file mode 100644
index 00000000000..0c8a8d2f032
--- /dev/null
+++ b/spec/helpers/environments_helper_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe EnvironmentsHelper do
+ set(:environment) { create(:environment) }
+ set(:project) { environment.project }
+ set(:user) { create(:user) }
+
+ describe '#metrics_data' do
+ before do
+ # This is so that this spec also passes in EE.
+ allow(helper).to receive(:current_user).and_return(user)
+ allow(helper).to receive(:can?).and_return(true)
+ end
+
+ let(:metrics_data) { helper.metrics_data(project, environment) }
+
+ it 'returns data' do
+ expect(metrics_data).to include(
+ 'settings-path' => edit_project_service_path(project, 'prometheus'),
+ 'clusters-path' => project_clusters_path(project),
+ 'current-environment-name': environment.name,
+ 'documentation-path' => help_page_path('administration/monitoring/prometheus/index.md'),
+ 'empty-getting-started-svg-path' => match_asset_path('/assets/illustrations/monitoring/getting_started.svg'),
+ 'empty-loading-svg-path' => match_asset_path('/assets/illustrations/monitoring/loading.svg'),
+ 'empty-no-data-svg-path' => match_asset_path('/assets/illustrations/monitoring/no_data.svg'),
+ 'empty-unable-to-connect-svg-path' => match_asset_path('/assets/illustrations/monitoring/unable_to_connect.svg'),
+ 'metrics-endpoint' => additional_metrics_project_environment_path(project, environment, format: :json),
+ 'deployment-endpoint' => project_environment_deployments_path(project, environment, format: :json),
+ 'environments-endpoint': project_environments_path(project, format: :json),
+ 'project-path' => project_path(project),
+ 'tags-path' => project_tags_path(project),
+ 'has-metrics' => "#{environment.has_metrics?}",
+ 'external-dashboard-url' => nil
+ )
+ end
+
+ context 'with metrics_setting' do
+ before do
+ create(:project_metrics_setting, project: project, external_dashboard_url: 'http://gitlab.com')
+ end
+
+ it 'adds external_dashboard_url' do
+ expect(metrics_data['external-dashboard-url']).to eq('http://gitlab.com')
+ end
+ end
+ end
+end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 83271aa24a3..3716879c458 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -819,4 +819,26 @@ describe ProjectsHelper do
expect(helper.can_import_members?).to eq true
end
end
+
+ describe '#metrics_external_dashboard_url' do
+ let(:project) { create(:project) }
+
+ before do
+ helper.instance_variable_set(:@project, project)
+ end
+
+ context 'metrics_setting exists' do
+ it 'returns external_dashboard_url' do
+ metrics_setting = create(:project_metrics_setting, project: project)
+
+ expect(helper.metrics_external_dashboard_url).to eq(metrics_setting.external_dashboard_url)
+ end
+ end
+
+ context 'metrics_setting does not exist' do
+ it 'returns nil' do
+ expect(helper.metrics_external_dashboard_url).to eq(nil)
+ end
+ end
+ end
end
diff --git a/spec/helpers/storage_helper_spec.rb b/spec/helpers/storage_helper_spec.rb
index 50c74a7c2f9..62c00964524 100644
--- a/spec/helpers/storage_helper_spec.rb
+++ b/spec/helpers/storage_helper_spec.rb
@@ -26,11 +26,12 @@ describe StorageHelper do
namespace: namespace,
statistics: build(:project_statistics,
repository_size: 10.kilobytes,
+ wiki_size: 10.bytes,
lfs_objects_size: 20.gigabytes,
build_artifacts_size: 30.megabytes))
end
- let(:message) { '10 KB repositories, 30 MB build artifacts, 20 GB LFS' }
+ let(:message) { '10 KB repositories, 10 Bytes wikis, 30 MB build artifacts, 20 GB LFS' }
it 'works on ProjectStatistics' do
expect(helper.storage_counters_details(project.statistics)).to eq(message)
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
index 22f192bc7f3..26e851b5641 100644
--- a/spec/javascripts/boards/boards_store_spec.js
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -268,4 +268,14 @@ describe('Store', () => {
});
});
});
+
+ describe('clearDetailIssue', () => {
+ it('resets issue details', () => {
+ boardsStore.detail.issue = 'something';
+
+ boardsStore.clearDetailIssue();
+
+ expect(boardsStore.detail.issue).toEqual({});
+ });
+ });
});
diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js
index dc5790f6562..de4becec1cd 100644
--- a/spec/javascripts/ide/components/ide_spec.js
+++ b/spec/javascripts/ide/components/ide_spec.js
@@ -5,21 +5,53 @@ import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helpe
import { file, resetStore } from '../helpers';
import { projectData } from '../mock_data';
-describe('ide component', () => {
+function bootstrap(projData) {
+ const Component = Vue.extend(ide);
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = Object.assign({}, projData);
+ Vue.set(store.state.trees, 'abcproject/master', {
+ tree: [],
+ loading: false,
+ });
+
+ return createComponentWithStore(Component, store, {
+ emptyStateSvgPath: 'svg',
+ noChangesStateSvgPath: 'svg',
+ committedStateSvgPath: 'svg',
+ });
+}
+
+describe('ide component, empty repo', () => {
let vm;
beforeEach(() => {
- const Component = Vue.extend(ide);
+ const emptyProjData = Object.assign({}, projectData, { empty_repo: true, branches: {} });
+ vm = bootstrap(emptyProjData);
+ vm.$mount();
+ });
- store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'master';
- store.state.projects.abcproject = Object.assign({}, projectData);
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
- vm = createComponentWithStore(Component, store, {
- emptyStateSvgPath: 'svg',
- noChangesStateSvgPath: 'svg',
- committedStateSvgPath: 'svg',
- }).$mount();
+ it('renders "New file" button in empty repo', done => {
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).not.toBeNull();
+ done();
+ });
+ });
+});
+
+describe('ide component, non-empty repo', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = bootstrap(projectData);
+ vm.$mount();
});
afterEach(() => {
@@ -28,17 +60,15 @@ describe('ide component', () => {
resetStore(vm.$store);
});
- it('does not render right when no files open', () => {
- expect(vm.$el.querySelector('.panel-right')).toBeNull();
- });
+ it('shows error message when set', done => {
+ expect(vm.$el.querySelector('.flash-container')).toBe(null);
- it('renders right panel when files are open', done => {
- vm.$store.state.trees['abcproject/mybranch'] = {
- tree: [file()],
+ vm.$store.state.errorMessage = {
+ text: 'error',
};
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.panel-right')).toBeNull();
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.flash-container')).not.toBe(null);
done();
});
@@ -71,17 +101,25 @@ describe('ide component', () => {
});
});
- it('shows error message when set', done => {
- expect(vm.$el.querySelector('.flash-container')).toBe(null);
-
- vm.$store.state.errorMessage = {
- text: 'error',
- };
+ describe('non-existent branch', () => {
+ it('does not render "New file" button for non-existent branch when repo is not empty', done => {
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).toBeNull();
+ done();
+ });
+ });
+ });
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.flash-container')).not.toBe(null);
+ describe('branch with files', () => {
+ beforeEach(() => {
+ store.state.trees['abcproject/master'].tree = [file()];
+ });
- done();
+ it('does not render "New file" button', done => {
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).toBeNull();
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/ide/components/ide_tree_list_spec.js b/spec/javascripts/ide/components/ide_tree_list_spec.js
index 4ecbdb8a55e..f63007c7dd2 100644
--- a/spec/javascripts/ide/components/ide_tree_list_spec.js
+++ b/spec/javascripts/ide/components/ide_tree_list_spec.js
@@ -7,25 +7,23 @@ import { projectData } from '../mock_data';
describe('IDE tree list', () => {
const Component = Vue.extend(IdeTreeList);
+ const normalBranchTree = [file('fileName')];
+ const emptyBranchTree = [];
let vm;
- beforeEach(() => {
+ const bootstrapWithTree = (tree = normalBranchTree) => {
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = Object.assign({}, projectData);
Vue.set(store.state.trees, 'abcproject/master', {
- tree: [file('fileName')],
+ tree,
loading: false,
});
vm = createComponentWithStore(Component, store, {
viewerType: 'edit',
});
-
- spyOn(vm, 'updateViewer').and.callThrough();
-
- vm.$mount();
- });
+ };
afterEach(() => {
vm.$destroy();
@@ -33,22 +31,47 @@ describe('IDE tree list', () => {
resetStore(vm.$store);
});
- it('updates viewer on mount', () => {
- expect(vm.updateViewer).toHaveBeenCalledWith('edit');
- });
+ describe('normal branch', () => {
+ beforeEach(() => {
+ bootstrapWithTree();
+
+ spyOn(vm, 'updateViewer').and.callThrough();
+
+ vm.$mount();
+ });
+
+ it('updates viewer on mount', () => {
+ expect(vm.updateViewer).toHaveBeenCalledWith('edit');
+ });
+
+ it('renders loading indicator', done => {
+ store.state.trees['abcproject/master'].loading = true;
- it('renders loading indicator', done => {
- store.state.trees['abcproject/master'].loading = true;
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
+ expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
- expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
+ done();
+ });
+ });
- done();
+ it('renders list of files', () => {
+ expect(vm.$el.textContent).toContain('fileName');
});
});
- it('renders list of files', () => {
- expect(vm.$el.textContent).toContain('fileName');
+ describe('empty-branch state', () => {
+ beforeEach(() => {
+ bootstrapWithTree(emptyBranchTree);
+
+ spyOn(vm, 'updateViewer').and.callThrough();
+
+ vm.$mount();
+ });
+
+ it('does not load files if the branch is empty', () => {
+ expect(vm.$el.textContent).not.toContain('fileName');
+ expect(vm.$el.textContent).toContain('No files');
+ });
});
});
diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js
index cd519eaed7c..8ecb6129c63 100644
--- a/spec/javascripts/ide/stores/actions/project_spec.js
+++ b/spec/javascripts/ide/stores/actions/project_spec.js
@@ -4,7 +4,7 @@ import {
refreshLastCommitData,
showBranchNotFoundError,
createNewBranchFromDefault,
- getBranchData,
+ showEmptyState,
openBranch,
} from '~/ide/stores/actions';
import store from '~/ide/stores';
@@ -196,39 +196,44 @@ describe('IDE store project actions', () => {
});
});
- describe('getBranchData', () => {
- describe('error', () => {
- it('dispatches branch not found action when response is 404', done => {
- const dispatch = jasmine.createSpy('dispatchSpy');
-
- mock.onGet(/(.*)/).replyOnce(404);
-
- getBranchData(
+ describe('showEmptyState', () => {
+ it('commits proper mutations when supplied error is 404', done => {
+ testAction(
+ showEmptyState,
+ {
+ err: {
+ response: {
+ status: 404,
+ },
+ },
+ projectId: 'abc/def',
+ branchId: 'master',
+ },
+ store.state,
+ [
{
- commit() {},
- dispatch,
- state: store.state,
+ type: 'CREATE_TREE',
+ payload: {
+ treePath: 'abc/def/master',
+ },
},
{
- projectId: 'abc/def',
- branchId: 'master-testing',
+ type: 'TOGGLE_LOADING',
+ payload: {
+ entry: store.state.trees['abc/def/master'],
+ forceValue: false,
+ },
},
- )
- .then(done.fail)
- .catch(() => {
- expect(dispatch.calls.argsFor(0)).toEqual([
- 'showBranchNotFoundError',
- 'master-testing',
- ]);
- done();
- });
- });
+ ],
+ [],
+ done,
+ );
});
});
describe('openBranch', () => {
const branch = {
- projectId: 'feature/lorem-ipsum',
+ projectId: 'abc/def',
branchId: '123-lorem',
};
@@ -238,63 +243,113 @@ describe('IDE store project actions', () => {
'foo/bar-pending': { pending: true },
'foo/bar': { pending: false },
};
-
- spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
});
- it('dispatches branch actions', done => {
- openBranch(store, branch)
- .then(() => {
- expect(store.dispatch.calls.allArgs()).toEqual([
- ['setCurrentBranchId', branch.branchId],
- ['getBranchData', branch],
- ['getFiles', branch],
- ['getMergeRequestsForBranch', branch],
- ]);
- })
- .then(done)
- .catch(done.fail);
- });
+ describe('empty repo', () => {
+ beforeEach(() => {
+ spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
- it('handles tree entry action, if basePath is given', done => {
- openBranch(store, { ...branch, basePath: 'foo/bar/' })
- .then(() => {
- expect(store.dispatch).toHaveBeenCalledWith(
- 'handleTreeEntryAction',
- store.state.entries['foo/bar'],
- );
- })
- .then(done)
- .catch(done.fail);
+ store.state.currentProjectId = 'abc/def';
+ store.state.projects['abc/def'] = {
+ empty_repo: true,
+ };
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ it('dispatches showEmptyState action right away', done => {
+ openBranch(store, branch)
+ .then(() => {
+ expect(store.dispatch.calls.allArgs()).toEqual([
+ ['setCurrentBranchId', branch.branchId],
+ ['showEmptyState', branch],
+ ]);
+ done();
+ })
+ .catch(done.fail);
+ });
});
- it('does not handle tree entry action, if entry is pending', done => {
- openBranch(store, { ...branch, basePath: 'foo/bar-pending' })
- .then(() => {
- expect(store.dispatch).not.toHaveBeenCalledWith(
- 'handleTreeEntryAction',
- jasmine.anything(),
- );
- })
- .then(done)
- .catch(done.fail);
+ describe('existing branch', () => {
+ beforeEach(() => {
+ spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
+ });
+
+ it('dispatches branch actions', done => {
+ openBranch(store, branch)
+ .then(() => {
+ expect(store.dispatch.calls.allArgs()).toEqual([
+ ['setCurrentBranchId', branch.branchId],
+ ['getBranchData', branch],
+ ['getMergeRequestsForBranch', branch],
+ ['getFiles', branch],
+ ]);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('handles tree entry action, if basePath is given', done => {
+ openBranch(store, { ...branch, basePath: 'foo/bar/' })
+ .then(() => {
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'handleTreeEntryAction',
+ store.state.entries['foo/bar'],
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not handle tree entry action, if entry is pending', done => {
+ openBranch(store, { ...branch, basePath: 'foo/bar-pending' })
+ .then(() => {
+ expect(store.dispatch).not.toHaveBeenCalledWith(
+ 'handleTreeEntryAction',
+ jasmine.anything(),
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('creates a new file supplied via URL if the file does not exist yet', done => {
+ openBranch(store, { ...branch, basePath: 'not-existent.md' })
+ .then(() => {
+ expect(store.dispatch).not.toHaveBeenCalledWith(
+ 'handleTreeEntryAction',
+ jasmine.anything(),
+ );
+
+ expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', {
+ name: 'not-existent.md',
+ type: 'blob',
+ });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
- it('creates a new file supplied via URL if the file does not exist yet', done => {
- openBranch(store, { ...branch, basePath: 'not-existent.md' })
- .then(() => {
- expect(store.dispatch).not.toHaveBeenCalledWith(
- 'handleTreeEntryAction',
- jasmine.anything(),
- );
+ describe('non-existent branch', () => {
+ beforeEach(() => {
+ spyOn(store, 'dispatch').and.returnValue(Promise.reject());
+ });
- expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', {
- name: 'not-existent.md',
- type: 'blob',
- });
- })
- .then(done)
- .catch(done.fail);
+ it('dispatches correct branch actions', done => {
+ openBranch(store, branch)
+ .then(() => {
+ expect(store.dispatch.calls.allArgs()).toEqual([
+ ['setCurrentBranchId', branch.branchId],
+ ['getBranchData', branch],
+ ['showBranchNotFoundError', branch.branchId],
+ ]);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
});
});
diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js
index 5ed9b9003a7..674ecdc6764 100644
--- a/spec/javascripts/ide/stores/actions/tree_spec.js
+++ b/spec/javascripts/ide/stores/actions/tree_spec.js
@@ -93,38 +93,6 @@ describe('Multi-file store tree actions', () => {
});
describe('error', () => {
- it('dispatches branch not found actions when response is 404', done => {
- const dispatch = jasmine.createSpy('dispatchSpy');
-
- store.state.projects = {
- 'abc/def': {
- web_url: `${gl.TEST_HOST}/files`,
- },
- };
-
- mock.onGet(/(.*)/).replyOnce(404);
-
- getFiles(
- {
- commit() {},
- dispatch,
- state: store.state,
- },
- {
- projectId: 'abc/def',
- branchId: 'master-testing',
- },
- )
- .then(done.fail)
- .catch(() => {
- expect(dispatch.calls.argsFor(0)).toEqual([
- 'showBranchNotFoundError',
- 'master-testing',
- ]);
- done();
- });
- });
-
it('dispatches error action', done => {
const dispatch = jasmine.createSpy('dispatchSpy');
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index 0b5587d02ae..04e236fb042 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -9,12 +9,15 @@ import actions, {
setErrorMessage,
deleteEntry,
renameEntry,
+ getBranchData,
} from '~/ide/stores/actions';
+import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores';
import * as types from '~/ide/stores/mutation_types';
import router from '~/ide/ide_router';
import { resetStore, file } from '../helpers';
import testAction from '../../helpers/vuex_action_helper';
+import MockAdapter from 'axios-mock-adapter';
describe('Multi-file store actions', () => {
beforeEach(() => {
@@ -560,4 +563,65 @@ describe('Multi-file store actions', () => {
);
});
});
+
+ describe('getBranchData', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('error', () => {
+ let dispatch;
+ const callParams = [
+ {
+ commit() {},
+ state: store.state,
+ },
+ {
+ projectId: 'abc/def',
+ branchId: 'master-testing',
+ },
+ ];
+
+ beforeEach(() => {
+ dispatch = jasmine.createSpy('dispatchSpy');
+ document.body.innerHTML += '<div class="flash-container"></div>';
+ });
+
+ afterEach(() => {
+ document.querySelector('.flash-container').remove();
+ });
+
+ it('passes the error further unchanged without dispatching any action when response is 404', done => {
+ mock.onGet(/(.*)/).replyOnce(404);
+
+ getBranchData(...callParams)
+ .then(done.fail)
+ .catch(e => {
+ expect(dispatch.calls.count()).toEqual(0);
+ expect(e.response.status).toEqual(404);
+ expect(document.querySelector('.flash-alert')).toBeNull();
+ done();
+ });
+ });
+
+ it('does not pass the error further and flashes an alert if error is not 404', done => {
+ mock.onGet(/(.*)/).replyOnce(418);
+
+ getBranchData(...callParams)
+ .then(done.fail)
+ .catch(e => {
+ expect(dispatch.calls.count()).toEqual(0);
+ expect(e.response).toBeUndefined();
+ expect(document.querySelector('.flash-alert')).not.toBeNull();
+ done();
+ });
+ });
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
index cdeb9b4b896..4413a12fac4 100644
--- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
@@ -272,6 +272,7 @@ describe('IDE commit module actions', () => {
short_id: '123',
message: 'test message',
committed_date: 'date',
+ parent_ids: '321',
stats: {
additions: '1',
deletions: '2',
@@ -463,5 +464,63 @@ describe('IDE commit module actions', () => {
.catch(done.fail);
});
});
+
+ describe('first commit of a branch', () => {
+ const COMMIT_RESPONSE = {
+ id: '123456',
+ short_id: '123',
+ message: 'test message',
+ committed_date: 'date',
+ parent_ids: [],
+ stats: {
+ additions: '1',
+ deletions: '2',
+ },
+ };
+
+ it('commits TOGGLE_EMPTY_STATE mutation on empty repo', done => {
+ spyOn(service, 'commit').and.returnValue(
+ Promise.resolve({
+ data: COMMIT_RESPONSE,
+ }),
+ );
+
+ spyOn(store, 'commit').and.callThrough();
+
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(store.commit.calls.allArgs()).toEqual(
+ jasmine.arrayContaining([
+ ['TOGGLE_EMPTY_STATE', jasmine.any(Object), jasmine.any(Object)],
+ ]),
+ );
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('does not commmit TOGGLE_EMPTY_STATE mutation on existing project', done => {
+ COMMIT_RESPONSE.parent_ids.push('1234');
+ spyOn(service, 'commit').and.returnValue(
+ Promise.resolve({
+ data: COMMIT_RESPONSE,
+ }),
+ );
+ spyOn(store, 'commit').and.callThrough();
+
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(store.commit.calls.allArgs()).not.toEqual(
+ jasmine.arrayContaining([
+ ['TOGGLE_EMPTY_STATE', jasmine.any(Object), jasmine.any(Object)],
+ ]),
+ );
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
});
});
diff --git a/spec/javascripts/jobs/components/stages_dropdown_spec.js b/spec/javascripts/jobs/components/stages_dropdown_spec.js
index 52bb5161123..e98639bf21e 100644
--- a/spec/javascripts/jobs/components/stages_dropdown_spec.js
+++ b/spec/javascripts/jobs/components/stages_dropdown_spec.js
@@ -9,6 +9,7 @@ describe('Stages Dropdown', () => {
const mockPipelineData = {
id: 28029444,
+ iid: 123,
details: {
status: {
details_path: '/gitlab-org/gitlab-ce/pipelines/28029444',
@@ -77,8 +78,8 @@ describe('Stages Dropdown', () => {
expect(vm.$el.querySelector('.dropdown .js-selected-stage').textContent).toContain('deploy');
});
- it(`renders the pipeline info text like "Pipeline #123 for source_branch"`, () => {
- const expected = `Pipeline #${pipeline.id} for ${pipeline.ref.name}`;
+ it(`renders the pipeline info text like "Pipeline #123 (#12) for source_branch"`, () => {
+ const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) for ${pipeline.ref.name}`;
const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText);
expect(actual).toBe(expected);
@@ -100,10 +101,10 @@ describe('Stages Dropdown', () => {
});
});
- it(`renders the pipeline info text like "Pipeline #123 for !456 with source_branch into target_branch"`, () => {
- const expected = `Pipeline #${pipeline.id} for !${pipeline.merge_request.iid} with ${
- pipeline.merge_request.source_branch
- } into ${pipeline.merge_request.target_branch}`;
+ it(`renders the pipeline info text like "Pipeline #123 (#12) for !456 with source_branch into target_branch"`, () => {
+ const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) for !${
+ pipeline.merge_request.iid
+ } with ${pipeline.merge_request.source_branch} into ${pipeline.merge_request.target_branch}`;
const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText);
expect(actual).toBe(expected);
@@ -143,10 +144,10 @@ describe('Stages Dropdown', () => {
});
});
- it(`renders the pipeline info like "Pipeline #123 for !456 with source_branch"`, () => {
- const expected = `Pipeline #${pipeline.id} for !${pipeline.merge_request.iid} with ${
- pipeline.merge_request.source_branch
- }`;
+ it(`renders the pipeline info like "Pipeline #123 (#12) for !456 with source_branch"`, () => {
+ const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) for !${
+ pipeline.merge_request.iid
+ } with ${pipeline.merge_request.source_branch}`;
const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText);
expect(actual).toBe(expected);
diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js
index 3d40e94d219..88b0bb206ee 100644
--- a/spec/javascripts/jobs/mock_data.js
+++ b/spec/javascripts/jobs/mock_data.js
@@ -960,6 +960,7 @@ export default {
},
pipeline: {
id: 140,
+ iid: 13,
user: {
name: 'Root',
username: 'root',
diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js
index f04af04f852..ef876dc2941 100644
--- a/spec/javascripts/notes/components/note_app_spec.js
+++ b/spec/javascripts/notes/components/note_app_spec.js
@@ -195,7 +195,7 @@ describe('note_app', () => {
setTimeout(() => {
done();
});
- }, 2000);
+ });
});
describe('discussion note', () => {
@@ -230,7 +230,7 @@ describe('note_app', () => {
setTimeout(() => {
done();
});
- }, 2000);
+ });
});
});
diff --git a/spec/javascripts/pipelines/mock_data.js b/spec/javascripts/pipelines/mock_data.js
index 03ead6cd8ba..8eef9166b8d 100644
--- a/spec/javascripts/pipelines/mock_data.js
+++ b/spec/javascripts/pipelines/mock_data.js
@@ -1,5 +1,6 @@
export const pipelineWithStages = {
id: 20333396,
+ iid: 304399,
user: {
id: 128633,
name: 'Rémy Coutable',
diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js
index aa196af2f33..88c0137dc58 100644
--- a/spec/javascripts/pipelines/pipeline_url_spec.js
+++ b/spec/javascripts/pipelines/pipeline_url_spec.js
@@ -13,6 +13,7 @@ describe('Pipeline Url Component', () => {
propsData: {
pipeline: {
id: 1,
+ iid: 1,
path: 'foo',
flags: {},
},
@@ -28,6 +29,7 @@ describe('Pipeline Url Component', () => {
propsData: {
pipeline: {
id: 1,
+ iid: 1,
path: 'foo',
flags: {},
},
@@ -47,6 +49,7 @@ describe('Pipeline Url Component', () => {
propsData: {
pipeline: {
id: 1,
+ iid: 1,
path: 'foo',
flags: {
latest: true,
@@ -78,6 +81,7 @@ describe('Pipeline Url Component', () => {
propsData: {
pipeline: {
id: 1,
+ iid: 1,
path: 'foo',
flags: {
latest: true,
@@ -100,6 +104,7 @@ describe('Pipeline Url Component', () => {
propsData: {
pipeline: {
id: 1,
+ iid: 1,
path: 'foo',
flags: {
failure_reason: true,
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index 87ef0885d8c..8c80a425581 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -111,7 +111,7 @@ let longRunningTestTimeoutHandle;
beforeEach(done => {
longRunningTestTimeoutHandle = setTimeout(() => {
done.fail('Test is running too long!');
- }, 2000);
+ }, 4000);
done();
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
index 75017d20473..a2308b0dfdb 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -103,7 +103,7 @@ describe('MRWidgetPipeline', () => {
it('should render pipeline ID', () => {
expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual(
- `#${mockData.pipeline.id}`,
+ `#${mockData.pipeline.id} (#${mockData.pipeline.iid})`,
);
});
@@ -150,7 +150,7 @@ describe('MRWidgetPipeline', () => {
it('should render pipeline ID', () => {
expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual(
- `#${mockData.pipeline.id}`,
+ `#${mockData.pipeline.id} (#${mockData.pipeline.iid})`,
);
});
@@ -222,9 +222,9 @@ describe('MRWidgetPipeline', () => {
sourceBranchLink: mockCopy.source_branch_link,
});
- const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${
- pipeline.commit.short_id
- } on ${mockCopy.source_branch_link}`;
+ const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) ${
+ pipeline.details.status.label
+ } for ${pipeline.commit.short_id} on ${mockCopy.source_branch_link}`;
const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
@@ -247,11 +247,11 @@ describe('MRWidgetPipeline', () => {
sourceBranchLink: mockCopy.source_branch_link,
});
- const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${
- pipeline.commit.short_id
- } on !${pipeline.merge_request.iid} with ${pipeline.merge_request.source_branch} into ${
- pipeline.merge_request.target_branch
- }`;
+ const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) ${
+ pipeline.details.status.label
+ } for ${pipeline.commit.short_id} on !${pipeline.merge_request.iid} with ${
+ pipeline.merge_request.source_branch
+ } into ${pipeline.merge_request.target_branch}`;
const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
@@ -274,9 +274,11 @@ describe('MRWidgetPipeline', () => {
sourceBranchLink: mockCopy.source_branch_link,
});
- const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${
- pipeline.commit.short_id
- } on !${pipeline.merge_request.iid} with ${pipeline.merge_request.source_branch}`;
+ const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) ${
+ pipeline.details.status.label
+ } for ${pipeline.commit.short_id} on !${pipeline.merge_request.iid} with ${
+ pipeline.merge_request.source_branch
+ }`;
const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index bec16b0aab0..edbd0d54151 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -62,6 +62,7 @@ export default {
"Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
pipeline: {
id: 172,
+ iid: 32,
user: {
name: 'Administrator',
username: 'root',
@@ -241,6 +242,8 @@ export default {
export const mockStore = {
pipeline: {
id: 0,
+ iid: 0,
+ path: '/root/acets-app/pipelines/0',
details: {
status: {
details_path: '/root/review-app-tester/pipelines/66',
@@ -258,6 +261,8 @@ export const mockStore = {
},
mergePipeline: {
id: 1,
+ iid: 1,
+ path: '/root/acets-app/pipelines/0',
details: {
status: {
details_path: '/root/review-app-tester/pipelines/66',
diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
index a0628fdcebe..7653c10b94b 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -600,6 +600,7 @@ describe('mrWidgetOptions', () => {
];
const deploymentMockData = {
id: 15,
+ iid: 7,
name: 'review/diplo',
url: '/root/acets-review-apps/environments/15',
stop_url: '/root/acets-review-apps/environments/15/stop',
@@ -646,6 +647,7 @@ describe('mrWidgetOptions', () => {
vm.mr.state = 'merged';
vm.mr.mergePipeline = {
id: 127,
+ iid: 35,
user: {
id: 1,
name: 'Administrator',
diff --git a/spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb b/spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb
new file mode 100644
index 00000000000..d494ce68c5b
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20190527194900_schedule_calculate_wiki_sizes.rb')
+
+describe ScheduleCalculateWikiSizes, :migration, :sidekiq do
+ let(:migration_class) { Gitlab::BackgroundMigration::CalculateWikiSizes }
+ let(:migration_name) { migration_class.to_s.demodulize }
+
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:project_statistics) { table(:project_statistics) }
+
+ context 'when missing wiki sizes exist' do
+ before do
+ namespaces.create!(id: 1, name: 'wiki-migration', path: 'wiki-migration')
+ projects.create!(id: 1, name: 'wiki-project-1', path: 'wiki-project-1', namespace_id: 1)
+ projects.create!(id: 2, name: 'wiki-project-2', path: 'wiki-project-2', namespace_id: 1)
+ projects.create!(id: 3, name: 'wiki-project-3', path: 'wiki-project-3', namespace_id: 1)
+ project_statistics.create!(id: 1, project_id: 1, namespace_id: 1, wiki_size: 1000)
+ project_statistics.create!(id: 2, project_id: 2, namespace_id: 1, wiki_size: nil)
+ project_statistics.create!(id: 3, project_id: 3, namespace_id: 1, wiki_size: nil)
+ end
+
+ it 'schedules a background migration' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(migration_name).to be_scheduled_delayed_migration(5.minutes, 2, 3)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 1
+ end
+ end
+ end
+
+ it 'calculates missing wiki sizes' do
+ expect(project_statistics.find_by(id: 2).wiki_size).to be_nil
+ expect(project_statistics.find_by(id: 3).wiki_size).to be_nil
+
+ migrate!
+
+ expect(project_statistics.find_by(id: 2).wiki_size).not_to be_nil
+ expect(project_statistics.find_by(id: 3).wiki_size).not_to be_nil
+ end
+ end
+
+ context 'when missing wiki sizes do not exist' do
+ before do
+ namespaces.create!(id: 1, name: 'wiki-migration', path: 'wiki-migration')
+ projects.create!(id: 1, name: 'wiki-project-1', path: 'wiki-project-1', namespace_id: 1)
+ project_statistics.create!(id: 1, project_id: 1, namespace_id: 1, wiki_size: 1000)
+ end
+
+ it 'does not schedule a background migration' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq 0
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb
new file mode 100644
index 00000000000..c471c30a194
--- /dev/null
+++ b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Samplers::PumaSampler do
+ subject { described_class.new(5) }
+ let(:null_metric) { double('null_metric', set: nil, observe: nil) }
+
+ before do
+ allow(Gitlab::Metrics::NullMetric).to receive(:instance).and_return(null_metric)
+ end
+
+ describe '#sample' do
+ before do
+ expect(subject).to receive(:puma_stats).and_return(puma_stats)
+ end
+
+ context 'in cluster mode' do
+ let(:puma_stats) do
+ <<~EOS
+ {
+ "workers": 2,
+ "phase": 2,
+ "booted_workers": 2,
+ "old_workers": 0,
+ "worker_status": [{
+ "pid": 32534,
+ "index": 0,
+ "phase": 1,
+ "booted": true,
+ "last_checkin": "2019-05-15T07:57:55Z",
+ "last_status": {
+ "backlog":0,
+ "running":1,
+ "pool_capacity":4,
+ "max_threads": 4
+ }
+ }]
+ }
+ EOS
+ end
+
+ it 'samples master statistics' do
+ labels = { worker: 'master' }
+
+ expect(subject.metrics[:puma_workers]).to receive(:set).with(labels, 2)
+ expect(subject.metrics[:puma_running_workers]).to receive(:set).with(labels, 2)
+ expect(subject.metrics[:puma_stale_workers]).to receive(:set).with(labels, 0)
+ expect(subject.metrics[:puma_phase]).to receive(:set).once.with(labels, 2)
+ expect(subject.metrics[:puma_phase]).to receive(:set).once.with({ worker: 'worker_0' }, 1)
+
+ subject.sample
+ end
+
+ it 'samples worker statistics' do
+ labels = { worker: 'worker_0' }
+
+ expect_worker_stats(labels)
+
+ subject.sample
+ end
+ end
+
+ context 'in single mode' do
+ let(:puma_stats) do
+ <<~EOS
+ {
+ "backlog":0,
+ "running":1,
+ "pool_capacity":4,
+ "max_threads": 4
+ }
+ EOS
+ end
+
+ it 'samples worker statistics' do
+ labels = {}
+
+ expect(subject.metrics[:puma_workers]).to receive(:set).with(labels, 1)
+ expect(subject.metrics[:puma_running_workers]).to receive(:set).with(labels, 1)
+ expect_worker_stats(labels)
+
+ subject.sample
+ end
+ end
+ end
+
+ def expect_worker_stats(labels)
+ expect(subject.metrics[:puma_queued_connections]).to receive(:set).with(labels, 0)
+ expect(subject.metrics[:puma_active_connections]).to receive(:set).with(labels, 0)
+ expect(subject.metrics[:puma_running]).to receive(:set).with(labels, 1)
+ expect(subject.metrics[:puma_pool_capacity]).to receive(:set).with(labels, 4)
+ expect(subject.metrics[:puma_max_threads]).to receive(:set).with(labels, 4)
+ expect(subject.metrics[:puma_idle_threads]).to receive(:set).with(labels, 1)
+ end
+end
diff --git a/spec/lib/gitlab/rack_timeout_observer_spec.rb b/spec/lib/gitlab/rack_timeout_observer_spec.rb
new file mode 100644
index 00000000000..3dc1a8b68fb
--- /dev/null
+++ b/spec/lib/gitlab/rack_timeout_observer_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::RackTimeoutObserver do
+ let(:counter) { Gitlab::Metrics::NullMetric.instance }
+
+ before do
+ allow(Gitlab::Metrics).to receive(:counter)
+ .with(any_args)
+ .and_return(counter)
+ end
+
+ describe '#callback' do
+ context 'when request times out' do
+ let(:env) do
+ {
+ ::Rack::Timeout::ENV_INFO_KEY => double(state: :timed_out),
+ 'action_dispatch.request.parameters' => {
+ 'controller' => 'foo',
+ 'action' => 'bar'
+ }
+ }
+ end
+
+ subject { described_class.new }
+
+ it 'increments timeout counter' do
+ expect(counter)
+ .to receive(:increment)
+ .with({ controller: 'foo', action: 'bar', route: nil, state: :timed_out })
+
+ subject.callback.call(env)
+ end
+ end
+
+ context 'when request expires' do
+ let(:endpoint) { double }
+ let(:env) do
+ {
+ ::Rack::Timeout::ENV_INFO_KEY => double(state: :expired),
+ Grape::Env::API_ENDPOINT => endpoint
+ }
+ end
+
+ subject { described_class.new }
+
+ it 'increments timeout counter' do
+ allow(endpoint).to receive_message_chain('route.pattern.origin') { 'foobar' }
+ expect(counter)
+ .to receive(:increment)
+ .with({ controller: nil, action: nil, route: 'foobar', state: :expired })
+
+ subject.callback.call(env)
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index bc81c34f7ab..32eef9e0e01 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -3490,6 +3490,18 @@ describe Ci::Build do
end
end
+ describe '#report_artifacts' do
+ subject { build.report_artifacts }
+
+ context 'when the build has reports' do
+ let!(:report) { create(:ci_job_artifact, :codequality, job: build) }
+
+ it 'returns the artifacts with reports' do
+ expect(subject).to contain_exactly(report)
+ end
+ end
+ end
+
describe '#artifacts_metadata_entry' do
set(:build) { create(:ci_build, project: project) }
let(:path) { 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' }
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index 5964f66c398..e6d682c24d9 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -23,6 +23,21 @@ describe Ci::JobArtifact do
it_behaves_like 'having unique enum values'
+ describe '.with_reports' do
+ let!(:artifact) { create(:ci_job_artifact, :archive) }
+
+ subject { described_class.with_reports }
+
+ it { is_expected.to be_empty }
+
+ context 'when there are reports' do
+ let!(:metrics_report) { create(:ci_job_artifact, :junit) }
+ let!(:codequality_report) { create(:ci_job_artifact, :codequality) }
+
+ it { is_expected.to eq([metrics_report, codequality_report]) }
+ end
+ end
+
describe '.test_reports' do
subject { described_class.test_reports }
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index bfde367c47f..d80183af33e 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -147,6 +147,7 @@ describe Namespace do
namespace: namespace,
statistics: build(:project_statistics,
repository_size: 101,
+ wiki_size: 505,
lfs_objects_size: 202,
build_artifacts_size: 303,
packages_size: 404))
@@ -157,6 +158,7 @@ describe Namespace do
namespace: namespace,
statistics: build(:project_statistics,
repository_size: 10,
+ wiki_size: 50,
lfs_objects_size: 20,
build_artifacts_size: 30,
packages_size: 40))
@@ -167,8 +169,9 @@ describe Namespace do
project2
statistics = described_class.with_statistics.find(namespace.id)
- expect(statistics.storage_size).to eq 1110
+ expect(statistics.storage_size).to eq 1665
expect(statistics.repository_size).to eq 111
+ expect(statistics.wiki_size).to eq 555
expect(statistics.lfs_objects_size).to eq 222
expect(statistics.build_artifacts_size).to eq 333
expect(statistics.packages_size).to eq 444
@@ -179,6 +182,7 @@ describe Namespace do
expect(statistics.storage_size).to eq 0
expect(statistics.repository_size).to eq 0
+ expect(statistics.wiki_size).to eq 0
expect(statistics.lfs_objects_size).to eq 0
expect(statistics.build_artifacts_size).to eq 0
expect(statistics.packages_size).to eq 0
diff --git a/spec/models/project_services/pipelines_email_service_spec.rb b/spec/models/project_services/pipelines_email_service_spec.rb
index ca17e7453b8..b85565e0c25 100644
--- a/spec/models/project_services/pipelines_email_service_spec.rb
+++ b/spec/models/project_services/pipelines_email_service_spec.rb
@@ -4,7 +4,11 @@ require 'spec_helper'
describe PipelinesEmailService, :mailer do
let(:pipeline) do
- create(:ci_pipeline, project: project, sha: project.commit('master').sha)
+ create(:ci_pipeline, :failed,
+ project: project,
+ sha: project.commit('master').sha,
+ ref: project.default_branch
+ )
end
let(:project) { create(:project, :repository) }
@@ -84,12 +88,7 @@ describe PipelinesEmailService, :mailer do
subject.test(data)
end
- context 'when pipeline is failed' do
- before do
- data[:object_attributes][:status] = 'failed'
- pipeline.update(status: 'failed')
- end
-
+ context 'when pipeline is failed and on default branch' do
it_behaves_like 'sending email'
end
@@ -101,6 +100,25 @@ describe PipelinesEmailService, :mailer do
it_behaves_like 'sending email'
end
+
+ context 'when pipeline is failed and on a non-default branch' do
+ before do
+ data[:object_attributes][:ref] = 'not-the-default-branch'
+ pipeline.update(ref: 'not-the-default-branch')
+ end
+
+ context 'with notify_only_default branch on' do
+ before do
+ subject.notify_only_default_branch = true
+ end
+
+ it_behaves_like 'sending email'
+ end
+
+ context 'with notify_only_default_branch off' do
+ it_behaves_like 'sending email'
+ end
+ end
end
describe '#execute' do
@@ -110,11 +128,6 @@ describe PipelinesEmailService, :mailer do
context 'with recipients' do
context 'with failed pipeline' do
- before do
- data[:object_attributes][:status] = 'failed'
- pipeline.update(status: 'failed')
- end
-
it_behaves_like 'sending email'
end
@@ -133,11 +146,6 @@ describe PipelinesEmailService, :mailer do
end
context 'with failed pipeline' do
- before do
- data[:object_attributes][:status] = 'failed'
- pipeline.update(status: 'failed')
- end
-
it_behaves_like 'sending email'
end
@@ -150,6 +158,40 @@ describe PipelinesEmailService, :mailer do
it_behaves_like 'not sending email'
end
end
+
+ context 'with notify_only_default_branch off' do
+ context 'with default branch' do
+ it_behaves_like 'sending email'
+ end
+
+ context 'with non default branch' do
+ before do
+ data[:object_attributes][:ref] = 'not-the-default-branch'
+ pipeline.update(ref: 'not-the-default-branch')
+ end
+
+ it_behaves_like 'sending email'
+ end
+ end
+
+ context 'with notify_only_default_branch on' do
+ before do
+ subject.notify_only_default_branch = true
+ end
+
+ context 'with default branch' do
+ it_behaves_like 'sending email'
+ end
+
+ context 'with non default branch' do
+ before do
+ data[:object_attributes][:ref] = 'not-the-default-branch'
+ pipeline.update(ref: 'not-the-default-branch')
+ end
+
+ it_behaves_like 'not sending email'
+ end
+ end
end
context 'with empty recipients list' do
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index 738398a06f9..f985c114d4b 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -16,16 +16,18 @@ describe ProjectStatistics do
statistics.update!(
commit_count: 8.exabytes - 1,
repository_size: 2.exabytes,
+ wiki_size: 1.exabytes,
lfs_objects_size: 2.exabytes,
- build_artifacts_size: 4.exabytes - 1
+ build_artifacts_size: 3.exabytes - 1
)
statistics.reload
expect(statistics.commit_count).to eq(8.exabytes - 1)
expect(statistics.repository_size).to eq(2.exabytes)
+ expect(statistics.wiki_size).to eq(1.exabytes)
expect(statistics.lfs_objects_size).to eq(2.exabytes)
- expect(statistics.build_artifacts_size).to eq(4.exabytes - 1)
+ expect(statistics.build_artifacts_size).to eq(3.exabytes - 1)
expect(statistics.storage_size).to eq(8.exabytes - 1)
end
end
@@ -33,6 +35,7 @@ describe ProjectStatistics do
describe '#total_repository_size' do
it "sums repository and LFS object size" do
statistics.repository_size = 2
+ statistics.wiki_size = 6
statistics.lfs_objects_size = 3
statistics.build_artifacts_size = 4
@@ -40,10 +43,17 @@ describe ProjectStatistics do
end
end
+ describe '#wiki_size' do
+ it "is initialized with not null value" do
+ expect(statistics.wiki_size).to eq 0
+ end
+ end
+
describe '#refresh!' do
before do
allow(statistics).to receive(:update_commit_count)
allow(statistics).to receive(:update_repository_size)
+ allow(statistics).to receive(:update_wiki_size)
allow(statistics).to receive(:update_lfs_objects_size)
allow(statistics).to receive(:update_storage_size)
end
@@ -56,6 +66,7 @@ describe ProjectStatistics do
it "sums all counters" do
expect(statistics).to have_received(:update_commit_count)
expect(statistics).to have_received(:update_repository_size)
+ expect(statistics).to have_received(:update_wiki_size)
expect(statistics).to have_received(:update_lfs_objects_size)
end
end
@@ -69,6 +80,45 @@ describe ProjectStatistics do
expect(statistics).to have_received(:update_lfs_objects_size)
expect(statistics).not_to have_received(:update_commit_count)
expect(statistics).not_to have_received(:update_repository_size)
+ expect(statistics).not_to have_received(:update_wiki_size)
+ end
+ end
+
+ context 'without repositories' do
+ it 'does not crash' do
+ expect(project.repository.exists?).to be_falsey
+ expect(project.wiki.repository.exists?).to be_falsey
+
+ statistics.refresh!
+
+ expect(statistics).to have_received(:update_commit_count)
+ expect(statistics).to have_received(:update_repository_size)
+ expect(statistics).to have_received(:update_wiki_size)
+ expect(statistics.repository_size).to eq(0)
+ expect(statistics.commit_count).to eq(0)
+ expect(statistics.wiki_size).to eq(0)
+ end
+ end
+
+ context 'with deleted repositories' do
+ let(:project) { create(:project, :repository, :wiki_repo) }
+
+ before do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ FileUtils.rm_rf(project.repository.path)
+ FileUtils.rm_rf(project.wiki.repository.path)
+ end
+ end
+
+ it 'does not crash' do
+ statistics.refresh!
+
+ expect(statistics).to have_received(:update_commit_count)
+ expect(statistics).to have_received(:update_repository_size)
+ expect(statistics).to have_received(:update_wiki_size)
+ expect(statistics.repository_size).to eq(0)
+ expect(statistics.commit_count).to eq(0)
+ expect(statistics.wiki_size).to eq(0)
end
end
end
@@ -95,6 +145,17 @@ describe ProjectStatistics do
end
end
+ describe '#update_wiki_size' do
+ before do
+ allow(project.wiki.repository).to receive(:size).and_return(34)
+ statistics.update_wiki_size
+ end
+
+ it "stores the size of the wiki" do
+ expect(statistics.wiki_size).to eq 34.megabytes
+ end
+ end
+
describe '#update_lfs_objects_size' do
let!(:lfs_object1) { create(:lfs_object, size: 23.megabytes) }
let!(:lfs_object2) { create(:lfs_object, size: 34.megabytes) }
@@ -114,12 +175,13 @@ describe ProjectStatistics do
it "sums all storage counters" do
statistics.update!(
repository_size: 2,
+ wiki_size: 4,
lfs_objects_size: 3
)
statistics.reload
- expect(statistics.storage_size).to eq 5
+ expect(statistics.storage_size).to eq 9
end
end
diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb
index a724c5c3f1c..9beea2e2594 100644
--- a/spec/requests/api/graphql/gitlab_schema_spec.rb
+++ b/spec/requests/api/graphql/gitlab_schema_spec.rb
@@ -52,13 +52,22 @@ describe 'GitlabSchema configurations' do
end
context 'multiplexed queries' do
+ let(:current_user) { nil }
+
subject do
queries = [
- { query: graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id name description)) },
- { query: graphql_query_for('echo', { 'text' => "$test" }, []), variables: { "test" => "Hello world" } }
+ { query: graphql_query_for('project', { 'fullPath' => '$fullPath' }, %w(id name description)) },
+ { query: graphql_query_for('echo', { 'text' => "$test" }, []), variables: { "test" => "Hello world" } },
+ { query: graphql_query_for('project', { 'fullPath' => project.full_path }, "userPermissions { createIssue }") }
]
- post_multiplex(queries)
+ post_multiplex(queries, current_user: current_user)
+ end
+
+ it 'does not authenticate all queries' do
+ subject
+
+ expect(json_response.last['data']['project']).to be_nil
end
it_behaves_like 'imposing query limits' do
@@ -69,18 +78,28 @@ describe 'GitlabSchema configurations' do
subject
# Expect a response for each query, even though it will be empty
- expect(json_response.size).to eq(2)
+ expect(json_response.size).to eq(3)
json_response.each do |single_query_response|
expect(single_query_response).not_to have_key('data')
end
# Expect errors for each query
- expect(graphql_errors.size).to eq(2)
+ expect(graphql_errors.size).to eq(3)
graphql_errors.each do |single_query_errors|
expect(single_query_errors.first['message']).to include('which exceeds max complexity of 4')
end
end
end
+
+ context 'authentication' do
+ let(:current_user) { project.owner }
+
+ it 'authenticates all queries' do
+ subject
+
+ expect(json_response.last['data']['project']['userPermissions']['createIssue']).to be(true)
+ end
+ end
end
context 'when IntrospectionQuery' do
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 7176bc23e34..c41408fba65 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -90,8 +90,9 @@ describe API::Groups do
it "includes statistics if requested" do
attributes = {
- storage_size: 702,
+ storage_size: 1158,
repository_size: 123,
+ wiki_size: 456,
lfs_objects_size: 234,
build_artifacts_size: 345
}.stringify_keys
diff --git a/spec/rubocop/cop/qa/element_with_pattern_spec.rb b/spec/rubocop/cop/qa/element_with_pattern_spec.rb
index c5beb40f9fd..ef20d9a1f26 100644
--- a/spec/rubocop/cop/qa/element_with_pattern_spec.rb
+++ b/spec/rubocop/cop/qa/element_with_pattern_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
@@ -23,7 +25,7 @@ describe RuboCop::Cop::QA::ElementWithPattern do
element :groups_filter, 'search_field_tag :filter'
^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't use a pattern for element, create a corresponding `qa-groups-filter` instead.
element :groups_filter_placeholder, /Search by name/
- ^^^^^^^^^^^^^^^^ Don't use a pattern for element, create a corresponding `qa-groups-filter-placeholder` instead.
+ ^^^^^^^^^^^^^^ Don't use a pattern for element, create a corresponding `qa-groups-filter-placeholder` instead.
end
RUBY
end
@@ -35,6 +37,13 @@ describe RuboCop::Cop::QA::ElementWithPattern do
element :groups_filter_placeholder
end
RUBY
+
+ expect_no_offenses(<<-RUBY)
+ view 'app/views/shared/groups/_search_form.html.haml' do
+ element :groups_filter, required: true
+ element :groups_filter_placeholder, required: false
+ end
+ RUBY
end
end
diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb
index 9c2e5c79a9d..d922e8246c7 100644
--- a/spec/serializers/build_details_entity_spec.rb
+++ b/spec/serializers/build_details_entity_spec.rb
@@ -146,5 +146,14 @@ describe BuildDetailsEntity do
end
end
end
+
+ context 'when the build has reports' do
+ let!(:report) { create(:ci_job_artifact, :codequality, job: build) }
+
+ it 'exposes the report artifacts' do
+ expect(subject[:reports].count).to eq(1)
+ expect(subject[:reports].first[:file_type]).to eq('codequality')
+ end
+ end
end
end
diff --git a/spec/serializers/job_artifact_report_entity_spec.rb b/spec/serializers/job_artifact_report_entity_spec.rb
new file mode 100644
index 00000000000..eef5c16d0fb
--- /dev/null
+++ b/spec/serializers/job_artifact_report_entity_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe JobArtifactReportEntity do
+ let(:report) { create(:ci_job_artifact, :codequality) }
+ let(:entity) { described_class.new(report, request: double) }
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ it 'exposes file_type' do
+ expect(subject[:file_type]).to eq(report.file_type)
+ end
+
+ it 'exposes file_format' do
+ expect(subject[:file_format]).to eq(report.file_format)
+ end
+
+ it 'exposes size' do
+ expect(subject[:size]).to eq(report.size)
+ end
+
+ it 'exposes download path' do
+ expect(subject[:download_path]).to include("jobs/#{report.job.id}/artifacts/download")
+ end
+ end
+end
diff --git a/spec/services/projects/update_statistics_service_spec.rb b/spec/services/projects/update_statistics_service_spec.rb
index 5000ea58e5f..8534853fbc7 100644
--- a/spec/services/projects/update_statistics_service_spec.rb
+++ b/spec/services/projects/update_statistics_service_spec.rb
@@ -17,19 +17,9 @@ describe Projects::UpdateStatisticsService do
end
end
- context 'with an existing project without a repository' do
+ context 'with an existing project' do
let(:project) { create(:project) }
- it 'does nothing' do
- expect_any_instance_of(ProjectStatistics).not_to receive(:refresh!)
-
- service.execute
- end
- end
-
- context 'with an existing project with a repository' do
- let(:project) { create(:project, :repository) }
-
it 'refreshes the project statistics' do
expect_any_instance_of(ProjectStatistics).to receive(:refresh!)
.with(only: statistics.map(&:to_sym))
diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
index 1086546c10d..457dd2e940f 100644
--- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb
+++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
@@ -27,7 +27,7 @@ describe 'projects/commit/_commit_box.html.haml' do
render
- expect(rendered).to have_text("Pipeline ##{third_pipeline.id} failed")
+ expect(rendered).to have_text("Pipeline ##{third_pipeline.id} (##{third_pipeline.iid}) failed")
end
end
@@ -40,7 +40,7 @@ describe 'projects/commit/_commit_box.html.haml' do
it 'shows correct pipeline description' do
render
- expect(rendered).to have_text "Pipeline ##{pipeline.id} " \
+ expect(rendered).to have_text "Pipeline ##{pipeline.id} (##{pipeline.iid}) " \
'waiting for manual action'
end
end
diff --git a/spec/views/projects/jobs/_build.html.haml_spec.rb b/spec/views/projects/jobs/_build.html.haml_spec.rb
index 1d58891036e..97b25a6976f 100644
--- a/spec/views/projects/jobs/_build.html.haml_spec.rb
+++ b/spec/views/projects/jobs/_build.html.haml_spec.rb
@@ -4,7 +4,7 @@ describe 'projects/ci/jobs/_build' do
include Devise::Test::ControllerHelpers
let(:project) { create(:project, :repository) }
- let(:pipeline) { create(:ci_empty_pipeline, id: 1337, project: project, sha: project.commit.id) }
+ let(:pipeline) { create(:ci_empty_pipeline, id: 1337, iid: 57, project: project, sha: project.commit.id) }
let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', stage_idx: 1, name: 'rspec 0:2', status: :pending) }
before do
@@ -15,14 +15,14 @@ describe 'projects/ci/jobs/_build' do
it 'won\'t include a column with a link to its pipeline by default' do
render partial: 'projects/ci/builds/build', locals: { build: build }
- expect(rendered).not_to have_link('#1337')
- expect(rendered).not_to have_text('#1337 by API')
+ expect(rendered).not_to have_link('#1337 (#57)')
+ expect(rendered).not_to have_text('#1337 (#57) by API')
end
it 'can include a column with a link to its pipeline' do
render partial: 'projects/ci/builds/build', locals: { build: build, pipeline_link: true }
- expect(rendered).to have_link('#1337')
- expect(rendered).to have_text('#1337 by API')
+ expect(rendered).to have_link('#1337 (#57)')
+ expect(rendered).to have_text('#1337 (#57) by API')
end
end
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index 3c40269adc7..51afb076da1 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -25,10 +25,11 @@ describe ProjectCacheWorker do
end
context 'with an existing project without a repository' do
- it 'does nothing' do
+ it 'updates statistics but does not refresh the method cashes' do
allow_any_instance_of(Repository).to receive(:exists?).and_return(false)
- expect(worker).not_to receive(:update_statistics)
+ expect(worker).to receive(:update_statistics)
+ expect_any_instance_of(Repository).not_to receive(:refresh_method_caches)
worker.perform(project.id)
end
diff --git a/public/visual-review-toolbar.js b/vendor/assets/javascripts/visual_review_toolbar.js
index 6a0fdb29cc2..12a3a4c9672 100644
--- a/public/visual-review-toolbar.js
+++ b/vendor/assets/javascripts/visual_review_toolbar.js
@@ -2,260 +2,11 @@
/////////////////// STYLES ////////////////////
///////////////////////////////////////////////
+// this style must be applied inline
const buttonClearStyles = `
-webkit-appearance: none;
`;
-const buttonBaseStyles = `
- cursor: pointer;
- transition: background-color 100ms linear, border-color 100ms linear, color 100ms linear, box-shadow 100ms linear;
-`;
-
-const buttonSuccessActiveStyles = `
- background-color: #168f48;
- border-color: #12753a;
- color: #fff;
-`;
-
-const buttonSuccessHoverStyles = `
- color: #fff;
- background-color: #137e3f;
- border-color: #127339;
-`;
-
-const buttonSuccessStyles = `
- ${buttonBaseStyles}
- background-color: #1aaa55;
- border: 1px solid #168f48;
- color: #fff;
-`;
-
-const buttonSecondaryStyles = `
- ${buttonBaseStyles}
- background: none #fff;
- margin: 0 .5rem;
- border: 1px solid #e3e3e3;
-`;
-
-const buttonSecondaryActiveStyles = `
- color: #2e2e2e;
- background-color: #e1e1e1;
- border-color: #dadada;
-`;
-
-const buttonSecondaryHoverStyles = `
- background-color: #f0f0f0;
- border-color: #e3e3e3;
- color: #2e2e2e;
-`;
-
-const buttonWideStyles = `
- width: 100%;
-`;
-
-const buttonWrapperStyles = `
- margin-top: 1rem;
- display: flex;
- align-items: baseline;
- justify-content: flex-end;
-`;
-
-const collapseStyles = `
- ${buttonBaseStyles}
- width: 2.4rem;
- height: 2.2rem;
- margin-left: 1rem;
- padding: .5rem;
-`;
-
-const collapseClosedStyles = `
- ${collapseStyles}
- align-self: center;
-`;
-
-const collapseOpenStyles = `
- ${collapseStyles}
-`;
-
-const checkboxLabelStyles = `
- padding: 0 .2rem;
-`;
-
-const checkboxWrapperStyles = `
- display: flex;
- align-items: baseline;
-`;
-
-const formStyles = `
- display: flex;
- flex-direction: column;
- width: 100%
-`;
-
-const labelStyles = `
- font-weight: 600;
- display: inline-block;
- width: 100%;
-`;
-
-const linkStyles = `
- color: #1b69b6;
- text-decoration: none;
- background-color: transparent;
- background-image: none;
-`;
-
-const messageStyles = `
- padding: .25rem 0;
- margin: 0;
- line-height: 1.2rem;
-`;
-
-const metadataNoteStyles = `
- font-size: .7rem;
- line-height: 1rem;
- color: #666;
- margin-bottom: 0;
-`;
-
-const inputStyles = `
- width: 100%;
- border: 1px solid #dfdfdf;
- border-radius: 4px;
- padding: .1rem .2rem;
- min-height: 2rem;
- max-width: 17rem;
-`;
-
-const svgInnerStyles = `
- pointer-events: none;
-`;
-
-const wrapperClosedStyles = `
- max-width: 3.4rem;
- max-height: 3.4rem;
-`;
-
-const wrapperOpenStyles = `
- max-width: 22rem;
- max-height: 22rem;
-`;
-
-const wrapperStyles = `
- max-width: 22rem;
- max-height: 22rem;
- overflow: scroll;
- position: fixed;
- bottom: 1rem;
- right: 1rem;
- display: flex;
- flex-direction: row-reverse;
- padding: 1rem;
- background-color: #fff;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
- 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
- 'Noto Color Emoji';
- font-size: .8rem;
- font-weight: 400;
- color: #2e2e2e;
-`;
-
-const gitlabStyles = `
- #gitlab-collapse > * {
- ${svgInnerStyles}
- }
-
- #gitlab-form-wrapper {
- ${formStyles}
- }
-
- #gitlab-review-container {
- ${wrapperStyles}
- }
-
- .gitlab-open-wrapper {
- ${wrapperOpenStyles}
- }
-
- .gitlab-closed-wrapper {
- ${wrapperClosedStyles}
- }
-
- .gitlab-button-secondary {
- ${buttonSecondaryStyles}
- }
-
- .gitlab-button-secondary:hover {
- ${buttonSecondaryHoverStyles}
- }
-
- .gitlab-button-secondary:active {
- ${buttonSecondaryActiveStyles}
- }
-
- .gitlab-button-success:hover {
- ${buttonSuccessHoverStyles}
- }
-
- .gitlab-button-success:active {
- ${buttonSuccessActiveStyles}
- }
-
- .gitlab-button-success {
- ${buttonSuccessStyles}
- }
-
- .gitlab-button-wide {
- ${buttonWideStyles}
- }
-
- .gitlab-button-wrapper {
- ${buttonWrapperStyles}
- }
-
- .gitlab-collapse-closed {
- ${collapseClosedStyles}
- }
-
- .gitlab-collapse-open {
- ${collapseOpenStyles}
- }
-
- .gitlab-checkbox-label {
- ${checkboxLabelStyles}
- }
-
- .gitlab-checkbox-wrapper {
- ${checkboxWrapperStyles}
- }
-
- .gitlab-label {
- ${labelStyles}
- }
-
- .gitlab-link {
- ${linkStyles}
- }
-
- .gitlab-message {
- ${messageStyles}
- }
-
- .gitlab-metadata-note {
- ${metadataNoteStyles}
- }
-
- .gitlab-input {
- ${inputStyles}
- }
-`;
-
-function addStylesheet() {
- const styleEl = document.createElement('style');
- styleEl.insertAdjacentHTML('beforeend', gitlabStyles);
- document.head.appendChild(styleEl);
-}
-
///////////////////////////////////////////////
/////////////////// STATE ////////////////////
///////////////////////////////////////////////
@@ -275,25 +26,24 @@ const comment = `
<p class='gitlab-metadata-note'>Additional metadata will be included: browser, OS, current page, user agent, and viewport dimensions.</p>
</div>
<div class='gitlab-button-wrapper''>
- <button class='gitlab-button-secondary' style='${buttonClearStyles}' type='button' id='gitlab-logout-button'> Logout </button>
- <button class='gitlab-button-success' style='${buttonClearStyles}' type='button' id='gitlab-comment-button'> Send feedback </button>
+ <button class='gitlab-button gitlab-button-secondary' style='${buttonClearStyles}' type='button' id='gitlab-logout-button'> Logout </button>
+ <button class='gitlab-button gitlab-button-success' style='${buttonClearStyles}' type='button' id='gitlab-comment-button'> Send feedback </button>
</div>
`;
const commentIcon = `
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/comment</title><path d="M4 11.132l1.446-.964A1 1 0 0 1 6 10h5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v6.132zM6.303 12l-2.748 1.832A1 1 0 0 1 2 13V5a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3H6.303z" id="gitlab-comment-icon"/></svg>
-`
+`;
const compressIcon = `
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/compress</title><path d="M5.27 12.182l-1.562 1.561a1 1 0 0 1-1.414 0h-.001a1 1 0 0 1 0-1.415l1.56-1.56L2.44 9.353a.5.5 0 0 1 .353-.854H7.09a.5.5 0 0 1 .5.5v4.294a.5.5 0 0 1-.853.353l-1.467-1.465zm6.911-6.914l1.464 1.464a.5.5 0 0 1-.353.854H8.999a.5.5 0 0 1-.5-.5V2.793a.5.5 0 0 1 .854-.354l1.414 1.415 1.56-1.561a1 1 0 1 1 1.415 1.414l-1.561 1.56z" id="gitlab-compress-icon"/></svg>
`;
const collapseButton = `
- <button id='gitlab-collapse' style='${buttonClearStyles}' class='gitlab-collapse-open gitlab-button-secondary'>${compressIcon}</button>
+ <button id='gitlab-collapse' style='${buttonClearStyles}' class='gitlab-button gitlab-button-secondary gitlab-collapse gitlab-collapse-open'>${compressIcon}</button>
`;
-
-const form = (content) => `
+const form = content => `
<div id='gitlab-form-wrapper'>
${content}
</div>
@@ -310,7 +60,7 @@ const login = `
<label for="remember_token" class='gitlab-checkbox-label'>Remember me</label>
</div>
<div class='gitlab-button-wrapper'>
- <button class='gitlab-button-wide gitlab-button-success' style='${buttonClearStyles}' type='button' id='gitlab-login'> Submit </button>
+ <button class='gitlab-button-wide gitlab-button gitlab-button-success' style='${buttonClearStyles}' type='button' id='gitlab-login'> Submit </button>
</div>
`;
@@ -319,25 +69,25 @@ const login = `
///////////////////////////////////////////////
// from https://developer.mozilla.org/en-US/docs/Web/API/Window/navigator
-function getBrowserId (sUsrAg) {
- var aKeys = ["MSIE", "Edge", "Firefox", "Safari", "Chrome", "Opera"],
- nIdx = aKeys.length - 1;
+function getBrowserId(sUsrAg) {
+ var aKeys = ['MSIE', 'Edge', 'Firefox', 'Safari', 'Chrome', 'Opera'],
+ nIdx = aKeys.length - 1;
for (nIdx; nIdx > -1 && sUsrAg.indexOf(aKeys[nIdx]) === -1; nIdx--);
return aKeys[nIdx];
}
-function addCommentForm () {
+function addCommentForm() {
const formWrapper = document.getElementById('gitlab-form-wrapper');
formWrapper.innerHTML = comment;
}
-function addLoginForm () {
+function addLoginForm() {
const formWrapper = document.getElementById('gitlab-form-wrapper');
formWrapper.innerHTML = login;
}
-function authorizeUser () {
+function authorizeUser() {
// Clear any old errors
clearNote('gitlab-token');
@@ -357,13 +107,12 @@ function authorizeUser () {
return;
}
-function authSuccess (token) {
+function authSuccess(token) {
data.token = token;
addCommentForm();
}
-
-function clearNote (inputId) {
+function clearNote(inputId) {
const note = document.getElementById('gitlab-validation-note');
note.innerText = '';
note.style.color = '';
@@ -374,7 +123,7 @@ function clearNote (inputId) {
}
}
-function confirmAndClear (mergeRequestId) {
+function confirmAndClear(mergeRequestId) {
const commentButton = document.getElementById('gitlab-comment-button');
const note = document.getElementById('gitlab-validation-note');
@@ -384,7 +133,7 @@ function confirmAndClear (mergeRequestId) {
setTimeout(resetCommentButton, 1000);
}
-function getInitialState () {
+function getInitialState() {
const { localStorage } = window;
try {
@@ -396,22 +145,21 @@ function getInitialState () {
}
return login;
-
} catch (err) {
return login;
}
}
-function getProjectDetails () {
- const { innerWidth,
- innerHeight,
- location: { href },
- navigator: {
- platform, userAgent
- } } = window;
+function getProjectDetails() {
+ const {
+ innerWidth,
+ innerHeight,
+ location: { href },
+ navigator: { platform, userAgent },
+ } = window;
const browser = getBrowserId(userAgent);
- const scriptEl = document.getElementById('review-app-toolbar-script')
+ const scriptEl = document.getElementById('review-app-toolbar-script');
const { projectId, mergeRequestId, mrUrl } = scriptEl.dataset;
return {
@@ -427,7 +175,7 @@ function getProjectDetails () {
};
}
-function logoutUser () {
+function logoutUser() {
const { localStorage } = window;
// All the browsers we support have localStorage, so let's silently fail
@@ -441,7 +189,7 @@ function logoutUser () {
addLoginForm();
}
-function postComment ({
+function postComment({
href,
platform,
browser,
@@ -478,32 +226,34 @@ function postComment ({
const url = `
${mrUrl}/api/v4/projects/${projectId}/merge_requests/${mergeRequestId}/discussions`;
-
const body = `${commentText} ${detailText}`;
fetch(url, {
- method: 'POST',
- headers: {
+ method: 'POST',
+ headers: {
'PRIVATE-TOKEN': data.token,
'Content-Type': 'application/json',
},
body: JSON.stringify({ body }),
})
- .then((response) => {
- if (response.ok) {
- confirmAndClear(mergeRequestId);
- return;
- }
-
- throw new Error(`${response.status}: ${response.statusText}`)
- })
- .catch((err) => {
- postError(`The feedback was not sent successfully. Please try again. Error: ${err.message}`, 'gitlab-comment');
- resetCommentBox();
- });
+ .then(response => {
+ if (response.ok) {
+ confirmAndClear(mergeRequestId);
+ return;
+ }
+
+ throw new Error(`${response.status}: ${response.statusText}`);
+ })
+ .catch(err => {
+ postError(
+ `The feedback was not sent successfully. Please try again. Error: ${err.message}`,
+ 'gitlab-comment',
+ );
+ resetCommentBox();
+ });
}
-function postError (message, inputId) {
+function postError(message, inputId) {
const note = document.getElementById('gitlab-validation-note');
const field = document.getElementById(inputId);
field.style.borderColor = '#db3b21';
@@ -543,8 +293,7 @@ function setInProgressState() {
commentBox.style.pointerEvents = 'none';
}
-function storeToken (token) {
-
+function storeToken(token) {
const { localStorage } = window;
// All the browsers we support have localStorage, so let's silently fail
@@ -556,7 +305,7 @@ function storeToken (token) {
}
}
-function toggleForm () {
+function toggleForm() {
const container = document.getElementById('gitlab-review-container');
const collapseButton = document.getElementById('gitlab-collapse');
const form = document.getElementById('gitlab-form-wrapper');
@@ -578,7 +327,7 @@ function toggleForm () {
display: 'none',
backgroundColor: 'rgba(255, 255, 255, 0)',
},
- }
+ };
const nextState = collapseButton.classList.contains('gitlab-collapse-open') ? CLOSED : OPEN;
@@ -593,9 +342,9 @@ function toggleForm () {
///////////////// INJECTION //////////////////
///////////////////////////////////////////////
-function noop() {};
+function noop() {}
-const eventLookup = ({target: { id }}) => {
+const eventLookup = ({ target: { id } }) => {
switch (id) {
case 'gitlab-collapse':
return toggleForm;
@@ -620,9 +369,9 @@ window.addEventListener('load', () => {
container.insertAdjacentHTML('beforeend', form(content));
document.body.insertBefore(container, document.body.firstChild);
- addStylesheet();
- document.getElementById('gitlab-review-container').addEventListener('click', (event) => {
+ document.getElementById('gitlab-review-container').addEventListener('click', event => {
eventLookup(event)();
});
+
});
diff --git a/yarn.lock b/yarn.lock
index 7c119e2c9dd..421dc718038 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -698,10 +698,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.63.0.tgz#9dd544026d203e4ce6efed72b05db68f710c4d49"
integrity sha512-YztrReFTg31B7v5wtUC5j15KHNcMebtW+kACytEU42XomMaIwk4USIbygqWlq0VRHA2VHJrHApfJHIjxiCCQcA==
-"@gitlab/ui@^3.10.0":
- version "3.10.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.10.0.tgz#0f16772b7fe8052dabd37aba2ae436255b9e0f0a"
- integrity sha512-po6fh2T8esa2Nach73AYLdoTg8N0YrRa5GkJk5GoxVrHYoAUD8T1Rn3pXXXKSsQdQcYjIZJ6uvY8sL+qg+Yjww==
+"@gitlab/ui@^3.10.1":
+ version "3.10.1"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-3.10.1.tgz#473e1383b05847d7226257228a8fd8d2905a9415"
+ integrity sha512-LReB+EIBvZlfeEX2s/azuz6NvegrwwzficvBxK5yPCztjDeegcEsTDlt0F32mX6DcvNFacuuAyWN8QtgpaWkhA==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.2.1"
@@ -712,7 +712,7 @@
js-beautify "^1.8.8"
lodash "^4.17.11"
url-search-params-polyfill "^5.0.0"
- vue "^2.5.21"
+ vue "^2.6.10"
vue-loader "^15.4.2"
"@gitlab/vue-toasted@^1.2.1":
@@ -1398,6 +1398,24 @@ apollo-client@^2.5.1:
tslib "^1.9.3"
zen-observable "^0.8.0"
+apollo-link-batch-http@^1.2.11:
+ version "1.2.11"
+ resolved "https://registry.yarnpkg.com/apollo-link-batch-http/-/apollo-link-batch-http-1.2.11.tgz#ae42dbcc02820658e1e267d05bf2aae7ac208088"
+ integrity sha512-f+KEdbP51I3AeEaBDW2lKS3eaPK/1IXaTM9F2moj02s1hgC/TzeUORRuUeOExW8ggXveW1Jzp6aYMJ2SQkZJyA==
+ dependencies:
+ apollo-link "^1.2.11"
+ apollo-link-batch "^1.1.12"
+ apollo-link-http-common "^0.2.13"
+ tslib "^1.9.3"
+
+apollo-link-batch@^1.1.12:
+ version "1.1.12"
+ resolved "https://registry.yarnpkg.com/apollo-link-batch/-/apollo-link-batch-1.1.12.tgz#64eb231082f182b0395ef7ab903600627f6c7fe8"
+ integrity sha512-6NqLiB9tEGxRiyhtnX/7CPHkmFG0IXfEP7pC5kirhjV+4KxqBaWvJnJGKpGp7Owgdph7KJlV+9+niOKEkcwreg==
+ dependencies:
+ apollo-link "^1.2.11"
+ tslib "^1.9.3"
+
apollo-link-dedup@^1.0.0:
version "1.0.10"
resolved "https://registry.yarnpkg.com/apollo-link-dedup/-/apollo-link-dedup-1.0.10.tgz#7b94589fe7f969777efd18a129043c78430800ae"
@@ -1405,7 +1423,7 @@ apollo-link-dedup@^1.0.0:
dependencies:
apollo-link "^1.2.3"
-apollo-link-http-common@^0.2.8:
+apollo-link-http-common@^0.2.13, apollo-link-http-common@^0.2.8:
version "0.2.13"
resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.13.tgz#c688f6baaffdc7b269b2db7ae89dae7c58b5b350"
integrity sha512-Uyg1ECQpTTA691Fwx5e6Rc/6CPSu4TB4pQRTGIpwZ4l5JDOQ+812Wvi/e3IInmzOZpwx5YrrOfXrtN8BrsDXoA==
@@ -11353,7 +11371,7 @@ vue-virtual-scroll-list@^1.3.1:
resolved "https://registry.yarnpkg.com/vue-virtual-scroll-list/-/vue-virtual-scroll-list-1.3.1.tgz#efcb83d3a3dcc69cd886fa4de1130a65493e8f76"
integrity sha512-PMTxiK9/P1LtgoWWw4n1QnmDDkYqIdWWCNdt1L4JD9g6rwDgnsGsSV10bAnd5n7DQLHGWHjRex+zAbjXWT8t0g==
-vue@^2.5.21, vue@^2.6.10:
+vue@^2.6.10:
version "2.6.10"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"
integrity sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==