summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml1
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile7
-rw-r--r--Gemfile.lock49
-rw-r--r--app/assets/images/icons.json2
-rw-r--r--app/assets/images/icons.svg2
-rw-r--r--app/assets/images/illustrations/clusters_empty.svg2
-rw-r--r--app/assets/images/illustrations/merge_request_changes_empty.svg1
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js4
-rw-r--r--app/assets/javascripts/clusters/clusters_index.js58
-rw-r--r--app/assets/javascripts/clusters/services/clusters_service.js4
-rw-r--r--app/assets/javascripts/commit/image_file.js393
-rw-r--r--app/assets/javascripts/deploy_keys/components/action_btn.vue9
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue5
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js3
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js2
-rw-r--r--app/assets/javascripts/dispatcher.js27
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js75
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue17
-rw-r--r--app/assets/javascripts/image_diff/helpers/utils_helper.js4
-rw-r--r--app/assets/javascripts/issue.js13
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue88
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue24
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description_template.vue4
-rw-r--r--app/assets/javascripts/issue_show/index.js27
-rw-r--r--app/assets/javascripts/job.js8
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js2
-rw-r--r--app/assets/javascripts/merge_request.js2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue24
-rw-r--r--app/assets/javascripts/monitoring/components/graph/deployment.vue101
-rw-r--r--app/assets/javascripts/monitoring/mixins/monitoring_mixins.js2
-rw-r--r--app/assets/javascripts/monitoring/utils/date_time_formatters.js1
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue2
-rw-r--r--app/assets/javascripts/notes/index.js10
-rw-r--r--app/assets/javascripts/projects/ci_cd_settings_bundle.js19
-rw-r--r--app/assets/javascripts/projects/permissions/components/project_feature_setting.vue2
-rw-r--r--app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue51
-rw-r--r--app/assets/javascripts/projects/permissions/components/settings_panel.vue2
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/index.vue15
-rw-r--r--app/assets/javascripts/search_autocomplete.js141
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js102
-rw-r--r--app/assets/javascripts/templates/issuable_template_selectors.js49
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/icon.vue30
-rw-r--r--app/assets/javascripts/vue_shared/components/popup_dialog.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/recaptcha_dialog.vue85
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_button.vue77
-rw-r--r--app/assets/javascripts/vue_shared/mixins/recaptcha_dialog_implementor.js36
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/buttons.scss10
-rw-r--r--app/assets/stylesheets/framework/common.scss13
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss27
-rw-r--r--app/assets/stylesheets/framework/filters.scss1
-rw-r--r--app/assets/stylesheets/framework/lists.scss8
-rw-r--r--app/assets/stylesheets/framework/modal.scss7
-rw-r--r--app/assets/stylesheets/framework/toggle.scss138
-rw-r--r--app/assets/stylesheets/framework/variables.scss6
-rw-r--r--app/assets/stylesheets/pages/clusters.scss14
-rw-r--r--app/assets/stylesheets/pages/commits.scss1
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss35
-rw-r--r--app/assets/stylesheets/pages/environments.scss19
-rw-r--r--app/assets/stylesheets/pages/groups.scss12
-rw-r--r--app/assets/stylesheets/pages/issuable.scss38
-rw-r--r--app/assets/stylesheets/pages/issues.scss17
-rw-r--r--app/assets/stylesheets/pages/labels.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss6
-rw-r--r--app/assets/stylesheets/pages/note_form.scss16
-rw-r--r--app/assets/stylesheets/pages/notes.scss21
-rw-r--r--app/assets/stylesheets/pages/projects.scss87
-rw-r--r--app/assets/stylesheets/pages/tree.scss13
-rw-r--r--app/controllers/admin/appearances_controller.rb6
-rw-r--r--app/controllers/admin/health_check_controller.rb2
-rw-r--r--app/controllers/concerns/issuable_actions.rb2
-rw-r--r--app/controllers/concerns/renders_member_access.rb23
-rw-r--r--app/controllers/concerns/spammable_actions.rb17
-rw-r--r--app/controllers/concerns/uploads_actions.rb23
-rw-r--r--app/controllers/dashboard/projects_controller.rb11
-rw-r--r--app/controllers/explore/projects_controller.rb13
-rw-r--r--app/controllers/groups/uploads_controller.rb35
-rw-r--r--app/controllers/health_controller.rb11
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb4
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb16
-rw-r--r--app/controllers/projects/boards_controller.rb2
-rw-r--r--app/controllers/projects/clusters_controller.rb39
-rw-r--r--app/controllers/projects/commit_controller.rb17
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb1
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb42
-rw-r--r--app/controllers/projects/merge_requests_controller.rb9
-rw-r--r--app/controllers/projects/pipelines_settings_controller.rb1
-rw-r--r--app/controllers/projects/uploads_controller.rb28
-rw-r--r--app/controllers/users_controller.rb7
-rw-r--r--app/finders/clusters_finder.rb29
-rw-r--r--app/helpers/appearances_helper.rb20
-rw-r--r--app/helpers/application_settings_helper.rb19
-rw-r--r--app/helpers/auto_devops_helper.rb16
-rw-r--r--app/helpers/boards_helper.rb2
-rw-r--r--app/helpers/builds_helper.rb3
-rw-r--r--app/helpers/button_helper.rb2
-rw-r--r--app/helpers/commits_helper.rb8
-rw-r--r--app/helpers/graph_helper.rb2
-rw-r--r--app/helpers/markup_helper.rb2
-rw-r--r--app/helpers/merge_requests_helper.rb24
-rw-r--r--app/helpers/storage_health_helper.rb6
-rw-r--r--app/models/appearance.rb3
-rw-r--r--app/models/application_setting.rb12
-rw-r--r--app/models/ci/build.rb39
-rw-r--r--app/models/ci/runner.rb6
-rw-r--r--app/models/clusters/cluster.rb4
-rw-r--r--app/models/commit.rb3
-rw-r--r--app/models/commit_status.rb3
-rw-r--r--app/models/concerns/bulk_member_access_load.rb46
-rw-r--r--app/models/concerns/discussion_on_diff.rb4
-rw-r--r--app/models/concerns/throttled_touch.rb10
-rw-r--r--app/models/diff_discussion.rb6
-rw-r--r--app/models/diff_note.rb7
-rw-r--r--app/models/discussion.rb1
-rw-r--r--app/models/epic.rb10
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/group.rb4
-rw-r--r--app/models/issue.rb1
-rw-r--r--app/models/merge_request.rb40
-rw-r--r--app/models/milestone.rb7
-rw-r--r--app/models/namespace.rb11
-rw-r--r--app/models/note.rb56
-rw-r--r--app/models/personal_access_token.rb21
-rw-r--r--app/models/project.rb6
-rw-r--r--app/models/project_team.rb37
-rw-r--r--app/models/redirect_route.rb28
-rw-r--r--app/models/repository.rb17
-rw-r--r--app/models/route.rb26
-rw-r--r--app/models/user.rb29
-rw-r--r--app/policies/group_policy.rb7
-rw-r--r--app/presenters/clusters/cluster_presenter.rb4
-rw-r--r--app/presenters/merge_request_presenter.rb2
-rw-r--r--app/serializers/merge_request_entity.rb2
-rw-r--r--app/services/boards/issues/list_service.rb9
-rw-r--r--app/services/ci/create_pipeline_service.rb48
-rw-r--r--app/services/ci/register_job_service.rb13
-rw-r--r--app/services/clusters/create_service.rb6
-rw-r--r--app/services/merge_requests/refresh_service.rb3
-rw-r--r--app/services/metrics_service.rb2
-rw-r--r--app/services/projects/autocomplete_service.rb21
-rw-r--r--app/services/projects/fork_service.rb36
-rw-r--r--app/services/projects/update_service.rb10
-rw-r--r--app/services/protected_branches/access_level_params.rb33
-rw-r--r--app/services/protected_branches/api_service.rb24
-rw-r--r--app/uploaders/file_uploader.rb8
-rw-r--r--app/uploaders/namespace_file_uploader.rb15
-rw-r--r--app/views/admin/appearances/_form.html.haml38
-rw-r--r--app/views/admin/appearances/preview_sign_in.html.haml (renamed from app/views/admin/appearances/preview.html.haml)0
-rw-r--r--app/views/admin/application_settings/_form.html.haml18
-rw-r--r--app/views/dashboard/projects/_blank_state_admin_welcome.html.haml2
-rw-r--r--app/views/dashboard/projects/_projects.html.haml2
-rw-r--r--app/views/discussions/_discussion.html.haml14
-rw-r--r--app/views/explore/projects/_projects.html.haml2
-rw-r--r--app/views/layouts/_init_auto_complete.html.haml2
-rw-r--r--app/views/layouts/_page.html.haml1
-rw-r--r--app/views/layouts/_recaptcha_verification.html.haml15
-rw-r--r--app/views/layouts/_search.html.haml10
-rw-r--r--app/views/layouts/devise.html.haml2
-rw-r--r--app/views/layouts/group.html.haml6
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml2
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml7
-rw-r--r--app/views/projects/_md_preview.html.haml4
-rw-r--r--app/views/projects/ci/builds/_build.html.haml2
-rw-r--r--app/views/projects/clusters/_advanced_settings.html.haml8
-rw-r--r--app/views/projects/clusters/_banner.html.haml6
-rw-r--r--app/views/projects/clusters/_cluster.html.haml22
-rw-r--r--app/views/projects/clusters/_dropdown.html.haml2
-rw-r--r--app/views/projects/clusters/_empty_state.html.haml12
-rw-r--r--app/views/projects/clusters/_enabled.html.haml7
-rw-r--r--app/views/projects/clusters/_tabs.html.haml16
-rw-r--r--app/views/projects/clusters/gcp/_header.html.haml8
-rw-r--r--app/views/projects/clusters/gcp/login.html.haml2
-rw-r--r--app/views/projects/clusters/gcp/new.html.haml2
-rw-r--r--app/views/projects/clusters/index.html.haml24
-rw-r--r--app/views/projects/clusters/new.html.haml2
-rw-r--r--app/views/projects/clusters/show.html.haml4
-rw-r--r--app/views/projects/commit/_commit_box.html.haml12
-rw-r--r--app/views/projects/commit/show.html.haml3
-rw-r--r--app/views/projects/commits/_commit.html.haml29
-rw-r--r--app/views/projects/commits/_commits.html.haml7
-rw-r--r--app/views/projects/commits/show.html.haml2
-rw-r--r--app/views/projects/environments/metrics.html.haml2
-rw-r--r--app/views/projects/issues/_discussion.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml16
-rw-r--r--app/views/projects/merge_requests/_commits.html.haml2
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml12
-rw-r--r--app/views/projects/merge_requests/diffs/_commit_widget.html.haml5
-rw-r--r--app/views/projects/merge_requests/diffs/_different_base.html.haml11
-rw-r--r--app/views/projects/merge_requests/diffs/_diffs.html.haml23
-rw-r--r--app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml17
-rw-r--r--app/views/projects/merge_requests/diffs/_version_controls.html.haml (renamed from app/views/projects/merge_requests/diffs/_versions.html.haml)26
-rw-r--r--app/views/projects/merge_requests/show.html.haml10
-rw-r--r--app/views/projects/new.html.haml4
-rw-r--r--app/views/projects/notes/_actions.html.haml2
-rw-r--r--app/views/projects/pipelines_settings/_show.html.haml23
-rw-r--r--app/views/projects/tree/_old_tree_header.html.haml16
-rw-r--r--app/views/shared/_clone_panel.html.haml2
-rw-r--r--app/views/shared/_outdated_browser.html.haml13
-rw-r--r--app/views/shared/_recaptcha_form.html.haml19
-rw-r--r--app/views/shared/groups/_group.html.haml25
-rw-r--r--app/views/shared/groups/_list.html.haml4
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml2
-rw-r--r--app/views/shared/projects/_list.html.haml6
-rw-r--r--app/views/shared/projects/_project.html.haml11
-rw-r--r--app/views/shared/snippets/_header.html.haml21
-rw-r--r--app/workers/update_head_pipeline_for_merge_request_worker.rb15
-rwxr-xr-xbin/storage_check11
-rw-r--r--changelogs/unreleased/15774-fix-39233-500-in-merge-request.yml5
-rw-r--r--changelogs/unreleased/22680-unlabel-slash-command-limit-autocomplete-to-applied-labels.yml5
-rw-r--r--changelogs/unreleased/29483-no-feedback-when-checking-on-checklist-if-potential-spam-was-detected.yml5
-rw-r--r--changelogs/unreleased/35385-allow-git-pull-push-on-project-redirects.yml5
-rw-r--r--changelogs/unreleased/37354-pipelines-update.yml5
-rw-r--r--changelogs/unreleased/38032-deploy-markers-should-be-more-verbose.yml5
-rw-r--r--changelogs/unreleased/38869-templates.yml5
-rw-r--r--changelogs/unreleased/39364-in-issue-board-url-doesn-t-take-in-account-hostname-settings.yml5
-rw-r--r--changelogs/unreleased/40031-include-assset_sync-gem.yml5
-rw-r--r--changelogs/unreleased/40508-snippets-zen-mode.yml5
-rw-r--r--changelogs/unreleased/40555-replace-absolute-urls-with-related-branches-to-avoid-hostname.yml6
-rw-r--r--changelogs/unreleased/40573-rename-gke-as-kubernetes-engine.yml5
-rw-r--r--changelogs/unreleased/40715-updateendpoint-undefined-on-issue-page.yml5
-rw-r--r--changelogs/unreleased/admin-welcome-new-group-link.yml5
-rw-r--r--changelogs/unreleased/anchor-issue-references.yml6
-rw-r--r--changelogs/unreleased/bvl-circuitbreaker-process.yml5
-rw-r--r--changelogs/unreleased/commit-title-wrapping.yml5
-rw-r--r--changelogs/unreleased/deploy-keys-loading-icon.yml5
-rw-r--r--changelogs/unreleased/dm-commit-diff-discussions-in-mr-context.yml5
-rw-r--r--changelogs/unreleased/docs-add-why-do-i-get-signed-out-authentication-section.yml5
-rw-r--r--changelogs/unreleased/feature-custom-text-for-new-projects.yml5
-rw-r--r--changelogs/unreleased/feature-sm-34834-missing-dependency-should-fail-job-2.yml5
-rw-r--r--changelogs/unreleased/fix-event-target-author-preloading.yml5
-rw-r--r--changelogs/unreleased/fix-new-project-guidelines-styling.yml5
-rw-r--r--changelogs/unreleased/fj-40752-forks-api-not-using-services.yml5
-rw-r--r--changelogs/unreleased/merge-request-lock-icon-size-fix.yml5
-rw-r--r--changelogs/unreleased/mk-add-old-attachments-to-uploads-table.yml5
-rw-r--r--changelogs/unreleased/outdated-browser-position-fix.yml5
-rw-r--r--changelogs/unreleased/patch-24.yml5
-rw-r--r--changelogs/unreleased/perform-sql-matching-of-tags.yml5
-rw-r--r--changelogs/unreleased/throttle-touching-of-objects.yml5
-rw-r--r--changelogs/unreleased/update_mr_changes_empty_page.yml5
-rw-r--r--config/initializers/7_prometheus_metrics.rb9
-rw-r--r--config/initializers/asset_sync.rb31
-rw-r--r--config/routes.rb1
-rw-r--r--config/routes/admin.rb2
-rw-r--r--config/routes/group.rb6
-rw-r--r--db/migrate/20171103000000_set_uploads_path_size_for_mysql.rb25
-rw-r--r--db/migrate/20171122131600_add_new_project_guidelines_to_appearances.rb12
-rw-r--r--db/migrate/20171123094802_add_circuitbreaker_check_interval_to_application_settings.rb20
-rw-r--r--db/migrate/20171204204233_add_permanent_to_redirect_route.rb18
-rw-r--r--db/migrate/20171206221519_add_permanent_index_to_redirect_route.rb19
-rw-r--r--db/post_migrate/20171103140253_track_untracked_uploads.rb21
-rw-r--r--db/post_migrate/20171123101020_update_circuitbreaker_defaults.rb34
-rw-r--r--db/post_migrate/20171123101046_remove_old_circuitbreaker_config.rb26
-rw-r--r--db/post_migrate/20171205190711_reschedule_fork_network_creation_caller.rb27
-rw-r--r--db/schema.rb15
-rw-r--r--doc/README.md95
-rw-r--r--doc/administration/index.md121
-rw-r--r--doc/administration/job_artifacts.md39
-rw-r--r--doc/administration/pages/index.md3
-rw-r--r--doc/administration/raketasks/maintenance.md4
-rw-r--r--doc/api/protected_branches.md2
-rw-r--r--doc/api/settings.md3
-rw-r--r--doc/ci/autodeploy/quick_start_guide.md4
-rw-r--r--doc/ci/yaml/README.md14
-rw-r--r--doc/customization/new_project_page.md20
-rw-r--r--doc/customization/new_project_page/appearance_settings.pngbin0 -> 71178 bytes
-rw-r--r--doc/customization/new_project_page/custom_new_project_page.pngbin0 -> 164962 bytes
-rw-r--r--doc/customization/new_project_page/default_new_project_page.pngbin0 -> 146906 bytes
-rw-r--r--doc/install/kubernetes/gitlab_chart.md4
-rw-r--r--doc/install/kubernetes/gitlab_omnibus.md4
-rw-r--r--doc/install/kubernetes/gitlab_runner_chart.md2
-rw-r--r--doc/install/kubernetes/index.md2
-rw-r--r--doc/integration/slash_commands.md3
-rw-r--r--doc/topics/authentication/index.md1
-rw-r--r--doc/topics/autodevops/img/auto_devops_settings.pngbin67845 -> 95233 bytes
-rw-r--r--doc/topics/autodevops/index.md2
-rw-r--r--doc/topics/autodevops/quick_start_guide.md4
-rw-r--r--doc/user/permissions.md17
-rw-r--r--doc/user/profile/index.md26
-rw-r--r--doc/user/project/index.md2
-rw-r--r--doc/user/project/pages/index.md121
-rw-r--r--doc/user/project/pipelines/job_artifacts.md18
-rw-r--r--lib/api/circuit_breakers.rb2
-rw-r--r--lib/api/entities.rb3
-rw-r--r--lib/api/internal.rb13
-rw-r--r--lib/api/projects.rb21
-rw-r--r--lib/api/protected_branches.rb18
-rw-r--r--lib/backup/repository.rb9
-rw-r--r--lib/banzai/cross_project_reference.rb2
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb98
-rw-r--r--lib/banzai/filter/commit_reference_filter.rb34
-rw-r--r--lib/banzai/filter/epic_reference_filter.rb12
-rw-r--r--lib/banzai/filter/issuable_reference_filter.rb31
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb32
-rw-r--r--lib/banzai/filter/label_reference_filter.rb4
-rw-r--r--lib/banzai/filter/merge_request_reference_filter.rb37
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb2
-rw-r--r--lib/banzai/filter/table_of_contents_filter.rb1
-rw-r--r--lib/banzai/filter/upload_link_filter.rb18
-rw-r--r--lib/banzai/issuable_extractor.rb4
-rw-r--r--lib/banzai/object_renderer.rb12
-rw-r--r--lib/banzai/reference_parser/epic_parser.rb12
-rw-r--r--lib/banzai/reference_parser/issuable_parser.rb25
-rw-r--r--lib/banzai/reference_parser/issue_parser.rb12
-rw-r--r--lib/banzai/reference_parser/merge_request_parser.rb24
-rw-r--r--lib/gitlab/background_migration/populate_untracked_uploads.rb259
-rw-r--r--lib/gitlab/background_migration/prepare_untracked_uploads.rb163
-rw-r--r--lib/gitlab/checks/project_moved.rb65
-rw-r--r--lib/gitlab/ci/pipeline/chain/base.rb7
-rw-r--r--lib/gitlab/ci/pipeline/chain/build.rb42
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb61
-rw-r--r--lib/gitlab/ci/pipeline/chain/create.rb16
-rw-r--r--lib/gitlab/ci/pipeline/chain/helpers.rb12
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/abilities.rb12
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/repository.rb7
-rw-r--r--lib/gitlab/database.rb14
-rw-r--r--lib/gitlab/diff/diff_refs.rb22
-rw-r--r--lib/gitlab/diff/inline_diff.rb2
-rw-r--r--lib/gitlab/git.rb12
-rw-r--r--lib/gitlab/git/commit.rb17
-rw-r--r--lib/gitlab/git/operation_service.rb2
-rw-r--r--lib/gitlab/git/remote_repository.rb6
-rw-r--r--lib/gitlab/git/repository.rb103
-rw-r--r--lib/gitlab/git/storage/checker.rb98
-rw-r--r--lib/gitlab/git/storage/circuit_breaker.rb106
-rw-r--r--lib/gitlab/git/storage/circuit_breaker_settings.rb12
-rw-r--r--lib/gitlab/git/storage/failure_info.rb39
-rw-r--r--lib/gitlab/git/storage/null_circuit_breaker.rb22
-rw-r--r--lib/gitlab/git_access.rb23
-rw-r--r--lib/gitlab/git_access_wiki.rb6
-rw-r--r--lib/gitlab/gitaly_client.rb8
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb20
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb32
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb11
-rw-r--r--lib/gitlab/gitaly_client/wiki_service.rb10
-rw-r--r--lib/gitlab/identifier.rb5
-rw-r--r--lib/gitlab/metrics/samplers/ruby_sampler.rb6
-rw-r--r--lib/gitlab/reference_extractor.rb2
-rw-r--r--lib/gitlab/storage_check.rb11
-rw-r--r--lib/gitlab/storage_check/cli.rb69
-rw-r--r--lib/gitlab/storage_check/gitlab_caller.rb39
-rw-r--r--lib/gitlab/storage_check/option_parser.rb39
-rw-r--r--lib/gitlab/storage_check/response.rb77
-rw-r--r--lib/gitlab/utils.rb17
-rw-r--r--locale/bg/gitlab.po16
-rw-r--r--locale/de/gitlab.po16
-rw-r--r--locale/eo/gitlab.po16
-rw-r--r--locale/es/gitlab.po16
-rw-r--r--locale/fr/gitlab.po32
-rw-r--r--locale/gitlab.pot513
-rw-r--r--locale/it/gitlab.po16
-rw-r--r--locale/ja/gitlab.po16
-rw-r--r--locale/ko/gitlab.po16
-rw-r--r--locale/nl_NL/gitlab.po16
-rw-r--r--locale/pl_PL/gitlab.po16
-rw-r--r--locale/pt_BR/gitlab.po32
-rw-r--r--locale/ru/gitlab.po32
-rw-r--r--locale/uk/gitlab.po32
-rw-r--r--locale/zh_CN/gitlab.po32
-rw-r--r--locale/zh_HK/gitlab.po16
-rw-r--r--locale/zh_TW/gitlab.po22
-rw-r--r--qa/qa/page/group/new.rb1
-rw-r--r--spec/bin/storage_check_spec.rb13
-rw-r--r--spec/controllers/admin/health_check_controller_spec.rb4
-rw-r--r--spec/controllers/groups/uploads_controller_spec.rb10
-rw-r--r--spec/controllers/health_controller_spec.rb42
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb12
-rw-r--r--spec/controllers/projects/clusters/gcp_controller_spec.rb6
-rw-r--r--spec/controllers/projects/clusters/user_controller_spec.rb4
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb160
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb18
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb23
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb3
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb20
-rw-r--r--spec/controllers/projects/pipelines_settings_controller_spec.rb15
-rw-r--r--spec/controllers/projects/uploads_controller_spec.rb247
-rw-r--r--spec/factories/appearances.rb1
-rw-r--r--spec/factories/clusters/clusters.rb4
-rw-r--r--spec/factories/notes.rb10
-rw-r--r--spec/factories/uploads.rb16
-rw-r--r--spec/features/admin/admin_appearance_spec.rb33
-rw-r--r--spec/features/admin/admin_health_check_spec.rb12
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb106
-rw-r--r--spec/features/markdown_spec.rb3
-rw-r--r--spec/features/merge_requests/pipelines_spec.rb15
-rw-r--r--spec/features/merge_requests/versions_spec.rb105
-rw-r--r--spec/features/merge_requests/widget_spec.rb12
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb10
-rw-r--r--spec/features/projects/clusters/user_spec.rb3
-rw-r--r--spec/features/projects/clusters_spec.rb72
-rw-r--r--spec/features/projects/features_visibility_spec.rb6
-rw-r--r--spec/features/projects/issuable_templates_spec.rb4
-rw-r--r--spec/features/projects/settings/pipelines_settings_spec.rb101
-rw-r--r--spec/features/projects/snippets_spec.rb5
-rw-r--r--spec/finders/clusters_finder_spec.rb31
-rw-r--r--spec/helpers/auto_devops_helper_spec.rb100
-rw-r--r--spec/helpers/boards_helper_spec.rb21
-rw-r--r--spec/helpers/button_helper_spec.rb28
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb17
-rw-r--r--spec/javascripts/clusters/clusters_bundle_spec.js2
-rw-r--r--spec/javascripts/clusters/clusters_index_spec.js58
-rw-r--r--spec/javascripts/deploy_keys/components/action_btn_spec.js2
-rw-r--r--spec/javascripts/deploy_keys/components/app_spec.js14
-rw-r--r--spec/javascripts/fixtures/clusters.rb15
-rw-r--r--spec/javascripts/image_diff/helpers/utils_helper_spec.js10
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js49
-rw-r--r--spec/javascripts/issue_show/components/description_spec.js30
-rw-r--r--spec/javascripts/issue_show/components/form_spec.js1
-rw-r--r--spec/javascripts/job_spec.js2
-rw-r--r--spec/javascripts/lib/utils/datefix_spec.js2
-rw-r--r--spec/javascripts/merge_request_spec.js4
-rw-r--r--spec/javascripts/monitoring/graph/deployment_spec.js29
-rw-r--r--spec/javascripts/monitoring/graph_spec.js14
-rw-r--r--spec/javascripts/monitoring/mock_data.js6
-rw-r--r--spec/javascripts/search_autocomplete_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/popup_dialog_spec.js12
-rw-r--r--spec/javascripts/vue_shared/components/toggle_button_spec.js91
-rw-r--r--spec/lib/backup/manager_spec.rb (renamed from spec/lib/gitlab/backup/manager_spec.rb)0
-rw-r--r--spec/lib/backup/repository_spec.rb69
-rw-r--r--spec/lib/banzai/cross_project_reference_spec.rb8
-rw-r--r--spec/lib/banzai/filter/abstract_reference_filter_spec.rb38
-rw-r--r--spec/lib/banzai/filter/commit_reference_filter_spec.rb12
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb47
-rw-r--r--spec/lib/banzai/filter/table_of_contents_filter_spec.rb7
-rw-r--r--spec/lib/banzai/filter/upload_link_filter_spec.rb30
-rw-r--r--spec/lib/banzai/reference_parser/issue_parser_spec.rb4
-rw-r--r--spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb510
-rw-r--r--spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb242
-rw-r--r--spec/lib/gitlab/backup/repository_spec.rb117
-rw-r--r--spec/lib/gitlab/checks/project_moved_spec.rb81
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/build_spec.rb57
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/command_spec.rb185
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/create_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb9
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb5
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb26
-rw-r--r--spec/lib/gitlab/database_spec.rb38
-rw-r--r--spec/lib/gitlab/diff/inline_diff_spec.rb4
-rw-r--r--spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb10
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb29
-rw-r--r--spec/lib/gitlab/git/remote_repository_spec.rb4
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb2
-rw-r--r--spec/lib/gitlab/git/storage/checker_spec.rb132
-rw-r--r--spec/lib/gitlab/git/storage/circuit_breaker_spec.rb163
-rw-r--r--spec/lib/gitlab/git/storage/failure_info_spec.rb70
-rw-r--r--spec/lib/gitlab/git/storage/health_spec.rb2
-rw-r--r--spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb4
-rw-r--r--spec/lib/gitlab/git_access_spec.rb60
-rw-r--r--spec/lib/gitlab/git_spec.rb25
-rw-r--r--spec/lib/gitlab/identifier_spec.rb4
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb39
-rw-r--r--spec/lib/gitlab/storage_check/cli_spec.rb19
-rw-r--r--spec/lib/gitlab/storage_check/gitlab_caller_spec.rb46
-rw-r--r--spec/lib/gitlab/storage_check/option_parser_spec.rb31
-rw-r--r--spec/lib/gitlab/storage_check/response_spec.rb54
-rw-r--r--spec/lib/gitlab/utils_spec.rb10
-rw-r--r--spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb4
-rw-r--r--spec/migrations/track_untracked_uploads_spec.rb27
-rw-r--r--spec/models/appearance_spec.rb3
-rw-r--r--spec/models/application_setting_spec.rb15
-rw-r--r--spec/models/ci/build_spec.rb161
-rw-r--r--spec/models/ci/pipeline_spec.rb4
-rw-r--r--spec/models/clusters/cluster_spec.rb22
-rw-r--r--spec/models/concerns/issuable_spec.rb2
-rw-r--r--spec/models/diff_note_spec.rb29
-rw-r--r--spec/models/issue_spec.rb4
-rw-r--r--spec/models/merge_request_spec.rb55
-rw-r--r--spec/models/namespace_spec.rb30
-rw-r--r--spec/models/note_spec.rb2
-rw-r--r--spec/models/personal_access_token_spec.rb25
-rw-r--r--spec/models/project_spec.rb21
-rw-r--r--spec/models/repository_spec.rb74
-rw-r--r--spec/models/route_spec.rb104
-rw-r--r--spec/models/user_spec.rb183
-rw-r--r--spec/policies/group_policy_spec.rb27
-rw-r--r--spec/presenters/clusters/cluster_presenter_spec.rb40
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb2
-rw-r--r--spec/requests/api/circuit_breakers_spec.rb2
-rw-r--r--spec/requests/api/internal_spec.rb38
-rw-r--r--spec/requests/api/protected_branches_spec.rb36
-rw-r--r--spec/requests/api/settings_spec.rb4
-rw-r--r--spec/requests/git_http_spec.rb8
-rw-r--r--spec/serializers/merge_request_entity_spec.rb30
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb58
-rw-r--r--spec/services/ci/register_job_service_spec.rb106
-rw-r--r--spec/services/clusters/create_service_spec.rb75
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb14
-rw-r--r--spec/services/projects/fork_service_spec.rb353
-rw-r--r--spec/services/projects/update_service_spec.rb45
-rw-r--r--spec/services/system_note_service_spec.rb4
-rw-r--r--spec/spec_helper.rb12
-rw-r--r--spec/support/google_api/cloud_platform_helpers.rb2
-rw-r--r--spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb240
-rw-r--r--spec/support/shared_examples/throttled_touch.rb20
-rw-r--r--spec/support/stored_repositories.rb21
-rw-r--r--spec/support/stub_configuration.rb2
-rw-r--r--spec/support/track_untracked_uploads_helpers.rb20
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb2
-rw-r--r--spec/uploaders/namespace_file_uploader_spec.rb21
-rw-r--r--spec/views/dashboard/projects/_blank_state_admin_welcome.haml.rb15
-rw-r--r--spec/views/projects/commit/show.html.haml_spec.rb22
-rw-r--r--spec/views/projects/merge_requests/_commits.html.haml_spec.rb4
-rw-r--r--spec/views/projects/merge_requests/diffs/_diffs.html.haml_spec.rb36
-rw-r--r--spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb38
-rw-r--r--yarn.lock4
512 files changed, 10025 insertions, 3392 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 60a2b5d5b5b..19540e4331e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -586,6 +586,7 @@ codequality:
paths: [codeclimate.json]
qa:internal:
+ <<: *except-docs
stage: test
variables:
SETUP_DB: "false"
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4930b541ba2..01d4a546b97 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -598,6 +598,7 @@ merge request:
present time and never use past tense (has been/was). For example instead
of _prohibited this user from being saved due to the following errors:_ the
text should be _sorry, we could not create your account because:_
+1. Code should be written in [US English][us-english]
This is also the style used by linting tools such as
[RuboCop](https://github.com/bbatsov/rubocop),
@@ -663,6 +664,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues
[polling-etag]: https://docs.gitlab.com/ce/development/polling.html
[testing]: doc/development/testing_guide/index.md
+[us-english]: https://en.wikipedia.org/wiki/American_English
[^1]: Please note that specs other than JavaScript specs are considered backend
code.
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 2f4c74eb2dd..cb6b534abe1 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.56.0
+0.59.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 509b0b618ad..269fb5dfe2c 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-5.10.0
+5.10.2
diff --git a/Gemfile b/Gemfile
index 044778498a2..93003ed96c4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -171,7 +171,7 @@ gem 're2', '~> 1.1.1'
gem 'version_sorter', '~> 2.1.0'
# Cache
-gem 'redis-rails', '~> 5.0.1'
+gem 'redis-rails', '~> 5.0.2'
# Redis
gem 'redis', '~> 3.2'
@@ -400,7 +400,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.58.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.59.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
@@ -411,3 +411,6 @@ gem 'flipper-active_record', '~> 0.10.2'
# Structured logging
gem 'lograge', '~> 0.5'
gem 'grape_logging', '~> 1.7'
+
+# Asset synchronization
+gem 'asset_sync', '~> 2.2.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index fdb81b101f5..b5ca351fea8 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -58,6 +58,11 @@ GEM
asciidoctor (1.5.3)
asciidoctor-plantuml (0.0.7)
asciidoctor (~> 1.5)
+ asset_sync (2.2.0)
+ activemodel (>= 4.1.0)
+ fog-core
+ mime-types (>= 2.99)
+ unf
ast (2.3.0)
atomic (1.1.99)
attr_encrypted (3.0.3)
@@ -276,7 +281,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly-proto (0.58.0)
+ gitaly-proto (0.59.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@@ -328,8 +333,6 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
google-protobuf (3.4.1.1)
- googleapis-common-protos-types (1.0.0)
- google-protobuf (~> 3.0)
googleauth (0.5.3)
faraday (~> 0.12)
jwt (~> 1.4)
@@ -356,10 +359,9 @@ GEM
rake
grape_logging (1.7.0)
grape
- grpc (1.7.2)
+ grpc (1.4.5)
google-protobuf (~> 3.1)
- googleapis-common-protos-types (~> 1.0.0)
- googleauth (>= 0.5.1, < 0.7)
+ googleauth (~> 0.5.1)
haml (4.0.7)
tilt
haml_lint (0.26.0)
@@ -489,7 +491,6 @@ GEM
mini_mime (0.1.4)
mini_portile2 (2.3.0)
minitest (5.7.0)
- mmap2 (2.2.9)
mousetrap-rails (1.4.6)
multi_json (1.12.2)
multi_xml (0.6.0)
@@ -626,8 +627,7 @@ GEM
parser
unparser
procto (0.0.3)
- prometheus-client-mmap (0.7.0.beta39)
- mmap2 (~> 2.2, >= 2.2.9)
+ prometheus-client-mmap (0.7.0.beta43)
pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
@@ -702,24 +702,24 @@ GEM
recursive-open-struct (1.0.0)
redcarpet (3.4.0)
redis (3.3.3)
- redis-actionpack (5.0.1)
+ redis-actionpack (5.0.2)
actionpack (>= 4.0, < 6)
redis-rack (>= 1, < 3)
- redis-store (>= 1.1.0, < 1.4.0)
- redis-activesupport (5.0.1)
+ redis-store (>= 1.1.0, < 2)
+ redis-activesupport (5.0.4)
activesupport (>= 3, < 6)
- redis-store (~> 1.2.0)
+ redis-store (>= 1.3, < 2)
redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4)
- redis-rack (1.6.0)
- rack (~> 1.5)
- redis-store (~> 1.2.0)
- redis-rails (5.0.1)
- redis-actionpack (~> 5.0.0)
- redis-activesupport (~> 5.0.0)
- redis-store (~> 1.2.0)
- redis-store (1.2.0)
- redis (>= 2.2)
+ redis-rack (2.0.3)
+ rack (>= 1.5, < 3)
+ redis-store (>= 1.2, < 2)
+ redis-rails (5.0.2)
+ redis-actionpack (>= 5.0, < 6)
+ redis-activesupport (>= 5.0, < 6)
+ redis-store (>= 1.2, < 2)
+ redis-store (1.4.1)
+ redis (>= 2.2, < 5)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
@@ -980,6 +980,7 @@ DEPENDENCIES
asana (~> 0.6.0)
asciidoctor (~> 1.5.2)
asciidoctor-plantuml (= 0.0.7)
+ asset_sync (~> 2.2.0)
attr_encrypted (~> 3.0.0)
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
@@ -1037,7 +1038,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
- gitaly-proto (~> 0.58.0)
+ gitaly-proto (~> 0.59.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
@@ -1133,7 +1134,7 @@ DEPENDENCIES
redcarpet (~> 3.4)
redis (~> 3.2)
redis-namespace (~> 1.5.2)
- redis-rails (~> 5.0.1)
+ redis-rails (~> 5.0.2)
request_store (~> 1.3)
responders (~> 2.0)
rouge (~> 2.0)
diff --git a/app/assets/images/icons.json b/app/assets/images/icons.json
index 6befc551263..4e967936ce0 100644
--- a/app/assets/images/icons.json
+++ b/app/assets/images/icons.json
@@ -1 +1 @@
-{"iconCount":179,"spriteSize":81882,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]} \ No newline at end of file
+{"iconCount":180,"spriteSize":82176,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]} \ No newline at end of file
diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg
index 74e1c8c22f6..77ce6b2d89f 100644
--- a/app/assets/images/icons.svg
+++ b/app/assets/images/icons.svg
@@ -1 +1 @@
-<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 16" id="abuse" xmlns="http://www.w3.org/2000/svg"><path d="M11.408.328l4.029 3.222A1.5 1.5 0 0 1 16 4.72v6.555a1.5 1.5 0 0 1-.563 1.171l-4.026 3.224a1.5 1.5 0 0 1-.937.329H5.529a1.5 1.5 0 0 1-.937-.328L.563 12.45A1.5 1.5 0 0 1 0 11.28V4.724a1.5 1.5 0 0 1 .563-1.171L4.589.329A1.5 1.5 0 0 1 5.526 0h4.945c.34 0 .67.116.937.328zM10.296 2H5.702L2 4.964v6.074L5.704 14h4.594L14 11.036V4.962L10.296 2zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="account" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.195 9.965l-.568-.875a.25.25 0 0 1 .015-.294l.405-.5a.25.25 0 0 1 .283-.075l.938.36c.257-.183.543-.325.851-.42l.322-.988A.25.25 0 0 1 11.679 7h.642a.25.25 0 0 1 .238.173l.322.988c.308.095.594.237.851.42l.938-.36a.25.25 0 0 1 .283.076l.405.5a.25.25 0 0 1 .015.293l-.568.875c.113.297.18.616.193.95l.898.54a.25.25 0 0 1 .115.27l-.144.626a.25.25 0 0 1-.222.193l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.281a.25.25 0 0 1-.29-.05l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.904a.25.25 0 0 1-.289.051l-.577-.281a.25.25 0 0 1-.138-.26l.165-1.18a3.015 3.015 0 0 1-.512-.607l-1.115-.098a.25.25 0 0 1-.222-.193l-.144-.626a.25.25 0 0 1 .115-.27l.898-.54c.013-.334.08-.653.193-.95zM6.789 8.023A12.845 12.845 0 0 0 6 8c-5.036 0-6 2.74-6 4.48C0 14.22.076 15 6 15c.553 0 1.055-.006 1.51-.02A5.977 5.977 0 0 1 6 11c0-1.083.287-2.1.79-2.977zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="admin" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.162 2.5a3.5 3.5 0 0 1-3.163 5.479L6.08 14.766a1.5 1.5 0 0 1-2.598-1.5L7.4 6.479A3.5 3.5 0 0 1 10.564 1L8.9 3.88l2.599 1.5 1.663-2.88zm-8.63 11.949a.5.5 0 1 0 .5-.866.5.5 0 0 0-.5.866z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.414 7.95l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 1 0 1.414-1.415L10.414 7.95zm-7 0l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 0 0 1.414-1.415L3.414 7.95z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.536 7.95L1.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 1 1-1.414-1.415L5.536 7.95zm7 0L8.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 0 1-1.414-1.415l4.243-4.242z"/></symbol><symbol viewBox="0 0 16 16" id="angle-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></symbol><symbol viewBox="0 0 16 16" id="angle-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.757 8l4.95-4.95a1 1 0 1 0-1.414-1.414L3.636 7.293a.997.997 0 0 0 0 1.414l5.657 5.657a1 1 0 0 0 1.414-1.414L5.757 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.243 8l-4.95-4.95a1 1 0 0 1 1.414-1.414l5.657 5.657a.997.997 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414-1.414L10.243 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 6.757l-4.95 4.95a1 1 0 1 1-1.414-1.414l5.657-5.657a.997.997 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1-1.414 1.414L8 6.757z"/></symbol><symbol viewBox="0 0 16 16" id="appearance" xmlns="http://www.w3.org/2000/svg"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858a23.85 23.85 0 0 1-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023a9.7 9.7 0 0 0 .478-.035c-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="applications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="approval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.536 10.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 1 1 9.12 9.243l1.415 1.414zM7.632 8.109A2 2 0 0 0 7 11.364l2.121 2.121a1.996 1.996 0 0 0 2.807.021C11.686 14.554 10.627 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.6 0 1.142.038 1.632.109zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4 12.5v-9A1.5 1.5 0 0 1 5.5 2h2.104c2.182 0 3.879.681 3.879 2.982 0 1.067-.517 2.227-1.374 2.595v.073C11.176 7.963 12 8.865 12 10.466 12 12.914 10.19 14 7.911 14H5.5A1.5 1.5 0 0 1 4 12.5zm2.376-5.696H7.49c1.164 0 1.665-.552 1.665-1.417 0-.94-.534-1.289-1.649-1.289h-1.13v2.706zm0 5.098h1.341c1.293 0 1.956-.515 1.956-1.62 0-1.049-.647-1.472-1.956-1.472H6.376v3.092z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="bullhorn" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.143 10H7V4H3a3 3 0 1 0 0 6h.143l.734 5.141a1 1 0 0 0 .99.859h1.556a.5.5 0 0 0 .495-.57L6.143 10zM8 4c1.034.02 2.039-.274 3.014-.883.727-.455 1.836-1.334 3.328-2.637A1 1 0 0 1 16 1.233v10.764a1 1 0 0 1-1.595.803c-1.658-1.227-2.788-1.992-3.392-2.294-.781-.39-1.785-.559-3.013-.506V4z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chart" xmlns="http://www.w3.org/2000/svg"><path d="M15 14a1 1 0 0 1 0 2H2a2 2 0 0 1-2-2V1a1 1 0 1 1 2 0v13h13zM3.142 8.735l2.502-2.561a.5.5 0 0 1 .714-.003L8 7.833l3.592-4.553a.5.5 0 0 1 .796.015l2.516 3.454a.5.5 0 0 1 .096.295V12.5a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5V9.085a.5.5 0 0 1 .142-.35z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 9 13" id="collapse"><path d="M.084.25C.01.18-.015.12.008.071.031.024.093 0 .194 0h8.521c.1 0 .162.024.185.072.023.048-.002.107-.075.177l-4.11 3.935a.372.372 0 0 1-.11.072h-.301a.508.508 0 0 1-.11-.072L.084.249zM.377 6.88a.364.364 0 0 1-.26-.105.334.334 0 0 1-.11-.25v-.709c0-.096.036-.179.11-.249a.364.364 0 0 1 .26-.105h8.15c.101 0 .188.035.261.105.074.07.11.153.11.25v.709c0 .096-.036.179-.11.249a.364.364 0 0 1-.26.105H.377zM.084 12.132c-.074.07-.099.129-.076.177.023.048.085.072.186.072h8.521c.1 0 .162-.024.185-.072.023-.048-.002-.107-.075-.177l-4.11-3.935a.372.372 0 0 0-.11-.072h-.301a.508.508 0 0 0-.11.072l-4.11 3.935z"/></symbol><symbol viewBox="0 0 16 16" id="comment" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comment-dots" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586zM5 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="comment-next" xmlns="http://www.w3.org/2000/svg"><path d="M8 5V4a.5.5 0 0 1 .8-.4l2.667 2a.5.5 0 0 1 0 .8L8.8 8.4A.5.5 0 0 1 8 8V7H6a1 1 0 1 1 0-2h2zM1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comments" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.75 10L0 13V3a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3.75zM13 5h1a2 2 0 0 1 2 2v8l-2.667-2H8a2 2 0 0 1-2-2h4a3 3 0 0 0 3-3V5z"/></symbol><symbol viewBox="0 0 16 16" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3.876-1.008a4.002 4.002 0 0 1-7.752 0A1.01 1.01 0 0 1 4 9H1a1 1 0 1 1 0-2h3c.042 0 .083.003.124.008a4.002 4.002 0 0 1 7.752 0A1.01 1.01 0 0 1 12 7h3a1 1 0 0 1 0 2h-3a1.01 1.01 0 0 1-.124-.008z"/></symbol><symbol viewBox="0 0 16 16" id="credit-card" xmlns="http://www.w3.org/2000/svg"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="cut" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="2" y="7" fill-rule="evenodd" rx="1"/></symbol><symbol viewBox="0 0 16 16" id="dashboard" xmlns="http://www.w3.org/2000/svg"><path d="M7.709 10.021l.696-2.6a.5.5 0 0 1 .966.26l-.657 2.45A2 2 0 0 1 10 12H6a2 2 0 0 1 1.709-1.979zM0 8.9a8 8 0 0 1 15.998 0H16v3.6a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5V8.9zM14 9A6 6 0 1 0 2 9v3.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V9zM3.5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm9 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-7-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm5 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1z"/></symbol><symbol viewBox="0 0 16 16" id="disk" xmlns="http://www.w3.org/2000/svg"><path d="M16 11.764V3a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8.764A2.989 2.989 0 0 1 2 11V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v8c.768 0 1.47.289 2 .764zM2 12h12a2 2 0 1 1 0 4H2a2 2 0 1 1 0-4zm10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="doc_code" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zm1.036 7.607a.498.498 0 0 1-.147.354l-1.414 1.414a.5.5 0 0 1-.707-.707l1.06-1.06-1.06-1.061a.5.5 0 0 1 .707-.707l1.414 1.414a.498.498 0 0 1 .147.353zm-4.822 0l1.06 1.061a.5.5 0 0 1-.706.707l-1.414-1.414a.498.498 0 0 1 0-.707l1.414-1.414a.5.5 0 1 1 .707.707l-1.06 1.06zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="doc_image" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM7.333 9.667l1.313-1.313a.5.5 0 0 1 .708 0L12 11H4l2.188-1.75a.5.5 0 0 1 .624 0l.521.417zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4 11h8v.7a.3.3 0 0 1-.3.3H4.3a.3.3 0 0 1-.3-.3V11z"/></symbol><symbol viewBox="0 0 16 16" id="doc_text" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 105 26" id="double-headed-arrow" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.018 11.089L15.138.614c1.23-.911 3.086-.795 4.147.26.461.46.715 1.045.715 1.651v20.95C20 24.869 18.684 26 17.06 26a3.238 3.238 0 0 1-1.921-.614L1.019 14.911C-.212 14-.347 12.405.714 11.35c.094-.094.195-.18.303-.261zm102.964 0c.108.08.21.167.303.26 1.061 1.056.925 2.65-.303 3.562l-14.12 10.475A3.238 3.238 0 0 1 87.94 26C86.316 26 85 24.87 85 23.475V2.525c0-.606.254-1.192.715-1.65 1.061-1.056 2.917-1.172 4.146-.26l14.12 10.474zM35 17a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 1600 1600" id="ellipsis_v" xmlns="http://www.w3.org/2000/svg"><path d="M1088 1248v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V224q0-40 28-68t68-28h192q40 0 68 28t28 68z"/></symbol><symbol viewBox="0 0 18 18" id="emoji_slightly_smiling_face" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369.721.721 0 0 1 .568.047.715.715 0 0 1 .37.445 2.91 2.91 0 0 0 1.084 1.518A2.93 2.93 0 0 0 9 12.75a2.93 2.93 0 0 0 1.775-.58 2.913 2.913 0 0 0 1.084-1.518.711.711 0 0 1 .375-.445.737.737 0 0 1 .575-.047c.195.063.34.186.433.37.094.183.11.372.047.568zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smile" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568zM14 6.37c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm-6.5 0c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm9 2.63a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smiley" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568h.001zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6c.92.397 1.91.6 2.912.598a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39c.397-.92.6-1.91.598-2.912zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z"/></symbol><symbol viewBox="0 0 16 16" id="epic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.985 8.044l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637A2 2 0 0 0 1.618 9h11.661a2 2 0 0 0 1.706-.956zm0 3l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637a2 2 0 0 0 .576.084h11.661a2 2 0 0 0 1.706-.956zM3.618 2h10.995a1 1 0 0 1 .948 1.316l-1.333 4a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l2-4A1 1 0 0 1 3.618 2zm-.382 4h9.322l.667-2H4.236l-1 2z"/></symbol><symbol viewBox="0 0 16 16" id="external-link" xmlns="http://www.w3.org/2000/svg"><path d="M13.121 4.177l-4.95 4.95a1 1 0 1 1-1.414-1.414l4.95-4.95-1.386-1.386a.5.5 0 0 1 .299-.85l4.709-.524a.5.5 0 0 1 .552.552l-.523 4.71a.5.5 0 0 1-.851.297l-1.386-1.385zM12 8.884a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-4z"/></symbol><symbol viewBox="0 0 16 16" id="eye" xmlns="http://www.w3.org/2000/svg"><path d="M8 14C4.816 14 2.253 12.284.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2s5.747 1.716 7.607 5.019a2 2 0 0 1 0 1.962C13.747 12.284 11.184 14 8 14zm0-2c2.41 0 4.338-1.29 5.864-4C12.338 5.29 10.411 4 8 4 5.59 4 3.662 5.29 2.136 8 3.662 10.71 5.589 12 8 12zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm1-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="eye-slash" xmlns="http://www.w3.org/2000/svg"><path d="M13.618 2.62L1.62 14.619a1 1 0 0 1-.985-1.668l1.525-1.526C1.516 10.742.926 9.927.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2c1.074 0 2.076.195 3.006.58l.944-.944a1 1 0 0 1 1.668.985zM8.068 11a3 3 0 0 0 2.931-2.932l-2.931 2.931zm-3.02-2.462a3 3 0 0 1 3.49-3.49l.884-.884A6.044 6.044 0 0 0 8 4C5.59 4 3.662 5.29 2.136 8c.445.79.924 1.46 1.439 2.011l1.473-1.473zm.421 5.06l1.658-1.658c.283.04.575.06.873.06 2.41 0 4.338-1.29 5.864-4a11.023 11.023 0 0 0-1.133-1.664l1.418-1.418a12.799 12.799 0 0 1 1.458 2.1 2 2 0 0 1 0 1.963C13.747 12.284 11.184 14 8 14a7.883 7.883 0 0 1-2.53-.402z"/></symbol><symbol viewBox="0 0 16 16" id="file-addition" xmlns="http://www.w3.org/2000/svg"><path d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3z"/></symbol><symbol viewBox="0 0 16 16" id="file-deletion" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm2 6h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="file-modified" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm5 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></symbol><symbol viewBox="0 0 16 16" id="filter" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 6v9l-3.724-1.862A.5.5 0 0 1 6 12.691V6L1.854 1.854A.5.5 0 0 1 2.207 1h11.586a.5.5 0 0 1 .353.854L10 6z"/></symbol><symbol viewBox="0 0 16 16" id="folder" xmlns="http://www.w3.org/2000/svg"><path d="M7.228 5l-.475-1.335A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H7.228zM13 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="fork" xmlns="http://www.w3.org/2000/svg"><path d="M9 12.268a2 2 0 1 1-2 0V8.874A4.002 4.002 0 0 1 4 5V3.732a2 2 0 1 1 2 0V5a2 2 0 1 0 4 0V3.732a2 2 0 1 1 2 0V5a4.002 4.002 0 0 1-3 3.874v3.394zM11 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="geo-nodes" xmlns="http://www.w3.org/2000/svg"><path d="M9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4M7.3 12.7c.4.4 1 .3 1.4-.1C11.6 9.5 13 7 13 5.3 13 2.4 10.8 0 8 0S3 2.4 3 5.3C3 7 4.4 9.5 7.3 12.7M8 2c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2C5 3.4 6.4 2 8 2"/><circle cx="8" cy="5" r="1"/></symbol><symbol viewBox="0 0 16 16" id="git-merge" xmlns="http://www.w3.org/2000/svg"><path d="M11 12.268V5a1 1 0 0 0-1-1v1a.5.5 0 0 1-.8.4l-2.667-2a.5.5 0 0 1 0-.8L9.2.6a.5.5 0 0 1 .8.4v1a3 3 0 0 1 3 3v7.268a2 2 0 1 1-2 0zm-6 0a2 2 0 1 1-2 0V4.732a2 2 0 1 1 2 0v7.536zM4 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="group" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.048 11.997C-.377 11.975.013 11.782.013 10.56.013 9.235.653 8 4 8c.444 0 .84.022 1.194.062.164.435.426.82.76 1.132-1.786.389-2.721 1.353-2.906 2.803zm2.94-7.222a2.993 2.993 0 0 0-.976 1.95 2 2 0 1 1 .975-1.95zm6.964 7.222c-.185-1.45-1.12-2.414-2.906-2.803.334-.311.596-.697.76-1.132C11.16 8.022 11.556 8 12 8c3.346 0 3.987 1.235 3.987 2.56 0 1.222.39 1.415-3.035 1.437zm-1.964-5.272a2.993 2.993 0 0 0-.976-1.95 2 2 0 1 1 .976 1.95zM8 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 5c-2.177 0-3.987-.115-3.987-1.44S4.653 10 8 10c3.346 0 3.987 1.235 3.987 2.56S10.177 14 8 14z"/></symbol><symbol viewBox="0 0 16 16" id="history" xmlns="http://www.w3.org/2000/svg"><path d="M2.868 3.24a7 7 0 1 1-.043 9.475 1 1 0 0 1 1.478-1.348 5 5 0 1 0 .124-6.865l.796.645a.5.5 0 0 1-.193.873l-3.232.814a.5.5 0 0 1-.622-.504L1.3 3a.5.5 0 0 1 .814-.37l.754.61zM9 8h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V6a1 1 0 1 1 2 0v2z"/></symbol><symbol viewBox="0 0 16 16" id="home" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177a.505.505 0 0 1-.038.044l.038-.044zm-.787 0l.038.043a.5.5 0 0 1-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="hook" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></symbol><symbol viewBox="0 0 16 16" id="hourglass" xmlns="http://www.w3.org/2000/svg"><path d="M10.331 4.889A2.988 2.988 0 0 0 11 3V2H5v1c0 .362.064.709.182 1.03l5.15.859zM3 14v-1c0-1.78.93-3.342 2.33-4.228.447-.327.67-.582.67-.764 0-.19-.242-.46-.725-.815A4.996 4.996 0 0 1 3 3V2H2a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2h-1v1a4.997 4.997 0 0 1-2.39 4.266c-.407.3-.61.545-.61.734 0 .19.203.434.61.734A4.997 4.997 0 0 1 13 13v1h1a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2h1zm8 0v-1a3 3 0 0 0-6 0v1h6z"/></symbol><symbol viewBox="0 0 24 30" id="image-comment-dark" xmlns="http://www.w3.org/2000/svg"><title>cursor_active</title><g fill="none" fill-rule="evenodd"><path d="M24 12.105c0 6.686-5.74 11.58-12 17.895C5.74 23.684 0 18.79 0 12.105 0 5.42 5.373 0 12 0s12 5.42 12 12.105z" fill="#FFF" fill-rule="nonzero"/><path d="M15.28 25.249c1.458-1.475 2.539-2.635 3.474-3.747 2.851-3.394 4.203-6.265 4.203-9.397 0-6.111-4.908-11.062-10.957-11.062-6.05 0-10.957 4.951-10.957 11.062 0 3.132 1.352 6.003 4.203 9.397.935 1.112 2.016 2.272 3.474 3.747.511.517 2.216 2.213 3.28 3.275 1.064-1.062 2.769-2.758 3.28-3.275z" fill="#1F78D1"/><path d="M14.551 8.256A6.874 6.874 0 0 0 12 7.787a6.92 6.92 0 0 0-2.558.469c-.79.308-1.42.725-1.888 1.252-.465.527-.697 1.096-.697 1.708 0 .5.159.977.476 1.433.321.45.772.841 1.352 1.172l.583.334-.181.643c-.107.407-.263.79-.469 1.152a6.604 6.604 0 0 0 1.842-1.145l.288-.254.381.04c.309.035.599.053.871.053.91 0 1.761-.154 2.551-.462.795-.312 1.424-.732 1.889-1.259.468-.526.703-1.096.703-1.707 0-.612-.235-1.181-.703-1.708-.465-.527-1.094-.944-1.889-1.252zm2.645.81c.536.656.804 1.373.804 2.15 0 .776-.268 1.495-.804 2.156-.535.656-1.263 1.176-2.183 1.56-.92.38-1.924.57-3.013.57a9.16 9.16 0 0 1-.971-.054 7.32 7.32 0 0 1-3.08 1.62 5.044 5.044 0 0 1-.764.148h-.033a.26.26 0 0 1-.181-.074.324.324 0 0 1-.107-.18v-.007c-.014-.018-.016-.045-.007-.08.014-.037.018-.059.014-.068a.19.19 0 0 1 .033-.067.645.645 0 0 0 .04-.06 1.73 1.73 0 0 0 .047-.054l.054-.06a53.034 53.034 0 0 1 .435-.489c.049-.049.118-.136.207-.26a2.57 2.57 0 0 0 .221-.342c.054-.103.114-.235.181-.395a4.18 4.18 0 0 0 .174-.51c-.7-.397-1.254-.888-1.66-1.473A3.261 3.261 0 0 1 6 11.216c0-.777.268-1.494.804-2.15.535-.66 1.263-1.18 2.183-1.56.92-.384 1.924-.576 3.013-.576 1.09 0 2.094.192 3.013.576.92.38 1.648.9 2.183 1.56z" fill="#FFF" fill-rule="nonzero"/></g></symbol><symbol viewBox="0 0 16 16" id="import" xmlns="http://www.w3.org/2000/svg"><path d="M9 8h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0L5.6 8.8A.5.5 0 0 1 6 8h1V1a1 1 0 1 1 2 0v7zM0 8a1 1 0 1 1 2 0 6 6 0 1 0 12 0 1 1 0 0 1 2 0A8 8 0 1 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-block" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.803 8a5.97 5.97 0 0 0-.462 1H4.5a.5.5 0 0 1 0-1h1.303zM4.5 5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm7.5.083a6.04 6.04 0 0 0-2 0V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.083a5.96 5.96 0 0 0 .72 2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v2.083zm1.121 3.796zM11 16a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm-1.293-2.292a3 3 0 0 0 4.001-4.001l-4.001 4zm-1.415-1.415l4.001-4a3 3 0 0 0-4.001 4.001z"/></symbol><symbol viewBox="0 0 16 16" id="issue-child" xmlns="http://www.w3.org/2000/svg"><path d="M11 8H5v1h1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2V7a.997.997 0 0 1 1-1h3V4H4.5a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9v2h3a.997.997 0 0 1 1 1v2h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1V8zm-9 3v2h3v-2H2zm9 0v2h3v-2h-3z"/></symbol><symbol viewBox="0 0 16 16" id="issue-close" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M10.874 2H12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-2c-.918 0-1.74-.413-2.29-1.063a3.987 3.987 0 0 0 1.988-.984A1 1 0 0 0 10 14h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-1V3c0-.345-.044-.68-.126-1zM4 0h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-new" xmlns="http://www.w3.org/2000/svg"><path d="M10 2V1a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V4H9a1 1 0 1 1 0-2h1zm0 6a1 1 0 0 1 2 0v5a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h1a1 1 0 1 1 0 2H5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open-m" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-parent" xmlns="http://www.w3.org/2000/svg"><path d="M11 11H5v1h1.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H3v-2a.997.997 0 0 1 1-1h3V7H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9v2h3a.997.997 0 0 1 1 1v2h2.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H11v-1zM6 3v2h4V3H6z"/></symbol><symbol viewBox="0 0 16 16" id="issues" xmlns="http://www.w3.org/2000/svg"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="italic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.5 12l2-8H6a1 1 0 1 1 0-2h6a1 1 0 0 1 0 2h-1.5l-2 8H10a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2h1.5z"/></symbol><symbol viewBox="0 0 16 16" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7.575 6.689a4.002 4.002 0 0 1 6.274-4.86 4 4 0 0 1-4.86 6.274l-2.21 2.21.706.708a1 1 0 1 1-1.414 1.414l-.707-.707-.707.707.707.707a1 1 0 1 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.414-1.414l5.746-5.746zm2.032-.618a2 2 0 1 0 2.828-2.828A2 2 0 0 0 9.607 6.07z"/></symbol><symbol viewBox="0 0 16 16" id="key-2" xmlns="http://www.w3.org/2000/svg"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></symbol><symbol viewBox="0 0 16 16" id="label" xmlns="http://www.w3.org/2000/svg"><path d="M11.782 14.718a3 3 0 0 1-4.242 0L1.652 8.829a2 2 0 0 1-.565-1.702l.54-3.703a2 2 0 0 1 1.69-1.69l3.703-.54a2 2 0 0 1 1.703.564l5.888 5.888a3 3 0 0 1 0 4.243l-2.829 2.829zm1.415-5.657L7.309 3.173l-3.703.54-.54 3.702 5.888 5.888a1 1 0 0 0 1.414 0l2.829-2.828a1 1 0 0 0 0-1.414zM5.732 5.525A1 1 0 1 1 7.146 6.94a1 1 0 0 1-1.414-1.414z"/></symbol><symbol viewBox="0 0 16 16" id="labels" xmlns="http://www.w3.org/2000/svg"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667zM.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="leave" xmlns="http://www.w3.org/2000/svg"><path d="M11 7V5.883a.5.5 0 0 1 .757-.429l3.528 2.117a.5.5 0 0 1 0 .858l-3.528 2.117a.5.5 0 0 1-.757-.43V9H7a1 1 0 1 1 0-2h4zm-2 6.256a1 1 0 0 1 2 0A2.744 2.744 0 0 1 8.256 16H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h5.19A2.81 2.81 0 0 1 11 2.81a1 1 0 0 1-2 0A.81.81 0 0 0 8.19 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.256c.41 0 .744-.333.744-.744z"/></symbol><symbol viewBox="0 0 16 16" id="level-up" xmlns="http://www.w3.org/2000/svg"><path fill="#2E2E2E" fill-rule="evenodd" d="M7 6h3.489a.5.5 0 0 0 .373-.832L6.374.117a.5.5 0 0 0-.748 0l-4.488 5.05A.5.5 0 0 0 1.51 6H5v7a3 3 0 0 0 3 3h6a1 1 0 0 0 0-2H8a1 1 0 0 1-1-1V6z"/></symbol><symbol viewBox="0 0 16 16" id="license" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.56 8.9l2.66 4.606a.3.3 0 0 1-.243.45l-1.678.094a.1.1 0 0 0-.078.044l-.953 1.432a.3.3 0 0 1-.51-.016L9.097 10.9a5.994 5.994 0 0 0 3.464-2zm-5.23 2.063L4.707 15.51a.3.3 0 0 1-.51.016l-.953-1.432a.1.1 0 0 0-.078-.044l-1.678-.094a.3.3 0 0 1-.243-.45l2.48-4.297a5.983 5.983 0 0 0 3.607 1.754zM8 10A5 5 0 1 1 8 0a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="link" xmlns="http://www.w3.org/2000/svg"><path d="M6.986 3.35l2.12-2.122a4 4 0 0 1 5.657 5.657l-2.828 2.829a4 4 0 0 1-5.657 0 1 1 0 0 1 1.414-1.415 2 2 0 0 0 2.829 0l2.828-2.828a2 2 0 1 0-2.828-2.828l-1.001 1a5.018 5.018 0 0 0-2.534-.294zm2.12 9.192l-2.12 2.121a4 4 0 1 1-5.658-5.656l2.829-2.829a4 4 0 0 1 5.657 0 1 1 0 1 1-1.415 1.414 2 2 0 0 0-2.828 0l-2.828 2.829a2 2 0 1 0 2.828 2.828l1.001-1.001a5.018 5.018 0 0 0 2.534.294z"/></symbol><symbol viewBox="0 0 16 16" id="list-bulleted" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-7h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm0 5h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm-4 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-2h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="list-numbered" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2zM1.156 5v-.828h.816V2.204h-.72v-.636c.432-.084.708-.192.996-.372h.756v2.976h.684V5H1.156zm-.18 5v-.588c.9-.828 1.596-1.464 1.596-1.98 0-.342-.192-.504-.468-.504-.252 0-.444.18-.624.36l-.552-.552c.396-.42.756-.612 1.32-.612.768 0 1.308.492 1.308 1.248 0 .612-.576 1.284-1.092 1.812.192-.024.468-.048.636-.048h.636V10H.976zm1.26 5.072c-.618 0-1.068-.204-1.356-.54l.468-.648c.234.216.51.36.78.36.336 0 .552-.12.552-.36 0-.288-.15-.456-.948-.456v-.72c.636 0 .828-.168.828-.432 0-.228-.138-.348-.396-.348-.252 0-.432.108-.672.312l-.516-.624c.372-.312.768-.492 1.236-.492.84 0 1.38.384 1.38 1.074 0 .366-.204.642-.612.822v.024c.432.132.732.432.732.912 0 .72-.684 1.116-1.476 1.116z"/></symbol><symbol viewBox="0 0 16 16" id="location" xmlns="http://www.w3.org/2000/svg"><path d="M8.755 15.144a1 1 0 0 1-1.51 0C3.748 11.114 2 8.065 2 6a6 6 0 1 1 12 0c0 2.065-1.748 5.113-5.245 9.144zM12 6a4 4 0 1 0-8 0c0 1.314 1.312 3.71 4 6.944C10.688 9.71 12 7.314 12 6zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="location-dot" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.314 13.087C4.382 13.295 3 13.85 3 14.5c0 .828 2.239 1.5 5 1.5s5-.672 5-1.5c0-.65-1.382-1.205-3.314-1.413l-.202.225a2 2 0 0 1-2.968 0l-.202-.225zm2.428-.445a1 1 0 0 1-1.484 0C4.419 9.5 3 7.037 3 5.252 3 2.353 5.239 0 8 0s5 2.352 5 5.253c0 1.784-1.42 4.247-4.258 7.389zM11 5.252C11 3.436 9.634 2 8 2S5 3.435 5 5.253c0 1.027.974 2.824 3 5.203 2.026-2.38 3-4.176 3-5.203zM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M10 5V4h2v1a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3V4h2v1h4zM4 7a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4zm0-3a4 4 0 1 1 8 0h-2a2 2 0 1 0-4 0H4z"/></symbol><symbol viewBox="0 0 16 16" id="lock-open" xmlns="http://www.w3.org/2000/svg"><path d="M4.044 4a4 4 0 0 1 6.99-2.658 1 1 0 1 1-1.495 1.33A2 2 0 0 0 6.044 4a.998.998 0 0 1-.07.367v.701H12a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3v-5a3 3 0 0 1 2.974-3V4h.07zM4 7.07a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="log" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="mail" xmlns="http://www.w3.org/2000/svg"><path d="M14 5.6L9.338 9.796a2 2 0 0 1-2.676 0L2 5.6V11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.6zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm.212 2L8 8.31 12.788 4H3.212z"/></symbol><symbol viewBox="0 0 16 16" id="menu" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.143 2h13.714C15.488 2 16 2.448 16 3s-.512 1-1.143 1H1.143C.512 4 0 3.552 0 3s.512-1 1.143-1zm0 5h13.714C15.488 7 16 7.448 16 8s-.512 1-1.143 1H1.143C.512 9 0 8.552 0 8s.512-1 1.143-1zm0 5h13.714c.631 0 1.143.448 1.143 1s-.512 1-1.143 1H1.143C.512 14 0 13.552 0 13s.512-1 1.143-1z"/></symbol><symbol viewBox="0 0 16 16" id="merge-request-close" xmlns="http://www.w3.org/2000/svg"><path d="M9.414 8l1.414 1.414a1 1 0 1 1-1.414 1.414L8 9.414l-1.414 1.414a1 1 0 1 1-1.414-1.414L6.586 8 5.172 6.586a1 1 0 1 1 1.414-1.414L8 6.586l1.414-1.414a1 1 0 1 1 1.414 1.414L9.414 8zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="messages" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="mobile-issue-close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.657 10.728L2.12 7.192A1 1 0 1 0 .707 8.607l4.243 4.242a.997.997 0 0 0 1.414 0l8.485-8.485a1 1 0 1 0-1.414-1.414l-7.778 7.778z"/></symbol><symbol viewBox="0 0 16 16" id="monitor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></symbol><symbol viewBox="0 0 16 16" id="more" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="notifications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></symbol><symbol viewBox="0 0 16 16" id="notifications-off" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.26 5.089c.243.757.382 1.478.5 2.017.187.86.74 2.188 1.66 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0H4.35l2-2h7.29c-.993-1.937-1.6-3.396-1.835-4.468-.07-.326-.129-.59-.178-.81l1.634-1.633zM10.943 1.75l-1.48 1.48C9.07 3.076 8.612 3 8.069 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.065.317-.17.673-.317 1.073L.45 12.242a1.99 1.99 0 0 1 .224-1.19c.962-1.787 1.521-3.064 1.68-3.831.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024 4.867 4.867 0 0 1 1.944.688zm2.932-.105a1 1 0 0 1 0 1.415L2.561 14.374a1 1 0 1 1-1.415-1.414L12.46 1.646a1 1 0 0 1 1.414 0z"/></symbol><symbol viewBox="0 0 16 16" id="overview" xmlns="http://www.w3.org/2000/svg"><path d="M2 0h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2h-3zM2 9h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3h-3z"/></symbol><symbol viewBox="0 0 16 16" id="pencil" xmlns="http://www.w3.org/2000/svg"><path d="M13.02 1.293l1.414 1.414a1 1 0 0 1 0 1.414L4.119 14.436a1 1 0 0 1-.704.293l-2.407.008L1 12.316a1 1 0 0 1 .293-.71L11.605 1.292a1 1 0 0 1 1.414 0zm-1.416 1.415l-.707.707L12.31 4.83l.707-.707-1.414-1.415zM3.411 13.73l1.123-1.122H3.12v-1.415L2 12.312l.005 1.422 1.406-.005z"/></symbol><symbol viewBox="0 0 16 16" id="pencil-square" xmlns="http://www.w3.org/2000/svg"><path d="M12 9a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V9zm.778-7.179l1.414 1.415-6.476 6.476a1 1 0 0 1-.498.27l-1.51.325.323-1.512a1 1 0 0 1 .27-.497l6.477-6.477zM15.607.407a1 1 0 0 1 0 1.414l-.708.707-1.414-1.414.707-.707a1 1 0 0 1 1.415 0z"/></symbol><symbol viewBox="0 0 16 16" id="pipeline" xmlns="http://www.w3.org/2000/svg"><path d="M8.969 7.25a2 2 0 1 1-1.938 0A1.002 1.002 0 0 1 7 7V5.083a.2.2 0 0 1 .06-.142l.877-.87a.1.1 0 0 1 .141 0l.864.87A.2.2 0 0 1 9 5.083V7c0 .086-.01.17-.031.25zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm4.5-4a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="play" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.765 15.835c-.545.321-1.258.159-1.593-.363A1.075 1.075 0 0 1 1 14.89V1.11C1 .496 1.518 0 2.158 0c.214 0 .424.057.607.165l11.684 6.89c.544.321.714 1.005.38 1.526a1.135 1.135 0 0 1-.38.364l-11.684 6.89z"/></symbol><symbol viewBox="0 0 16 16" id="plus" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V1a1 1 0 1 1 2 0v6h6a1 1 0 0 1 0 2H9v6a1 1 0 0 1-2 0V9H1a1 1 0 1 1 0-2h6z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 7V4a1 1 0 1 0-2 0v3H4a1 1 0 1 0 0 2h3v3a1 1 0 0 0 2 0V9h3a1 1 0 0 0 0-2H9zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square-o" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="preferences" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 12h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2zm11-5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zM6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2z"/></symbol><symbol viewBox="0 0 16 16" id="profile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="push-rules" xmlns="http://www.w3.org/2000/svg"><path d="M6.268 9a2 2 0 0 1 3.464 0H11a1 1 0 0 1 0 2H9.732a2 2 0 0 1-3.464 0H5a1 1 0 0 1 0-2h1.268zM7 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 7 5.515V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="question" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-1.46-5.602h2.233a3.97 3.97 0 0 1 .051-.558c.029-.17.073-.326.133-.469.06-.143.14-.28.242-.41.102-.13.228-.263.38-.399.26-.24.504-.467.733-.683a5.03 5.03 0 0 0 .598-.668c.17-.23.302-.477.399-.742a2.66 2.66 0 0 0 .144-.907c0-.505-.083-.95-.25-1.335a2.55 2.55 0 0 0-.723-.97 3.2 3.2 0 0 0-1.152-.589 5.441 5.441 0 0 0-1.531-.2c-.516 0-.998.063-1.445.188a3.19 3.19 0 0 0-1.168.59c-.331.268-.594.61-.79 1.027-.195.417-.295.917-.3 1.5h2.64c.006-.224.04-.416.102-.578.062-.161.142-.293.238-.394a.921.921 0 0 1 .332-.227 1.04 1.04 0 0 1 .39-.074c.34 0 .593.095.763.285.169.19.254.488.254.895 0 .328-.106.63-.317.906-.21.276-.499.565-.863.867-.214.182-.39.374-.531.574-.141.2-.253.42-.336.657a3.656 3.656 0 0 0-.176.777 7.89 7.89 0 0 0-.05.937zm-.321 2.375c0 .188.035.362.105.524.07.161.17.3.301.418.13.117.284.21.46.277.178.068.376.102.595.102.218 0 .416-.034.593-.102.178-.068.331-.16.461-.277a1.2 1.2 0 0 0 .301-.418c.07-.162.106-.336.106-.524a1.3 1.3 0 0 0-.106-.523 1.2 1.2 0 0 0-.3-.418 1.461 1.461 0 0 0-.462-.277 1.651 1.651 0 0 0-.593-.102c-.22 0-.417.034-.594.102a1.46 1.46 0 0 0-.461.277 1.2 1.2 0 0 0-.3.418 1.284 1.284 0 0 0-.106.523z"/></symbol><symbol viewBox="0 0 16 16" id="question-o" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-.778-4.151c0-.301.014-.575.044-.82a3.2 3.2 0 0 1 .154-.68c.073-.208.17-.4.294-.575.123-.176.278-.343.465-.503a4.81 4.81 0 0 0 .755-.758c.185-.242.277-.506.277-.793 0-.356-.074-.617-.222-.783-.148-.166-.37-.25-.667-.25a.92.92 0 0 0-.342.065.806.806 0 0 0-.29.199 1.04 1.04 0 0 0-.209.345 1.5 1.5 0 0 0-.088.506H5.082c.005-.51.092-.948.263-1.313.171-.364.401-.664.69-.899.29-.234.63-.406 1.023-.516a4.66 4.66 0 0 1 1.264-.164c.497 0 .944.058 1.34.174.397.117.733.289 1.008.517.276.227.487.51.633.847.146.337.218.727.218 1.17 0 .295-.042.56-.126.792a2.52 2.52 0 0 1-.349.65 4.4 4.4 0 0 1-.523.584c-.2.19-.414.389-.642.598a2.73 2.73 0 0 0-.332.349c-.089.114-.16.233-.212.359a1.868 1.868 0 0 0-.116.41 3.39 3.39 0 0 0-.044.489H7.222zm-.28 2.078c0-.164.03-.317.092-.458a1.05 1.05 0 0 1 .263-.366c.114-.103.248-.183.403-.243a1.45 1.45 0 0 1 .52-.089c.191 0 .364.03.52.09.154.059.289.14.403.242.114.103.201.224.263.366.061.141.092.294.092.458 0 .164-.03.316-.092.458a1.05 1.05 0 0 1-.263.365 1.278 1.278 0 0 1-.404.243 1.43 1.43 0 0 1-.52.089c-.19 0-.364-.03-.519-.089-.155-.06-.29-.14-.403-.243a1.05 1.05 0 0 1-.263-.365 1.135 1.135 0 0 1-.093-.458z"/></symbol><symbol viewBox="0 0 16 16" id="quote" xmlns="http://www.w3.org/2000/svg"><path d="M15 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1zM7 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="redo" xmlns="http://www.w3.org/2000/svg"><path d="M4.666 4.423a5 5 0 1 1-.203 6.944 1 1 0 1 0-1.478 1.347 7 7 0 1 0 .12-9.556L1.842 2.137a.5.5 0 0 0-.815.385L1 7.26a.5.5 0 0 0 .607.492l4.629-1.013a.5.5 0 0 0 .207-.877L4.666 4.423z"/></symbol><symbol viewBox="0 0 16 16" id="remove" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V3zm3-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5zM4 3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3H4zm2.5 2a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="repeat" xmlns="http://www.w3.org/2000/svg"><path d="M11.494 4.423a5 5 0 1 0 .203 6.944 1 1 0 1 1 1.478 1.347 7 7 0 1 1-.12-9.556l1.262-1.021a.5.5 0 0 1 .815.385l.028 4.738a.5.5 0 0 1-.607.492L9.924 6.739a.5.5 0 0 1-.207-.877l1.777-1.439z"/></symbol><symbol viewBox="0 0 16 16" id="retry" xmlns="http://www.w3.org/2000/svg"><path d="M4.114 6.958a4 4 0 0 0 5.283 4.775 1 1 0 1 1 .712 1.87A6 6 0 0 1 2.182 6.44l-.741-.2a.5.5 0 0 1-.12-.915l2.195-1.268a.5.5 0 0 1 .683.183l1.268 2.196a.5.5 0 0 1-.563.733l-.79-.212zm7.777 2.084a4 4 0 0 0-5.284-4.775 1 1 0 0 1-.712-1.87 6 6 0 0 1 7.927 7.162l.742.2a.5.5 0 0 1 .12.915l-2.196 1.268a.5.5 0 0 1-.683-.183l-1.267-2.196a.5.5 0 0 1 .562-.733l.79.212z"/></symbol><symbol viewBox="0 0 16 16" id="scale" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.99 9a.792.792 0 0 0-.078-.231L13 7l-.912 1.769a.791.791 0 0 0-.077.231h1.978zm-10 0a.792.792 0 0 0-.078-.231L3 7l-.912 1.769A.791.791 0 0 0 2.011 9h1.978zM2 0h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm3 14h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zM8 4a1 1 0 0 1 1 1v9H7V5a1 1 0 0 1 1-1zm-4.53-.714l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 3 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158L2.53 3.286a.53.53 0 0 1 .94 0zm10 0l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 13 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158l2.266-4.735a.53.53 0 0 1 .94 0z"/></symbol><symbol viewBox="0 0 16 16" id="screen-full" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 14v-2a1 1 0 0 1 2 0v3a.997.997 0 0 1-1 1h-3a1 1 0 0 1 0-2h2zM2 14v-2a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1h3a1 1 0 0 0 0-2H2zM15.707.293A.997.997 0 0 1 16 1v3a1 1 0 0 1-2 0V2h-2a1 1 0 0 1 0-2h3c.276 0 .526.112.707.293zM2 2v2a1 1 0 1 1-2 0V1a.997.997 0 0 1 1-1h3a1 1 0 1 1 0 2H2zm4 4h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="screen-normal" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3V1a1 1 0 1 1 2 0v3a.997.997 0 0 1-1 1H1a1 1 0 1 1 0-2h2zm10 0h2a1 1 0 0 1 0 2h-3a.997.997 0 0 1-1-1V1a1 1 0 0 1 2 0v2zM3 13H1a1 1 0 0 1 0-2h3a.997.997 0 0 1 1 1v3a1 1 0 0 1-2 0v-2zm10 0v2a1 1 0 0 1-2 0v-3a.997.997 0 0 1 1-1h3a1 1 0 0 1 0 2h-2zM6.5 7h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 12 16" id="scroll_down" xmlns="http://www.w3.org/2000/svg"><path class="eufirst-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043a.51.51 0 0 0 .321-.105c.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/><path class="eusecond-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/><path class="euthird-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91a.458.458 0 0 1-.136.09h-.37a.626.626 0 0 1-.136-.09"/></symbol><symbol viewBox="0 0 12 16" id="scroll_up" xmlns="http://www.w3.org/2000/svg"><path d="M1.048 1.845a.508.508 0 0 1-.32-.105c-.091-.07-.136-.154-.136-.25V.78c0-.095.045-.178.135-.249a.508.508 0 0 1 .321-.105h10.043a.51.51 0 0 1 .321.105c.09.07.136.154.136.25v.71c0 .095-.045.178-.136.249a.508.508 0 0 1-.32.105M.687 7.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 3.09A.458.458 0 0 0 6.257 3h-.37a.626.626 0 0 0-.136.09M.687 14.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 10.09a.458.458 0 0 0-.136-.09h-.37a.626.626 0 0 0-.136.09"/></symbol><symbol viewBox="0 0 16 16" id="search" xmlns="http://www.w3.org/2000/svg"><path d="M8.853 8.854a3.5 3.5 0 1 0-4.95-4.95 3.5 3.5 0 0 0 4.95 4.95zm.207 2.328a5.5 5.5 0 1 1 2.121-2.121l3.329 3.328a1.5 1.5 0 0 1-2.121 2.121L9.06 11.182z"/></symbol><symbol viewBox="0 0 16 16" id="settings" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918a5.97 5.97 0 0 1 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="shield" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v7.186a3 3 0 0 1-1.426 2.554l-4 2.465a3 3 0 0 1-3.148 0l-4-2.465A3 3 0 0 1 1 10.186V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v7.186a1 1 0 0 0 .475.852l4 2.464a1 1 0 0 0 1.05 0l4-2.464a1 1 0 0 0 .475-.852V3a1 1 0 0 0-1-1H4zm0 1.5a.5.5 0 0 1 .5-.5h4v8.837a.5.5 0 0 1-.753.431l-3.5-2.052A.5.5 0 0 1 4 9.785V3.5z"/></symbol><symbol viewBox="0 0 16 16" id="slight-frown" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="slight-smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-5.163 2.254a.5.5 0 1 1 .865-.502 1.499 1.499 0 0 0 2.607-.018.5.5 0 1 1 .871.49 2.499 2.499 0 0 1-4.343.03z"/></symbol><symbol viewBox="0 0 16 16" id="smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM6.18 6.27a.5.5 0 0 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zm6 0a.5.5 0 1 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zM5 9a3 3 0 0 0 6 0H5z"/></symbol><symbol viewBox="0 0 16 16" id="smiley" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM5 9h6a3 3 0 0 1-6 0z"/></symbol><symbol viewBox="0 0 16 16" id="snippet" xmlns="http://www.w3.org/2000/svg"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 0 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 0 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></symbol><symbol viewBox="0 0 16 16" id="spam" xmlns="http://www.w3.org/2000/svg"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 14 14" id="spinner" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="7" cy="7" r="6" stroke="#000" stroke-opacity=".1" stroke-width="2"/><path fill="#000" fill-opacity=".1" fill-rule="nonzero" d="M7 0a7 7 0 0 1 7 7h-2a5 5 0 0 0-5-5V0z"/></g></symbol><symbol viewBox="0 0 16 16" id="star" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.609 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="star-o" xmlns="http://www.w3.org/2000/svg"><path d="M10.975 10.99a3 3 0 0 1 .655-2.083l1.54-1.916-2.219-.576a3 3 0 0 1-1.825-1.37L8 3.15 6.874 5.044a3 3 0 0 1-1.825 1.371l-2.218.576 1.54 1.916a3 3 0 0 1 .654 2.083l-.165 2.4 1.965-.836a3 3 0 0 1 2.348 0l1.965.836-.164-2.399zM7.61 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 14 14" id="status_canceled" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></symbol><symbol viewBox="0 0 22 22" id="status_canceled_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M8.171 5.971l7.7 7.7a.76.76 0 0 1 0 1.1l-1.1 1.1a.76.76 0 0 1-1.1 0l-7.7-7.7a.76.76 0 0 1 0-1.1l1.1-1.1a.76.76 0 0 1 1.1 0"/></symbol><symbol viewBox="0 0 16 16" id="status_closed" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.83a1 1 0 0 1 1.414 1.416l-3.535 3.535a1 1 0 0 1-1.415.001l-2.12-2.12a1 1 0 1 1 1.413-1.415zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 14 14" id="status_created" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></symbol><symbol viewBox="0 0 22 22" id="status_created_borderless" xmlns="http://www.w3.org/2000/svg"><circle cx="11" cy="11" r="5.107"/></symbol><symbol viewBox="0 0 14 14" id="status_failed" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_failed_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 9.38L8.798 7.178a.455.455 0 0 0-.65.006l-.964.965a.462.462 0 0 0-.006.65L9.38 11l-2.202 2.202a.455.455 0 0 0 .006.65l.965.964a.462.462 0 0 0 .65.006L11 12.62l2.202 2.202a.455.455 0 0 0 .65-.006l.964-.965a.462.462 0 0 0 .006-.65L12.62 11l2.202-2.202a.455.455 0 0 0-.006-.65l-.965-.964a.462.462 0 0 0-.65-.006L11 9.38z"/></symbol><symbol viewBox="0 0 14 14" id="status_manual" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_manual_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 11.99v-1.98l-1.238-.206c-.068-.273-.206-.546-.412-.956l.756-1.025-1.444-1.435-1.03.752a3.686 3.686 0 0 0-.963-.41L12.03 5.5h-1.994l-.206 1.23c-.343.068-.618.205-.962.41l-1.031-.752-1.444 1.435.687 1.025c-.206.341-.275.615-.412.956L5.5 9.941v1.981l1.237.205c.07.342.207.615.413.957l-.688 1.025 1.444 1.434 1.032-.683c.274.137.618.274.962.41l.206 1.23h2.063l.206-1.23c.344-.068.619-.205.963-.41l1.03.752 1.444-1.435-.756-1.025c.207-.341.344-.683.413-.956l1.031-.205zM11 13.017c-1.169 0-2.063-.889-2.063-2.05 0-1.162.894-2.05 2.063-2.05s2.063.888 2.063 2.05c0 1.161-.894 2.05-2.063 2.05z"/></symbol><symbol viewBox="0 0 22 22" id="status_notfound_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0v-1.43a5.9 5.9 0 0 0 .827-.492z"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></symbol><symbol viewBox="0 0 14 14" id="status_open" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M7 9.219a2.218 2.218 0 1 0 0-4.436A2.218 2.218 0 0 0 7 9.22zm0 1.12a3.338 3.338 0 1 1 0-6.676 3.338 3.338 0 0 1 0 6.676z"/></symbol><symbol viewBox="0 0 14 14" id="status_pending" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_pending_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M7.386 8.329c0-.315.157-.472.471-.472h1.414c.315 0 .472.157.472.472v5.342c0 .315-.157.472-.472.472H7.857c-.314 0-.471-.157-.471-.472V8.33m4.714 0c0-.315.157-.472.471-.472h1.415c.314 0 .471.157.471.472v5.342c0 .315-.157.472-.471.472H12.57c-.314 0-.471-.157-.471-.472V8.33"/></symbol><symbol viewBox="0 0 14 14" id="status_running" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_running_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 4.714c3.457 0 6.286 2.829 6.286 6.286 0 3.457-2.829 6.286-6.286 6.286-2.043 0-3.929-1.1-5.186-2.672L11 11V4.714"/></symbol><symbol viewBox="0 0 14 14" id="status_skipped" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M6.415 7.04L4.579 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L5.341 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L6.415 7.04zm2.54 0L7.119 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L7.881 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L8.955 7.04z"/></symbol><symbol viewBox="0 0 22 22" id="status_skipped_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M14.072 11.063l-2.82 2.82a.46.46 0 0 0-.001.652l.495.495a.457.457 0 0 0 .653-.001l3.7-3.7a.46.46 0 0 0 .001-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.479-3.479a.464.464 0 0 0-.654.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/><path d="M10.08 11.063l-2.819 2.82a.46.46 0 0 0-.002.652l.496.495a.457.457 0 0 0 .652-.001l3.7-3.7a.46.46 0 0 0 .002-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.48-3.479a.464.464 0 0 0-.653.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/></symbol><symbol viewBox="0 0 14 14" id="status_success" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_success_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.866 12.095l-1.95-1.95a.462.462 0 0 0-.647.01l-.964.964a.46.46 0 0 0-.01.646l3.013 3.014a.787.787 0 0 0 1.106.008l.425-.425 4.854-4.853a.462.462 0 0 0 .002-.659l-.964-.964a.468.468 0 0 0-.658.002l-4.207 4.207z"/></symbol><symbol viewBox="0 0 14 14" id="status_success_solid" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7zm6.278.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 14 14" id="status_warning" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></symbol><symbol viewBox="0 0 22 22" id="status_warning_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.429 5.5c0-.471.314-.786.785-.786h1.572c.471 0 .785.315.785.786v6.286c0 .471-.314.785-.785.785h-1.572c-.471 0-.785-.314-.785-.785V5.5m0 9.429c0-.472.314-.786.785-.786h1.572c.471 0 .785.314.785.786V16.5c0 .471-.314.786-.785.786h-1.572c-.471 0-.785-.315-.785-.786v-1.571"/></symbol><symbol viewBox="0 0 16 16" id="stop" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 0h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="terminal" xmlns="http://www.w3.org/2000/svg"><path d="M7 8a.997.997 0 0 1-.293.707l-1.414 1.414a1 1 0 1 1-1.414-1.414L4.586 8l-.707-.707a1 1 0 1 1 1.414-1.414l1.414 1.414A.997.997 0 0 1 7 8zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm0 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H4zm5 7h2a1 1 0 0 1 0 2H9a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="thumbtack" xmlns="http://www.w3.org/2000/svg"><path d="M7.125 9h-2.19a.5.5 0 0 1-.417-.777L6 6V2L5.362.724A.5.5 0 0 1 5.809 0h4.382a.5.5 0 0 1 .447.724L10 2v4l1.482 2.223a.5.5 0 0 1-.416.777H8.875L8 16l-.875-7z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.34 10.479A3 3 0 0 1 12.756 15h-9.51A3 3 0 0 1 .66 10.479l4.755-8.083a3 3 0 0 1 5.172 0l4.755 8.083zm-6.478-7.07a1 1 0 0 0-1.724 0l-4.755 8.084A1 1 0 0 0 3.245 13h9.51a1 1 0 0 0 .862-1.507L8.862 3.41zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg> \ No newline at end of file
+<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 16" id="abuse" xmlns="http://www.w3.org/2000/svg"><path d="M11.408.328l4.029 3.222A1.5 1.5 0 0 1 16 4.72v6.555a1.5 1.5 0 0 1-.563 1.171l-4.026 3.224a1.5 1.5 0 0 1-.937.329H5.529a1.5 1.5 0 0 1-.937-.328L.563 12.45A1.5 1.5 0 0 1 0 11.28V4.724a1.5 1.5 0 0 1 .563-1.171L4.589.329A1.5 1.5 0 0 1 5.526 0h4.945c.34 0 .67.116.937.328zM10.296 2H5.702L2 4.964v6.074L5.704 14h4.594L14 11.036V4.962L10.296 2zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="account" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.195 9.965l-.568-.875a.25.25 0 0 1 .015-.294l.405-.5a.25.25 0 0 1 .283-.075l.938.36c.257-.183.543-.325.851-.42l.322-.988A.25.25 0 0 1 11.679 7h.642a.25.25 0 0 1 .238.173l.322.988c.308.095.594.237.851.42l.938-.36a.25.25 0 0 1 .283.076l.405.5a.25.25 0 0 1 .015.293l-.568.875c.113.297.18.616.193.95l.898.54a.25.25 0 0 1 .115.27l-.144.626a.25.25 0 0 1-.222.193l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.281a.25.25 0 0 1-.29-.05l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.904a.25.25 0 0 1-.289.051l-.577-.281a.25.25 0 0 1-.138-.26l.165-1.18a3.015 3.015 0 0 1-.512-.607l-1.115-.098a.25.25 0 0 1-.222-.193l-.144-.626a.25.25 0 0 1 .115-.27l.898-.54c.013-.334.08-.653.193-.95zM6.789 8.023A12.845 12.845 0 0 0 6 8c-5.036 0-6 2.74-6 4.48C0 14.22.076 15 6 15c.553 0 1.055-.006 1.51-.02A5.977 5.977 0 0 1 6 11c0-1.083.287-2.1.79-2.977zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="admin" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.162 2.5a3.5 3.5 0 0 1-3.163 5.479L6.08 14.766a1.5 1.5 0 0 1-2.598-1.5L7.4 6.479A3.5 3.5 0 0 1 10.564 1L8.9 3.88l2.599 1.5 1.663-2.88zm-8.63 11.949a.5.5 0 1 0 .5-.866.5.5 0 0 0-.5.866z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.414 7.95l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 1 0 1.414-1.415L10.414 7.95zm-7 0l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 0 0 1.414-1.415L3.414 7.95z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.536 7.95L1.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 1 1-1.414-1.415L5.536 7.95zm7 0L8.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 0 1-1.414-1.415l4.243-4.242z"/></symbol><symbol viewBox="0 0 16 16" id="angle-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></symbol><symbol viewBox="0 0 16 16" id="angle-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.757 8l4.95-4.95a1 1 0 1 0-1.414-1.414L3.636 7.293a.997.997 0 0 0 0 1.414l5.657 5.657a1 1 0 0 0 1.414-1.414L5.757 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.243 8l-4.95-4.95a1 1 0 0 1 1.414-1.414l5.657 5.657a.997.997 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414-1.414L10.243 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 6.757l-4.95 4.95a1 1 0 1 1-1.414-1.414l5.657-5.657a.997.997 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1-1.414 1.414L8 6.757z"/></symbol><symbol viewBox="0 0 16 16" id="appearance" xmlns="http://www.w3.org/2000/svg"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858a23.85 23.85 0 0 1-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023a9.7 9.7 0 0 0 .478-.035c-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="applications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="approval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.536 10.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 1 1 9.12 9.243l1.415 1.414zM7.632 8.109A2 2 0 0 0 7 11.364l2.121 2.121a1.996 1.996 0 0 0 2.807.021C11.686 14.554 10.627 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.6 0 1.142.038 1.632.109zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-down" xmlns="http://www.w3.org/2000/svg"><path d="M10.472 7.282a.862.862 0 0 1 1.26-.006c.357.364.357.958 0 1.285L8.627 11.73A.886.886 0 0 1 8 12a.849.849 0 0 1-.627-.27L4.275 8.561a.904.904 0 0 1-.013-1.285.861.861 0 0 1 1.26-.007l2.486 2.527z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4 12.5v-9A1.5 1.5 0 0 1 5.5 2h2.104c2.182 0 3.879.681 3.879 2.982 0 1.067-.517 2.227-1.374 2.595v.073C11.176 7.963 12 8.865 12 10.466 12 12.914 10.19 14 7.911 14H5.5A1.5 1.5 0 0 1 4 12.5zm2.376-5.696H7.49c1.164 0 1.665-.552 1.665-1.417 0-.94-.534-1.289-1.649-1.289h-1.13v2.706zm0 5.098h1.341c1.293 0 1.956-.515 1.956-1.62 0-1.049-.647-1.472-1.956-1.472H6.376v3.092z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="bullhorn" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.143 10H7V4H3a3 3 0 1 0 0 6h.143l.734 5.141a1 1 0 0 0 .99.859h1.556a.5.5 0 0 0 .495-.57L6.143 10zM8 4c1.034.02 2.039-.274 3.014-.883.727-.455 1.836-1.334 3.328-2.637A1 1 0 0 1 16 1.233v10.764a1 1 0 0 1-1.595.803c-1.658-1.227-2.788-1.992-3.392-2.294-.781-.39-1.785-.559-3.013-.506V4z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chart" xmlns="http://www.w3.org/2000/svg"><path d="M15 14a1 1 0 0 1 0 2H2a2 2 0 0 1-2-2V1a1 1 0 1 1 2 0v13h13zM3.142 8.735l2.502-2.561a.5.5 0 0 1 .714-.003L8 7.833l3.592-4.553a.5.5 0 0 1 .796.015l2.516 3.454a.5.5 0 0 1 .096.295V12.5a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5V9.085a.5.5 0 0 1 .142-.35z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 9 13" id="collapse"><path d="M.084.25C.01.18-.015.12.008.071.031.024.093 0 .194 0h8.521c.1 0 .162.024.185.072.023.048-.002.107-.075.177l-4.11 3.935a.372.372 0 0 1-.11.072h-.301a.508.508 0 0 1-.11-.072L.084.249zM.377 6.88a.364.364 0 0 1-.26-.105.334.334 0 0 1-.11-.25v-.709c0-.096.036-.179.11-.249a.364.364 0 0 1 .26-.105h8.15c.101 0 .188.035.261.105.074.07.11.153.11.25v.709c0 .096-.036.179-.11.249a.364.364 0 0 1-.26.105H.377zM.084 12.132c-.074.07-.099.129-.076.177.023.048.085.072.186.072h8.521c.1 0 .162-.024.185-.072.023-.048-.002-.107-.075-.177l-4.11-3.935a.372.372 0 0 0-.11-.072h-.301a.508.508 0 0 0-.11.072l-4.11 3.935z"/></symbol><symbol viewBox="0 0 16 16" id="comment" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comment-dots" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586zM5 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="comment-next" xmlns="http://www.w3.org/2000/svg"><path d="M8 5V4a.5.5 0 0 1 .8-.4l2.667 2a.5.5 0 0 1 0 .8L8.8 8.4A.5.5 0 0 1 8 8V7H6a1 1 0 1 1 0-2h2zM1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comments" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.75 10L0 13V3a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3.75zM13 5h1a2 2 0 0 1 2 2v8l-2.667-2H8a2 2 0 0 1-2-2h4a3 3 0 0 0 3-3V5z"/></symbol><symbol viewBox="0 0 16 16" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3.876-1.008a4.002 4.002 0 0 1-7.752 0A1.01 1.01 0 0 1 4 9H1a1 1 0 1 1 0-2h3c.042 0 .083.003.124.008a4.002 4.002 0 0 1 7.752 0A1.01 1.01 0 0 1 12 7h3a1 1 0 0 1 0 2h-3a1.01 1.01 0 0 1-.124-.008z"/></symbol><symbol viewBox="0 0 16 16" id="credit-card" xmlns="http://www.w3.org/2000/svg"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="cut" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="2" y="7" fill-rule="evenodd" rx="1"/></symbol><symbol viewBox="0 0 16 16" id="dashboard" xmlns="http://www.w3.org/2000/svg"><path d="M7.709 10.021l.696-2.6a.5.5 0 0 1 .966.26l-.657 2.45A2 2 0 0 1 10 12H6a2 2 0 0 1 1.709-1.979zM0 8.9a8 8 0 0 1 15.998 0H16v3.6a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5V8.9zM14 9A6 6 0 1 0 2 9v3.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V9zM3.5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm9 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-7-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm5 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1z"/></symbol><symbol viewBox="0 0 16 16" id="disk" xmlns="http://www.w3.org/2000/svg"><path d="M16 11.764V3a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8.764A2.989 2.989 0 0 1 2 11V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v8c.768 0 1.47.289 2 .764zM2 12h12a2 2 0 1 1 0 4H2a2 2 0 1 1 0-4zm10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="doc_code" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zm1.036 7.607a.498.498 0 0 1-.147.354l-1.414 1.414a.5.5 0 0 1-.707-.707l1.06-1.06-1.06-1.061a.5.5 0 0 1 .707-.707l1.414 1.414a.498.498 0 0 1 .147.353zm-4.822 0l1.06 1.061a.5.5 0 0 1-.706.707l-1.414-1.414a.498.498 0 0 1 0-.707l1.414-1.414a.5.5 0 1 1 .707.707l-1.06 1.06zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="doc_image" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM7.333 9.667l1.313-1.313a.5.5 0 0 1 .708 0L12 11H4l2.188-1.75a.5.5 0 0 1 .624 0l.521.417zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4 11h8v.7a.3.3 0 0 1-.3.3H4.3a.3.3 0 0 1-.3-.3V11z"/></symbol><symbol viewBox="0 0 16 16" id="doc_text" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 105 26" id="double-headed-arrow" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.018 11.089L15.138.614c1.23-.911 3.086-.795 4.147.26.461.46.715 1.045.715 1.651v20.95C20 24.869 18.684 26 17.06 26a3.238 3.238 0 0 1-1.921-.614L1.019 14.911C-.212 14-.347 12.405.714 11.35c.094-.094.195-.18.303-.261zm102.964 0c.108.08.21.167.303.26 1.061 1.056.925 2.65-.303 3.562l-14.12 10.475A3.238 3.238 0 0 1 87.94 26C86.316 26 85 24.87 85 23.475V2.525c0-.606.254-1.192.715-1.65 1.061-1.056 2.917-1.172 4.146-.26l14.12 10.474zM35 17a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 1600 1600" id="ellipsis_v" xmlns="http://www.w3.org/2000/svg"><path d="M1088 1248v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V224q0-40 28-68t68-28h192q40 0 68 28t28 68z"/></symbol><symbol viewBox="0 0 18 18" id="emoji_slightly_smiling_face" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369.721.721 0 0 1 .568.047.715.715 0 0 1 .37.445 2.91 2.91 0 0 0 1.084 1.518A2.93 2.93 0 0 0 9 12.75a2.93 2.93 0 0 0 1.775-.58 2.913 2.913 0 0 0 1.084-1.518.711.711 0 0 1 .375-.445.737.737 0 0 1 .575-.047c.195.063.34.186.433.37.094.183.11.372.047.568zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smile" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568zM14 6.37c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm-6.5 0c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm9 2.63a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smiley" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568h.001zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6c.92.397 1.91.6 2.912.598a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39c.397-.92.6-1.91.598-2.912zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z"/></symbol><symbol viewBox="0 0 16 16" id="epic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.985 8.044l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637A2 2 0 0 0 1.618 9h11.661a2 2 0 0 0 1.706-.956zm0 3l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637a2 2 0 0 0 .576.084h11.661a2 2 0 0 0 1.706-.956zM3.618 2h10.995a1 1 0 0 1 .948 1.316l-1.333 4a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l2-4A1 1 0 0 1 3.618 2zm-.382 4h9.322l.667-2H4.236l-1 2z"/></symbol><symbol viewBox="0 0 16 16" id="external-link" xmlns="http://www.w3.org/2000/svg"><path d="M13.121 4.177l-4.95 4.95a1 1 0 1 1-1.414-1.414l4.95-4.95-1.386-1.386a.5.5 0 0 1 .299-.85l4.709-.524a.5.5 0 0 1 .552.552l-.523 4.71a.5.5 0 0 1-.851.297l-1.386-1.385zM12 8.884a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-4z"/></symbol><symbol viewBox="0 0 16 16" id="eye" xmlns="http://www.w3.org/2000/svg"><path d="M8 14C4.816 14 2.253 12.284.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2s5.747 1.716 7.607 5.019a2 2 0 0 1 0 1.962C13.747 12.284 11.184 14 8 14zm0-2c2.41 0 4.338-1.29 5.864-4C12.338 5.29 10.411 4 8 4 5.59 4 3.662 5.29 2.136 8 3.662 10.71 5.589 12 8 12zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm1-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="eye-slash" xmlns="http://www.w3.org/2000/svg"><path d="M13.618 2.62L1.62 14.619a1 1 0 0 1-.985-1.668l1.525-1.526C1.516 10.742.926 9.927.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2c1.074 0 2.076.195 3.006.58l.944-.944a1 1 0 0 1 1.668.985zM8.068 11a3 3 0 0 0 2.931-2.932l-2.931 2.931zm-3.02-2.462a3 3 0 0 1 3.49-3.49l.884-.884A6.044 6.044 0 0 0 8 4C5.59 4 3.662 5.29 2.136 8c.445.79.924 1.46 1.439 2.011l1.473-1.473zm.421 5.06l1.658-1.658c.283.04.575.06.873.06 2.41 0 4.338-1.29 5.864-4a11.023 11.023 0 0 0-1.133-1.664l1.418-1.418a12.799 12.799 0 0 1 1.458 2.1 2 2 0 0 1 0 1.963C13.747 12.284 11.184 14 8 14a7.883 7.883 0 0 1-2.53-.402z"/></symbol><symbol viewBox="0 0 16 16" id="file-addition" xmlns="http://www.w3.org/2000/svg"><path d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3z"/></symbol><symbol viewBox="0 0 16 16" id="file-deletion" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm2 6h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="file-modified" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm5 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></symbol><symbol viewBox="0 0 16 16" id="filter" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 6v9l-3.724-1.862A.5.5 0 0 1 6 12.691V6L1.854 1.854A.5.5 0 0 1 2.207 1h11.586a.5.5 0 0 1 .353.854L10 6z"/></symbol><symbol viewBox="0 0 16 16" id="folder" xmlns="http://www.w3.org/2000/svg"><path d="M7.228 5l-.475-1.335A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H7.228zM13 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="fork" xmlns="http://www.w3.org/2000/svg"><path d="M9 12.268a2 2 0 1 1-2 0V8.874A4.002 4.002 0 0 1 4 5V3.732a2 2 0 1 1 2 0V5a2 2 0 1 0 4 0V3.732a2 2 0 1 1 2 0V5a4.002 4.002 0 0 1-3 3.874v3.394zM11 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="geo-nodes" xmlns="http://www.w3.org/2000/svg"><path d="M9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4M7.3 12.7c.4.4 1 .3 1.4-.1C11.6 9.5 13 7 13 5.3 13 2.4 10.8 0 8 0S3 2.4 3 5.3C3 7 4.4 9.5 7.3 12.7M8 2c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2C5 3.4 6.4 2 8 2"/><circle cx="8" cy="5" r="1"/></symbol><symbol viewBox="0 0 16 16" id="git-merge" xmlns="http://www.w3.org/2000/svg"><path d="M11 12.268V5a1 1 0 0 0-1-1v1a.5.5 0 0 1-.8.4l-2.667-2a.5.5 0 0 1 0-.8L9.2.6a.5.5 0 0 1 .8.4v1a3 3 0 0 1 3 3v7.268a2 2 0 1 1-2 0zm-6 0a2 2 0 1 1-2 0V4.732a2 2 0 1 1 2 0v7.536zM4 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="group" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.048 11.997C-.377 11.975.013 11.782.013 10.56.013 9.235.653 8 4 8c.444 0 .84.022 1.194.062.164.435.426.82.76 1.132-1.786.389-2.721 1.353-2.906 2.803zm2.94-7.222a2.993 2.993 0 0 0-.976 1.95 2 2 0 1 1 .975-1.95zm6.964 7.222c-.185-1.45-1.12-2.414-2.906-2.803.334-.311.596-.697.76-1.132C11.16 8.022 11.556 8 12 8c3.346 0 3.987 1.235 3.987 2.56 0 1.222.39 1.415-3.035 1.437zm-1.964-5.272a2.993 2.993 0 0 0-.976-1.95 2 2 0 1 1 .976 1.95zM8 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 5c-2.177 0-3.987-.115-3.987-1.44S4.653 10 8 10c3.346 0 3.987 1.235 3.987 2.56S10.177 14 8 14z"/></symbol><symbol viewBox="0 0 16 16" id="history" xmlns="http://www.w3.org/2000/svg"><path d="M2.868 3.24a7 7 0 1 1-.043 9.475 1 1 0 0 1 1.478-1.348 5 5 0 1 0 .124-6.865l.796.645a.5.5 0 0 1-.193.873l-3.232.814a.5.5 0 0 1-.622-.504L1.3 3a.5.5 0 0 1 .814-.37l.754.61zM9 8h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V6a1 1 0 1 1 2 0v2z"/></symbol><symbol viewBox="0 0 16 16" id="home" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177a.505.505 0 0 1-.038.044l.038-.044zm-.787 0l.038.043a.5.5 0 0 1-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="hook" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></symbol><symbol viewBox="0 0 16 16" id="hourglass" xmlns="http://www.w3.org/2000/svg"><path d="M10.331 4.889A2.988 2.988 0 0 0 11 3V2H5v1c0 .362.064.709.182 1.03l5.15.859zM3 14v-1c0-1.78.93-3.342 2.33-4.228.447-.327.67-.582.67-.764 0-.19-.242-.46-.725-.815A4.996 4.996 0 0 1 3 3V2H2a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2h-1v1a4.997 4.997 0 0 1-2.39 4.266c-.407.3-.61.545-.61.734 0 .19.203.434.61.734A4.997 4.997 0 0 1 13 13v1h1a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2h1zm8 0v-1a3 3 0 0 0-6 0v1h6z"/></symbol><symbol viewBox="0 0 24 30" id="image-comment-dark" xmlns="http://www.w3.org/2000/svg"><title>cursor_active</title><g fill="none" fill-rule="evenodd"><path d="M24 12.105c0 6.686-5.74 11.58-12 17.895C5.74 23.684 0 18.79 0 12.105 0 5.42 5.373 0 12 0s12 5.42 12 12.105z" fill="#FFF" fill-rule="nonzero"/><path d="M15.28 25.249c1.458-1.475 2.539-2.635 3.474-3.747 2.851-3.394 4.203-6.265 4.203-9.397 0-6.111-4.908-11.062-10.957-11.062-6.05 0-10.957 4.951-10.957 11.062 0 3.132 1.352 6.003 4.203 9.397.935 1.112 2.016 2.272 3.474 3.747.511.517 2.216 2.213 3.28 3.275 1.064-1.062 2.769-2.758 3.28-3.275z" fill="#1F78D1"/><path d="M14.551 8.256A6.874 6.874 0 0 0 12 7.787a6.92 6.92 0 0 0-2.558.469c-.79.308-1.42.725-1.888 1.252-.465.527-.697 1.096-.697 1.708 0 .5.159.977.476 1.433.321.45.772.841 1.352 1.172l.583.334-.181.643c-.107.407-.263.79-.469 1.152a6.604 6.604 0 0 0 1.842-1.145l.288-.254.381.04c.309.035.599.053.871.053.91 0 1.761-.154 2.551-.462.795-.312 1.424-.732 1.889-1.259.468-.526.703-1.096.703-1.707 0-.612-.235-1.181-.703-1.708-.465-.527-1.094-.944-1.889-1.252zm2.645.81c.536.656.804 1.373.804 2.15 0 .776-.268 1.495-.804 2.156-.535.656-1.263 1.176-2.183 1.56-.92.38-1.924.57-3.013.57a9.16 9.16 0 0 1-.971-.054 7.32 7.32 0 0 1-3.08 1.62 5.044 5.044 0 0 1-.764.148h-.033a.26.26 0 0 1-.181-.074.324.324 0 0 1-.107-.18v-.007c-.014-.018-.016-.045-.007-.08.014-.037.018-.059.014-.068a.19.19 0 0 1 .033-.067.645.645 0 0 0 .04-.06 1.73 1.73 0 0 0 .047-.054l.054-.06a53.034 53.034 0 0 1 .435-.489c.049-.049.118-.136.207-.26a2.57 2.57 0 0 0 .221-.342c.054-.103.114-.235.181-.395a4.18 4.18 0 0 0 .174-.51c-.7-.397-1.254-.888-1.66-1.473A3.261 3.261 0 0 1 6 11.216c0-.777.268-1.494.804-2.15.535-.66 1.263-1.18 2.183-1.56.92-.384 1.924-.576 3.013-.576 1.09 0 2.094.192 3.013.576.92.38 1.648.9 2.183 1.56z" fill="#FFF" fill-rule="nonzero"/></g></symbol><symbol viewBox="0 0 16 16" id="import" xmlns="http://www.w3.org/2000/svg"><path d="M9 8h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0L5.6 8.8A.5.5 0 0 1 6 8h1V1a1 1 0 1 1 2 0v7zM0 8a1 1 0 1 1 2 0 6 6 0 1 0 12 0 1 1 0 0 1 2 0A8 8 0 1 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-block" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.803 8a5.97 5.97 0 0 0-.462 1H4.5a.5.5 0 0 1 0-1h1.303zM4.5 5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm7.5.083a6.04 6.04 0 0 0-2 0V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.083a5.96 5.96 0 0 0 .72 2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v2.083zm1.121 3.796zM11 16a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm-1.293-2.292a3 3 0 0 0 4.001-4.001l-4.001 4zm-1.415-1.415l4.001-4a3 3 0 0 0-4.001 4.001z"/></symbol><symbol viewBox="0 0 16 16" id="issue-child" xmlns="http://www.w3.org/2000/svg"><path d="M11 8H5v1h1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2V7a.997.997 0 0 1 1-1h3V4H4.5a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9v2h3a.997.997 0 0 1 1 1v2h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1V8zm-9 3v2h3v-2H2zm9 0v2h3v-2h-3z"/></symbol><symbol viewBox="0 0 16 16" id="issue-close" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M10.874 2H12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-2c-.918 0-1.74-.413-2.29-1.063a3.987 3.987 0 0 0 1.988-.984A1 1 0 0 0 10 14h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-1V3c0-.345-.044-.68-.126-1zM4 0h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-new" xmlns="http://www.w3.org/2000/svg"><path d="M10 2V1a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V4H9a1 1 0 1 1 0-2h1zm0 6a1 1 0 0 1 2 0v5a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h1a1 1 0 1 1 0 2H5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open-m" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-parent" xmlns="http://www.w3.org/2000/svg"><path d="M11 11H5v1h1.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H3v-2a.997.997 0 0 1 1-1h3V7H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9v2h3a.997.997 0 0 1 1 1v2h2.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H11v-1zM6 3v2h4V3H6z"/></symbol><symbol viewBox="0 0 16 16" id="issues" xmlns="http://www.w3.org/2000/svg"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="italic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.5 12l2-8H6a1 1 0 1 1 0-2h6a1 1 0 0 1 0 2h-1.5l-2 8H10a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2h1.5z"/></symbol><symbol viewBox="0 0 16 16" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7.575 6.689a4.002 4.002 0 0 1 6.274-4.86 4 4 0 0 1-4.86 6.274l-2.21 2.21.706.708a1 1 0 1 1-1.414 1.414l-.707-.707-.707.707.707.707a1 1 0 1 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.414-1.414l5.746-5.746zm2.032-.618a2 2 0 1 0 2.828-2.828A2 2 0 0 0 9.607 6.07z"/></symbol><symbol viewBox="0 0 16 16" id="key-2" xmlns="http://www.w3.org/2000/svg"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></symbol><symbol viewBox="0 0 16 16" id="label" xmlns="http://www.w3.org/2000/svg"><path d="M11.782 14.718a3 3 0 0 1-4.242 0L1.652 8.829a2 2 0 0 1-.565-1.702l.54-3.703a2 2 0 0 1 1.69-1.69l3.703-.54a2 2 0 0 1 1.703.564l5.888 5.888a3 3 0 0 1 0 4.243l-2.829 2.829zm1.415-5.657L7.309 3.173l-3.703.54-.54 3.702 5.888 5.888a1 1 0 0 0 1.414 0l2.829-2.828a1 1 0 0 0 0-1.414zM5.732 5.525A1 1 0 1 1 7.146 6.94a1 1 0 0 1-1.414-1.414z"/></symbol><symbol viewBox="0 0 16 16" id="labels" xmlns="http://www.w3.org/2000/svg"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667zM.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="leave" xmlns="http://www.w3.org/2000/svg"><path d="M11 7V5.883a.5.5 0 0 1 .757-.429l3.528 2.117a.5.5 0 0 1 0 .858l-3.528 2.117a.5.5 0 0 1-.757-.43V9H7a1 1 0 1 1 0-2h4zm-2 6.256a1 1 0 0 1 2 0A2.744 2.744 0 0 1 8.256 16H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h5.19A2.81 2.81 0 0 1 11 2.81a1 1 0 0 1-2 0A.81.81 0 0 0 8.19 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.256c.41 0 .744-.333.744-.744z"/></symbol><symbol viewBox="0 0 16 16" id="level-up" xmlns="http://www.w3.org/2000/svg"><path fill="#2E2E2E" fill-rule="evenodd" d="M7 6h3.489a.5.5 0 0 0 .373-.832L6.374.117a.5.5 0 0 0-.748 0l-4.488 5.05A.5.5 0 0 0 1.51 6H5v7a3 3 0 0 0 3 3h6a1 1 0 0 0 0-2H8a1 1 0 0 1-1-1V6z"/></symbol><symbol viewBox="0 0 16 16" id="license" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.56 8.9l2.66 4.606a.3.3 0 0 1-.243.45l-1.678.094a.1.1 0 0 0-.078.044l-.953 1.432a.3.3 0 0 1-.51-.016L9.097 10.9a5.994 5.994 0 0 0 3.464-2zm-5.23 2.063L4.707 15.51a.3.3 0 0 1-.51.016l-.953-1.432a.1.1 0 0 0-.078-.044l-1.678-.094a.3.3 0 0 1-.243-.45l2.48-4.297a5.983 5.983 0 0 0 3.607 1.754zM8 10A5 5 0 1 1 8 0a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="link" xmlns="http://www.w3.org/2000/svg"><path d="M6.986 3.35l2.12-2.122a4 4 0 0 1 5.657 5.657l-2.828 2.829a4 4 0 0 1-5.657 0 1 1 0 0 1 1.414-1.415 2 2 0 0 0 2.829 0l2.828-2.828a2 2 0 1 0-2.828-2.828l-1.001 1a5.018 5.018 0 0 0-2.534-.294zm2.12 9.192l-2.12 2.121a4 4 0 1 1-5.658-5.656l2.829-2.829a4 4 0 0 1 5.657 0 1 1 0 1 1-1.415 1.414 2 2 0 0 0-2.828 0l-2.828 2.829a2 2 0 1 0 2.828 2.828l1.001-1.001a5.018 5.018 0 0 0 2.534.294z"/></symbol><symbol viewBox="0 0 16 16" id="list-bulleted" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-7h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm0 5h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm-4 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-2h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="list-numbered" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2zM1.156 5v-.828h.816V2.204h-.72v-.636c.432-.084.708-.192.996-.372h.756v2.976h.684V5H1.156zm-.18 5v-.588c.9-.828 1.596-1.464 1.596-1.98 0-.342-.192-.504-.468-.504-.252 0-.444.18-.624.36l-.552-.552c.396-.42.756-.612 1.32-.612.768 0 1.308.492 1.308 1.248 0 .612-.576 1.284-1.092 1.812.192-.024.468-.048.636-.048h.636V10H.976zm1.26 5.072c-.618 0-1.068-.204-1.356-.54l.468-.648c.234.216.51.36.78.36.336 0 .552-.12.552-.36 0-.288-.15-.456-.948-.456v-.72c.636 0 .828-.168.828-.432 0-.228-.138-.348-.396-.348-.252 0-.432.108-.672.312l-.516-.624c.372-.312.768-.492 1.236-.492.84 0 1.38.384 1.38 1.074 0 .366-.204.642-.612.822v.024c.432.132.732.432.732.912 0 .72-.684 1.116-1.476 1.116z"/></symbol><symbol viewBox="0 0 16 16" id="location" xmlns="http://www.w3.org/2000/svg"><path d="M8.755 15.144a1 1 0 0 1-1.51 0C3.748 11.114 2 8.065 2 6a6 6 0 1 1 12 0c0 2.065-1.748 5.113-5.245 9.144zM12 6a4 4 0 1 0-8 0c0 1.314 1.312 3.71 4 6.944C10.688 9.71 12 7.314 12 6zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="location-dot" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.314 13.087C4.382 13.295 3 13.85 3 14.5c0 .828 2.239 1.5 5 1.5s5-.672 5-1.5c0-.65-1.382-1.205-3.314-1.413l-.202.225a2 2 0 0 1-2.968 0l-.202-.225zm2.428-.445a1 1 0 0 1-1.484 0C4.419 9.5 3 7.037 3 5.252 3 2.353 5.239 0 8 0s5 2.352 5 5.253c0 1.784-1.42 4.247-4.258 7.389zM11 5.252C11 3.436 9.634 2 8 2S5 3.435 5 5.253c0 1.027.974 2.824 3 5.203 2.026-2.38 3-4.176 3-5.203zM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M10 5V4h2v1a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3V4h2v1h4zM4 7a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4zm0-3a4 4 0 1 1 8 0h-2a2 2 0 1 0-4 0H4z"/></symbol><symbol viewBox="0 0 16 16" id="lock-open" xmlns="http://www.w3.org/2000/svg"><path d="M4.044 4a4 4 0 0 1 6.99-2.658 1 1 0 1 1-1.495 1.33A2 2 0 0 0 6.044 4a.998.998 0 0 1-.07.367v.701H12a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3v-5a3 3 0 0 1 2.974-3V4h.07zM4 7.07a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="log" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="mail" xmlns="http://www.w3.org/2000/svg"><path d="M14 5.6L9.338 9.796a2 2 0 0 1-2.676 0L2 5.6V11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.6zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm.212 2L8 8.31 12.788 4H3.212z"/></symbol><symbol viewBox="0 0 16 16" id="menu" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.143 2h13.714C15.488 2 16 2.448 16 3s-.512 1-1.143 1H1.143C.512 4 0 3.552 0 3s.512-1 1.143-1zm0 5h13.714C15.488 7 16 7.448 16 8s-.512 1-1.143 1H1.143C.512 9 0 8.552 0 8s.512-1 1.143-1zm0 5h13.714c.631 0 1.143.448 1.143 1s-.512 1-1.143 1H1.143C.512 14 0 13.552 0 13s.512-1 1.143-1z"/></symbol><symbol viewBox="0 0 16 16" id="merge-request-close" xmlns="http://www.w3.org/2000/svg"><path d="M9.414 8l1.414 1.414a1 1 0 1 1-1.414 1.414L8 9.414l-1.414 1.414a1 1 0 1 1-1.414-1.414L6.586 8 5.172 6.586a1 1 0 1 1 1.414-1.414L8 6.586l1.414-1.414a1 1 0 1 1 1.414 1.414L9.414 8zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="messages" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="mobile-issue-close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.657 10.728L2.12 7.192A1 1 0 1 0 .707 8.607l4.243 4.242a.997.997 0 0 0 1.414 0l8.485-8.485a1 1 0 1 0-1.414-1.414l-7.778 7.778z"/></symbol><symbol viewBox="0 0 16 16" id="monitor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></symbol><symbol viewBox="0 0 16 16" id="more" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="notifications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></symbol><symbol viewBox="0 0 16 16" id="notifications-off" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.26 5.089c.243.757.382 1.478.5 2.017.187.86.74 2.188 1.66 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0H4.35l2-2h7.29c-.993-1.937-1.6-3.396-1.835-4.468-.07-.326-.129-.59-.178-.81l1.634-1.633zM10.943 1.75l-1.48 1.48C9.07 3.076 8.612 3 8.069 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.065.317-.17.673-.317 1.073L.45 12.242a1.99 1.99 0 0 1 .224-1.19c.962-1.787 1.521-3.064 1.68-3.831.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024 4.867 4.867 0 0 1 1.944.688zm2.932-.105a1 1 0 0 1 0 1.415L2.561 14.374a1 1 0 1 1-1.415-1.414L12.46 1.646a1 1 0 0 1 1.414 0z"/></symbol><symbol viewBox="0 0 16 16" id="overview" xmlns="http://www.w3.org/2000/svg"><path d="M2 0h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2h-3zM2 9h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3h-3z"/></symbol><symbol viewBox="0 0 16 16" id="pencil" xmlns="http://www.w3.org/2000/svg"><path d="M13.02 1.293l1.414 1.414a1 1 0 0 1 0 1.414L4.119 14.436a1 1 0 0 1-.704.293l-2.407.008L1 12.316a1 1 0 0 1 .293-.71L11.605 1.292a1 1 0 0 1 1.414 0zm-1.416 1.415l-.707.707L12.31 4.83l.707-.707-1.414-1.415zM3.411 13.73l1.123-1.122H3.12v-1.415L2 12.312l.005 1.422 1.406-.005z"/></symbol><symbol viewBox="0 0 16 16" id="pencil-square" xmlns="http://www.w3.org/2000/svg"><path d="M12 9a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V9zm.778-7.179l1.414 1.415-6.476 6.476a1 1 0 0 1-.498.27l-1.51.325.323-1.512a1 1 0 0 1 .27-.497l6.477-6.477zM15.607.407a1 1 0 0 1 0 1.414l-.708.707-1.414-1.414.707-.707a1 1 0 0 1 1.415 0z"/></symbol><symbol viewBox="0 0 16 16" id="pipeline" xmlns="http://www.w3.org/2000/svg"><path d="M8.969 7.25a2 2 0 1 1-1.938 0A1.002 1.002 0 0 1 7 7V5.083a.2.2 0 0 1 .06-.142l.877-.87a.1.1 0 0 1 .141 0l.864.87A.2.2 0 0 1 9 5.083V7c0 .086-.01.17-.031.25zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm4.5-4a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="play" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.765 15.835c-.545.321-1.258.159-1.593-.363A1.075 1.075 0 0 1 1 14.89V1.11C1 .496 1.518 0 2.158 0c.214 0 .424.057.607.165l11.684 6.89c.544.321.714 1.005.38 1.526a1.135 1.135 0 0 1-.38.364l-11.684 6.89z"/></symbol><symbol viewBox="0 0 16 16" id="plus" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V1a1 1 0 1 1 2 0v6h6a1 1 0 0 1 0 2H9v6a1 1 0 0 1-2 0V9H1a1 1 0 1 1 0-2h6z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 7V4a1 1 0 1 0-2 0v3H4a1 1 0 1 0 0 2h3v3a1 1 0 0 0 2 0V9h3a1 1 0 0 0 0-2H9zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square-o" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="preferences" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 12h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2zm11-5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zM6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2z"/></symbol><symbol viewBox="0 0 16 16" id="profile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="push-rules" xmlns="http://www.w3.org/2000/svg"><path d="M6.268 9a2 2 0 0 1 3.464 0H11a1 1 0 0 1 0 2H9.732a2 2 0 0 1-3.464 0H5a1 1 0 0 1 0-2h1.268zM7 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 7 5.515V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="question" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-1.46-5.602h2.233a3.97 3.97 0 0 1 .051-.558c.029-.17.073-.326.133-.469.06-.143.14-.28.242-.41.102-.13.228-.263.38-.399.26-.24.504-.467.733-.683a5.03 5.03 0 0 0 .598-.668c.17-.23.302-.477.399-.742a2.66 2.66 0 0 0 .144-.907c0-.505-.083-.95-.25-1.335a2.55 2.55 0 0 0-.723-.97 3.2 3.2 0 0 0-1.152-.589 5.441 5.441 0 0 0-1.531-.2c-.516 0-.998.063-1.445.188a3.19 3.19 0 0 0-1.168.59c-.331.268-.594.61-.79 1.027-.195.417-.295.917-.3 1.5h2.64c.006-.224.04-.416.102-.578.062-.161.142-.293.238-.394a.921.921 0 0 1 .332-.227 1.04 1.04 0 0 1 .39-.074c.34 0 .593.095.763.285.169.19.254.488.254.895 0 .328-.106.63-.317.906-.21.276-.499.565-.863.867-.214.182-.39.374-.531.574-.141.2-.253.42-.336.657a3.656 3.656 0 0 0-.176.777 7.89 7.89 0 0 0-.05.937zm-.321 2.375c0 .188.035.362.105.524.07.161.17.3.301.418.13.117.284.21.46.277.178.068.376.102.595.102.218 0 .416-.034.593-.102.178-.068.331-.16.461-.277a1.2 1.2 0 0 0 .301-.418c.07-.162.106-.336.106-.524a1.3 1.3 0 0 0-.106-.523 1.2 1.2 0 0 0-.3-.418 1.461 1.461 0 0 0-.462-.277 1.651 1.651 0 0 0-.593-.102c-.22 0-.417.034-.594.102a1.46 1.46 0 0 0-.461.277 1.2 1.2 0 0 0-.3.418 1.284 1.284 0 0 0-.106.523z"/></symbol><symbol viewBox="0 0 16 16" id="question-o" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-.778-4.151c0-.301.014-.575.044-.82a3.2 3.2 0 0 1 .154-.68c.073-.208.17-.4.294-.575.123-.176.278-.343.465-.503a4.81 4.81 0 0 0 .755-.758c.185-.242.277-.506.277-.793 0-.356-.074-.617-.222-.783-.148-.166-.37-.25-.667-.25a.92.92 0 0 0-.342.065.806.806 0 0 0-.29.199 1.04 1.04 0 0 0-.209.345 1.5 1.5 0 0 0-.088.506H5.082c.005-.51.092-.948.263-1.313.171-.364.401-.664.69-.899.29-.234.63-.406 1.023-.516a4.66 4.66 0 0 1 1.264-.164c.497 0 .944.058 1.34.174.397.117.733.289 1.008.517.276.227.487.51.633.847.146.337.218.727.218 1.17 0 .295-.042.56-.126.792a2.52 2.52 0 0 1-.349.65 4.4 4.4 0 0 1-.523.584c-.2.19-.414.389-.642.598a2.73 2.73 0 0 0-.332.349c-.089.114-.16.233-.212.359a1.868 1.868 0 0 0-.116.41 3.39 3.39 0 0 0-.044.489H7.222zm-.28 2.078c0-.164.03-.317.092-.458a1.05 1.05 0 0 1 .263-.366c.114-.103.248-.183.403-.243a1.45 1.45 0 0 1 .52-.089c.191 0 .364.03.52.09.154.059.289.14.403.242.114.103.201.224.263.366.061.141.092.294.092.458 0 .164-.03.316-.092.458a1.05 1.05 0 0 1-.263.365 1.278 1.278 0 0 1-.404.243 1.43 1.43 0 0 1-.52.089c-.19 0-.364-.03-.519-.089-.155-.06-.29-.14-.403-.243a1.05 1.05 0 0 1-.263-.365 1.135 1.135 0 0 1-.093-.458z"/></symbol><symbol viewBox="0 0 16 16" id="quote" xmlns="http://www.w3.org/2000/svg"><path d="M15 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1zM7 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="redo" xmlns="http://www.w3.org/2000/svg"><path d="M4.666 4.423a5 5 0 1 1-.203 6.944 1 1 0 1 0-1.478 1.347 7 7 0 1 0 .12-9.556L1.842 2.137a.5.5 0 0 0-.815.385L1 7.26a.5.5 0 0 0 .607.492l4.629-1.013a.5.5 0 0 0 .207-.877L4.666 4.423z"/></symbol><symbol viewBox="0 0 16 16" id="remove" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V3zm3-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5zM4 3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3H4zm2.5 2a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="repeat" xmlns="http://www.w3.org/2000/svg"><path d="M11.494 4.423a5 5 0 1 0 .203 6.944 1 1 0 1 1 1.478 1.347 7 7 0 1 1-.12-9.556l1.262-1.021a.5.5 0 0 1 .815.385l.028 4.738a.5.5 0 0 1-.607.492L9.924 6.739a.5.5 0 0 1-.207-.877l1.777-1.439z"/></symbol><symbol viewBox="0 0 16 16" id="retry" xmlns="http://www.w3.org/2000/svg"><path d="M4.114 6.958a4 4 0 0 0 5.283 4.775 1 1 0 1 1 .712 1.87A6 6 0 0 1 2.182 6.44l-.741-.2a.5.5 0 0 1-.12-.915l2.195-1.268a.5.5 0 0 1 .683.183l1.268 2.196a.5.5 0 0 1-.563.733l-.79-.212zm7.777 2.084a4 4 0 0 0-5.284-4.775 1 1 0 0 1-.712-1.87 6 6 0 0 1 7.927 7.162l.742.2a.5.5 0 0 1 .12.915l-2.196 1.268a.5.5 0 0 1-.683-.183l-1.267-2.196a.5.5 0 0 1 .562-.733l.79.212z"/></symbol><symbol viewBox="0 0 16 16" id="scale" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.99 9a.792.792 0 0 0-.078-.231L13 7l-.912 1.769a.791.791 0 0 0-.077.231h1.978zm-10 0a.792.792 0 0 0-.078-.231L3 7l-.912 1.769A.791.791 0 0 0 2.011 9h1.978zM2 0h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm3 14h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zM8 4a1 1 0 0 1 1 1v9H7V5a1 1 0 0 1 1-1zm-4.53-.714l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 3 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158L2.53 3.286a.53.53 0 0 1 .94 0zm10 0l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 13 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158l2.266-4.735a.53.53 0 0 1 .94 0z"/></symbol><symbol viewBox="0 0 16 16" id="screen-full" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 14v-2a1 1 0 0 1 2 0v3a.997.997 0 0 1-1 1h-3a1 1 0 0 1 0-2h2zM2 14v-2a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1h3a1 1 0 0 0 0-2H2zM15.707.293A.997.997 0 0 1 16 1v3a1 1 0 0 1-2 0V2h-2a1 1 0 0 1 0-2h3c.276 0 .526.112.707.293zM2 2v2a1 1 0 1 1-2 0V1a.997.997 0 0 1 1-1h3a1 1 0 1 1 0 2H2zm4 4h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="screen-normal" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3V1a1 1 0 1 1 2 0v3a.997.997 0 0 1-1 1H1a1 1 0 1 1 0-2h2zm10 0h2a1 1 0 0 1 0 2h-3a.997.997 0 0 1-1-1V1a1 1 0 0 1 2 0v2zM3 13H1a1 1 0 0 1 0-2h3a.997.997 0 0 1 1 1v3a1 1 0 0 1-2 0v-2zm10 0v2a1 1 0 0 1-2 0v-3a.997.997 0 0 1 1-1h3a1 1 0 0 1 0 2h-2zM6.5 7h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 12 16" id="scroll_down" xmlns="http://www.w3.org/2000/svg"><path class="evfirst-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043a.51.51 0 0 0 .321-.105c.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/><path class="evsecond-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/><path class="evthird-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91a.458.458 0 0 1-.136.09h-.37a.626.626 0 0 1-.136-.09"/></symbol><symbol viewBox="0 0 12 16" id="scroll_up" xmlns="http://www.w3.org/2000/svg"><path d="M1.048 1.845a.508.508 0 0 1-.32-.105c-.091-.07-.136-.154-.136-.25V.78c0-.095.045-.178.135-.249a.508.508 0 0 1 .321-.105h10.043a.51.51 0 0 1 .321.105c.09.07.136.154.136.25v.71c0 .095-.045.178-.136.249a.508.508 0 0 1-.32.105M.687 7.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 3.09A.458.458 0 0 0 6.257 3h-.37a.626.626 0 0 0-.136.09M.687 14.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 10.09a.458.458 0 0 0-.136-.09h-.37a.626.626 0 0 0-.136.09"/></symbol><symbol viewBox="0 0 16 16" id="search" xmlns="http://www.w3.org/2000/svg"><path d="M8.853 8.854a3.5 3.5 0 1 0-4.95-4.95 3.5 3.5 0 0 0 4.95 4.95zm.207 2.328a5.5 5.5 0 1 1 2.121-2.121l3.329 3.328a1.5 1.5 0 0 1-2.121 2.121L9.06 11.182z"/></symbol><symbol viewBox="0 0 16 16" id="settings" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918a5.97 5.97 0 0 1 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="shield" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v7.186a3 3 0 0 1-1.426 2.554l-4 2.465a3 3 0 0 1-3.148 0l-4-2.465A3 3 0 0 1 1 10.186V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v7.186a1 1 0 0 0 .475.852l4 2.464a1 1 0 0 0 1.05 0l4-2.464a1 1 0 0 0 .475-.852V3a1 1 0 0 0-1-1H4zm0 1.5a.5.5 0 0 1 .5-.5h4v8.837a.5.5 0 0 1-.753.431l-3.5-2.052A.5.5 0 0 1 4 9.785V3.5z"/></symbol><symbol viewBox="0 0 16 16" id="slight-frown" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="slight-smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-5.163 2.254a.5.5 0 1 1 .865-.502 1.499 1.499 0 0 0 2.607-.018.5.5 0 1 1 .871.49 2.499 2.499 0 0 1-4.343.03z"/></symbol><symbol viewBox="0 0 16 16" id="smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM6.18 6.27a.5.5 0 0 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zm6 0a.5.5 0 1 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zM5 9a3 3 0 0 0 6 0H5z"/></symbol><symbol viewBox="0 0 16 16" id="smiley" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM5 9h6a3 3 0 0 1-6 0z"/></symbol><symbol viewBox="0 0 16 16" id="snippet" xmlns="http://www.w3.org/2000/svg"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 0 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 0 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></symbol><symbol viewBox="0 0 16 16" id="spam" xmlns="http://www.w3.org/2000/svg"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 14 14" id="spinner" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="7" cy="7" r="6" stroke="#000" stroke-opacity=".1" stroke-width="2"/><path fill="#000" fill-opacity=".1" fill-rule="nonzero" d="M7 0a7 7 0 0 1 7 7h-2a5 5 0 0 0-5-5V0z"/></g></symbol><symbol viewBox="0 0 16 16" id="star" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.609 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="star-o" xmlns="http://www.w3.org/2000/svg"><path d="M10.975 10.99a3 3 0 0 1 .655-2.083l1.54-1.916-2.219-.576a3 3 0 0 1-1.825-1.37L8 3.15 6.874 5.044a3 3 0 0 1-1.825 1.371l-2.218.576 1.54 1.916a3 3 0 0 1 .654 2.083l-.165 2.4 1.965-.836a3 3 0 0 1 2.348 0l1.965.836-.164-2.399zM7.61 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 14 14" id="status_canceled" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></symbol><symbol viewBox="0 0 22 22" id="status_canceled_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M8.171 5.971l7.7 7.7a.76.76 0 0 1 0 1.1l-1.1 1.1a.76.76 0 0 1-1.1 0l-7.7-7.7a.76.76 0 0 1 0-1.1l1.1-1.1a.76.76 0 0 1 1.1 0"/></symbol><symbol viewBox="0 0 16 16" id="status_closed" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.83a1 1 0 0 1 1.414 1.416l-3.535 3.535a1 1 0 0 1-1.415.001l-2.12-2.12a1 1 0 1 1 1.413-1.415zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 14 14" id="status_created" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></symbol><symbol viewBox="0 0 22 22" id="status_created_borderless" xmlns="http://www.w3.org/2000/svg"><circle cx="11" cy="11" r="5.107"/></symbol><symbol viewBox="0 0 14 14" id="status_failed" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_failed_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 9.38L8.798 7.178a.455.455 0 0 0-.65.006l-.964.965a.462.462 0 0 0-.006.65L9.38 11l-2.202 2.202a.455.455 0 0 0 .006.65l.965.964a.462.462 0 0 0 .65.006L11 12.62l2.202 2.202a.455.455 0 0 0 .65-.006l.964-.965a.462.462 0 0 0 .006-.65L12.62 11l2.202-2.202a.455.455 0 0 0-.006-.65l-.965-.964a.462.462 0 0 0-.65-.006L11 9.38z"/></symbol><symbol viewBox="0 0 14 14" id="status_manual" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_manual_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 11.99v-1.98l-1.238-.206c-.068-.273-.206-.546-.412-.956l.756-1.025-1.444-1.435-1.03.752a3.686 3.686 0 0 0-.963-.41L12.03 5.5h-1.994l-.206 1.23c-.343.068-.618.205-.962.41l-1.031-.752-1.444 1.435.687 1.025c-.206.341-.275.615-.412.956L5.5 9.941v1.981l1.237.205c.07.342.207.615.413.957l-.688 1.025 1.444 1.434 1.032-.683c.274.137.618.274.962.41l.206 1.23h2.063l.206-1.23c.344-.068.619-.205.963-.41l1.03.752 1.444-1.435-.756-1.025c.207-.341.344-.683.413-.956l1.031-.205zM11 13.017c-1.169 0-2.063-.889-2.063-2.05 0-1.162.894-2.05 2.063-2.05s2.063.888 2.063 2.05c0 1.161-.894 2.05-2.063 2.05z"/></symbol><symbol viewBox="0 0 22 22" id="status_notfound_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0v-1.43a5.9 5.9 0 0 0 .827-.492z"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></symbol><symbol viewBox="0 0 14 14" id="status_open" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M7 9.219a2.218 2.218 0 1 0 0-4.436A2.218 2.218 0 0 0 7 9.22zm0 1.12a3.338 3.338 0 1 1 0-6.676 3.338 3.338 0 0 1 0 6.676z"/></symbol><symbol viewBox="0 0 14 14" id="status_pending" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_pending_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M7.386 8.329c0-.315.157-.472.471-.472h1.414c.315 0 .472.157.472.472v5.342c0 .315-.157.472-.472.472H7.857c-.314 0-.471-.157-.471-.472V8.33m4.714 0c0-.315.157-.472.471-.472h1.415c.314 0 .471.157.471.472v5.342c0 .315-.157.472-.471.472H12.57c-.314 0-.471-.157-.471-.472V8.33"/></symbol><symbol viewBox="0 0 14 14" id="status_running" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_running_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 4.714c3.457 0 6.286 2.829 6.286 6.286 0 3.457-2.829 6.286-6.286 6.286-2.043 0-3.929-1.1-5.186-2.672L11 11V4.714"/></symbol><symbol viewBox="0 0 14 14" id="status_skipped" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M6.415 7.04L4.579 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L5.341 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L6.415 7.04zm2.54 0L7.119 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L7.881 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L8.955 7.04z"/></symbol><symbol viewBox="0 0 22 22" id="status_skipped_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M14.072 11.063l-2.82 2.82a.46.46 0 0 0-.001.652l.495.495a.457.457 0 0 0 .653-.001l3.7-3.7a.46.46 0 0 0 .001-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.479-3.479a.464.464 0 0 0-.654.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/><path d="M10.08 11.063l-2.819 2.82a.46.46 0 0 0-.002.652l.496.495a.457.457 0 0 0 .652-.001l3.7-3.7a.46.46 0 0 0 .002-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.48-3.479a.464.464 0 0 0-.653.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/></symbol><symbol viewBox="0 0 14 14" id="status_success" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_success_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.866 12.095l-1.95-1.95a.462.462 0 0 0-.647.01l-.964.964a.46.46 0 0 0-.01.646l3.013 3.014a.787.787 0 0 0 1.106.008l.425-.425 4.854-4.853a.462.462 0 0 0 .002-.659l-.964-.964a.468.468 0 0 0-.658.002l-4.207 4.207z"/></symbol><symbol viewBox="0 0 14 14" id="status_success_solid" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7zm6.278.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 14 14" id="status_warning" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></symbol><symbol viewBox="0 0 22 22" id="status_warning_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.429 5.5c0-.471.314-.786.785-.786h1.572c.471 0 .785.315.785.786v6.286c0 .471-.314.785-.785.785h-1.572c-.471 0-.785-.314-.785-.785V5.5m0 9.429c0-.472.314-.786.785-.786h1.572c.471 0 .785.314.785.786V16.5c0 .471-.314.786-.785.786h-1.572c-.471 0-.785-.315-.785-.786v-1.571"/></symbol><symbol viewBox="0 0 16 16" id="stop" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 0h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="terminal" xmlns="http://www.w3.org/2000/svg"><path d="M7 8a.997.997 0 0 1-.293.707l-1.414 1.414a1 1 0 1 1-1.414-1.414L4.586 8l-.707-.707a1 1 0 1 1 1.414-1.414l1.414 1.414A.997.997 0 0 1 7 8zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm0 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H4zm5 7h2a1 1 0 0 1 0 2H9a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="thumbtack" xmlns="http://www.w3.org/2000/svg"><path d="M7.125 9h-2.19a.5.5 0 0 1-.417-.777L6 6V2L5.362.724A.5.5 0 0 1 5.809 0h4.382a.5.5 0 0 1 .447.724L10 2v4l1.482 2.223a.5.5 0 0 1-.416.777H8.875L8 16l-.875-7z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.34 10.479A3 3 0 0 1 12.756 15h-9.51A3 3 0 0 1 .66 10.479l4.755-8.083a3 3 0 0 1 5.172 0l4.755 8.083zm-6.478-7.07a1 1 0 0 0-1.724 0l-4.755 8.084A1 1 0 0 0 3.245 13h9.51a1 1 0 0 0 .862-1.507L8.862 3.41zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/clusters_empty.svg b/app/assets/images/illustrations/clusters_empty.svg
index c13228638be..39627a1c314 100644
--- a/app/assets/images/illustrations/clusters_empty.svg
+++ b/app/assets/images/illustrations/clusters_empty.svg
@@ -1 +1 @@
-<svg height="128" viewBox="0 0 142 128" width="142" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M94 62h20v4H94z" fill="#f0edf8"/><path d="M84.828 84l17.678 17.678-2.828 2.828L82 86.828z" fill="#fee1d3"/><path d="M42.828 24l17.678 17.678-2.828 2.828L40 26.828zM40 101.678L57.678 84l2.828 2.828-17.678 17.678z" fill="#f0edf8"/><g fill="#fee1d3"><path d="M82 41.678L99.678 24l2.828 2.828-17.678 17.678zM28 62h20v4H28z"/><rect height="30" rx="5" width="30" y="49"/></g><rect height="26" rx="5" stroke="#fdc4a8" stroke-width="4" width="26" x="2" y="51"/><rect fill="#c3b8e3" height="50" rx="10" width="50" x="46" y="39"/><rect height="46" rx="10" stroke="#6b4fbb" stroke-width="4" width="46" x="48" y="41"/><rect fill="#fef0e8" height="30" rx="5" width="30" x="84"/><rect height="26" rx="5" stroke="#fee1d3" stroke-width="4" width="26" x="86" y="2"/><rect fill="#fee1d3" height="30" rx="5" width="30" x="84" y="98"/><rect height="26" rx="5" stroke="#fdc4a8" stroke-width="4" width="26" x="86" y="100"/><rect fill="#f0edf8" height="30" rx="5" width="30" x="112" y="49"/><rect height="26" rx="5" stroke="#e1dbf1" stroke-width="4" width="26" x="114" y="51"/><rect fill="#f0edf8" height="30" rx="5" width="30" x="28" y="98"/><rect height="26" rx="5" stroke="#e1dbf1" stroke-width="4" width="26" x="30" y="100"/><rect fill="#f0edf8" height="30" rx="5" width="30" x="28"/><rect height="26" rx="5" stroke="#e1dbf1" stroke-width="4" width="26" x="30" y="2"/></g></svg> \ No newline at end of file
+<svg height="128" viewBox="0 0 142 128" width="142" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M94 62h20v4H94z" fill="#f0edf8"/><path d="M84.828 84l17.678 17.678-2.828 2.828L82 86.828z" fill="#fee1d3"/><path d="M42.828 24l17.678 17.678-2.828 2.828L40 26.828zM40 101.678L57.678 84l2.828 2.828-17.678 17.678z" fill="#f0edf8"/><path d="M82 41.678L99.678 24l2.828 2.828-17.678 17.678zM28 62h20v4H28zM3 52h24v24H3z" fill="#fee1d3"/><path d="M31 3h24v24H31z" fill="#f0edf8"/><path d="M87 3h24v24H87z" fill="#fef0e8"/><path d="M115 52h24v24h-24z" fill="#f0edf8"/><path d="M87 101h24v24H87z" fill="#fee1d3"/><path d="M31 101h24v24H31z" fill="#f0edf8"/><path d="M49 42h44v44H49z" fill="#c3b8e3"/><g fill-rule="nonzero"><path d="M5 53a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1V54a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5H5a5 5 0 0 1-5-5V54a5 5 0 0 1 5-5z" fill="#fdc4a8"/><path d="M56 43a6 6 0 0 0-6 6v30a6 6 0 0 0 6 6h30a6 6 0 0 0 6-6V49a6 6 0 0 0-6-6zm0-4h30c5.523 0 10 4.477 10 10v30c0 5.523-4.477 10-10 10H56c-5.523 0-10-4.477-10-10V49c0-5.523 4.477-10 10-10z" fill="#6b4fbb"/><path d="M89 4a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5H89a5 5 0 0 1-5-5V5a5 5 0 0 1 5-5z" fill="#fee1d3"/><path d="M89 102a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1v-20a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5H89a5 5 0 0 1-5-5v-20a5 5 0 0 1 5-5z" fill="#fdc4a8"/><path d="M117 53a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1V54a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5h-20a5 5 0 0 1-5-5V54a5 5 0 0 1 5-5zM33 102a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1v-20a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5H33a5 5 0 0 1-5-5v-20a5 5 0 0 1 5-5zM33 4a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5H33a5 5 0 0 1-5-5V5a5 5 0 0 1 5-5z" fill="#e1dbf1"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/images/illustrations/merge_request_changes_empty.svg b/app/assets/images/illustrations/merge_request_changes_empty.svg
new file mode 100644
index 00000000000..707efa736e4
--- /dev/null
+++ b/app/assets/images/illustrations/merge_request_changes_empty.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 374 268" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="d" width="230" height="176" rx="10" fill="#fff"/><rect id="e" width="14" rx="2" height="4"/><rect id="f" width="14" x="40" rx="2" height="4"/><rect id="g" width="14" x="40" y="24" rx="2" height="4"/><rect id="h" width="7" x="20" y="12" rx="2" height="4"/><rect id="i" width="7" y="24" rx="2" height="4"/><rect id="j" width="7" x="33" y="12" rx="2" height="4"/><circle id="l" cx="31" cy="31" r="31"/><circle id="c" cx="35" cy="35" r="35"/><circle id="a" cx="44" cy="44" r="44"/><circle id="b" cx="31" cy="31" r="31"/></defs><g fill="none" fill-rule="evenodd"><g transform="translate(0 94)"><circle cx="57" cy="57" r="44" fill="#f9f9f9"/><g transform="rotate(-7.999 120.507 -22.508)"><use fill="#fff" xlink:href="#a"/><circle cx="44" cy="44" r="42" stroke="#eee" stroke-width="4"/><path fill="#fee1d3" fill-rule="nonzero" d="M34.394 55.736A4 4 0 0 1 36.706 55H56a6 6 0 0 0 6-6V35a6 6 0 0 0-6-6H34a6 6 0 0 0-6 6v25.26l6.394-4.529m2.312 3.264l-7.972 5.647A3.001 3.001 0 0 1 24 62.194v-27.2c0-5.523 4.477-10 10-10h22c5.523 0 10 4.477 10 10v14c0 5.523-4.477 10-10 10H36.706"/><path fill="#fc6d26" d="M38 40a2 2 0 1 1 .001 3.999A2 2 0 0 1 38 40m7 0a2 2 0 1 1 .001 3.999A2 2 0 0 1 45 40m7 0a2 2 0 1 1 .001 3.999A2 2 0 0 1 52 40"/></g></g><g transform="translate(48)"><circle cx="41" cy="41" r="31" fill="#f9f9f9"/><g transform="rotate(-7.999 84.554 -15.551)"><use fill="#fff" xlink:href="#b"/><circle cx="31" cy="31" r="29" stroke="#eee" stroke-width="4"/><rect width="20" height="4" x="21" y="29" fill="#6b4fbb" rx="2"/></g></g><path fill="#f9f9f9" d="M235.58 229H102c-6.627 0-12-5.373-12-12V65c0-6.627 5.373-12 12-12h206c6.627 0 12 5.373 12 12v18.399A34.834 34.834 0 0 1 337 79c19.33 0 35 15.67 35 35s-15.67 35-35 35a34.831 34.831 0 0 1-17-4.399v72.4c0 6.627-5.373 12-12 12h-11.58c.381 1.941.58 3.947.58 6 0 17.12-13.879 31-31 31-17.12 0-31-13.879-31-31 0-2.053.2-4.059.58-6"/><g transform="translate(87 50)"><g transform="rotate(7.999 -44.933 1563.894)"><use fill="#fff" xlink:href="#c"/><circle cx="35" cy="35" r="33" stroke="#eee" stroke-width="4"/><g transform="translate(20 19)"><circle cx="15" cy="16" r="15" fill="#f4f1fa" stroke="#6b4fbb" stroke-width="3"/><g fill="#6b4fbb"><path d="M19.419 6.996h-.007L16.959 4l-2.454 2.997H14.5L12.046 4 9.591 6.998h-.003L7.133 4 4.677 6.999H2.001c2.605-4.204 7.231-7 12.502-7 5.269 0 9.892 2.793 12.498 6.994h-2.676l-2.452-2.994-2.453 2.996"/><circle cx="9.5" cy="17.5" r="1.5"/><circle cx="20.5" cy="17.5" r="1.5"/></g></g></g><use xlink:href="#d"/><rect width="226" height="172" x="2" y="2" stroke="#eee" stroke-width="4" rx="10"/><rect width="4" height="122" x="33" y="42" fill="#eee" rx="2"/><g transform="translate(13 59)"><rect width="10" height="4" fill="#fee1d3" rx="2"/><rect width="10" height="4" y="12" fill="#f0edf8" rx="2"/><rect width="10" height="4" y="24" fill="#fef0e9" rx="2"/><rect width="10" height="4" y="36" fill="#fee1d3" rx="2"/><rect width="10" height="4" y="48" fill="#e1dbf1" rx="2"/><rect width="10" height="4" y="60" fill="#f0edf8" rx="2"/><rect width="10" height="4" y="72" fill="#fef0e9" rx="2"/><rect width="10" height="4" y="84" fill="#fee1d3" rx="2"/></g><g transform="translate(55 59)"><use fill="#6b4fbb" xlink:href="#e"/><rect width="14" height="4" x="20" fill="#f0edf8" rx="2"/><use fill="#fef0e9" xlink:href="#f"/><rect width="14" height="4" y="12" fill="#f0edf8" rx="2"/><use fill="#fef0e9" xlink:href="#g"/><rect width="14" height="4" y="48" fill="#e1dbf1" rx="2"/><rect width="14" height="4" x="40" y="36" fill="#fef0e9" rx="2"/><use fill="#fee1d3" xlink:href="#h"/><rect width="7" height="4" x="27" y="36" fill="#6b4fbb" rx="2"/><rect width="7" height="4" x="20" y="48" fill="#fee1d3" rx="2"/><use fill="#fc6d26" xlink:href="#i"/><rect width="21" height="4" x="13" y="24" fill="#e1dbf1" rx="2"/><rect width="21" height="4" y="36" fill="#eee" rx="2"/><use fill="#6b4fbb" xlink:href="#j"/><g transform="translate(98)"><use fill="#fee1d3" xlink:href="#e"/><rect width="14" height="4" x="20" fill="#f0edf8" rx="2"/><use fill="#fc6d26" xlink:href="#f"/><rect width="14" height="4" y="12" fill="#fef0e9" rx="2" id="k"/><use fill="#e1dbf1" xlink:href="#g"/><rect width="14" height="4" y="48" fill="#f0edf8" rx="2"/><rect width="14" height="4" x="40" y="36" fill="#fee1d3" rx="2"/><use fill="#fc6d26" xlink:href="#h"/><rect width="7" height="4" x="27" y="36" fill="#6b4fbb" rx="2"/><rect width="7" height="4" x="20" y="48" fill="#fc6d26" rx="2"/><use fill="#6b4fbb" xlink:href="#i"/><rect width="21" height="4" x="13" y="24" fill="#fee1d3" rx="2"/><rect width="21" height="4" y="36" fill="#fef0e9" rx="2"/><use fill="#6b4fbb" xlink:href="#j"/></g><g transform="translate(0 60)"><use fill="#f0edf8" xlink:href="#e"/><rect width="14" height="4" x="20" fill="#6b4fbb" rx="2"/><use fill="#e1dbf1" xlink:href="#f"/><use xlink:href="#k"/><use fill="#fee1d3" xlink:href="#g"/><use fill="#eee" xlink:href="#h"/><use fill="#6b4fbb" xlink:href="#i"/><rect width="21" height="4" x="13" y="24" fill="#fef0e9" rx="2"/><use fill="#fc6d26" xlink:href="#j"/></g><rect width="4" height="63" x="74" y="13" fill="#eee" rx="2"/></g><rect width="230" height="4" y="27" fill="#eee" rx="2"/></g><g transform="rotate(7.999 -1289.786 1797.583)"><use fill="#fff" xlink:href="#l"/><circle cx="31" cy="31" r="29" stroke="#eee" stroke-width="4"/><path fill="#fc6d26" d="M29 29h-6a2 2 0 1 0 0 4h6v6a2 2 0 1 0 4 0v-6h6a2 2 0 1 0 0-4h-6v-6a2 2 0 1 0-4 0v6"/></g></g></svg> \ No newline at end of file
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index cdb5c430aa9..2cfd6179a25 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -150,8 +150,8 @@ export default class Clusters {
}
toggle() {
- this.toggleButton.classList.toggle('checked');
- this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString());
+ this.toggleButton.classList.toggle('is-checked');
+ this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('is-checked').toString());
}
showToken() {
diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js
new file mode 100644
index 00000000000..6844d1dbd83
--- /dev/null
+++ b/app/assets/javascripts/clusters/clusters_index.js
@@ -0,0 +1,58 @@
+import Flash from '../flash';
+import { s__ } from '../locale';
+import ClustersService from './services/clusters_service';
+/**
+ * Toggles loading and disabled classes.
+ * @param {HTMLElement} button
+ */
+const toggleLoadingButton = (button) => {
+ if (button.getAttribute('disabled')) {
+ button.removeAttribute('disabled');
+ } else {
+ button.setAttribute('disabled', true);
+ }
+
+ button.classList.toggle('is-loading');
+};
+
+/**
+ * Toggles checked class for the given button
+ * @param {HTMLElement} button
+ */
+const toggleValue = (button) => {
+ button.classList.toggle('is-checked');
+};
+
+/**
+ * Handles toggle buttons in the cluster's table.
+ *
+ * When the user clicks the toggle button for each cluster, it:
+ * - toggles the button
+ * - shows a loading and disables button
+ * - Makes a put request to the given endpoint
+ * Once we receive the response, either:
+ * 1) Show updated status in case of successfull response
+ * 2) Show initial status in case of failed response
+ */
+export default function setClusterTableToggles() {
+ document.querySelectorAll('.js-toggle-cluster-list')
+ .forEach(button => button.addEventListener('click', (e) => {
+ const toggleButton = e.currentTarget;
+ const endpoint = toggleButton.getAttribute('data-endpoint');
+
+ toggleValue(toggleButton);
+ toggleLoadingButton(toggleButton);
+
+ const value = toggleButton.classList.contains('is-checked');
+
+ ClustersService.updateCluster(endpoint, { cluster: { enabled: value } })
+ .then(() => {
+ toggleLoadingButton(toggleButton);
+ })
+ .catch(() => {
+ toggleLoadingButton(toggleButton);
+ toggleValue(toggleButton);
+ Flash(s__('ClusterIntegration|Something went wrong on our end.'));
+ });
+ }));
+}
diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js
index ce14c9a9945..755c2981c2e 100644
--- a/app/assets/javascripts/clusters/services/clusters_service.js
+++ b/app/assets/javascripts/clusters/services/clusters_service.js
@@ -17,4 +17,8 @@ export default class ClusterService {
installApplication(appId) {
return axios.post(this.appInstallEndpointMap[appId]);
}
+
+ static updateCluster(endpoint, data) {
+ return axios.put(endpoint, data);
+ }
}
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index e7adf8814b8..50d0cb5c86d 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -1,217 +1,208 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-use-before-define, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, quotes, one-var, one-var-declaration-per-line, no-unused-vars, no-return-assign, comma-dangle, quote-props, no-unused-expressions, no-sequences, object-shorthand, max-len */
import 'vendor/jquery.waitforimages';
-(function() {
- gl.ImageFile = (function() {
- var prepareFrames;
-
- // Width where images must fits in, for 2-up this gets divided by 2
- ImageFile.availWidth = 900;
-
- ImageFile.viewModes = ['two-up', 'swipe'];
-
- function ImageFile(file) {
- this.file = file;
- this.requestImageInfo($('.two-up.view .frame.deleted img', this.file), (function(_this) {
- return function(deletedWidth, deletedHeight) {
- return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(width, height) {
- _this.initViewModes();
-
- // Load two-up view after images are loaded
- // so that we can display the correct width and height information
- const $images = $('.two-up.view img', _this.file);
-
- $images.waitForImages(function() {
- _this.initView('two-up');
- });
+// Width where images must fits in, for 2-up this gets divided by 2
+const availWidth = 900;
+const viewModes = ['two-up', 'swipe'];
+
+export default class ImageFile {
+ constructor(file) {
+ this.file = file;
+ this.requestImageInfo($('.two-up.view .frame.deleted img', this.file), (function(_this) {
+ return function(deletedWidth, deletedHeight) {
+ return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(width, height) {
+ _this.initViewModes();
+
+ // Load two-up view after images are loaded
+ // so that we can display the correct width and height information
+ const $images = $('.two-up.view img', _this.file);
+
+ $images.waitForImages(function() {
+ _this.initView('two-up');
+ });
+ });
+ };
+ })(this));
+ }
+
+ initViewModes() {
+ const viewMode = viewModes[0];
+ $('.view-modes', this.file).removeClass('hide');
+ $('.view-modes-menu', this.file).on('click', 'li', (function(_this) {
+ return function(event) {
+ if (!$(event.currentTarget).hasClass('active')) {
+ return _this.activateViewMode(event.currentTarget.className);
+ }
+ };
+ })(this));
+ return this.activateViewMode(viewMode);
+ }
+
+ activateViewMode(viewMode) {
+ $('.view-modes-menu li', this.file).removeClass('active').filter("." + viewMode).addClass('active');
+ return $(".view:visible:not(." + viewMode + ")", this.file).fadeOut(200, (function(_this) {
+ return function() {
+ $(".view." + viewMode, _this.file).fadeIn(200);
+ return _this.initView(viewMode);
+ };
+ })(this));
+ }
+
+ initView(viewMode) {
+ return this.views[viewMode].call(this);
+ }
+ // eslint-disable-next-line class-methods-use-this
+ initDraggable($el, padding, callback) {
+ var dragging = false;
+ var $body = $('body');
+ var $offsetEl = $el.parent();
+
+ $el.off('mousedown').on('mousedown', function() {
+ dragging = true;
+ $body.css('user-select', 'none');
+ });
+
+ $body.off('mouseup').off('mousemove').on('mouseup', function() {
+ dragging = false;
+ $body.css('user-select', '');
+ })
+ .on('mousemove', function(e) {
+ var left;
+ if (!dragging) return;
+
+ left = e.pageX - ($offsetEl.offset().left + padding);
+
+ callback(e, left);
+ });
+ }
+
+ prepareFrames(view) {
+ var maxHeight, maxWidth;
+ maxWidth = 0;
+ maxHeight = 0;
+ $('.frame', view).each((function(_this) {
+ return function(index, frame) {
+ var height, width;
+ width = $(frame).width();
+ height = $(frame).height();
+ maxWidth = width > maxWidth ? width : maxWidth;
+ return maxHeight = height > maxHeight ? height : maxHeight;
+ };
+ })(this)).css({
+ width: maxWidth,
+ height: maxHeight
+ });
+ return [maxWidth, maxHeight];
+ }
+
+ views = {
+ 'two-up': function() {
+ return $('.two-up.view .wrap', this.file).each((function(_this) {
+ return function(index, wrap) {
+ $('img', wrap).each(function() {
+ var currentWidth;
+ currentWidth = $(this).width();
+ if (currentWidth > availWidth / 2) {
+ return $(this).width(availWidth / 2);
+ }
+ });
+ return _this.requestImageInfo($('img', wrap), function(width, height) {
+ $('.image-info .meta-width', wrap).text(width + "px");
+ $('.image-info .meta-height', wrap).text(height + "px");
+ return $('.image-info', wrap).removeClass('hide');
});
};
})(this));
- }
+ },
+ 'swipe': function() {
+ var maxHeight, maxWidth;
+ maxWidth = 0;
+ maxHeight = 0;
+ return $('.swipe.view', this.file).each((function(_this) {
+ return function(index, view) {
+ var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref;
+ ref = this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
+ $swipeFrame = $('.swipe-frame', view);
+ $swipeWrap = $('.swipe-wrap', view);
+ $swipeBar = $('.swipe-bar', view);
+
+ $swipeFrame.css({
+ width: maxWidth + 16,
+ height: maxHeight + 28
+ });
+ $swipeWrap.css({
+ width: maxWidth + 1,
+ height: maxHeight + 2
+ });
+ // Set swipeBar left position to match image frame
+ $swipeBar.css({
+ left: 1
+ });
- ImageFile.prototype.initViewModes = function() {
- var viewMode;
- viewMode = ImageFile.viewModes[0];
- $('.view-modes', this.file).removeClass('hide');
- $('.view-modes-menu', this.file).on('click', 'li', (function(_this) {
- return function(event) {
- if (!$(event.currentTarget).hasClass('active')) {
- return _this.activateViewMode(event.currentTarget.className);
- }
- };
- })(this));
- return this.activateViewMode(viewMode);
- };
-
- ImageFile.prototype.activateViewMode = function(viewMode) {
- $('.view-modes-menu li', this.file).removeClass('active').filter("." + viewMode).addClass('active');
- return $(".view:visible:not(." + viewMode + ")", this.file).fadeOut(200, (function(_this) {
- return function() {
- $(".view." + viewMode, _this.file).fadeIn(200);
- return _this.initView(viewMode);
+ wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10);
+
+ _this.initDraggable($swipeBar, wrapPadding, function(e, left) {
+ if (left > 0 && left < $swipeFrame.width() - (wrapPadding * 2)) {
+ $swipeWrap.width((maxWidth + 1) - left);
+ $swipeBar.css('left', left);
+ }
+ });
};
})(this));
- };
-
- ImageFile.prototype.initView = function(viewMode) {
- return this.views[viewMode].call(this);
- };
-
- ImageFile.prototype.initDraggable = function($el, padding, callback) {
- var dragging = false;
- var $body = $('body');
- var $offsetEl = $el.parent();
-
- $el.off('mousedown').on('mousedown', function() {
- dragging = true;
- $body.css('user-select', 'none');
- });
-
- $body.off('mouseup').off('mousemove').on('mouseup', function() {
- dragging = false;
- $body.css('user-select', '');
- })
- .on('mousemove', function(e) {
- var left;
- if (!dragging) return;
+ },
+ 'onion-skin': function() {
+ var dragTrackWidth, maxHeight, maxWidth;
+ maxWidth = 0;
+ maxHeight = 0;
+ dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width();
+ return $('.onion-skin.view', this.file).each((function(_this) {
+ return function(index, view) {
+ var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false;
+ ref = this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
+ $frame = $('.onion-skin-frame', view);
+ $frameAdded = $('.frame.added', view);
+ $track = $('.drag-track', view);
+ $dragger = $('.dragger', $track);
+
+ $frame.css({
+ width: maxWidth + 16,
+ height: maxHeight + 28
+ });
+ $('.swipe-wrap', view).css({
+ width: maxWidth + 1,
+ height: maxHeight + 2
+ });
+ $dragger.css({
+ left: dragTrackWidth
+ });
- left = e.pageX - ($offsetEl.offset().left + padding);
+ framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
- callback(e, left);
- });
- };
+ _this.initDraggable($dragger, framePadding, function(e, left) {
+ var opacity = left / dragTrackWidth;
- prepareFrames = function(view) {
- var maxHeight, maxWidth;
- maxWidth = 0;
- maxHeight = 0;
- $('.frame', view).each((function(_this) {
- return function(index, frame) {
- var height, width;
- width = $(frame).width();
- height = $(frame).height();
- maxWidth = width > maxWidth ? width : maxWidth;
- return maxHeight = height > maxHeight ? height : maxHeight;
+ if (opacity >= 0 && opacity <= 1) {
+ $dragger.css('left', left);
+ $frameAdded.css('opacity', opacity);
+ }
+ });
};
- })(this)).css({
- width: maxWidth,
- height: maxHeight
- });
- return [maxWidth, maxHeight];
- };
-
- ImageFile.prototype.views = {
- 'two-up': function() {
- return $('.two-up.view .wrap', this.file).each((function(_this) {
- return function(index, wrap) {
- $('img', wrap).each(function() {
- var currentWidth;
- currentWidth = $(this).width();
- if (currentWidth > ImageFile.availWidth / 2) {
- return $(this).width(ImageFile.availWidth / 2);
- }
- });
- return _this.requestImageInfo($('img', wrap), function(width, height) {
- $('.image-info .meta-width', wrap).text(width + "px");
- $('.image-info .meta-height', wrap).text(height + "px");
- return $('.image-info', wrap).removeClass('hide');
- });
- };
- })(this));
- },
- 'swipe': function() {
- var maxHeight, maxWidth;
- maxWidth = 0;
- maxHeight = 0;
- return $('.swipe.view', this.file).each((function(_this) {
- return function(index, view) {
- var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref;
- ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
- $swipeFrame = $('.swipe-frame', view);
- $swipeWrap = $('.swipe-wrap', view);
- $swipeBar = $('.swipe-bar', view);
-
- $swipeFrame.css({
- width: maxWidth + 16,
- height: maxHeight + 28
- });
- $swipeWrap.css({
- width: maxWidth + 1,
- height: maxHeight + 2
- });
- // Set swipeBar left position to match image frame
- $swipeBar.css({
- left: 1
- });
-
- wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10);
-
- _this.initDraggable($swipeBar, wrapPadding, function(e, left) {
- if (left > 0 && left < $swipeFrame.width() - (wrapPadding * 2)) {
- $swipeWrap.width((maxWidth + 1) - left);
- $swipeBar.css('left', left);
- }
- });
- };
- })(this));
- },
- 'onion-skin': function() {
- var dragTrackWidth, maxHeight, maxWidth;
- maxWidth = 0;
- maxHeight = 0;
- dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width();
- return $('.onion-skin.view', this.file).each((function(_this) {
- return function(index, view) {
- var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false;
- ref = prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1];
- $frame = $('.onion-skin-frame', view);
- $frameAdded = $('.frame.added', view);
- $track = $('.drag-track', view);
- $dragger = $('.dragger', $track);
-
- $frame.css({
- width: maxWidth + 16,
- height: maxHeight + 28
- });
- $('.swipe-wrap', view).css({
- width: maxWidth + 1,
- height: maxHeight + 2
- });
- $dragger.css({
- left: dragTrackWidth
- });
-
- framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
-
- _this.initDraggable($dragger, framePadding, function(e, left) {
- var opacity = left / dragTrackWidth;
-
- if (opacity >= 0 && opacity <= 1) {
- $dragger.css('left', left);
- $frameAdded.css('opacity', opacity);
- }
- });
+ })(this));
+ }
+ }
+
+ requestImageInfo(img, callback) {
+ const domImg = img.get(0);
+ if (domImg) {
+ if (domImg.complete) {
+ return callback.call(this, domImg.naturalWidth, domImg.naturalHeight);
+ } else {
+ return img.on('load', (function(_this) {
+ return function() {
+ return callback.call(_this, domImg.naturalWidth, domImg.naturalHeight);
};
})(this));
}
- };
-
- ImageFile.prototype.requestImageInfo = function(img, callback) {
- var domImg;
- domImg = img.get(0);
- if (domImg) {
- if (domImg.complete) {
- return callback.call(this, domImg.naturalWidth, domImg.naturalHeight);
- } else {
- return img.on('load', (function(_this) {
- return function() {
- return callback.call(_this, domImg.naturalWidth, domImg.naturalHeight);
- };
- })(this));
- }
- }
- };
-
- return ImageFile;
- })();
-}).call(window);
+ }
+ }
+}
diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue
index 3f993213dd0..f9f2f9bf693 100644
--- a/app/assets/javascripts/deploy_keys/components/action_btn.vue
+++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue
@@ -32,7 +32,9 @@
doAction() {
this.isLoading = true;
- eventHub.$emit(`${this.type}.key`, this.deployKey);
+ eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
+ this.isLoading = false;
+ });
},
},
computed: {
@@ -50,6 +52,9 @@
:disabled="isLoading"
@click="doAction">
{{ text }}
- <loading-icon v-if="isLoading" />
+ <loading-icon
+ v-if="isLoading"
+ :inline="true"
+ />
</button>
</template>
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index 54e13b79a4f..fe046449054 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -47,12 +47,15 @@
.then(() => this.fetchKeys())
.catch(() => new Flash('Error enabling deploy key'));
},
- disableKey(deployKey) {
+ disableKey(deployKey, callback) {
// eslint-disable-next-line no-alert
if (confirm('You are going to remove this deploy key. Are you sure?')) {
this.service.disableKey(deployKey.id)
.then(() => this.fetchKeys())
+ .then(callback)
.catch(() => new Flash('Error removing deploy key'));
+ } else {
+ callback();
}
},
},
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
index 0863c3406bd..e0422057090 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
@@ -16,7 +16,8 @@ import './components/diff_note_avatars';
import './components/new_issue_for_discussion';
$(() => {
- const projectPath = document.querySelector('.merge-request').dataset.projectPath;
+ const projectPathHolder = document.querySelector('.merge-request') || document.querySelector('.commit-box');
+ const projectPath = projectPathHolder.dataset.projectPath;
const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js
index 6eae54f830b..96fe23640af 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js
+++ b/app/assets/javascripts/diff_notes/services/resolve.js
@@ -43,7 +43,7 @@ class ResolveServiceClass {
discussion.resolveAllNotes(resolvedBy);
}
- gl.mrWidget.checkStatus();
+ if (gl.mrWidget) gl.mrWidget.checkStatus();
discussion.updateHeadline(data);
})
.catch(() => new Flash('An error occurred when trying to resolve a discussion. Please try again.'));
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index a21c92f24d6..678af8f7b7a 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -30,7 +30,7 @@ import projectImport from './project_import';
import Labels from './labels';
import LabelManager from './label_manager';
/* global Sidebar */
-
+import IssuableTemplateSelectors from './templates/issuable_template_selectors';
import Flash from './flash';
import CommitsList from './commits';
import Issue from './issue';
@@ -264,7 +264,7 @@ import ProjectVariables from './project_variables';
new IssuableForm($('.issue-form'));
new LabelsSelect();
new MilestoneSelect();
- new gl.IssuableTemplateSelectors();
+ new IssuableTemplateSelectors();
break;
case 'projects:merge_requests:creations:new':
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
@@ -288,7 +288,7 @@ import ProjectVariables from './project_variables';
new IssuableForm($('.merge-request-form'));
new LabelsSelect();
new MilestoneSelect();
- new gl.IssuableTemplateSelectors();
+ new IssuableTemplateSelectors();
new AutoWidthDropdownSelect($('.js-target-branch-select')).init();
break;
case 'projects:tags:new':
@@ -298,18 +298,21 @@ import ProjectVariables from './project_variables';
break;
case 'projects:snippets:show':
initNotes();
+ new ZenMode();
break;
case 'projects:snippets:new':
case 'projects:snippets:edit':
case 'projects:snippets:create':
case 'projects:snippets:update':
new GLForm($('.snippet-form'), true);
+ new ZenMode();
break;
case 'snippets:new':
case 'snippets:edit':
case 'snippets:create':
case 'snippets:update':
new GLForm($('.snippet-form'), false);
+ new ZenMode();
break;
case 'projects:releases:edit':
new ZenMode();
@@ -522,13 +525,6 @@ import ProjectVariables from './project_variables';
case 'projects:settings:ci_cd:show':
// Initialize expandable settings panels
initSettingsPanels();
-
- import(/* webpackChunkName: "ci-cd-settings" */ './projects/ci_cd_settings_bundle')
- .then(ciCdSettings => ciCdSettings.default())
- .catch((err) => {
- Flash(s__('ProjectSettings|Problem setting up the CI/CD settings JavaScript'));
- throw err;
- });
case 'groups:settings:ci_cd:show':
new ProjectVariables();
break;
@@ -546,6 +542,7 @@ import ProjectVariables from './project_variables';
new LineHighlighter();
new BlobViewer();
initNotes();
+ new ZenMode();
break;
case 'import:fogbugz:new_user_map':
new UsersSelect();
@@ -558,7 +555,15 @@ import ProjectVariables from './project_variables';
import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle')
.then(cluster => new cluster.default()) // eslint-disable-line new-cap
.catch((err) => {
- Flash(s__('ClusterIntegration|Problem setting up the cluster JavaScript'));
+ Flash(s__('ClusterIntegration|Problem setting up the cluster'));
+ throw err;
+ });
+ break;
+ case 'projects:clusters:index':
+ import(/* webpackChunkName: "clusters_index" */ './clusters/clusters_index')
+ .then(clusterIndex => clusterIndex.default())
+ .catch((err) => {
+ Flash(s__('ClusterIntegration|Problem setting up the clusters list'));
throw err;
});
break;
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index a642464c920..d918d80df8d 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -287,6 +287,10 @@ class GfmAutoComplete {
}
setupLabels($input) {
+ const fetchData = this.fetchData.bind(this);
+ const LABEL_COMMAND = { LABEL: '/label', UNLABEL: '/unlabel', RELABEL: '/relabel' };
+ let command = '';
+
$input.atwho({
at: '~',
alias: 'labels',
@@ -309,8 +313,45 @@ class GfmAutoComplete {
title: sanitize(m.title),
color: m.color,
search: m.title,
+ set: m.set,
}));
},
+ matcher(flag, subtext) {
+ const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
+ const subtextNodes = subtext.split(/\n+/g).pop().split(GfmAutoComplete.regexSubtext);
+
+ // Check if ~ is followed by '/label', '/relabel' or '/unlabel' commands.
+ command = subtextNodes.find((node) => {
+ if (node === LABEL_COMMAND.LABEL ||
+ node === LABEL_COMMAND.RELABEL ||
+ node === LABEL_COMMAND.UNLABEL) { return node; }
+ return null;
+ });
+
+ return match && match.length ? match[1] : null;
+ },
+ filter(query, data, searchKey) {
+ if (GfmAutoComplete.isLoading(data)) {
+ fetchData(this.$inputor, this.at);
+ return data;
+ }
+
+ if (data === GfmAutoComplete.defaultLoadingData) {
+ return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
+ }
+
+ // The `LABEL_COMMAND.RELABEL` is intentionally skipped
+ // because we want to return all the labels (unfiltered) for that command.
+ if (command === LABEL_COMMAND.LABEL) {
+ // Return labels with set: undefined.
+ return data.filter(label => !label.set);
+ } else if (command === LABEL_COMMAND.UNLABEL) {
+ // Return labels with set: true.
+ return data.filter(label => label.set);
+ }
+
+ return data;
+ },
},
});
}
@@ -346,20 +387,7 @@ class GfmAutoComplete {
return resultantValue;
},
matcher(flag, subtext) {
- // The below is taken from At.js source
- // Tweaked to commands to start without a space only if char before is a non-word character
- // https://github.com/ichord/At.js
- const atSymbolsWithBar = Object.keys(this.app.controllers).join('|');
- const atSymbolsWithoutBar = Object.keys(this.app.controllers).join('');
- const targetSubtext = subtext.split(/\s+/g).pop();
- const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
-
- const accentAChar = decodeURI('%C3%80');
- const accentYChar = decodeURI('%C3%BF');
-
- const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi');
-
- const match = regexp.exec(targetSubtext);
+ const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
if (match) {
return match[1];
@@ -420,8 +448,27 @@ class GfmAutoComplete {
return dataToInspect &&
(dataToInspect === loadingState || dataToInspect.name === loadingState);
}
+
+ static defaultMatcher(flag, subtext, controllers) {
+ // The below is taken from At.js source
+ // Tweaked to commands to start without a space only if char before is a non-word character
+ // https://github.com/ichord/At.js
+ const atSymbolsWithBar = Object.keys(controllers).join('|');
+ const atSymbolsWithoutBar = Object.keys(controllers).join('');
+ const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop();
+ const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
+
+ const accentAChar = decodeURI('%C3%80');
+ const accentYChar = decodeURI('%C3%BF');
+
+ const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi');
+
+ return regexp.exec(targetSubtext);
+ }
}
+GfmAutoComplete.regexSubtext = new RegExp(/\s+/g);
+
GfmAutoComplete.defaultLoadingData = ['loading'];
GfmAutoComplete.atTypeMap = {
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index 0cd0c59a275..c76ce762b54 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -123,18 +123,23 @@ export default {
:title="group.fullName"
class="no-expand"
data-placement="top"
- >
- {{group.name}}
- </a>
+ >{{
+ // ending bracket must be by closing tag to prevent
+ // link hover text-decoration from over-extending
+ group.name
+ }}</a>
<span
v-if="group.permission"
- class="access-type"
+ class="user-access-role"
>
- {{s__('GroupsTreeRole|as')}} {{group.permission}}
+ {{group.permission}}
</span>
</div>
<div
- class="description">{{group.description}}</div>
+ v-if="group.description"
+ class="description">
+ {{group.description}}
+ </div>
</div>
<group-folder
v-if="group.isOpen && hasChildren"
diff --git a/app/assets/javascripts/image_diff/helpers/utils_helper.js b/app/assets/javascripts/image_diff/helpers/utils_helper.js
index 96fc735e629..28d9a969143 100644
--- a/app/assets/javascripts/image_diff/helpers/utils_helper.js
+++ b/app/assets/javascripts/image_diff/helpers/utils_helper.js
@@ -1,7 +1,7 @@
import ImageBadge from '../image_badge';
import ImageDiff from '../image_diff';
import ReplacedImageDiff from '../replaced_image_diff';
-import '../../commit/image_file';
+import ImageFile from '../../commit/image_file';
export function resizeCoordinatesToImageElement(imageEl, meta) {
const { x, y, width, height } = meta;
@@ -81,7 +81,7 @@ export function initImageDiff(fileEl, canCreateNote, renderCommentBadge) {
// ImageFile needs to be invoked before initImageDiff so that badges
// can mount to the correct location
- new gl.ImageFile(fileEl); // eslint-disable-line no-new
+ new ImageFile(fileEl); // eslint-disable-line no-new
if (fileEl.querySelector('.diff-file .js-single-image')) {
diff = new ImageDiff(fileEl, options);
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 7de07e9403d..91b5ef1c10a 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -8,18 +8,7 @@ import IssuablesHelper from './helpers/issuables_helper';
export default class Issue {
constructor() {
- if ($('a.btn-close').length) {
- this.taskList = new TaskList({
- dataType: 'issue',
- fieldName: 'description',
- selector: '.detail-page-description',
- onSuccess: (result) => {
- document.querySelector('#task_status').innerText = result.task_status;
- document.querySelector('#task_status_short').innerText = result.task_status_short;
- }
- });
- this.initIssueBtnEventListeners();
- }
+ if ($('a.btn-close').length) this.initIssueBtnEventListeners();
Issue.$btnNewBranch = $('#new-branch');
Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap');
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index 5bdc7c99503..c7ce16bb623 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -9,6 +9,7 @@ import descriptionComponent from './description.vue';
import editedComponent from './edited.vue';
import formComponent from './form.vue';
import '../../lib/utils/url_utility';
+import RecaptchaDialogImplementor from '../../vue_shared/mixins/recaptcha_dialog_implementor';
export default {
props: {
@@ -149,6 +150,11 @@ export default {
editedComponent,
formComponent,
},
+
+ mixins: [
+ RecaptchaDialogImplementor,
+ ],
+
methods: {
openForm() {
if (!this.showForm) {
@@ -164,9 +170,11 @@ export default {
closeForm() {
this.showForm = false;
},
+
updateIssuable() {
this.service.updateIssuable(this.store.formState)
.then(res => res.json())
+ .then(data => this.checkForSpam(data))
.then((data) => {
if (location.pathname !== data.web_url) {
gl.utils.visitUrl(data.web_url);
@@ -179,11 +187,24 @@ export default {
this.store.updateState(data);
eventHub.$emit('close.form');
})
- .catch(() => {
- eventHub.$emit('close.form');
- window.Flash(`Error updating ${this.issuableType}`);
+ .catch((error) => {
+ if (error && error.name === 'SpamError') {
+ this.openRecaptcha();
+ } else {
+ eventHub.$emit('close.form');
+ window.Flash(`Error updating ${this.issuableType}`);
+ }
});
},
+
+ closeRecaptchaDialog() {
+ this.store.setFormState({
+ updateLoading: false,
+ });
+
+ this.closeRecaptcha();
+ },
+
deleteIssuable() {
this.service.deleteIssuable()
.then(res => res.json())
@@ -237,9 +258,9 @@ export default {
</script>
<template>
- <div>
+<div>
+ <div v-if="canUpdate && showForm">
<form-component
- v-if="canUpdate && showForm"
:form-state="formState"
:can-destroy="canDestroy"
:issuable-templates="issuableTemplates"
@@ -251,30 +272,37 @@ export default {
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
/>
- <div v-else>
- <title-component
- :issuable-ref="issuableRef"
- :can-update="canUpdate"
- :title-html="state.titleHtml"
- :title-text="state.titleText"
- :show-inline-edit-button="showInlineEditButton"
- />
- <description-component
- v-if="state.descriptionHtml"
- :can-update="canUpdate"
- :description-html="state.descriptionHtml"
- :description-text="state.descriptionText"
- :updated-at="state.updatedAt"
- :task-status="state.taskStatus"
- :issuable-type="issuableType"
- :update-url="updateEndpoint"
- />
- <edited-component
- v-if="hasUpdated"
- :updated-at="state.updatedAt"
- :updated-by-name="state.updatedByName"
- :updated-by-path="state.updatedByPath"
- />
- </div>
+
+ <recaptcha-dialog
+ v-show="showRecaptcha"
+ :html="recaptchaHTML"
+ @close="closeRecaptchaDialog"
+ />
+ </div>
+ <div v-else>
+ <title-component
+ :issuable-ref="issuableRef"
+ :can-update="canUpdate"
+ :title-html="state.titleHtml"
+ :title-text="state.titleText"
+ :show-inline-edit-button="showInlineEditButton"
+ />
+ <description-component
+ v-if="state.descriptionHtml"
+ :can-update="canUpdate"
+ :description-html="state.descriptionHtml"
+ :description-text="state.descriptionText"
+ :updated-at="state.updatedAt"
+ :task-status="state.taskStatus"
+ :issuable-type="issuableType"
+ :update-url="updateEndpoint"
+ />
+ <edited-component
+ v-if="hasUpdated"
+ :updated-at="state.updatedAt"
+ :updated-by-name="state.updatedByName"
+ :updated-by-path="state.updatedByPath"
+ />
</div>
+</div>
</template>
diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue
index b7559ced946..feb73481422 100644
--- a/app/assets/javascripts/issue_show/components/description.vue
+++ b/app/assets/javascripts/issue_show/components/description.vue
@@ -1,9 +1,14 @@
<script>
import animateMixin from '../mixins/animate';
import TaskList from '../../task_list';
+ import RecaptchaDialogImplementor from '../../vue_shared/mixins/recaptcha_dialog_implementor';
export default {
- mixins: [animateMixin],
+ mixins: [
+ animateMixin,
+ RecaptchaDialogImplementor,
+ ],
+
props: {
canUpdate: {
type: Boolean,
@@ -51,6 +56,7 @@
this.updateTaskStatusText();
},
},
+
methods: {
renderGFM() {
$(this.$refs['gfm-content']).renderGFM();
@@ -61,9 +67,19 @@
dataType: this.issuableType,
fieldName: 'description',
selector: '.detail-page-description',
+ onSuccess: this.taskListUpdateSuccess.bind(this),
});
}
},
+
+ taskListUpdateSuccess(data) {
+ try {
+ this.checkForSpam(data);
+ } catch (error) {
+ if (error && error.name === 'SpamError') this.openRecaptcha();
+ }
+ },
+
updateTaskStatusText() {
const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/);
const $issuableHeader = $('.issuable-meta');
@@ -109,5 +125,11 @@
:data-update-url="updateUrl"
>
</textarea>
+
+ <recaptcha-dialog
+ v-show="showRecaptcha"
+ :html="recaptchaHTML"
+ @close="closeRecaptcha"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/issue_show/components/fields/description_template.vue b/app/assets/javascripts/issue_show/components/fields/description_template.vue
index 1c40b286513..1ad0e59287e 100644
--- a/app/assets/javascripts/issue_show/components/fields/description_template.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue
@@ -1,4 +1,6 @@
<script>
+ import IssuableTemplateSelectors from '../../../templates/issuable_template_selectors';
+
export default {
props: {
formState: {
@@ -32,7 +34,7 @@
};
editor.getValue = () => this.formState.description;
- this.issuableTemplate = new gl.IssuableTemplateSelectors({
+ this.issuableTemplate = new IssuableTemplateSelectors({
$dropdowns: $(this.$refs.toggle),
editor,
});
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
index aca9dec2a96..a21ce41e65e 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -5,7 +5,7 @@ import '../vue_shared/vue_resource_interceptor';
document.addEventListener('DOMContentLoaded', () => {
const initialDataEl = document.getElementById('js-issuable-app-initial-data');
- const initialData = JSON.parse(initialDataEl.innerHTML.replace(/&quot;/g, '"'));
+ const props = JSON.parse(initialDataEl.innerHTML.replace(/&quot;/g, '"'));
$('.issuable-edit').on('click', (e) => {
e.preventDefault();
@@ -18,32 +18,9 @@ document.addEventListener('DOMContentLoaded', () => {
components: {
issuableApp,
},
- data() {
- return {
- ...initialData,
- };
- },
render(createElement) {
return createElement('issuable-app', {
- props: {
- canUpdate: this.canUpdate,
- canDestroy: this.canDestroy,
- endpoint: this.endpoint,
- issuableRef: this.issuableRef,
- initialTitleHtml: this.initialTitleHtml,
- initialTitleText: this.initialTitleText,
- initialDescriptionHtml: this.initialDescriptionHtml,
- initialDescriptionText: this.initialDescriptionText,
- issuableTemplates: this.issuableTemplates,
- markdownPreviewPath: this.markdownPreviewPath,
- markdownDocsPath: this.markdownDocsPath,
- projectPath: this.projectPath,
- projectNamespace: this.projectNamespace,
- updatedAt: this.updatedAt,
- updatedByName: this.updatedByName,
- updatedByPath: this.updatedByPath,
- initialTaskStatus: this.initialTaskStatus,
- },
+ props,
});
},
});
diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js
index cf8fda9a4fa..85ea6330ee9 100644
--- a/app/assets/javascripts/job.js
+++ b/app/assets/javascripts/job.js
@@ -9,7 +9,7 @@ export default class Job {
this.state = null;
this.options = options || $('.js-build-options').data();
- this.pageUrl = this.options.pageUrl;
+ this.pagePath = this.options.pagePath;
this.buildStatus = this.options.buildStatus;
this.state = this.options.logState;
this.buildStage = this.options.buildStage;
@@ -167,11 +167,11 @@ export default class Job {
getBuildTrace() {
return $.ajax({
- url: `${this.pageUrl}/trace.json`,
+ url: `${this.pagePath}/trace.json`,
data: { state: this.state },
})
.done((log) => {
- setCiStatusFavicon(`${this.pageUrl}/status.json`);
+ setCiStatusFavicon(`${this.pagePath}/status.json`);
if (log.state) {
this.state = log.state;
@@ -209,7 +209,7 @@ export default class Job {
}
if (log.status !== this.buildStatus) {
- gl.utils.visitUrl(this.pageUrl);
+ gl.utils.visitUrl(this.pagePath);
}
})
.fail(() => {
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 426a81a976d..d0578b230b1 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -35,8 +35,6 @@ window.dateFormat = dateFormat;
w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago = true) {
$timeagoEls.each((i, el) => {
- el.setAttribute('title', el.getAttribute('title'));
-
if (setTimeago) {
// Recreate with custom template
$(el).tooltip({
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index d30ff12bb59..a9c08df4f93 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -129,7 +129,7 @@ import { addDelimiter } from './lib/utils/text_utility';
};
MergeRequest.prototype.hideCloseButton = function() {
- const el = document.querySelector('.merge-request .issuable-actions');
+ const el = document.querySelector('.merge-request .js-issuable-actions');
const closeDropdownItem = el.querySelector('li.close-item');
if (closeDropdownItem) {
closeDropdownItem.classList.add('hidden');
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index cbe24c0915b..8da723ced03 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -21,6 +21,8 @@
hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics),
documentationPath: metricsData.documentationPath,
settingsPath: metricsData.settingsPath,
+ tagsPath: metricsData.tagsPath,
+ projectPath: metricsData.projectPath,
metricsEndpoint: metricsData.additionalMetrics,
deploymentEndpoint: metricsData.deploymentEndpoint,
emptyGettingStartedSvgPath: metricsData.emptyGettingStartedSvgPath,
@@ -112,6 +114,8 @@
:hover-data="hoverData"
:update-aspect-ratio="updateAspectRatio"
:deployment-data="store.deploymentData"
+ :project-path="projectPath"
+ :tags-path="tagsPath"
/>
</graph-group>
</div>
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index f8782fde927..cdae287658b 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -30,6 +30,14 @@
required: false,
default: () => ({}),
},
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ tagsPath: {
+ type: String,
+ required: true,
+ },
},
mixins: [MonitoringMixin],
@@ -251,6 +259,14 @@
:line-color="path.lineColor"
:area-color="path.areaColor"
/>
+ <rect
+ class="prometheus-graph-overlay"
+ :width="(graphWidth - 70)"
+ :height="(graphHeight - 100)"
+ transform="translate(-5, 20)"
+ ref="graphOverlay"
+ @mousemove="handleMouseOverGraph($event)">
+ </rect>
<graph-deployment
:show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData"
@@ -267,14 +283,6 @@
:graph-height-offset="graphHeightOffset"
:show-flag-content="showFlagContent"
/>
- <rect
- class="prometheus-graph-overlay"
- :width="(graphWidth - 70)"
- :height="(graphHeight - 100)"
- transform="translate(-5, 20)"
- ref="graphOverlay"
- @mousemove="handleMouseOverGraph($event)">
- </rect>
</svg>
</svg>
</div>
diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue
index e3b8be0c7fb..026e2fd0c49 100644
--- a/app/assets/javascripts/monitoring/components/graph/deployment.vue
+++ b/app/assets/javascripts/monitoring/components/graph/deployment.vue
@@ -1,5 +1,6 @@
<script>
- import { dateFormat, timeFormat } from '../../utils/date_time_formatters';
+ import { dateFormatWithName, timeFormat } from '../../utils/date_time_formatters';
+ import Icon from '../../../vue_shared/components/icon.vue';
export default {
props: {
@@ -25,6 +26,10 @@
},
},
+ components: {
+ Icon,
+ },
+
computed: {
calculatedHeight() {
return this.graphHeight - this.graphHeightOffset;
@@ -33,7 +38,7 @@
methods: {
refText(d) {
- return d.tag ? d.ref : d.sha.slice(0, 6);
+ return d.tag ? d.ref : d.sha.slice(0, 8);
},
formatTime(deploymentTime) {
@@ -41,7 +46,7 @@
},
formatDate(deploymentTime) {
- return dateFormat(deploymentTime);
+ return dateFormatWithName(deploymentTime);
},
nameDeploymentClass(deployment) {
@@ -54,11 +59,19 @@
positionFlag(deployment) {
let xPosition = 3;
- if (deployment.xPos > (this.graphWidth - 200)) {
- xPosition = -97;
+ if (deployment.xPos > (this.graphWidth - 225)) {
+ xPosition = -142;
}
return xPosition;
},
+
+ svgContainerHeight(tag) {
+ let svgHeight = 80;
+ if (!tag) {
+ svgHeight -= 20;
+ }
+ return svgHeight;
+ },
},
};
</script>
@@ -91,35 +104,75 @@
class="js-deploy-info-box"
:x="positionFlag(deployment)"
y="0"
- width="92"
- height="60">
+ width="134"
+ :height="svgContainerHeight(deployment.tag)">
<rect
class="rect-text-metric deploy-info-rect rect-metric"
x="1"
y="1"
rx="2"
- width="90"
- height="58">
+ width="132"
+ :height="svgContainerHeight(deployment.tag) - 2">
</rect>
- <g
- transform="translate(5, 2)">
- <text
- class="deploy-info-text text-metric-bold">
- {{refText(deployment)}}
- </text>
- </g>
- <text
- class="deploy-info-text"
- y="18"
- transform="translate(5, 2)">
- {{formatDate(deployment.time)}}
- </text>
<text
class="deploy-info-text text-metric-bold"
- y="38"
transform="translate(5, 2)">
- {{formatTime(deployment.time)}}
+ Deployed
</text>
+ <!--The date info-->
+ <g transform="translate(5, 20)">
+ <text class="deploy-info-text">
+ {{formatDate(deployment.time)}}
+ </text>
+ <text
+ class="deploy-info-text text-metric-bold"
+ x="62">
+ {{formatTime(deployment.time)}}
+ </text>
+ </g>
+ <line
+ class="divider-line"
+ x1="0"
+ y1="38"
+ x2="132"
+ :y2="38"
+ stroke="#000">
+ </line>
+ <!--Commit information-->
+ <g transform="translate(5, 40)">
+ <icon
+ name="commit"
+ :width="12"
+ :height="12"
+ :y="3">
+ </icon>
+ <a :xlink:href="deployment.commitUrl">
+ <text
+ class="deploy-info-text deploy-info-text-link"
+ transform="translate(20, 2)">
+ {{refText(deployment)}}
+ </text>
+ </a>
+ </g>
+ <!--Tag information-->
+ <g
+ transform="translate(5, 55)"
+ v-if="deployment.tag">
+ <icon
+ name="label"
+ :width="12"
+ :height="12"
+ :y="5">
+ </icon>
+ <a :xlink:href="deployment.tagUrl">
+ <text
+ class="deploy-info-text deploy-info-text-link"
+ transform="translate(20, 2)"
+ y="2">
+ {{deployment.tag}}
+ </text>
+ </a>
+ </g>
</svg>
</g>
<svg
diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
index 31f38aca5d6..cbca14ede02 100644
--- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
+++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
@@ -33,7 +33,9 @@ const mixins = {
id: deployment.id,
time,
sha: deployment.sha,
+ commitUrl: `${this.projectPath}/commit/${deployment.sha}`,
tag: deployment.tag,
+ tagUrl: `${this.tagsPath}/${deployment.tag}`,
ref: deployment.ref.name,
xPos,
showDeploymentFlag: false,
diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
index c4c6b1ac1f5..ad07a8465e2 100644
--- a/app/assets/javascripts/monitoring/utils/date_time_formatters.js
+++ b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
@@ -1,6 +1,7 @@
import d3 from 'd3';
export const dateFormat = d3.time.format('%b %-d, %Y');
+export const dateFormatWithName = d3.time.format('%a, %b %-d');
export const timeFormat = d3.time.format('%-I:%M%p');
export const bisectDate = d3.bisector(d => d.time).left;
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 45fc6196be4..7fb45ed4d4b 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -86,7 +86,7 @@
<div class="note-actions">
<span
v-if="accessLevel"
- class="note-role note-role-access">{{accessLevel}}</span>
+ class="note-role user-access-role">{{accessLevel}}</span>
<div
v-if="canAddAwardEmoji"
class="note-actions-item">
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index 8d74c5de5cf..a94163a5f87 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -8,10 +8,18 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
},
data() {
const notesDataset = document.getElementById('js-vue-notes').dataset;
+ const parsedUserData = JSON.parse(notesDataset.currentUserData);
+ const currentUserData = parsedUserData ? {
+ id: parsedUserData.id,
+ name: parsedUserData.name,
+ username: parsedUserData.username,
+ avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
+ path: parsedUserData.path,
+ } : {};
return {
noteableData: JSON.parse(notesDataset.noteableData),
- currentUserData: JSON.parse(notesDataset.currentUserData),
+ currentUserData,
notesData: {
lastFetchedAt: notesDataset.lastFetchedAt,
discussionsPath: notesDataset.discussionsPath,
diff --git a/app/assets/javascripts/projects/ci_cd_settings_bundle.js b/app/assets/javascripts/projects/ci_cd_settings_bundle.js
deleted file mode 100644
index 90e418f6771..00000000000
--- a/app/assets/javascripts/projects/ci_cd_settings_bundle.js
+++ /dev/null
@@ -1,19 +0,0 @@
-function updateAutoDevopsRadios(radioWrappers) {
- radioWrappers.forEach((radioWrapper) => {
- const radio = radioWrapper.querySelector('.js-auto-devops-enable-radio');
- const runPipelineCheckboxWrapper = radioWrapper.querySelector('.js-run-auto-devops-pipeline-checkbox-wrapper');
- const runPipelineCheckbox = radioWrapper.querySelector('.js-run-auto-devops-pipeline-checkbox');
-
- if (runPipelineCheckbox) {
- runPipelineCheckbox.checked = radio.checked;
- runPipelineCheckboxWrapper.classList.toggle('hide', !radio.checked);
- }
- });
-}
-
-export default function initCiCdSettings() {
- const radioWrappers = document.querySelectorAll('.js-auto-devops-enable-radio-wrapper');
- radioWrappers.forEach(radioWrapper =>
- radioWrapper.addEventListener('change', () => updateAutoDevopsRadios(radioWrappers)),
- );
-}
diff --git a/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue b/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue
index 80c5d39f736..8fce4c63872 100644
--- a/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue
+++ b/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue
@@ -1,5 +1,5 @@
<script>
-import projectFeatureToggle from './project_feature_toggle.vue';
+import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue';
export default {
props: {
diff --git a/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue b/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue
deleted file mode 100644
index 2403c60186a..00000000000
--- a/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<script>
-export default {
- props: {
- name: {
- type: String,
- required: false,
- default: '',
- },
- value: {
- type: Boolean,
- required: true,
- },
- disabledInput: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
-
- model: {
- prop: 'value',
- event: 'change',
- },
-
- methods: {
- toggleFeature() {
- if (!this.disabledInput) this.$emit('change', !this.value);
- },
- },
-};
-</script>
-
-<template>
- <label class="toggle-wrapper">
- <input
- v-if="name"
- type="hidden"
- :name="name"
- :value="value"
- />
- <button
- type="button"
- aria-label="Toggle"
- class="project-feature-toggle"
- data-enabled-text="Enabled"
- data-disabled-text="Disabled"
- :class="{ checked: value, disabled: disabledInput }"
- @click="toggleFeature"
- />
- </label>
-</template>
diff --git a/app/assets/javascripts/projects/permissions/components/settings_panel.vue b/app/assets/javascripts/projects/permissions/components/settings_panel.vue
index 326d9105666..639429baf26 100644
--- a/app/assets/javascripts/projects/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/projects/permissions/components/settings_panel.vue
@@ -1,6 +1,6 @@
<script>
import projectFeatureSetting from './project_feature_setting.vue';
-import projectFeatureToggle from './project_feature_toggle.vue';
+import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue';
import projectSettingRow from './project_setting_row.vue';
import { visibilityOptions, visibilityLevelDescriptions } from '../constants';
import { toggleHiddenClassBySelector } from '../external';
diff --git a/app/assets/javascripts/repo/components/new_dropdown/index.vue b/app/assets/javascripts/repo/components/new_dropdown/index.vue
index a5ee4f71281..781404cf8ca 100644
--- a/app/assets/javascripts/repo/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/repo/components/new_dropdown/index.vue
@@ -2,9 +2,11 @@
import { mapState } from 'vuex';
import newModal from './modal.vue';
import upload from './upload.vue';
+ import icon from '../../../vue_shared/components/icon.vue';
export default {
components: {
+ icon,
newModal,
upload,
},
@@ -41,11 +43,14 @@
data-toggle="dropdown"
aria-label="Create new file or directory"
>
- <i
- class="fa fa-plus"
- aria-hidden="true"
- >
- </i>
+ <icon
+ name="plus"
+ css-classes="pull-left"
+ />
+ <icon
+ name="arrow-down"
+ css-classes="pull-left"
+ />
</button>
<ul class="dropdown-menu">
<li>
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 9dec5d7645a..e40a3596200 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -1,13 +1,20 @@
-/* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */
+/* eslint-disable no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */
import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from './lib/utils/common_utils';
+/**
+ * Search input in top navigation bar.
+ * On click, opens a dropdown
+ * As the user types it filters the results
+ * When the user clicks `x` button it cleans the input and closes the dropdown.
+ */
+
((global) => {
const KEYCODE = {
ESCAPE: 27,
BACKSPACE: 8,
ENTER: 13,
UP: 38,
- DOWN: 40
+ DOWN: 40,
};
class SearchAutocomplete {
@@ -19,6 +26,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
this.projectId = projectId || (this.optsEl.data('autocomplete-project-id') || '');
this.projectRef = projectRef || (this.optsEl.data('autocomplete-project-ref') || '');
this.dropdown = this.wrap.find('.dropdown');
+ this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle');
this.dropdownContent = this.dropdown.find('.dropdown-content');
this.locationBadgeEl = this.getElement('.location-badge');
this.scopeInputEl = this.getElement('#scope');
@@ -29,13 +37,16 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
this.repositoryInputEl = this.getElement('#repository_ref');
this.clearInput = this.getElement('.js-clear-input');
this.saveOriginalState();
+
// Only when user is logged in
if (gon.current_user_id) {
this.createAutocomplete();
}
+
this.searchInput.addClass('disabled');
this.saveTextLength();
this.bindEvents();
+ this.dropdownToggle.dropdown();
}
// Finds an element inside wrapper element
@@ -43,7 +54,6 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
this.onSearchInputBlur = this.onSearchInputBlur.bind(this);
this.onClearInputClick = this.onClearInputClick.bind(this);
this.onSearchInputFocus = this.onSearchInputFocus.bind(this);
- this.onSearchInputClick = this.onSearchInputClick.bind(this);
this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this);
this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this);
}
@@ -68,12 +78,12 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
enterCallback: false,
filterInput: 'input#search',
search: {
- fields: ['text']
+ fields: ['text'],
},
id: this.getSearchText,
data: this.getData.bind(this),
selectable: true,
- clicked: this.onClick.bind(this)
+ clicked: this.onClick.bind(this),
});
}
@@ -82,32 +92,35 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
}
getData(term, callback) {
- var _this, contents, jqXHR;
- _this = this;
if (!term) {
- if (contents = this.getCategoryContents()) {
+ const contents = this.getCategoryContents();
+ if (contents) {
this.searchInput.data('glDropdown').filter.options.callback(contents);
this.enableAutocomplete();
}
return;
}
+
// Prevent multiple ajax calls
if (this.loadingSuggestions) {
return;
}
+
this.loadingSuggestions = true;
- return jqXHR = $.get(this.autocompletePath, {
+
+ return $.get(this.autocompletePath, {
project_id: this.projectId,
project_ref: this.projectRef,
- term: term
- }, function(response) {
- var data, firstCategory, i, lastCategory, len, suggestion;
+ term: term,
+ }, (response) => {
+ var firstCategory, i, lastCategory, len, suggestion;
// Hide dropdown menu if no suggestions returns
if (!response.length) {
- _this.disableAutocomplete();
+ this.disableAutocomplete();
return;
}
- data = [];
+
+ const data = [];
// List results
firstCategory = true;
for (i = 0, len = response.length; i < len; i += 1) {
@@ -121,7 +134,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
firstCategory = false;
}
data.push({
- header: suggestion.category
+ header: suggestion.category,
});
lastCategory = suggestion.category;
}
@@ -129,7 +142,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
id: (suggestion.category.toLowerCase()) + "-" + suggestion.id,
category: suggestion.category,
text: suggestion.label,
- url: suggestion.url
+ url: suggestion.url,
});
}
// Add option to proceed with the search
@@ -137,20 +150,21 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
data.push('separator');
data.push({
text: "Result name contains \"" + term + "\"",
- url: "/search?search=" + term + "&project_id=" + (_this.projectInputEl.val()) + "&group_id=" + (_this.groupInputEl.val())
+ url: "/search?search=" + term + "&project_id=" + (this.projectInputEl.val()) + "&group_id=" + (this.groupInputEl.val()),
});
}
return callback(data);
- }).always(function() {
- return _this.loadingSuggestions = false;
- });
+ })
+ .always(() => { this.loadingSuggestions = false; });
}
getCategoryContents() {
- var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName;
- userId = gon.current_user_id;
- userName = gon.current_username;
- projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions;
+ const userId = gon.current_user_id;
+ const userName = gon.current_username;
+ const { projectOptions, groupOptions, dashboardOptions } = gl;
+
+ // Get options
+ let options;
if (isInGroupsPage() && groupOptions) {
options = groupOptions[getGroupSlug()];
} else if (isInProjectPage() && projectOptions) {
@@ -158,37 +172,42 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
} else if (dashboardOptions) {
options = dashboardOptions;
}
- issuesPath = options.issuesPath, mrPath = options.mrPath, name = options.name;
- items = [
- {
- header: "" + name
- }
- ];
+
+ const { issuesPath, mrPath, name, issuesDisabled } = options;
+ const baseItems = [];
+
+ if (name) {
+ baseItems.push({
+ header: `${name}`,
+ });
+ }
+
const issueItems = [
{
text: 'Issues assigned to me',
- url: issuesPath + "/?assignee_username=" + userName
- }, {
+ url: `${issuesPath}/?assignee_username=${userName}`,
+ },
+ {
text: "Issues I've created",
- url: issuesPath + "/?author_username=" + userName
- }
+ url: `${issuesPath}/?author_username=${userName}`,
+ },
];
const mergeRequestItems = [
{
text: 'Merge requests assigned to me',
- url: mrPath + "/?assignee_username=" + userName
- }, {
+ url: `${mrPath}/?assignee_username=${userName}`,
+ },
+ {
text: "Merge requests I've created",
- url: mrPath + "/?author_username=" + userName
- }
+ url: `${mrPath}/?author_username=${userName}`,
+ },
];
- if (options.issuesDisabled) {
- items = items.concat(mergeRequestItems);
+
+ let items;
+ if (issuesDisabled) {
+ items = baseItems.concat(mergeRequestItems);
} else {
- items = items.concat(...issueItems, 'separator', ...mergeRequestItems);
- }
- if (!name) {
- items.splice(0, 1);
+ items = baseItems.concat(...issueItems, 'separator', ...mergeRequestItems);
}
return items;
}
@@ -202,39 +221,34 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
repository_ref: this.repositoryInputEl.val(),
scope: this.scopeInputEl.val(),
// Location badge
- _location: this.locationBadgeEl.text()
+ _location: this.locationBadgeEl.text(),
};
}
bindEvents() {
this.searchInput.on('keydown', this.onSearchInputKeyDown);
this.searchInput.on('keyup', this.onSearchInputKeyUp);
- this.searchInput.on('click', this.onSearchInputClick);
this.searchInput.on('focus', this.onSearchInputFocus);
this.searchInput.on('blur', this.onSearchInputBlur);
this.clearInput.on('click', this.onClearInputClick);
- return this.locationBadgeEl.on('click', (function(_this) {
- return function() {
- return _this.searchInput.focus();
- };
- })(this));
+ this.locationBadgeEl.on('click', () => this.searchInput.focus());
}
enableAutocomplete() {
- var _this;
// No need to enable anything if user is not logged in
if (!gon.current_user_id) {
return;
}
+
+ // If the dropdown is closed, we'll open it
if (!this.dropdown.hasClass('open')) {
- _this = this;
this.loadingSuggestions = false;
- this.dropdown.addClass('open').trigger('shown.bs.dropdown');
+ this.dropdownToggle.dropdown('toggle');
return this.searchInput.removeClass('disabled');
}
}
- // Saves last length of the entered text
+ // Saves last length of the entered text
onSearchInputKeyDown() {
return this.saveTextLength();
}
@@ -279,11 +293,6 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
this.wrap.toggleClass('has-value', !!e.target.value);
}
- // Avoid falsy value to be returned
- onSearchInputClick(e) {
- return e.stopImmediatePropagation();
- }
-
onSearchInputFocus() {
this.isFocused = true;
this.wrap.addClass('search-active');
@@ -335,7 +344,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
return this.locationBadgeEl.hide();
} else {
return this.addLocationBadge({
- value: this.originalState._location
+ value: this.originalState._location,
});
}
}
@@ -387,13 +396,13 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
if (item.category === 'Projects') {
this.projectInputEl.val(item.id);
this.addLocationBadge({
- value: 'This project'
+ value: 'This project',
});
}
if (item.category === 'Groups') {
this.groupInputEl.val(item.id);
this.addLocationBadge({
- value: 'This group'
+ value: 'This group',
});
}
}
@@ -420,7 +429,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
name: $projectOptionsDataEl.data('name'),
issuesPath: $projectOptionsDataEl.data('issues-path'),
issuesDisabled: $projectOptionsDataEl.data('issues-disabled'),
- mrPath: $projectOptionsDataEl.data('mr-path')
+ mrPath: $projectOptionsDataEl.data('mr-path'),
};
}
@@ -432,14 +441,14 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
gl.groupOptions[groupPath] = {
name: $groupOptionsDataEl.data('name'),
issuesPath: $groupOptionsDataEl.data('issues-path'),
- mrPath: $groupOptionsDataEl.data('mr-path')
+ mrPath: $groupOptionsDataEl.data('mr-path'),
};
}
if ($dashboardOptionsDataEl.length) {
gl.dashboardOptions = {
issuesPath: $dashboardOptionsDataEl.data('issues-path'),
- mrPath: $dashboardOptionsDataEl.data('mr-path')
+ mrPath: $dashboardOptionsDataEl.data('mr-path'),
};
}
});
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js
index 9dd14488f22..8e167f5bf08 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js
+++ b/app/assets/javascripts/templates/issuable_template_selector.js
@@ -1,60 +1,56 @@
-/* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */
-import Api from '../api';
+/* eslint-disable no-useless-return, max-len */
+import Api from '../api';
import TemplateSelector from '../blob/template_selector';
-((global) => {
- class IssuableTemplateSelector extends TemplateSelector {
- constructor(...args) {
- super(...args);
- this.projectPath = this.dropdown.data('project-path');
- this.namespacePath = this.dropdown.data('namespace-path');
- this.issuableType = this.$dropdownContainer.data('issuable-type');
- this.titleInput = $(`#${this.issuableType}_title`);
-
- const initialQuery = {
- name: this.dropdown.data('selected')
- };
-
- if (initialQuery.name) this.requestFile(initialQuery);
-
- $('.reset-template', this.dropdown.parent()).on('click', () => {
- this.setInputValueToTemplateContent();
- });
-
- $('.no-template', this.dropdown.parent()).on('click', () => {
- this.currentTemplate.content = '';
- this.setInputValueToTemplateContent();
- $('.dropdown-toggle-text', this.dropdown).text('Choose a template');
- });
- }
+export default class IssuableTemplateSelector extends TemplateSelector {
+ constructor(...args) {
+ super(...args);
+ this.projectPath = this.dropdown.data('project-path');
+ this.namespacePath = this.dropdown.data('namespace-path');
+ this.issuableType = this.$dropdownContainer.data('issuable-type');
+ this.titleInput = $(`#${this.issuableType}_title`);
+
+ const initialQuery = {
+ name: this.dropdown.data('selected'),
+ };
+
+ if (initialQuery.name) this.requestFile(initialQuery);
+
+ $('.reset-template', this.dropdown.parent()).on('click', () => {
+ this.setInputValueToTemplateContent();
+ });
+
+ $('.no-template', this.dropdown.parent()).on('click', () => {
+ this.currentTemplate.content = '';
+ this.setInputValueToTemplateContent();
+ $('.dropdown-toggle-text', this.dropdown).text('Choose a template');
+ });
+ }
- requestFile(query) {
- this.startLoadingSpinner();
- Api.issueTemplate(this.namespacePath, this.projectPath, query.name, this.issuableType, (err, currentTemplate) => {
- this.currentTemplate = currentTemplate;
- if (err) return; // Error handled by global AJAX error handler
- this.stopLoadingSpinner();
- this.setInputValueToTemplateContent();
- });
- return;
- }
+ requestFile(query) {
+ this.startLoadingSpinner();
+ Api.issueTemplate(this.namespacePath, this.projectPath, query.name, this.issuableType, (err, currentTemplate) => {
+ this.currentTemplate = currentTemplate;
+ if (err) return; // Error handled by global AJAX error handler
+ this.stopLoadingSpinner();
+ this.setInputValueToTemplateContent();
+ });
+ return;
+ }
- setInputValueToTemplateContent() {
- // `this.setEditorContent` sets the value of the description input field
- // to the content of the template selected.
- if (this.titleInput.val() === '') {
- // If the title has not yet been set, focus the title input and
- // skip focusing the description input by setting `true` as the
- // `skipFocus` option to `setEditorContent`.
- this.setEditorContent(this.currentTemplate, { skipFocus: true });
- this.titleInput.focus();
- } else {
- this.setEditorContent(this.currentTemplate, { skipFocus: false });
- }
- return;
+ setInputValueToTemplateContent() {
+ // `this.setEditorContent` sets the value of the description input field
+ // to the content of the template selected.
+ if (this.titleInput.val() === '') {
+ // If the title has not yet been set, focus the title input and
+ // skip focusing the description input by setting `true` as the
+ // `skipFocus` option to `setEditorContent`.
+ this.setEditorContent(this.currentTemplate, { skipFocus: true });
+ this.titleInput.focus();
+ } else {
+ this.setEditorContent(this.currentTemplate, { skipFocus: false });
}
+ return;
}
-
- global.IssuableTemplateSelector = IssuableTemplateSelector;
-})(window.gl || (window.gl = {}));
+}
diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js b/app/assets/javascripts/templates/issuable_template_selectors.js
index 97f6d37364d..66d868c5839 100644
--- a/app/assets/javascripts/templates/issuable_template_selectors.js
+++ b/app/assets/javascripts/templates/issuable_template_selectors.js
@@ -1,31 +1,28 @@
-/* eslint-disable no-new, comma-dangle, class-methods-use-this, no-param-reassign */
+/* eslint-disable no-new, class-methods-use-this */
+import IssuableTemplateSelector from './issuable_template_selector';
-((global) => {
- class IssuableTemplateSelectors {
- constructor({ $dropdowns, editor } = {}) {
- this.$dropdowns = $dropdowns || $('.js-issuable-selector');
- this.editor = editor || this.initEditor();
+export default class IssuableTemplateSelectors {
+ constructor({ $dropdowns, editor } = {}) {
+ this.$dropdowns = $dropdowns || $('.js-issuable-selector');
+ this.editor = editor || this.initEditor();
- this.$dropdowns.each((i, dropdown) => {
- const $dropdown = $(dropdown);
- new gl.IssuableTemplateSelector({
- pattern: /(\.md)/,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
- dropdown: $dropdown,
- editor: this.editor
- });
+ this.$dropdowns.each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+ new IssuableTemplateSelector({
+ pattern: /(\.md)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
+ dropdown: $dropdown,
+ editor: this.editor,
});
- }
-
- initEditor() {
- const editor = $('.markdown-area');
- // Proxy ace-editor's .setValue to jQuery's .val
- editor.setValue = editor.val;
- editor.getValue = editor.val;
- return editor;
- }
+ });
}
- global.IssuableTemplateSelectors = IssuableTemplateSelectors;
-})(window.gl || (window.gl = {}));
+ initEditor() {
+ const editor = $('.markdown-area');
+ // Proxy ace-editor's .setValue to jQuery's .val
+ editor.setValue = editor.val;
+ editor.getValue = editor.val;
+ return editor;
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
index 1274db2c4c8..9cb3edead86 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
@@ -1,3 +1,4 @@
+import Project from '~/project';
import SmartInterval from '~/smart_interval';
import Flash from '../flash';
import {
@@ -140,6 +141,7 @@ export default {
const el = document.createElement('div');
el.innerHTML = res.body;
document.body.appendChild(el);
+ Project.initRefSwitcher();
}
})
.catch(() => {
diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue
index 4216660da8c..365229ea274 100644
--- a/app/assets/javascripts/vue_shared/components/icon.vue
+++ b/app/assets/javascripts/vue_shared/components/icon.vue
@@ -36,6 +36,30 @@
required: false,
default: '',
},
+
+ width: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+
+ height: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+
+ y: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+
+ x: {
+ type: Number,
+ required: false,
+ default: null,
+ },
},
computed: {
@@ -51,7 +75,11 @@
<template>
<svg
- :class="[iconSizeClass, cssClasses]">
+ :class="[iconSizeClass, cssClasses]"
+ :width="width"
+ :height="height"
+ :x="x"
+ :y="y">
<use
v-bind="{'xlink:href':spriteHref}"/>
</svg>
diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue
index 47efee64c6e..6d15bbd84ba 100644
--- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue
+++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue
@@ -38,7 +38,8 @@ export default {
},
primaryButtonLabel: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
submitDisabled: {
type: Boolean,
@@ -113,8 +114,9 @@ export default {
{{ closeButtonLabel }}
</button>
<button
+ v-if="primaryButtonLabel"
type="button"
- class="btn pull-right"
+ class="btn pull-right js-primary-button"
:disabled="submitDisabled"
:class="btnKindClass"
@click="emitSubmit(true)">
diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_dialog.vue b/app/assets/javascripts/vue_shared/components/recaptcha_dialog.vue
new file mode 100644
index 00000000000..3ec50f14eb4
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/recaptcha_dialog.vue
@@ -0,0 +1,85 @@
+<script>
+import PopupDialog from './popup_dialog.vue';
+
+export default {
+ name: 'recaptcha-dialog',
+
+ props: {
+ html: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+
+ data() {
+ return {
+ script: {},
+ scriptSrc: 'https://www.google.com/recaptcha/api.js',
+ };
+ },
+
+ components: {
+ PopupDialog,
+ },
+
+ methods: {
+ appendRecaptchaScript() {
+ this.removeRecaptchaScript();
+
+ const script = document.createElement('script');
+ script.src = this.scriptSrc;
+ script.classList.add('js-recaptcha-script');
+ script.async = true;
+ script.defer = true;
+
+ this.script = script;
+
+ document.body.appendChild(script);
+ },
+
+ removeRecaptchaScript() {
+ if (this.script instanceof Element) this.script.remove();
+ },
+
+ close() {
+ this.removeRecaptchaScript();
+ this.$emit('close');
+ },
+
+ submit() {
+ this.$el.querySelector('form').submit();
+ },
+ },
+
+ watch: {
+ html() {
+ this.appendRecaptchaScript();
+ },
+ },
+
+ mounted() {
+ window.recaptchaDialogCallback = this.submit.bind(this);
+ },
+};
+</script>
+
+<template>
+<popup-dialog
+ kind="warning"
+ class="recaptcha-dialog js-recaptcha-dialog"
+ :hide-footer="true"
+ :title="__('Please solve the reCAPTCHA')"
+ @toggle="close"
+>
+ <div slot="body">
+ <p>
+ {{__('We want to be sure it is you, please confirm you are not a robot.')}}
+ </p>
+ <div
+ ref="recaptcha"
+ v-html="html"
+ ></div>
+ </div>
+</popup-dialog>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue
new file mode 100644
index 00000000000..ddc9ddbc3a3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue
@@ -0,0 +1,77 @@
+<script>
+ import loadingIcon from './loading_icon.vue';
+
+ export default {
+ props: {
+ name: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ value: {
+ type: Boolean,
+ required: true,
+ },
+ disabledInput: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ enabledText: {
+ type: String,
+ required: false,
+ default: 'Enabled',
+ },
+ disabledText: {
+ type: String,
+ required: false,
+ default: 'Disabled',
+ },
+ },
+
+ components: {
+ loadingIcon,
+ },
+
+ model: {
+ prop: 'value',
+ event: 'change',
+ },
+
+ methods: {
+ toggleFeature() {
+ if (!this.disabledInput) this.$emit('change', !this.value);
+ },
+ },
+ };
+</script>
+
+<template>
+ <label class="toggle-wrapper">
+ <input
+ type="hidden"
+ :name="name"
+ :value="value"
+ />
+ <button
+ type="button"
+ aria-label="Toggle"
+ class="project-feature-toggle"
+ :data-enabled-text="enabledText"
+ :data-disabled-text="disabledText"
+ :class="{
+ 'is-checked': value,
+ 'is-disabled': disabledInput,
+ 'is-loading': isLoading
+ }"
+ @click="toggleFeature"
+ >
+ <loadingIcon class="loading-icon" />
+ </button>
+ </label>
+</template>
diff --git a/app/assets/javascripts/vue_shared/mixins/recaptcha_dialog_implementor.js b/app/assets/javascripts/vue_shared/mixins/recaptcha_dialog_implementor.js
new file mode 100644
index 00000000000..ef70f9432e3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/mixins/recaptcha_dialog_implementor.js
@@ -0,0 +1,36 @@
+import RecaptchaDialog from '../components/recaptcha_dialog.vue';
+
+export default {
+ data() {
+ return {
+ showRecaptcha: false,
+ recaptchaHTML: '',
+ };
+ },
+
+ components: {
+ RecaptchaDialog,
+ },
+
+ methods: {
+ openRecaptcha() {
+ this.showRecaptcha = true;
+ },
+
+ closeRecaptcha() {
+ this.showRecaptcha = false;
+ },
+
+ checkForSpam(data) {
+ if (!data.recaptcha_html) return data;
+
+ this.recaptchaHTML = data.recaptcha_html;
+
+ const spamError = new Error(data.error_message);
+ spamError.name = 'SpamError';
+ spamError.message = 'SpamError';
+
+ throw spamError;
+ },
+ },
+};
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 66212be1b8f..43b16d3cf7d 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -44,6 +44,7 @@
@import "framework/tabs";
@import "framework/timeline";
@import "framework/tooltips";
+@import "framework/toggle";
@import "framework/typography";
@import "framework/zen";
@import "framework/blank";
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index cdc2aa196dd..3f630f82e29 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -239,6 +239,16 @@
}
}
+ &.dot-highlight::after {
+ content: '';
+ background-color: $blue-500;
+ width: $gl-padding * 0.5;
+ height: $gl-padding * 0.5;
+ display: inline-block;
+ border-radius: 50%;
+ margin-left: 3px;
+ }
+
svg {
height: 15px;
width: 15px;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index a42fab50db5..73524d5cf60 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -237,19 +237,6 @@ li.note {
}
}
-.browser-alert {
- padding: 10px;
- text-align: center;
- background: $error-bg;
- color: $white-light;
- font-weight: $gl-font-weight-bold;
-
- a {
- color: $white-light;
- text-decoration: underline;
- }
-}
-
.warning_message {
border-left: 4px solid $warning-message-border;
color: $warning-message-color;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 30d5d7a653b..8d83554d813 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -608,7 +608,7 @@
}
.dropdown-content {
- max-height: 215px;
+ max-height: $dropdown-max-height;
overflow-y: auto;
}
@@ -1030,3 +1030,28 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
}
}
}
+
+.dropdown-content-faded-mask {
+ position: relative;
+
+ .dropdown-list {
+ max-height: $dropdown-max-height;
+ overflow-y: auto;
+ position: relative;
+ }
+
+ &::after {
+ height: $dropdown-fade-mask-height;
+ width: 100%;
+ position: absolute;
+ bottom: 0;
+ background: linear-gradient(to top, $white-light 0, rgba($white-light, 0));
+ transition: opacity $fade-mask-transition-duration $fade-mask-transition-curve;
+ content: '';
+ pointer-events: none;
+ }
+
+ &.fade-out::after {
+ opacity: 0;
+ }
+}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index cf8165eab5b..cec38eea464 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -432,6 +432,7 @@
border-width: 1px;
width: 17px;
height: 17px;
+ top: 0;
}
&:hover,
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index cd505bcaf6d..e6e6c4c3963 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -457,13 +457,11 @@ ul.indent-list {
ul.group-list-tree {
li.group-row {
- &.has-description {
- .title {
- line-height: inherit;
- }
+ &.has-description .title {
+ line-height: inherit;
}
- .title {
+ &:not(.has-description) .title {
line-height: $list-text-height;
}
}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 5c9838c1029..ce551e6b7ce 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -48,3 +48,10 @@ body.modal-open {
display: block;
}
+.recaptcha-dialog .recaptcha-form {
+ display: inline-block;
+
+ .recaptcha {
+ margin: 0;
+ }
+}
diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss
new file mode 100644
index 00000000000..71765da3908
--- /dev/null
+++ b/app/assets/stylesheets/framework/toggle.scss
@@ -0,0 +1,138 @@
+/**
+* Toggle button
+*
+* @usage
+* ### Active and Inactive text should be provided as data attributes:
+* <button type="button" class="project-feature-toggle" data-enabled-text="Enabled" data-disabled-text="Disabled">
+* <i class="fa fa-spinner fa-spin loading-icon hidden"></i>
+* </button>
+
+* ### Checked should have `is-checked` class
+* <button type="button" class="project-feature-toggle is-checked" data-enabled-text="Enabled" data-disabled-text="Disabled">
+* <i class="fa fa-spinner fa-spin loading-icon hidden"></i>
+* </button>
+
+* ### Disabled should have `is-disabled` class
+* <button type="button" class="project-feature-toggle is-disabled" data-enabled-text="Enabled" data-disabled-text="Disabled" disabled="true">
+* <i class="fa fa-spinner fa-spin loading-icon hidden"></i>
+* </button>
+
+* ### Loading should have `is-loading` and an icon with `loading-icon` class
+* <button type="button" class="project-feature-toggle is-loading" data-enabled-text="Enabled" data-disabled-text="Disabled">
+* <i class="fa fa-spinner fa-spin loading-icon"></i>
+* </button>
+*/
+.project-feature-toggle {
+ position: relative;
+ border: 0;
+ outline: 0;
+ display: block;
+ width: 100px;
+ height: 24px;
+ cursor: pointer;
+ user-select: none;
+ background: $feature-toggle-color-disabled;
+ border-radius: 12px;
+ padding: 3px;
+ transition: all .4s ease;
+
+ &::selection,
+ &::before::selection,
+ &::after::selection {
+ background: none;
+ }
+
+ &::before {
+ color: $feature-toggle-text-color;
+ font-size: 12px;
+ line-height: 24px;
+ position: absolute;
+ top: 0;
+ left: 25px;
+ right: 5px;
+ text-align: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ animation: animate-disabled .2s ease-in;
+ content: attr(data-disabled-text);
+ }
+
+ &::after {
+ position: relative;
+ display: block;
+ content: "";
+ width: 22px;
+ height: 18px;
+ left: 0;
+ border-radius: 9px;
+ background: $feature-toggle-color;
+ transition: all .2s ease;
+ }
+
+ .loading-icon {
+ display: none;
+ font-size: 12px;
+ color: $white-light;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+
+ }
+
+ &.is-loading {
+ &::before {
+ display: none;
+ }
+
+ .loading-icon {
+ display: block;
+
+ &::before {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+ }
+ }
+
+ &.is-checked {
+ background: $feature-toggle-color-enabled;
+
+ &::before {
+ left: 5px;
+ right: 25px;
+ animation: animate-enabled .2s ease-in;
+ content: attr(data-enabled-text);
+ }
+
+ &::after {
+ left: calc(100% - 22px);
+ }
+ }
+
+ &.is-disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+ }
+
+ @media (max-width: $screen-xs-min) {
+ width: 50px;
+
+ &::before,
+ &.is-checked::before {
+ display: none;
+ }
+ }
+
+ @keyframes animate-enabled {
+ 0%, 35% { opacity: 0; }
+ 100% { opacity: 1; }
+ }
+
+ @keyframes animate-disabled {
+ 0%, 35% { opacity: 0; }
+ 100% { opacity: 1; }
+ }
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index cb2a237f574..4f99c27eff1 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -337,6 +337,7 @@ $regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-San
* Dropdowns
*/
$dropdown-width: 300px;
+$dropdown-max-height: 215px;
$dropdown-vertical-offset: 4px;
$dropdown-link-color: #555;
$dropdown-link-hover-bg: $row-hover;
@@ -353,6 +354,7 @@ $dropdown-loading-bg: rgba(#fff, .6);
$dropdown-chevron-size: 10px;
$dropdown-toggle-active-border-color: darken($border-color, 14%);
$dropdown-item-hover-bg: $gray-darker;
+$dropdown-fade-mask-height: 32px;
/*
* Filtered Search
@@ -406,7 +408,6 @@ $location-icon-color: #e7e9ed;
* Notes
*/
$notes-light-color: $gl-text-color-secondary;
-$notes-role-color: $gl-text-color-secondary;
$note-disabled-comment-color: #b2b2b2;
$note-targe3-outside: #fffff0;
$note-targe3-inside: #ffffd3;
@@ -555,6 +556,7 @@ $jq-ui-default-color: #777;
/*
* Label
*/
+$label-padding: 7px;
$label-gray-bg: #f8fafc;
$label-inverse-bg: #333;
$label-remove-border: rgba(0, 0, 0, .1);
@@ -564,6 +566,8 @@ $label-border-radius: 100px;
* Animation
*/
$fade-in-duration: 200ms;
+$fade-mask-transition-duration: .1s;
+$fade-mask-transition-curve: ease-in-out;
/*
* Lint
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 83e211d6086..c303f016ff9 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -14,3 +14,17 @@
}
@include new-style-dropdown('.clusters-dropdown ');
+
+.clusters-container {
+ .nav-bar-right {
+ padding: $gl-padding-top $gl-padding;
+ }
+
+ .empty-state .svg-content img {
+ width: 145px;
+ }
+
+ .top-area .nav-controls > .btn.btn-add-cluster {
+ margin-right: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index b1850be8a5f..0cba223c6a6 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -189,6 +189,7 @@
.commit-content {
padding-right: 10px;
+ white-space: normal;
}
.commit-actions {
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 538e50ee306..52e4d904b9b 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -13,6 +13,41 @@
.author_link {
white-space: nowrap;
}
+
+ @media (max-width: $screen-xs-max) {
+ display: block;
+ }
+}
+
+.detail-page-header-body {
+ position: relative;
+ line-height: 35px;
+ display: flex;
+ flex-grow: 1;
+
+ @media (min-width: $screen-sm-min) {
+ padding-left: 0;
+ padding-right: 0;
+ }
+}
+
+.detail-page-header-actions {
+ @include new-style-dropdown;
+
+ align-self: center;
+ flex-shrink: 0;
+ flex: 0 0 auto;
+
+ @media (max-width: $screen-xs-max) {
+ width: 100%;
+ margin-top: 10px;
+
+ > .issue-btn-group {
+ > .btn {
+ width: 100%;
+ }
+ }
+ }
}
.detail-page-description {
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index b0795353ec1..a5a6b7461a3 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -201,8 +201,9 @@
stroke-width: 1;
}
-.deploy-info-text {
- dominant-baseline: text-before-edge;
+.divider-line {
+ stroke-width: 1;
+ stroke: $gray-darkest;
}
.prometheus-state {
@@ -312,6 +313,20 @@
stroke: $gray-darker;
}
+ .deploy-info-text {
+ dominant-baseline: text-before-edge;
+ font-size: 12px;
+ }
+
+ .deploy-info-text-link {
+ font-family: $monospace_font;
+ fill: $gl-link-color;
+
+ &:hover {
+ fill: $gl-link-hover-color;
+ }
+ }
+
@media (max-width: $screen-sm-max) {
.label-axis-text,
.text-metric-usage,
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 9b7dda9b648..f9a761e85fe 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -212,3 +212,15 @@
height: 50px;
}
}
+
+.user-access-role {
+ display: inline-block;
+ color: $gl-text-color-secondary;
+ font-size: 12px;
+ line-height: 20px;
+ margin: -5px 3px;
+ padding: 0 $label-padding;
+ border: 1px solid $border-color;
+ border-radius: $label-border-radius;
+ font-weight: $gl-font-weight-normal;
+}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index b33825a506e..11ee1232bfe 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -624,50 +624,16 @@
margin-top: 0;
height: auto;
align-self: center;
-
- @media (max-width: $screen-xs-max) {
- position: absolute;
- top: 0;
- left: 0;
- }
-}
-
-.issuable-header {
- position: relative;
- padding-left: 45px;
- padding-right: 45px;
- line-height: 35px;
- display: flex;
- flex-grow: 1;
-
- @media (min-width: $screen-sm-min) {
- float: left;
- padding-left: 0;
- padding-right: 0;
- }
-}
-
-.issuable-actions {
- @include new-style-dropdown;
-
- align-self: center;
- flex-shrink: 0;
- flex: 0 0 auto;
-
- @media (min-width: $screen-sm-min) {
- float: right;
- }
}
.issuable-gutter-toggle {
@media (max-width: $screen-sm-max) {
- position: absolute;
- top: 0;
- right: 0;
+ margin-left: $btn-side-margin;
}
}
.issuable-meta {
+ flex: 1;
display: inline-block;
font-size: 14px;
line-height: 24px;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 8218326ae8f..af1df8b8802 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -134,26 +134,11 @@ ul.related-merge-requests > li {
}
@media (max-width: $screen-xs-max) {
- .detail-page-header,
- .issuable-header {
- display: block;
-
+ .detail-page-header {
.issuable-meta {
line-height: 18px;
}
}
-
- .issuable-actions {
- margin-top: 10px;
-
- .issue-btn-group {
- width: 100%;
-
- .btn {
- width: 100%;
- }
- }
- }
}
.issue-form {
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 443f5500684..92abe82df4c 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -104,7 +104,7 @@
}
.color-label {
- padding: 3px 7px;
+ padding: 3px $label-padding;
border-radius: $label-border-radius;
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 5832cf4637f..2afb17334e3 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -384,6 +384,12 @@
}
}
+.nothing-here-block {
+ img {
+ width: 230px;
+ }
+}
+
.mr-list {
.merge-request {
padding: 10px 0 10px 15px;
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 1e6992cb65e..ebb5d121433 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -141,20 +141,20 @@
.sidebar-item-icon {
border-radius: $border-radius-default;
- margin: 0 3px 0 -4px;
- vertical-align: middle;
+ margin: 0 5px 0 0;
+ vertical-align: text-bottom;
&.is-active {
fill: $orange-600;
}
-}
-.sidebar-collapsed-icon .sidebar-item-icon {
- margin: 0;
-}
+ .sidebar-collapsed-icon & {
+ margin: 0;
+ }
-.sidebar-item-value .sidebar-item-icon {
- fill: $theme-gray-700;
+ .sidebar-item-value & {
+ fill: $theme-gray-700;
+ }
}
.sidebar-item-warning-message {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index a6009ab328e..4d5613c292b 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -619,26 +619,17 @@ ul.notes {
}
.note-role {
+ margin: 0 3px;
+}
+
+.note-role-special {
position: relative;
display: inline-block;
- color: $notes-role-color;
+ color: $gl-text-color-secondary;
font-size: 12px;
- line-height: 20px;
- margin: 0 3px;
-
- &.note-role-access {
- padding: 0 7px;
- border: 1px solid $border-color;
- border-radius: $label-border-radius;
- }
-
- &.note-role-special {
- text-shadow: 0 0 15px $gl-text-color-inverted;
- }
+ text-shadow: 0 0 15px $gl-text-color-inverted;
}
-
-
/**
* Line note button on the side of diffs
*/
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 9345177a4dc..674588752d2 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -126,93 +126,6 @@
}
}
-.project-feature-toggle {
- position: relative;
- border: 0;
- outline: 0;
- display: block;
- width: 100px;
- height: 24px;
- cursor: pointer;
- user-select: none;
- background: $feature-toggle-color-disabled;
- border-radius: 12px;
- padding: 3px;
- transition: all .4s ease;
-
- &::selection,
- &::before::selection,
- &::after::selection {
- background: none;
- }
-
- &::before {
- color: $feature-toggle-text-color;
- font-size: 12px;
- line-height: 24px;
- position: absolute;
- top: 0;
- left: 25px;
- right: 5px;
- text-align: center;
- overflow: hidden;
- text-overflow: ellipsis;
- animation: animate-disabled .2s ease-in;
- content: attr(data-disabled-text);
- }
-
- &::after {
- position: relative;
- display: block;
- content: "";
- width: 22px;
- height: 18px;
- left: 0;
- border-radius: 9px;
- background: $feature-toggle-color;
- transition: all .2s ease;
- }
-
- &.checked {
- background: $feature-toggle-color-enabled;
-
- &::before {
- left: 5px;
- right: 25px;
- animation: animate-enabled .2s ease-in;
- content: attr(data-enabled-text);
- }
-
- &::after {
- left: calc(100% - 22px);
- }
- }
-
- &.disabled {
- opacity: 0.4;
- cursor: not-allowed;
- }
-
- @media (max-width: $screen-xs-min) {
- width: 50px;
-
- &::before,
- &.checked::before {
- display: none;
- }
- }
-
- @keyframes animate-enabled {
- 0%, 35% { opacity: 0; }
- 100% { opacity: 1; }
- }
-
- @keyframes animate-disabled {
- 0%, 35% { opacity: 0; }
- 100% { opacity: 1; }
- }
-}
-
.project-home-panel,
.group-home-panel {
padding-top: 24px;
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 65b334662c2..5d14323e4bc 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -4,6 +4,11 @@
.nav-block {
margin: 10px 0;
+ .btn .fa,
+ .btn svg {
+ color: $gl-text-color-secondary;
+ }
+
@media (min-width: $screen-sm-min) {
display: flex;
@@ -91,8 +96,12 @@
}
.add-to-tree {
- vertical-align: middle;
- padding: 6px 10px;
+ vertical-align: top;
+ padding: 8px;
+
+ svg {
+ top: 0;
+ }
}
.tree-table {
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
index 92df1c8dff0..dd0b38970bd 100644
--- a/app/controllers/admin/appearances_controller.rb
+++ b/app/controllers/admin/appearances_controller.rb
@@ -4,8 +4,8 @@ class Admin::AppearancesController < Admin::ApplicationController
def show
end
- def preview
- render 'preview', layout: 'devise'
+ def preview_sign_in
+ render 'preview_sign_in', layout: 'devise'
end
def create
@@ -52,7 +52,7 @@ class Admin::AppearancesController < Admin::ApplicationController
def appearance_params
params.require(:appearance).permit(
:title, :description, :logo, :logo_cache, :header_logo, :header_logo_cache,
- :updated_by
+ :new_project_guidelines, :updated_by
)
end
end
diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb
index 65a17828feb..61247b280b3 100644
--- a/app/controllers/admin/health_check_controller.rb
+++ b/app/controllers/admin/health_check_controller.rb
@@ -5,7 +5,7 @@ class Admin::HealthCheckController < Admin::ApplicationController
end
def reset_storage_health
- Gitlab::Git::Storage::CircuitBreaker.reset_all!
+ Gitlab::Git::Storage::FailureInfo.reset_all!
redirect_to admin_health_check_path,
notice: _('Git storage health information has been reset')
end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 744e448e8df..ecac9be0360 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -25,7 +25,7 @@ module IssuableActions
end
format.json do
- render_entity_json
+ recaptcha_check_with_fallback(false) { render_entity_json }
end
end
diff --git a/app/controllers/concerns/renders_member_access.rb b/app/controllers/concerns/renders_member_access.rb
new file mode 100644
index 00000000000..d640378c24d
--- /dev/null
+++ b/app/controllers/concerns/renders_member_access.rb
@@ -0,0 +1,23 @@
+module RendersMemberAccess
+ def prepare_groups_for_rendering(groups)
+ preload_max_member_access_for_collection(Group, groups)
+
+ groups
+ end
+
+ def prepare_projects_for_rendering(projects)
+ preload_max_member_access_for_collection(Project, projects)
+
+ projects
+ end
+
+ private
+
+ def preload_max_member_access_for_collection(klass, collection)
+ return if !current_user || collection.blank?
+
+ method_name = "max_member_access_for_#{klass.name.underscore}_ids"
+
+ current_user.public_send(method_name, collection.ids) # rubocop:disable GitlabSecurity/PublicSend
+ end
+end
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
index ada0dde87fb..03d8e188093 100644
--- a/app/controllers/concerns/spammable_actions.rb
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -23,8 +23,8 @@ module SpammableActions
@spam_config_loaded = Gitlab::Recaptcha.load_configurations!
end
- def recaptcha_check_with_fallback(&fallback)
- if spammable.valid?
+ def recaptcha_check_with_fallback(should_redirect = true, &fallback)
+ if should_redirect && spammable.valid?
redirect_to spammable_path
elsif render_recaptcha?
ensure_spam_config_loaded!
@@ -33,7 +33,18 @@ module SpammableActions
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
end
- render :verify
+ respond_to do |format|
+ format.html do
+ render :verify
+ end
+
+ format.json do
+ locals = { spammable: spammable, script: false, has_submit: false }
+ recaptcha_html = render_to_string(partial: 'shared/recaptcha_form', formats: :html, locals: locals)
+
+ render json: { recaptcha_html: recaptcha_html }
+ end
+ end
else
yield
end
diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb
index dec2e27335a..a6fb1f40001 100644
--- a/app/controllers/concerns/uploads_actions.rb
+++ b/app/controllers/concerns/uploads_actions.rb
@@ -1,4 +1,6 @@
module UploadsActions
+ include Gitlab::Utils::StrongMemoize
+
def create
link_to_file = UploadService.new(model, params[:file], uploader_class).execute
@@ -24,4 +26,25 @@ module UploadsActions
send_file uploader.file.path, disposition: disposition
end
+
+ private
+
+ def uploader
+ strong_memoize(:uploader) do
+ return if show_model.nil?
+
+ file_uploader = FileUploader.new(show_model, params[:secret])
+ file_uploader.retrieve_from_store!(params[:filename])
+
+ file_uploader
+ end
+ end
+
+ def image_or_video?
+ uploader && uploader.exists? && uploader.image_or_video?
+ end
+
+ def uploader_class
+ FileUploader
+ end
end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index d9884a47ec4..de9f8f9224a 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -1,5 +1,6 @@
class Dashboard::ProjectsController < Dashboard::ApplicationController
include ParamsBackwardCompatibility
+ include RendersMemberAccess
before_action :set_non_archived_param
before_action :default_sorting
@@ -45,10 +46,12 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
def load_projects(finder_params)
- ProjectsFinder
- .new(params: finder_params, current_user: current_user)
- .execute
- .includes(:route, :creator, namespace: [:route, :owner])
+ projects = ProjectsFinder
+ .new(params: finder_params, current_user: current_user)
+ .execute
+ .includes(:route, :creator, namespace: [:route, :owner])
+
+ prepare_projects_for_rendering(projects)
end
def load_events
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 762c6ebf3a3..c7273606a85 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -1,5 +1,6 @@
class Explore::ProjectsController < Explore::ApplicationController
include ParamsBackwardCompatibility
+ include RendersMemberAccess
before_action :set_non_archived_param
@@ -49,10 +50,12 @@ class Explore::ProjectsController < Explore::ApplicationController
private
def load_projects
- ProjectsFinder.new(current_user: current_user, params: params)
- .execute
- .includes(:route, namespace: :route)
- .page(params[:page])
- .without_count
+ projects = ProjectsFinder.new(current_user: current_user, params: params)
+ .execute
+ .includes(:route, namespace: :route)
+ .page(params[:page])
+ .without_count
+
+ prepare_projects_for_rendering(projects)
end
end
diff --git a/app/controllers/groups/uploads_controller.rb b/app/controllers/groups/uploads_controller.rb
new file mode 100644
index 00000000000..e6bd9806401
--- /dev/null
+++ b/app/controllers/groups/uploads_controller.rb
@@ -0,0 +1,35 @@
+class Groups::UploadsController < Groups::ApplicationController
+ include UploadsActions
+
+ skip_before_action :group, if: -> { action_name == 'show' && image_or_video? }
+
+ before_action :authorize_upload_file!, only: [:create]
+
+ private
+
+ def show_model
+ strong_memoize(:show_model) do
+ group_id = params[:group_id]
+
+ Group.find_by_full_path(group_id)
+ end
+ end
+
+ def authorize_upload_file!
+ render_404 unless can?(current_user, :upload_file, group)
+ end
+
+ def uploader
+ strong_memoize(:uploader) do
+ file_uploader = uploader_class.new(show_model, params[:secret])
+ file_uploader.retrieve_from_store!(params[:filename])
+ file_uploader
+ end
+ end
+
+ def uploader_class
+ NamespaceFileUploader
+ end
+
+ alias_method :model, :group
+end
diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb
index 98c2aaa3526..a931b456a93 100644
--- a/app/controllers/health_controller.rb
+++ b/app/controllers/health_controller.rb
@@ -1,5 +1,5 @@
class HealthController < ActionController::Base
- protect_from_forgery with: :exception
+ protect_from_forgery with: :exception, except: :storage_check
include RequiresWhitelistedMonitoringClient
CHECKS = [
@@ -23,6 +23,15 @@ class HealthController < ActionController::Base
render_check_results(results)
end
+ def storage_check
+ results = Gitlab::Git::Storage::Checker.check_all
+
+ render json: {
+ check_interval: Gitlab::CurrentSettings.current_application_settings.circuitbreaker_check_interval,
+ results: results
+ }
+ end
+
private
def render_check_results(results)
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 6d9873e38df..346eab4ba19 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -8,7 +8,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
@personal_access_token = finder.build(personal_access_token_params)
if @personal_access_token.save
- flash[:personal_access_token] = @personal_access_token.token
+ PersonalAccessToken.redis_store!(current_user.id, @personal_access_token.token)
redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created."
else
set_index_vars
@@ -43,5 +43,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
@inactive_personal_access_tokens = finder(state: 'inactive').execute
@active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at)
+
+ @new_personal_access_token = PersonalAccessToken.redis_getdel(current_user.id)
end
end
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
index ffb54390965..45c66b63ea5 100644
--- a/app/controllers/projects/autocomplete_sources_controller.rb
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -2,7 +2,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
before_action :load_autocomplete_service, except: [:members]
def members
- render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
+ render json: ::Projects::ParticipantsService.new(@project, current_user).execute(target)
end
def issues
@@ -14,7 +14,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
end
def labels
- render json: @autocomplete_service.labels
+ render json: @autocomplete_service.labels(target)
end
def milestones
@@ -22,7 +22,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
end
def commands
- render json: @autocomplete_service.commands(noteable, params[:type])
+ render json: @autocomplete_service.commands(target, params[:type])
end
private
@@ -31,13 +31,13 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
@autocomplete_service = ::Projects::AutocompleteService.new(@project, current_user)
end
- def noteable
- case params[:type]
- when 'Issue'
+ def target
+ case params[:type]&.downcase
+ when 'issue'
IssuesFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
- when 'MergeRequest'
+ when 'mergerequest'
MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
- when 'Commit'
+ when 'commit'
@project.commit(params[:type_id])
end
end
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index d1b99ecce4a..e36105ddc11 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -20,7 +20,7 @@ class Projects::BoardsController < Projects::ApplicationController
private
def assign_endpoint_vars
- @boards_endpoint = project_boards_url(project)
+ @boards_endpoint = project_boards_path(project)
@bulk_issues_path = bulk_update_project_issues_path(project)
@namespace_path = project.namespace.full_path
@labels_endpoint = project_labels_path(project)
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index d18b6d4b78c..0907daacbc3 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -8,11 +8,11 @@ class Projects::ClustersController < Projects::ApplicationController
STATUS_POLLING_INTERVAL = 10_000
def index
- if project.cluster
- redirect_to project_cluster_path(project, project.cluster)
- else
- redirect_to new_project_cluster_path(project)
- end
+ @scope = params[:scope] || 'all'
+ @clusters = ClustersFinder.new(project, current_user, @scope).execute.page(params[:page])
+ @active_count = ClustersFinder.new(project, current_user, :active).execute.count
+ @inactive_count = ClustersFinder.new(project, current_user, :inactive).execute.count
+ @all_count = @active_count + @inactive_count
end
def new
@@ -39,10 +39,20 @@ class Projects::ClustersController < Projects::ApplicationController
.execute(cluster)
if cluster.valid?
- flash[:notice] = "Cluster was successfully updated."
- redirect_to project_cluster_path(project, project.cluster)
+ respond_to do |format|
+ format.json do
+ head :no_content
+ end
+ format.html do
+ flash[:notice] = "Cluster was successfully updated."
+ redirect_to project_cluster_path(project, cluster)
+ end
+ end
else
- render :show
+ respond_to do |format|
+ format.json { head :bad_request }
+ format.html { render :show }
+ end
end
end
@@ -63,6 +73,19 @@ class Projects::ClustersController < Projects::ApplicationController
.present(current_user: current_user)
end
+ def create_params
+ params.require(:cluster).permit(
+ :enabled,
+ :name,
+ :provider_type,
+ provider_gcp_attributes: [
+ :gcp_project_id,
+ :zone,
+ :num_nodes,
+ :machine_type
+ ])
+ end
+
def update_params
if cluster.managed?
params.require(:cluster).permit(
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 6ff96a3f295..2e7344b1cad 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -134,6 +134,23 @@ class Projects::CommitController < Projects::ApplicationController
@grouped_diff_discussions = commit.grouped_diff_discussions
@discussions = commit.discussions
+ if merge_request_iid = params[:merge_request_iid]
+ @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: merge_request_iid)
+
+ if @merge_request
+ @new_diff_note_attrs.merge!(
+ noteable_type: 'MergeRequest',
+ noteable_id: @merge_request.id
+ )
+
+ merge_request_commit_notes = @merge_request.notes.where(commit_id: @commit.id).inc_relations_for_view
+ merge_request_commit_diff_discussions = merge_request_commit_notes.grouped_diff_discussions(@commit.diff_refs)
+ @grouped_diff_discussions.merge!(merge_request_commit_diff_discussions) do |line_code, left, right|
+ left + right
+ end
+ end
+ end
+
@notes = (@grouped_diff_discussions.values.flatten + @discussions).flat_map(&:notes)
@notes = prepare_notes_for_rendering(@notes, @commit)
end
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index 1269759fc2b..793ae03fb88 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -28,7 +28,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
:task_num,
:title,
:discussion_locked,
-
label_ids: []
]
end
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 9f966889995..fe8525a488c 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -4,6 +4,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
include RendersNotes
before_action :apply_diff_view_cookie!
+ before_action :commit
before_action :define_diff_vars
before_action :define_diff_comment_vars
@@ -20,18 +21,33 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
private
def define_diff_vars
+ @merge_request_diffs = @merge_request.merge_request_diffs.viewable.order_id_desc
+ @compare = commit || find_merge_request_diff_compare
+ return render_404 unless @compare
+
+ @diffs = @compare.diffs(diff_options)
+ end
+
+ def commit
+ return nil unless commit_id = params[:commit_id].presence
+ return nil unless @merge_request.all_commits.exists?(sha: commit_id)
+
+ @commit ||= @project.commit(commit_id)
+ end
+
+ def find_merge_request_diff_compare
@merge_request_diff =
- if params[:diff_id]
- @merge_request.merge_request_diffs.viewable.find(params[:diff_id])
+ if diff_id = params[:diff_id].presence
+ @merge_request.merge_request_diffs.viewable.find_by(id: diff_id)
else
@merge_request.merge_request_diff
end
- @merge_request_diffs = @merge_request.merge_request_diffs.viewable.order_id_desc
+ return unless @merge_request_diff
+
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
- if params[:start_sha].present?
- @start_sha = params[:start_sha]
+ if @start_sha = params[:start_sha].presence
@start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
unless @start_version
@@ -40,20 +56,18 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
end
end
- @compare =
- if @start_sha
- @merge_request_diff.compare_with(@start_sha)
- else
- @merge_request_diff
- end
-
- @diffs = @compare.diffs(diff_options)
+ if @start_sha
+ @merge_request_diff.compare_with(@start_sha)
+ else
+ @merge_request_diff
+ end
end
def define_diff_comment_vars
@new_diff_note_attrs = {
noteable_type: 'MergeRequest',
- noteable_id: @merge_request.id
+ noteable_id: @merge_request.id,
+ commit_id: @commit&.id
}
@diff_notes_disabled = false
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index abe4e5245b1..e7b3b73024b 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -7,11 +7,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include IssuableCollections
skip_before_action :merge_request, only: [:index, :bulk_update]
-
before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
-
before_action :set_issuables_index, only: [:index]
-
before_action :authenticate_user!, only: [:assign_related_issues]
def index
@@ -283,15 +280,15 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@merge_request.update(merge_error: nil)
if params[:merge_when_pipeline_succeeds].present?
- return :failed unless @merge_request.head_pipeline
+ return :failed unless @merge_request.actual_head_pipeline
- if @merge_request.head_pipeline.active?
+ if @merge_request.actual_head_pipeline.active?
::MergeRequests::MergeWhenPipelineSucceedsService
.new(@project, current_user, merge_params)
.execute(@merge_request)
:merge_when_pipeline_succeeds
- elsif @merge_request.head_pipeline.success?
+ elsif @merge_request.actual_head_pipeline.success?
# This can be triggered when a user clicks the auto merge button while
# the tests finish at about the same time
@merge_request.merge_async(current_user.id, params)
diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb
index b890818c475..06ce7328fb5 100644
--- a/app/controllers/projects/pipelines_settings_controller.rb
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -29,7 +29,6 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
:runners_token, :builds_enabled, :build_allow_git_fetch,
:build_timeout_in_minutes, :build_coverage_regex, :public_builds,
:auto_cancel_pending_pipelines, :ci_config_path,
- :run_auto_devops_pipeline_implicit, :run_auto_devops_pipeline_explicit,
auto_devops_attributes: [:id, :domain, :enabled]
)
end
diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
index 4d2fb17a19b..4685bbe80b4 100644
--- a/app/controllers/projects/uploads_controller.rb
+++ b/app/controllers/projects/uploads_controller.rb
@@ -8,31 +8,13 @@ class Projects::UploadsController < Projects::ApplicationController
private
- def uploader
- return @uploader if defined?(@uploader)
+ def show_model
+ strong_memoize(:show_model) do
+ namespace = params[:namespace_id]
+ id = params[:project_id]
- namespace = params[:namespace_id]
- id = params[:project_id]
-
- file_project = Project.find_by_full_path("#{namespace}/#{id}")
-
- if file_project.nil?
- @uploader = nil
- return
+ Project.find_by_full_path("#{namespace}/#{id}")
end
-
- @uploader = FileUploader.new(file_project, params[:secret])
- @uploader.retrieve_from_store!(params[:filename])
-
- @uploader
- end
-
- def image_or_video?
- uploader && uploader.exists? && uploader.image_or_video?
- end
-
- def uploader_class
- FileUploader
end
alias_method :model, :project
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 5fca31b4956..575ec5c20f0 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -1,5 +1,6 @@
class UsersController < ApplicationController
include RoutableActions
+ include RendersMemberAccess
skip_before_action :authenticate_user!
before_action :user, except: [:exists]
@@ -116,14 +117,20 @@ class UsersController < ApplicationController
@projects =
PersonalProjectsFinder.new(user).execute(current_user)
.page(params[:page])
+
+ prepare_projects_for_rendering(@projects)
end
def load_contributed_projects
@contributed_projects = contributed_projects.joined(user)
+
+ prepare_projects_for_rendering(@contributed_projects)
end
def load_groups
@groups = JoinedGroupsFinder.new(user).execute(current_user)
+
+ prepare_groups_for_rendering(@groups)
end
def load_snippets
diff --git a/app/finders/clusters_finder.rb b/app/finders/clusters_finder.rb
new file mode 100644
index 00000000000..c13f98257bf
--- /dev/null
+++ b/app/finders/clusters_finder.rb
@@ -0,0 +1,29 @@
+class ClustersFinder
+ def initialize(project, user, scope)
+ @project = project
+ @user = user
+ @scope = scope || :active
+ end
+
+ def execute
+ clusters = project.clusters
+ filter_by_scope(clusters)
+ end
+
+ private
+
+ attr_reader :project, :user, :scope
+
+ def filter_by_scope(clusters)
+ case scope.to_sym
+ when :all
+ clusters
+ when :inactive
+ clusters.disabled
+ when :active
+ clusters.enabled
+ else
+ raise "Invalid scope #{scope}"
+ end
+ end
+end
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index df590cf47c8..c037de33c22 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -1,30 +1,26 @@
module AppearancesHelper
def brand_title
- if brand_item && brand_item.title
- brand_item.title
- else
- 'GitLab Community Edition'
- end
+ brand_item&.title.presence || 'GitLab Community Edition'
end
def brand_image
- if brand_item.logo?
- image_tag brand_item.logo
- else
- nil
- end
+ image_tag(brand_item.logo) if brand_item&.logo?
end
def brand_text
markdown_field(brand_item, :description)
end
+ def brand_new_project_guidelines
+ markdown_field(brand_item, :new_project_guidelines)
+ end
+
def brand_item
@appearance ||= Appearance.current
end
def brand_header_logo
- if brand_item && brand_item.header_logo?
+ if brand_item&.header_logo?
image_tag brand_item.header_logo
else
render 'shared/logo.svg'
@@ -33,7 +29,7 @@ module AppearancesHelper
# Skip the 'GitLab' type logo when custom brand logo is set
def brand_header_logo_type
- unless brand_item && brand_item.header_logo?
+ unless brand_item&.header_logo?
render 'shared/logo_type.svg'
end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index dccde46fa33..b12ea760668 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -124,17 +124,6 @@ module ApplicationSettingsHelper
_('The number of attempts GitLab will make to access a storage.')
end
- def circuitbreaker_backoff_threshold_help_text
- _("The number of failures after which GitLab will start temporarily "\
- "disabling access to a storage shard on a host")
- end
-
- def circuitbreaker_failure_wait_time_help_text
- _("When access to a storage fails. GitLab will prevent access to the "\
- "storage for the time specified here. This allows the filesystem to "\
- "recover. Repositories on failing shards are temporarly unavailable")
- end
-
def circuitbreaker_failure_reset_time_help_text
_("The time in seconds GitLab will keep failure information. When no "\
"failures occur during this time, information about the mount is reset.")
@@ -145,6 +134,11 @@ module ApplicationSettingsHelper
"timeout error will be raised.")
end
+ def circuitbreaker_check_interval_help_text
+ _("The time in seconds between storage checks. When a previous check did "\
+ "complete yet, GitLab will skip a check.")
+ end
+
def visible_attributes
[
:admin_notification_email,
@@ -154,10 +148,9 @@ module ApplicationSettingsHelper
:akismet_enabled,
:auto_devops_enabled,
:circuitbreaker_access_retries,
- :circuitbreaker_backoff_threshold,
+ :circuitbreaker_check_interval,
:circuitbreaker_failure_count_threshold,
:circuitbreaker_failure_reset_time,
- :circuitbreaker_failure_wait_time,
:circuitbreaker_storage_timeout,
:clientside_sentry_dsn,
:clientside_sentry_enabled,
diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb
index ec6194d204f..f4310ca2f06 100644
--- a/app/helpers/auto_devops_helper.rb
+++ b/app/helpers/auto_devops_helper.rb
@@ -8,22 +8,6 @@ module AutoDevopsHelper
!project.ci_service
end
- def show_run_auto_devops_pipeline_checkbox_for_instance_setting?(project)
- return false if project.repository.gitlab_ci_yml
-
- if project&.auto_devops&.enabled.present?
- !project.auto_devops.enabled && current_application_settings.auto_devops_enabled?
- else
- current_application_settings.auto_devops_enabled?
- end
- end
-
- def show_run_auto_devops_pipeline_checkbox_for_explicit_setting?(project)
- return false if project.repository.gitlab_ci_yml
-
- !project.auto_devops_enabled?
- end
-
def auto_devops_warning_message(project)
missing_domain = !project.auto_devops&.has_domain?
missing_service = !project.deployment_platform&.active?
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index c4a621160af..12b3d9bac1a 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -6,7 +6,7 @@ module BoardsHelper
def board_data
{
boards_endpoint: @boards_endpoint,
- lists_endpoint: board_lists_url(board),
+ lists_endpoint: board_lists_path(board),
board_id: board.id,
disabled: "#{!can?(current_user, :admin_list, current_board_parent)}",
issue_link_base: build_issue_link_base,
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
index aa3a9a055a0..4ec63fdaffc 100644
--- a/app/helpers/builds_helper.rb
+++ b/app/helpers/builds_helper.rb
@@ -20,8 +20,7 @@ module BuildsHelper
def javascript_build_options
{
- page_url: project_job_url(@project, @build),
- build_url: project_job_url(@project, @build, :json),
+ page_path: project_job_path(@project, @build),
build_status: @build.status,
build_stage: @build.stage,
log_state: ''
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index d06cf2de2c3..3605d6a3c95 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -84,7 +84,7 @@ module ButtonHelper
button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description
content_tag (href ? :a : :span),
- button_content,
+ (href ? button_content : title),
class: "#{title.downcase}-selector",
href: (href if href)
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index f68e2cd3afa..2d304f7eb91 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -228,4 +228,12 @@ module CommitsHelper
[commits, 0]
end
end
+
+ def commit_path(project, commit, merge_request: nil)
+ if merge_request&.persisted?
+ diffs_project_merge_request_path(project, merge_request, commit_id: commit.id)
+ else
+ project_commit_path(project, commit)
+ end
+ end
end
diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb
index f7e17f5cc01..6d303ba857d 100644
--- a/app/helpers/graph_helper.rb
+++ b/app/helpers/graph_helper.rb
@@ -8,7 +8,7 @@ module GraphHelper
# append note count
notes_count = @graph.notes[commit.id]
- refs << "[#{notes_count} #{pluralize(notes_count, 'note')}]" if notes_count > 0
+ refs << "[#{pluralize(notes_count, 'note')}]" if notes_count > 0
refs
end
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 1e4be2d4bcf..f78d41a0448 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -86,6 +86,8 @@ module MarkupHelper
return '' unless text.present?
context[:project] ||= @project
+ context[:group] ||= @group
+
html = markdown_unsafe(text, context)
prepare_for_rendering(html, context)
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 5b2c58d193d..ce57422f45d 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -101,6 +101,30 @@ module MergeRequestsHelper
}.merge(merge_params_ee(merge_request))
end
+ def tab_link_for(merge_request, tab, options = {}, &block)
+ data_attrs = {
+ action: tab.to_s,
+ target: "##{tab}",
+ toggle: options.fetch(:force_link, false) ? '' : 'tab'
+ }
+
+ url = case tab
+ when :show
+ data_attrs[:target] = '#notes'
+ method(:project_merge_request_path)
+ when :commits
+ method(:commits_project_merge_request_path)
+ when :pipelines
+ method(:pipelines_project_merge_request_path)
+ when :diffs
+ method(:diffs_project_merge_request_path)
+ else
+ raise "Cannot create tab #{tab}."
+ end
+
+ link_to(url[merge_request.project, merge_request], data: data_attrs, &block)
+ end
+
def merge_params_ee(merge_request)
{}
end
diff --git a/app/helpers/storage_health_helper.rb b/app/helpers/storage_health_helper.rb
index 4d2180f7eee..b76c1228220 100644
--- a/app/helpers/storage_health_helper.rb
+++ b/app/helpers/storage_health_helper.rb
@@ -18,16 +18,12 @@ module StorageHealthHelper
current_failures = circuit_breaker.failure_count
translation_params = { number_of_failures: current_failures,
- maximum_failures: maximum_failures,
- number_of_seconds: circuit_breaker.failure_wait_time }
+ maximum_failures: maximum_failures }
if circuit_breaker.circuit_broken?
s_("%{number_of_failures} of %{maximum_failures} failures. GitLab will not "\
"retry automatically. Reset storage information when the problem is "\
"resolved.") % translation_params
- elsif circuit_breaker.backing_off?
- _("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\
- "block access for %{number_of_seconds} seconds.") % translation_params
else
_("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\
"allow access on the next attempt.") % translation_params
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index ff15689ecac..76cfe28742a 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -2,9 +2,8 @@ class Appearance < ActiveRecord::Base
include CacheMarkdownField
cache_markdown_field :description
+ cache_markdown_field :new_project_guidelines
- validates :title, presence: true
- validates :description, presence: true
validates :logo, file_size: { maximum: 1.megabyte }
validates :header_logo, file_size: { maximum: 1.megabyte }
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 3117c98c846..253e213af81 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -153,11 +153,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
numericality: { greater_than_or_equal_to: 0 }
- validates :circuitbreaker_backoff_threshold,
- :circuitbreaker_failure_count_threshold,
- :circuitbreaker_failure_wait_time,
+ validates :circuitbreaker_failure_count_threshold,
:circuitbreaker_failure_reset_time,
:circuitbreaker_storage_timeout,
+ :circuitbreaker_check_interval,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
@@ -165,13 +164,6 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 1 }
- validates_each :circuitbreaker_backoff_threshold do |record, attr, value|
- if value.to_i >= record.circuitbreaker_failure_count_threshold
- record.errors.add(attr, _("The circuitbreaker backoff threshold should be "\
- "lower than the failure count threshold"))
- end
- end
-
validates :gitaly_timeout_default,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 8738e094510..85960f1b6bb 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -6,6 +6,8 @@ module Ci
include Presentable
include Importable
+ MissingDependenciesError = Class.new(StandardError)
+
belongs_to :runner
belongs_to :trigger_request
belongs_to :erased_by, class_name: 'User'
@@ -47,6 +49,25 @@ module Ci
scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) }
scope :ref_protected, -> { where(protected: true) }
+ scope :matches_tag_ids, -> (tag_ids) do
+ matcher = ::ActsAsTaggableOn::Tagging
+ .where(taggable_type: CommitStatus)
+ .where(context: 'tags')
+ .where('taggable_id = ci_builds.id')
+ .where.not(tag_id: tag_ids).select('1')
+
+ where("NOT EXISTS (?)", matcher)
+ end
+
+ scope :with_any_tags, -> do
+ matcher = ::ActsAsTaggableOn::Tagging
+ .where(taggable_type: CommitStatus)
+ .where(context: 'tags')
+ .where('taggable_id = ci_builds.id').select('1')
+
+ where("EXISTS (?)", matcher)
+ end
+
mount_uploader :legacy_artifacts_file, LegacyArtifactUploader, mount_on: :artifacts_file
mount_uploader :legacy_artifacts_metadata, LegacyArtifactUploader, mount_on: :artifacts_metadata
@@ -120,6 +141,10 @@ module Ci
Ci::Build.retry(build, build.user)
end
end
+
+ before_transition any => [:running] do |build|
+ build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies')
+ end
end
def detailed_status(current_user)
@@ -459,6 +484,20 @@ module Ci
options[:dependencies]&.empty?
end
+ def validates_dependencies!
+ dependencies.each do |dependency|
+ raise MissingDependenciesError unless dependency.valid_dependency?
+ end
+ end
+
+ def valid_dependency?
+ return false unless complete?
+ return false if artifacts_expired?
+ return false if erased?
+
+ true
+ end
+
def hide_secrets(trace)
return unless trace
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index d39610a8995..dcbb397fb78 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -112,7 +112,7 @@ module Ci
def can_pick?(build)
return false if self.ref_protected? && !build.protected?
- assignable_for?(build.project) && accepting_tags?(build)
+ assignable_for?(build.project_id) && accepting_tags?(build)
end
def only_for?(project)
@@ -171,8 +171,8 @@ module Ci
end
end
- def assignable_for?(project)
- is_shared? || projects.exists?(id: project.id)
+ def assignable_for?(project_id)
+ is_shared? || projects.exists?(id: project_id)
end
def accepting_tags?(build)
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 45beced1427..55419189282 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -55,6 +55,10 @@ module Clusters
end
end
+ def created?
+ status_name == :created
+ end
+
def applications
[
application_helm || build_application_helm,
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 6b28d290f99..307e4fcedfe 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -1,3 +1,4 @@
+# coding: utf-8
class Commit
extend ActiveModel::Naming
extend Gitlab::Cache::RequestCache
@@ -25,7 +26,7 @@ class Commit
DIFF_HARD_LIMIT_FILES = 1000
DIFF_HARD_LIMIT_LINES = 50000
- MIN_SHA_LENGTH = 7
+ MIN_SHA_LENGTH = Gitlab::Git::Commit::MIN_SHA_LENGTH
COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze
def banzai_render_context(field)
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index ee21ed8e420..c0263c0b4e2 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -43,7 +43,8 @@ class CommitStatus < ActiveRecord::Base
script_failure: 1,
api_failure: 2,
stuck_or_timeout_failure: 3,
- runner_system_failure: 4
+ runner_system_failure: 4,
+ missing_dependency_failure: 5
}
##
diff --git a/app/models/concerns/bulk_member_access_load.rb b/app/models/concerns/bulk_member_access_load.rb
new file mode 100644
index 00000000000..984c4f53bf7
--- /dev/null
+++ b/app/models/concerns/bulk_member_access_load.rb
@@ -0,0 +1,46 @@
+# Returns and caches in thread max member access for a resource
+#
+module BulkMemberAccessLoad
+ extend ActiveSupport::Concern
+
+ included do
+ # Determine the maximum access level for a group of resources in bulk.
+ #
+ # Returns a Hash mapping resource ID -> maximum access level.
+ def max_member_access_for_resource_ids(resource_klass, resource_ids, memoization_index = self.id, &block)
+ raise 'Block is mandatory' unless block_given?
+
+ resource_ids = resource_ids.uniq
+ key = max_member_access_for_resource_key(resource_klass, memoization_index)
+ access = {}
+
+ if RequestStore.active?
+ RequestStore.store[key] ||= {}
+ access = RequestStore.store[key]
+ end
+
+ # Look up only the IDs we need
+ resource_ids = resource_ids - access.keys
+
+ return access if resource_ids.empty?
+
+ resource_access = yield(resource_ids)
+
+ access.merge!(resource_access)
+
+ missing_resource_ids = resource_ids - resource_access.keys
+
+ missing_resource_ids.each do |resource_id|
+ access[resource_id] = Gitlab::Access::NO_ACCESS
+ end
+
+ access
+ end
+
+ private
+
+ def max_member_access_for_resource_key(klass, memoization_index)
+ "max_member_access_for_#{klass.name.underscore.pluralize}:#{memoization_index}"
+ end
+ end
+end
diff --git a/app/models/concerns/discussion_on_diff.rb b/app/models/concerns/discussion_on_diff.rb
index f5cbb3becad..4b4d519f3df 100644
--- a/app/models/concerns/discussion_on_diff.rb
+++ b/app/models/concerns/discussion_on_diff.rb
@@ -32,6 +32,10 @@ module DiscussionOnDiff
first_note.position.new_path
end
+ def on_merge_request_commit?
+ for_merge_request? && commit_id.present?
+ end
+
# Returns an array of at most 16 highlighted lines above a diff note
def truncated_diff_lines(highlight: true)
lines = highlight ? highlighted_diff_lines : diff_lines
diff --git a/app/models/concerns/throttled_touch.rb b/app/models/concerns/throttled_touch.rb
new file mode 100644
index 00000000000..ad0ff0f20d4
--- /dev/null
+++ b/app/models/concerns/throttled_touch.rb
@@ -0,0 +1,10 @@
+# ThrottledTouch can be used to throttle the number of updates triggered by
+# calling "touch" on an ActiveRecord model.
+module ThrottledTouch
+ # The amount of time to wait before "touch" can update a record again.
+ TOUCH_INTERVAL = 1.minute
+
+ def touch(*args)
+ super if (Time.zone.now - updated_at) > TOUCH_INTERVAL
+ end
+end
diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb
index 6eba87da1a1..4a65738214b 100644
--- a/app/models/diff_discussion.rb
+++ b/app/models/diff_discussion.rb
@@ -24,7 +24,11 @@ class DiffDiscussion < Discussion
return unless for_merge_request?
return {} if active?
- noteable.version_params_for(position.diff_refs)
+ if on_merge_request_commit?
+ { commit_id: commit_id }
+ else
+ noteable.version_params_for(position.diff_refs)
+ end
end
def reply_attributes
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index ae5f138a920..b53d44cda95 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -17,6 +17,7 @@ class DiffNote < Note
validates :noteable_type, inclusion: { in: NOTEABLE_TYPES }
validate :positions_complete
validate :verify_supported
+ validate :diff_refs_match_commit, if: :for_commit?
before_validation :set_original_position, on: :create
before_validation :update_position, on: :create, if: :on_text?
@@ -135,6 +136,12 @@ class DiffNote < Note
errors.add(:position, "is invalid")
end
+ def diff_refs_match_commit
+ return if self.original_position.diff_refs == self.commit.diff_refs
+
+ errors.add(:commit_id, 'does not match the diff refs')
+ end
+
def keep_around_commits
project.repository.keep_around(self.original_position.base_sha)
project.repository.keep_around(self.original_position.start_sha)
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index 437df923d2d..92482a1a875 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -11,6 +11,7 @@ class Discussion
:author,
:noteable,
+ :commit_id,
:for_commit?,
:for_merge_request?,
diff --git a/app/models/epic.rb b/app/models/epic.rb
index 62898a02e2d..286b855de3f 100644
--- a/app/models/epic.rb
+++ b/app/models/epic.rb
@@ -1,7 +1,11 @@
# Placeholder class for model that is implemented in EE
-# It will reserve (ee#3853) '&' as a reference prefix, but the table does not exists in CE
+# It reserves '&' as a reference prefix, but the table does not exists in CE
class Epic < ActiveRecord::Base
- # TODO: this will be implemented as part of #3853
- def to_reference
+ def self.reference_prefix
+ '&'
+ end
+
+ def self.reference_prefix_escaped
+ '&amp;'
end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 0997b056c6a..6053594fab5 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -72,7 +72,7 @@ class Event < ActiveRecord::Base
# We're using preload for "push_event_payload" as otherwise the association
# is not always available (depending on the query being built).
includes(:author, :project, project: :namespace)
- .preload(:target, :push_event_payload)
+ .preload(:push_event_payload, target: :author)
end
scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
diff --git a/app/models/group.rb b/app/models/group.rb
index 505e943e464..fddace03387 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -298,6 +298,10 @@ class Group < Namespace
end
end
+ def hashed_storage?(_feature)
+ false
+ end
+
private
def update_two_factor_requirement
diff --git a/app/models/issue.rb b/app/models/issue.rb
index d6ef58d150b..33db197e612 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -9,6 +9,7 @@ class Issue < ActiveRecord::Base
include FasterCacheKeys
include RelativePositioning
include TimeTrackable
+ include ThrottledTouch
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index bbc01e9677c..422f138c4ea 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -7,6 +7,7 @@ class MergeRequest < ActiveRecord::Base
include TimeTrackable
include ManualInverseAssociation
include EachBatch
+ include ThrottledTouch
ignore_column :locked_at,
:ref_fetched
@@ -145,6 +146,13 @@ class MergeRequest < ActiveRecord::Base
'!'
end
+ # Use this method whenever you need to make sure the head_pipeline is synced with the
+ # branch head commit, for example checking if a merge request can be merged.
+ # For more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/40004
+ def actual_head_pipeline
+ head_pipeline&.sha == diff_head_sha ? head_pipeline : nil
+ end
+
# Pattern used to extract `!123` merge request references from text
#
# This pattern supports cross-project references.
@@ -641,6 +649,7 @@ class MergeRequest < ActiveRecord::Base
.to_sql
Note.from("(#{union}) #{Note.table_name}")
+ .includes(:noteable)
end
alias_method :discussion_notes, :related_notes
@@ -822,8 +831,9 @@ class MergeRequest < ActiveRecord::Base
def mergeable_ci_state?
return true unless project.only_allow_merge_if_pipeline_succeeds?
+ return true unless head_pipeline
- !head_pipeline || head_pipeline.success? || head_pipeline.skipped?
+ actual_head_pipeline&.success? || actual_head_pipeline&.skipped?
end
def environments_for(current_user)
@@ -916,21 +926,27 @@ class MergeRequest < ActiveRecord::Base
.order(id: :desc)
end
- # Note that this could also return SHA from now dangling commits
- #
- def all_commit_shas
- return commit_shas unless persisted?
-
- diffs_relation = merge_request_diffs
-
+ def all_commits
# MySQL doesn't support LIMIT in a subquery.
- diffs_relation = diffs_relation.recent if Gitlab::Database.postgresql?
+ diffs_relation = if Gitlab::Database.postgresql?
+ merge_request_diffs.recent
+ else
+ merge_request_diffs
+ end
MergeRequestDiffCommit
.where(merge_request_diff: diffs_relation)
.limit(10_000)
- .pluck('sha')
- .uniq
+ end
+
+ # Note that this could also return SHA from now dangling commits
+ #
+ def all_commit_shas
+ @all_commit_shas ||= begin
+ return commit_shas unless persisted?
+
+ all_commits.pluck(:sha).uniq
+ end
end
def merge_commit
@@ -997,7 +1013,7 @@ class MergeRequest < ActiveRecord::Base
return true if autocomplete_precheck
return false unless mergeable?(skip_ci_check: true)
- return false if head_pipeline && !(head_pipeline.success? || head_pipeline.active?)
+ return false if actual_head_pipeline && !(actual_head_pipeline.success? || actual_head_pipeline.active?)
return false if last_diff_sha != diff_head_sha
true
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index c06ee8083f0..77c19380e66 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -84,6 +84,13 @@ class Milestone < ActiveRecord::Base
else milestones.active
end
end
+
+ def predefined?(milestone)
+ milestone == Any ||
+ milestone == None ||
+ milestone == Upcoming ||
+ milestone == Started
+ end
end
def self.reference_prefix
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 901dbf2ba69..0ff169d4531 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -40,6 +40,7 @@ class Namespace < ActiveRecord::Base
namespace_path: true
validate :nesting_level_allowed
+ validate :allowed_path_by_redirects
delegate :name, to: :owner, allow_nil: true, prefix: true
@@ -257,4 +258,14 @@ class Namespace < ActiveRecord::Base
Namespace.where(id: descendants.select(:id))
.update_all(share_with_group_lock: true)
end
+
+ def allowed_path_by_redirects
+ return if path.nil?
+
+ errors.add(:path, "#{path} has been taken before. Please use another one") if namespace_previously_created_with_same_path?
+ end
+
+ def namespace_previously_created_with_same_path?
+ RedirectRoute.permanent.exists?(path: path)
+ end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 340fe087f82..c4c2ab8e67d 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -15,6 +15,7 @@ class Note < ActiveRecord::Base
include IgnorableColumn
include Editable
include Gitlab::SQL::Pattern
+ include ThrottledTouch
module SpecialRole
FIRST_TIME_CONTRIBUTOR = :first_time_contributor
@@ -55,7 +56,7 @@ class Note < ActiveRecord::Base
participant :author
belongs_to :project
- belongs_to :noteable, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations
+ belongs_to :noteable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
belongs_to :author, class_name: "User"
belongs_to :updated_by, class_name: "User"
belongs_to :last_edited_by, class_name: 'User'
@@ -118,6 +119,7 @@ class Note < ActiveRecord::Base
before_validation :set_discussion_id, on: :create
after_save :keep_around_commit, if: :for_project_noteable?
after_save :expire_etag_cache
+ after_save :touch_noteable
after_destroy :expire_etag_cache
class << self
@@ -228,16 +230,18 @@ class Note < ActiveRecord::Base
for_personal_snippet?
end
+ def commit
+ @commit ||= project.commit(commit_id) if commit_id.present?
+ end
+
# override to return commits, which are not active record
def noteable
- if for_commit?
- @commit ||= project.commit(commit_id)
- else
- super
- end
- # Temp fix to prevent app crash
- # if note commit id doesn't exist
+ return commit if for_commit?
+
+ super
rescue
+ # Temp fix to prevent app crash
+ # if note commit id doesn't exist
nil
end
@@ -367,6 +371,42 @@ class Note < ActiveRecord::Base
Gitlab::EtagCaching::Store.new.touch(key)
end
+ def touch(*args)
+ # We're not using an explicit transaction here because this would in all
+ # cases result in all future queries going to the primary, even if no writes
+ # are performed.
+ #
+ # We touch the noteable first so its SELECT query can run before our writes,
+ # ensuring it runs on a secondary (if no prior write took place).
+ touch_noteable
+ super
+ end
+
+ # By default Rails will issue an "SELECT *" for the relation, which is
+ # overkill for just updating the timestamps. To work around this we manually
+ # touch the data so we can SELECT only the columns we need.
+ def touch_noteable
+ # Commits are not stored in the DB so we can't touch them.
+ return if for_commit?
+
+ assoc = association(:noteable)
+
+ noteable_object =
+ if assoc.loaded?
+ noteable
+ else
+ # If the object is not loaded (e.g. when notes are loaded async) we
+ # _only_ want the data we actually need.
+ assoc.scope.select(:id, :updated_at).take
+ end
+
+ noteable_object&.touch
+ end
+
+ def banzai_render_context(field)
+ super.merge(noteable: noteable)
+ end
+
private
def keep_around_commit
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index cfcb03138b7..063dc521324 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -3,6 +3,8 @@ class PersonalAccessToken < ActiveRecord::Base
include TokenAuthenticatable
add_authentication_token_field :token
+ REDIS_EXPIRY_TIME = 3.minutes
+
serialize :scopes, Array # rubocop:disable Cop/ActiveRecordSerialize
belongs_to :user
@@ -27,6 +29,21 @@ class PersonalAccessToken < ActiveRecord::Base
!revoked? && !expired?
end
+ def self.redis_getdel(user_id)
+ Gitlab::Redis::SharedState.with do |redis|
+ token = redis.get(redis_shared_state_key(user_id))
+ redis.del(redis_shared_state_key(user_id))
+ token
+ end
+ end
+
+ def self.redis_store!(user_id, token)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(redis_shared_state_key(user_id), token, ex: REDIS_EXPIRY_TIME)
+ token
+ end
+ end
+
protected
def validate_scopes
@@ -38,4 +55,8 @@ class PersonalAccessToken < ActiveRecord::Base
def set_default_scopes
self.scopes = Gitlab::Auth::DEFAULT_SCOPES if self.scopes.empty?
end
+
+ def self.redis_shared_state_key(user_id)
+ "gitlab:personal_access_token:#{user_id}"
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index cc530076bf7..6ae15a0a50f 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -189,7 +189,6 @@ class Project < ActiveRecord::Base
has_one :statistics, class_name: 'ProjectStatistics'
has_one :cluster_project, class_name: 'Clusters::Project'
- has_one :cluster, through: :cluster_project, class_name: 'Clusters::Cluster'
has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
# Container repositories need to remove data from the container registry,
@@ -228,7 +227,6 @@ class Project < ActiveRecord::Base
delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
- delegate :empty_repo?, to: :repository
# Validations
validates :creator, presence: true, on: :create
@@ -500,6 +498,10 @@ class Project < ActiveRecord::Base
auto_devops&.enabled.nil? && !current_application_settings.auto_devops_enabled?
end
+ def empty_repo?
+ repository.empty?
+ end
+
def repository_storage_path
Gitlab.config.repositories.storages[repository_storage].try(:[], 'path')
end
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 1d35426050e..c679758973a 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -1,4 +1,6 @@
class ProjectTeam
+ include BulkMemberAccessLoad
+
attr_accessor :project
def initialize(project)
@@ -157,39 +159,16 @@ class ProjectTeam
#
# Returns a Hash mapping user ID -> maximum access level.
def max_member_access_for_user_ids(user_ids)
- user_ids = user_ids.uniq
- key = "max_member_access:#{project.id}"
-
- access = {}
-
- if RequestStore.active?
- RequestStore.store[key] ||= {}
- access = RequestStore.store[key]
+ max_member_access_for_resource_ids(User, user_ids, project.id) do |user_ids|
+ project.project_authorizations
+ .where(user: user_ids)
+ .group(:user_id)
+ .maximum(:access_level)
end
-
- # Look up only the IDs we need
- user_ids = user_ids - access.keys
-
- return access if user_ids.empty?
-
- users_access = project.project_authorizations
- .where(user: user_ids)
- .group(:user_id)
- .maximum(:access_level)
-
- access.merge!(users_access)
-
- missing_user_ids = user_ids - users_access.keys
-
- missing_user_ids.each do |user_id|
- access[user_id] = Gitlab::Access::NO_ACCESS
- end
-
- access
end
def max_member_access(user_id)
- max_member_access_for_user_ids([user_id])[user_id] || Gitlab::Access::NO_ACCESS
+ max_member_access_for_user_ids([user_id])[user_id]
end
private
diff --git a/app/models/redirect_route.rb b/app/models/redirect_route.rb
index 31de204d824..20532527346 100644
--- a/app/models/redirect_route.rb
+++ b/app/models/redirect_route.rb
@@ -17,4 +17,32 @@ class RedirectRoute < ActiveRecord::Base
where(wheres, path, "#{sanitize_sql_like(path)}/%")
end
+
+ scope :permanent, -> do
+ if column_permanent_exists?
+ where(permanent: true)
+ else
+ none
+ end
+ end
+
+ scope :temporary, -> do
+ if column_permanent_exists?
+ where(permanent: [false, nil])
+ else
+ all
+ end
+ end
+
+ default_value_for :permanent, false
+
+ def permanent=(value)
+ if self.class.column_permanent_exists?
+ super
+ end
+ end
+
+ def self.column_permanent_exists?
+ ActiveRecord::Base.connection.column_exists?(:redirect_routes, :permanent)
+ end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 82af299ec5e..751306188a0 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -37,7 +37,7 @@ class Repository
issue_template_names merge_request_template_names).freeze
# Methods that use cache_method but only memoize the value
- MEMOIZED_CACHED_METHODS = %i(license empty_repo?).freeze
+ MEMOIZED_CACHED_METHODS = %i(license).freeze
# Certain method caches should be refreshed when certain types of files are
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
@@ -497,7 +497,11 @@ class Repository
end
cache_method :exists?
- delegate :empty?, to: :raw_repository
+ def empty?
+ return true unless exists?
+
+ !has_visible_content?
+ end
cache_method :empty?
# The size of this repository in megabytes.
@@ -944,13 +948,8 @@ class Repository
end
end
- def empty_repo?
- !exists? || !has_visible_content?
- end
- cache_method :empty_repo?, memoize_only: true
-
def search_files_by_content(query, ref)
- return [] if empty_repo? || query.blank?
+ return [] if empty? || query.blank?
offset = 2
args = %W(grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
@@ -959,7 +958,7 @@ class Repository
end
def search_files_by_name(query, ref)
- return [] if empty_repo? || query.blank?
+ return [] if empty? || query.blank?
args = %W(ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)})
diff --git a/app/models/route.rb b/app/models/route.rb
index 97e8a6ad9e9..7ba3ec06041 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -8,6 +8,8 @@ class Route < ActiveRecord::Base
presence: true,
uniqueness: { case_sensitive: false }
+ validate :ensure_permanent_paths
+
after_create :delete_conflicting_redirects
after_update :delete_conflicting_redirects, if: :path_changed?
after_update :create_redirect_for_old_path
@@ -40,7 +42,7 @@ class Route < ActiveRecord::Base
# We are not calling route.delete_conflicting_redirects here, in hopes
# of avoiding deadlocks. The parent (self, in this method) already
# called it, which deletes conflicts for all descendants.
- route.create_redirect(old_path) if attributes[:path]
+ route.create_redirect(old_path, permanent: permanent_redirect?) if attributes[:path]
end
end
end
@@ -50,16 +52,30 @@ class Route < ActiveRecord::Base
end
def conflicting_redirects
- RedirectRoute.matching_path_and_descendants(path)
+ RedirectRoute.temporary.matching_path_and_descendants(path)
end
- def create_redirect(path)
- RedirectRoute.create(source: source, path: path)
+ def create_redirect(path, permanent: false)
+ RedirectRoute.create(source: source, path: path, permanent: permanent)
end
private
def create_redirect_for_old_path
- create_redirect(path_was) if path_changed?
+ create_redirect(path_was, permanent: permanent_redirect?) if path_changed?
+ end
+
+ def permanent_redirect?
+ source_type != "Project"
+ end
+
+ def ensure_permanent_paths
+ return if path.nil?
+
+ errors.add(:path, "#{path} has been taken before. Please use another one") if conflicting_redirect_exists?
+ end
+
+ def conflicting_redirect_exists?
+ RedirectRoute.permanent.matching_path_and_descendants(path).exists?
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 38ee4ed50c1..af1c36d9c93 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -17,6 +17,7 @@ class User < ActiveRecord::Base
include FeatureGate
include CreatedAtFilterable
include IgnorableColumn
+ include BulkMemberAccessLoad
DEFAULT_NOTIFICATION_LEVEL = :participating
@@ -1144,6 +1145,34 @@ class User < ActiveRecord::Base
super
end
+ # Determine the maximum access level for a group of projects in bulk.
+ #
+ # Returns a Hash mapping project ID -> maximum access level.
+ def max_member_access_for_project_ids(project_ids)
+ max_member_access_for_resource_ids(Project, project_ids) do |project_ids|
+ project_authorizations.where(project: project_ids)
+ .group(:project_id)
+ .maximum(:access_level)
+ end
+ end
+
+ def max_member_access_for_project(project_id)
+ max_member_access_for_project_ids([project_id])[project_id]
+ end
+
+ # Determine the maximum access level for a group of groups in bulk.
+ #
+ # Returns a Hash mapping project ID -> maximum access level.
+ def max_member_access_for_group_ids(group_ids)
+ max_member_access_for_resource_ids(Group, group_ids) do |group_ids|
+ group_members.where(source: group_ids).group(:source_id).maximum(:access_level)
+ end
+ end
+
+ def max_member_access_for_group(group_id)
+ max_member_access_for_group_ids([group_id])[group_id]
+ end
+
protected
# override, from Devise::Validatable
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index a2518bc1080..d2d45e402b0 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -30,7 +30,12 @@ class GroupPolicy < BasePolicy
rule { public_group } .enable :read_group
rule { logged_in_viewable }.enable :read_group
- rule { guest } .enable :read_group
+
+ rule { guest }.policy do
+ enable :read_group
+ enable :upload_file
+ end
+
rule { admin } .enable :read_group
rule { has_projects } .enable :read_group
diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index 01cb59d0d44..a424da5ab24 100644
--- a/app/presenters/clusters/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -5,5 +5,9 @@ module Clusters
def gke_cluster_url
"https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp?
end
+
+ def can_toggle_cluster?
+ can?(current_user, :update_cluster, cluster) && created?
+ end
end
end
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index a25882cbb62..ab4c87c0169 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -163,7 +163,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
def pipeline
- @pipeline ||= head_pipeline
+ @pipeline ||= actual_head_pipeline
end
def issues_sentence(project, issues)
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
index b53a49fe59e..eece9445dca 100644
--- a/app/serializers/merge_request_entity.rb
+++ b/app/serializers/merge_request_entity.rb
@@ -33,7 +33,7 @@ class MergeRequestEntity < IssuableEntity
end
expose :merge_commit_message
- expose :head_pipeline, with: PipelineDetailsEntity, as: :pipeline
+ expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline
# Booleans
expose :merge_ongoing?, as: :merge_ongoing
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index d85d93e251b..6078fe38064 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -54,10 +54,11 @@ module Boards
def without_board_labels(issues)
return issues unless board_label_ids.any?
- issues.where.not(
- LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id")
- .where(label_id: board_label_ids).limit(1).arel.exists
- )
+ issues.where.not(issues_label_links.limit(1).arel.exists)
+ end
+
+ def issues_label_links
+ LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id").where(label_id: board_label_ids)
end
def with_list_label(issues)
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 7b9ea223d26..85db2760e23 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -12,24 +12,25 @@ module Ci
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, &block)
@pipeline = Ci::Pipeline.new
- command = OpenStruct.new(source: source,
- origin_ref: params[:ref],
- checkout_sha: params[:checkout_sha],
- after_sha: params[:after],
- before_sha: params[:before],
- trigger_request: trigger_request,
- schedule: schedule,
- ignore_skip_ci: ignore_skip_ci,
- save_incompleted: save_on_errors,
- seeds_block: block,
- project: project,
- current_user: current_user)
+ command = Gitlab::Ci::Pipeline::Chain::Command.new(
+ source: source,
+ origin_ref: params[:ref],
+ checkout_sha: params[:checkout_sha],
+ after_sha: params[:after],
+ before_sha: params[:before],
+ trigger_request: trigger_request,
+ schedule: schedule,
+ ignore_skip_ci: ignore_skip_ci,
+ save_incompleted: save_on_errors,
+ seeds_block: block,
+ project: project,
+ current_user: current_user)
sequence = Gitlab::Ci::Pipeline::Chain::Sequence
.new(pipeline, command, SEQUENCE)
sequence.build! do |pipeline, sequence|
- update_merge_requests_head_pipeline if pipeline.persisted?
+ schedule_head_pipeline_update
if sequence.complete?
cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
@@ -38,15 +39,18 @@ module Ci
pipeline.process!
end
end
+
+ pipeline
end
private
- def update_merge_requests_head_pipeline
- return unless pipeline.latest?
+ def commit
+ @commit ||= project.commit(origin_sha || origin_ref)
+ end
- MergeRequest.where(source_project: @pipeline.project, source_branch: @pipeline.ref)
- .update_all(head_pipeline_id: @pipeline.id)
+ def sha
+ commit.try(:id)
end
def cancel_pending_pipelines
@@ -69,5 +73,15 @@ module Ci
@pipeline_created_counter ||= Gitlab::Metrics
.counter(:pipelines_created_total, "Counter of pipelines created")
end
+
+ def schedule_head_pipeline_update
+ related_merge_requests.each do |merge_request|
+ UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
+ end
+ end
+
+ def related_merge_requests
+ MergeRequest.where(source_project: pipeline.project, source_branch: pipeline.ref)
+ end
end
end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index b8db709211a..c8b6450c9b5 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -22,6 +22,16 @@ module Ci
valid = true
+ if Feature.enabled?('ci_job_request_with_tags_matcher')
+ # pick builds that does not have other tags than runner's one
+ builds = builds.matches_tag_ids(runner.tags.ids)
+
+ # pick builds that have at least one tag
+ unless runner.run_untagged?
+ builds = builds.with_any_tags
+ end
+ end
+
builds.find do |build|
next unless runner.can_pick?(build)
@@ -44,6 +54,9 @@ module Ci
# we still have to return 409 in the end,
# to make sure that this is properly handled by runner.
valid = false
+ rescue Ci::Build::MissingDependenciesError
+ build.drop!(:missing_dependency_failure)
+ valid = false
end
end
diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb
index 7b697f6d807..0471b0f17a2 100644
--- a/app/services/clusters/create_service.rb
+++ b/app/services/clusters/create_service.rb
@@ -5,6 +5,8 @@ module Clusters
def execute(access_token = nil)
@access_token = access_token
+ raise ArgumentError.new('Instance does not support multiple clusters') unless can_create_cluster?
+
create_cluster.tap do |cluster|
ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
end
@@ -25,5 +27,9 @@ module Clusters
@cluster_params = params.merge(user: current_user, projects: [project])
end
+
+ def can_create_cluster?
+ project.clusters.empty?
+ end
end
end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index bf3d4855122..9f05535d4d4 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -6,7 +6,7 @@ module MergeRequests
@oldrev, @newrev = oldrev, newrev
@branch_name = Gitlab::Git.ref_name(ref)
- find_new_commits
+ Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits))
# Be sure to close outstanding MRs before reloading them to avoid generating an
# empty diff during a manual merge
close_merge_requests
@@ -76,6 +76,7 @@ module MergeRequests
end
merge_request.mark_as_unchecked
+ UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
end
end
diff --git a/app/services/metrics_service.rb b/app/services/metrics_service.rb
index 6b3939aeba5..236e9fe8c44 100644
--- a/app/services/metrics_service.rb
+++ b/app/services/metrics_service.rb
@@ -20,7 +20,7 @@ class MetricsService
end
def metrics_text
- "#{health_metrics_text}#{prometheus_metrics_text}"
+ prometheus_metrics_text.concat(health_metrics_text)
end
private
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index 724a77c873a..1ae2c40872a 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -20,8 +20,23 @@ module Projects
MergeRequestsFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
end
- def labels
- LabelsFinder.new(current_user, project_id: project.id).execute.select([:title, :color])
+ def labels(target = nil)
+ labels = LabelsFinder.new(current_user, project_id: project.id).execute.select([:color, :title])
+
+ return labels unless target&.respond_to?(:labels)
+
+ issuable_label_titles = target.labels.pluck(:title)
+
+ if issuable_label_titles
+ labels = labels.as_json(only: [:title, :color])
+
+ issuable_label_titles.each do |issuable_label_title|
+ found_label = labels.find { |label| label['title'] == issuable_label_title }
+ found_label[:set] = true if found_label
+ end
+ end
+
+ labels
end
def commands(noteable, type)
@@ -33,7 +48,7 @@ module Projects
@project.merge_requests.build
end
- return [] unless noteable && noteable.is_a?(Issuable)
+ return [] unless noteable&.is_a?(Issuable)
opts = {
project: project,
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index eb5cce5ab98..03be7039b2a 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -1,6 +1,24 @@
module Projects
class ForkService < BaseService
- def execute
+ def execute(fork_to_project = nil)
+ if fork_to_project
+ link_existing_project(fork_to_project)
+ else
+ fork_new_project
+ end
+ end
+
+ private
+
+ def link_existing_project(fork_to_project)
+ return if fork_to_project.forked?
+
+ link_fork_network(fork_to_project)
+
+ fork_to_project
+ end
+
+ def fork_new_project
new_params = {
forked_from_project_id: @project.id,
visibility_level: allowed_visibility_level,
@@ -21,15 +39,11 @@ module Projects
builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
- refresh_forks_count
-
link_fork_network(new_project)
new_project
end
- private
-
def fork_network
if @project.fork_network
@project.fork_network
@@ -43,9 +57,17 @@ module Projects
end
end
- def link_fork_network(new_project)
- fork_network.fork_network_members.create(project: new_project,
+ def link_fork_network(fork_to_project)
+ fork_network.fork_network_members.create(project: fork_to_project,
forked_from_project: @project)
+
+ # TODO: remove this when ForkedProjectLink model is removed
+ unless fork_to_project.forked_project_link
+ fork_to_project.create_forked_project_link(forked_to_project: fork_to_project,
+ forked_from_project: @project)
+ end
+
+ refresh_forks_count
end
def refresh_forks_count
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 72eecc61c96..ff4c73c886e 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -15,7 +15,7 @@ module Projects
return error("Could not set the default branch") unless project.change_head(params[:default_branch])
end
- if project.update_attributes(update_params)
+ if project.update_attributes(params.except(:default_branch))
if project.previous_changes.include?('path')
project.rename_repo
else
@@ -32,15 +32,13 @@ module Projects
end
def run_auto_devops_pipeline?
- params.dig(:run_auto_devops_pipeline_explicit) == 'true' || params.dig(:run_auto_devops_pipeline_implicit) == 'true'
+ return false if project.repository.gitlab_ci_yml || !project.auto_devops.previous_changes.include?('enabled')
+
+ project.auto_devops.enabled? || (project.auto_devops.enabled.nil? && current_application_settings.auto_devops_enabled?)
end
private
- def update_params
- params.except(:default_branch, :run_auto_devops_pipeline_explicit, :run_auto_devops_pipeline_implicit)
- end
-
def renaming_project_with_container_registry_tags?
new_path = params[:path]
diff --git a/app/services/protected_branches/access_level_params.rb b/app/services/protected_branches/access_level_params.rb
new file mode 100644
index 00000000000..253ae8b0124
--- /dev/null
+++ b/app/services/protected_branches/access_level_params.rb
@@ -0,0 +1,33 @@
+module ProtectedBranches
+ class AccessLevelParams
+ attr_reader :type, :params
+
+ def initialize(type, params)
+ @type = type
+ @params = params_with_default(params)
+ end
+
+ def access_levels
+ ce_style_access_level
+ end
+
+ private
+
+ def params_with_default(params)
+ params[:"#{type}_access_level"] ||= Gitlab::Access::MASTER if use_default_access_level?(params)
+ params
+ end
+
+ def use_default_access_level?(params)
+ true
+ end
+
+ def ce_style_access_level
+ access_level = params[:"#{type}_access_level"]
+
+ return [] unless access_level
+
+ [{ access_level: access_level }]
+ end
+ end
+end
diff --git a/app/services/protected_branches/api_service.rb b/app/services/protected_branches/api_service.rb
new file mode 100644
index 00000000000..4b40200644b
--- /dev/null
+++ b/app/services/protected_branches/api_service.rb
@@ -0,0 +1,24 @@
+module ProtectedBranches
+ class ApiService < BaseService
+ def create
+ @push_params = AccessLevelParams.new(:push, params)
+ @merge_params = AccessLevelParams.new(:merge, params)
+
+ verify_params!
+
+ protected_branch_params = {
+ name: params[:name],
+ push_access_levels_attributes: @push_params.access_levels,
+ merge_access_levels_attributes: @merge_params.access_levels
+ }
+
+ ::ProtectedBranches::CreateService.new(@project, @current_user, protected_branch_params).execute
+ end
+
+ private
+
+ def verify_params!
+ # EE-only
+ end
+ end
+end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 71658df5b41..0b591e3bbbb 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -29,11 +29,11 @@ class FileUploader < GitlabUploader
# model - Object that responds to `full_path` and `disk_path`
#
# Returns a String without a trailing slash
- def self.dynamic_path_segment(project)
- if project.hashed_storage?(:attachments)
- dynamic_path_builder(project.disk_path)
+ def self.dynamic_path_segment(model)
+ if model.hashed_storage?(:attachments)
+ dynamic_path_builder(model.disk_path)
else
- dynamic_path_builder(project.full_path)
+ dynamic_path_builder(model.full_path)
end
end
diff --git a/app/uploaders/namespace_file_uploader.rb b/app/uploaders/namespace_file_uploader.rb
new file mode 100644
index 00000000000..672126e9ec2
--- /dev/null
+++ b/app/uploaders/namespace_file_uploader.rb
@@ -0,0 +1,15 @@
+class NamespaceFileUploader < FileUploader
+ def self.base_dir
+ File.join(root_dir, '-', 'system', 'namespace')
+ end
+
+ def self.dynamic_path_segment(model)
+ dynamic_path_builder(model.id.to_s)
+ end
+
+ private
+
+ def secure_url
+ File.join('/uploads', @secret, file.filename)
+ end
+end
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index 4a2238fe277..15bda97c3b5 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -1,6 +1,23 @@
= form_for @appearance, url: admin_appearances_path, html: { class: 'form-horizontal'} do |f|
= form_errors(@appearance)
+ %fieldset.app_logo
+ %legend
+ Navigation bar:
+ .form-group
+ = f.label :header_logo, 'Header logo', class: 'control-label'
+ .col-sm-10
+ - if @appearance.header_logo?
+ = image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview'
+ - if @appearance.persisted?
+ %br
+ = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo"
+ %hr
+ = f.hidden_field :header_logo_cache
+ = f.file_field :header_logo, class: ""
+ .hint
+ Maximum file size is 1MB. Pages are optimized for a 28px tall header logo
+
%fieldset.sign-in
%legend
Sign in/Sign up pages:
@@ -28,27 +45,22 @@
.hint
Maximum file size is 1MB. Pages are optimized for a 640x360 px logo.
- %fieldset.app_logo
+ %fieldset
%legend
- Navigation bar:
+ New project pages:
.form-group
- = f.label :header_logo, 'Header logo', class: 'control-label'
+ = f.label :new_project_guidelines, class: 'control-label'
.col-sm-10
- - if @appearance.header_logo?
- = image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview'
- - if @appearance.persisted?
- %br
- = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo"
- %hr
- = f.hidden_field :header_logo_cache
- = f.file_field :header_logo, class: ""
+ = f.text_area :new_project_guidelines, class: "form-control", rows: 10
.hint
- Maximum file size is 1MB. Pages are optimized for a 28px tall header logo
+ Guidelines parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}.
.form-actions
= f.submit 'Save', class: 'btn btn-save append-right-10'
- if @appearance.persisted?
- = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
+ Preview last save:
+ = link_to 'Sign-in page', preview_sign_in_admin_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
+ = link_to 'New project page', new_project_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
- if @appearance.updated_at
%span.pull-right
diff --git a/app/views/admin/appearances/preview.html.haml b/app/views/admin/appearances/preview_sign_in.html.haml
index 1af7dd5bb67..1af7dd5bb67 100644
--- a/app/views/admin/appearances/preview.html.haml
+++ b/app/views/admin/appearances/preview_sign_in.html.haml
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index a9d0503bc73..3e2dbb07a6c 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -546,6 +546,12 @@
%fieldset
%legend Git Storage Circuitbreaker settings
.form-group
+ = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :circuitbreaker_check_interval, class: 'form-control'
+ .help-block
+ = circuitbreaker_check_interval_help_text
+ .form-group
= f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :circuitbreaker_access_retries, class: 'form-control'
@@ -558,18 +564,6 @@
.help-block
= circuitbreaker_storage_timeout_help_text
.form-group
- = f.label :circuitbreaker_backoff_threshold, _('Number of failures before backing off'), class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :circuitbreaker_backoff_threshold, class: 'form-control'
- .help-block
- = circuitbreaker_backoff_threshold_help_text
- .form-group
- = f.label :circuitbreaker_failure_wait_time, _('Seconds to wait after a storage failure'), class: 'control-label col-sm-2'
- .col-sm-10
- = f.number_field :circuitbreaker_failure_wait_time, class: 'form-control'
- .help-block
- = circuitbreaker_failure_wait_time_help_text
- .form-group
= f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control'
diff --git a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
index 573a4b93d67..c50b20a83dc 100644
--- a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
+++ b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
@@ -10,7 +10,7 @@
Projects are where you store your code, access issues, wiki and other features of GitLab.
- if current_user.can_create_group?
- = link_to admin_root_path, class: "blank-state-link" do
+ = link_to new_group_path, class: "blank-state-link" do
.blank-state
.blank-state-icon
= custom_icon("add_new_group", size: 50)
diff --git a/app/views/dashboard/projects/_projects.html.haml b/app/views/dashboard/projects/_projects.html.haml
index 0ebd7c01bab..9e0e908e656 100644
--- a/app/views/dashboard/projects/_projects.html.haml
+++ b/app/views/dashboard/projects/_projects.html.haml
@@ -1 +1 @@
-= render 'shared/projects/list', projects: @projects, ci: true
+= render 'shared/projects/list', projects: @projects, ci: true, user: current_user
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index 0f03163a2e8..205320ed87c 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -32,9 +32,17 @@
- elsif discussion.diff_discussion?
on
= conditional_link_to url.present?, url do
- - unless discussion.active?
- an old version of
- the diff
+ - if discussion.on_merge_request_commit?
+ - unless discussion.active?
+ an outdated change in
+ commit
+
+ %span.commit-sha= Commit.truncate_sha(discussion.commit_id)
+ - else
+ - unless discussion.active?
+ an old version of
+ the diff
+
= time_ago_with_tooltip(discussion.created_at, placement: "bottom", html_class: "note-created-ago")
= render "discussions/headline", discussion: discussion
diff --git a/app/views/explore/projects/_projects.html.haml b/app/views/explore/projects/_projects.html.haml
index 708fbc27f55..67f2f897137 100644
--- a/app/views/explore/projects/_projects.html.haml
+++ b/app/views/explore/projects/_projects.html.haml
@@ -1 +1 @@
-= render 'shared/projects/list', projects: projects
+= render 'shared/projects/list', projects: projects, user: current_user
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index fe0ec35d003..4276e6ee4bb 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -10,7 +10,7 @@
members: "#{members_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}",
issues: "#{issues_project_autocomplete_sources_path(project)}",
mergeRequests: "#{merge_requests_project_autocomplete_sources_path(project)}",
- labels: "#{labels_project_autocomplete_sources_path(project)}",
+ labels: "#{labels_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}",
milestones: "#{milestones_project_autocomplete_sources_path(project)}",
commands: "#{commands_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}"
};
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 1fd301d6850..25ed610466a 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -2,6 +2,7 @@
- if defined?(nav) && nav
= render "layouts/nav/sidebar/#{nav}"
.content-wrapper.page-with-new-nav
+ = render 'shared/outdated_browser'
.mobile-overlay
.alert-wrapper
= render "layouts/broadcast"
diff --git a/app/views/layouts/_recaptcha_verification.html.haml b/app/views/layouts/_recaptcha_verification.html.haml
index 77c77dc6754..e6f87ddd383 100644
--- a/app/views/layouts/_recaptcha_verification.html.haml
+++ b/app/views/layouts/_recaptcha_verification.html.haml
@@ -1,5 +1,4 @@
- humanized_resource_name = spammable.class.model_name.human.downcase
-- resource_name = spammable.class.model_name.singular
%h3.page-title
Anti-spam verification
@@ -8,16 +7,4 @@
%p
#{"We detected potential spam in the #{humanized_resource_name}. Please solve the reCAPTCHA to proceed."}
-= form_for form do |f|
- .recaptcha
- - params[resource_name].each do |field, value|
- = hidden_field(resource_name, field, value: value)
- = hidden_field_tag(:spam_log_id, spammable.spam_log.id)
- = hidden_field_tag(:recaptcha_verification, true)
- = recaptcha_tags
-
- -# Yields a block with given extra params.
- = yield
-
- .row-content-block.footer-block
- = f.submit "Submit #{humanized_resource_name}", class: 'btn btn-create'
+= render 'shared/recaptcha_form', spammable: spammable
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 4c5cc249159..52587760ba4 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -13,7 +13,15 @@
.location-badge= label
.search-input-wrap
.dropdown{ data: { url: search_autocomplete_path } }
- = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }, aria: { label: 'Search' }
+ = search_field_tag 'search', nil, placeholder: 'Search',
+ class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options',
+ spellcheck: false,
+ tabindex: '1',
+ autocomplete: 'off',
+ data: { issues_path: issues_dashboard_path,
+ mr_path: merge_requests_dashboard_path },
+ aria: { label: 'Search' }
+ %button.hidden.js-dropdown-search-toggle{ type: 'button', data: { toggle: 'dropdown' } }
.dropdown-menu.dropdown-select
= dropdown_content do
%ul
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 97016d28535..691d2528022 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -15,8 +15,8 @@
.col-sm-7.brand-holder.pull-left
%h1
= brand_title
- - if brand_item
= brand_image
+ - if brand_item&.description?
= brand_text
- else
%h3 Open source software to collaborate on code
diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml
index 08bd6fc311e..bfbfeee7c4b 100644
--- a/app/views/layouts/group.html.haml
+++ b/app/views/layouts/group.html.haml
@@ -4,4 +4,10 @@
- nav "group"
- @left_sidebar = true
+- content_for :page_specific_javascripts do
+ - if current_user
+ -# haml-lint:disable InlineJavaScript
+ :javascript
+ window.uploads_path = "#{group_uploads_path(@group)}";
+
= render template: "layouts/application"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index e2407f6a428..99e7f3b568d 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -75,5 +75,3 @@
%span.sr-only Toggle navigation
= sprite_icon('more', size: 12, css_class: 'more-icon js-navbar-toggle-right')
= sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left')
-
-= render 'shared/outdated_browser'
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 8b2d2a5c74d..53a9162b703 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -187,7 +187,7 @@
= nav_link(controller: [:clusters, :user, :gcp]) do
= link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do
%span
- Cluster
+ Clusters
- if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
= nav_link(path: 'pipelines#charts') do
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 26c2e4c5936..f445e5a2417 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -15,14 +15,13 @@
They are the only accepted password when you have Two-Factor Authentication (2FA) enabled.
.col-lg-8
-
- - if flash[:personal_access_token]
+ - if @new_personal_access_token
.created-personal-access-token-container
%h5.prepend-top-0
Your New Personal Access Token
.form-group
- = text_field_tag 'created-personal-access-token', flash[:personal_access_token], readonly: true, class: "form-control js-select-on-focus", 'aria-describedby' => "created-personal-access-token-help-block"
- = clipboard_button(text: flash[:personal_access_token], title: "Copy personal access token to clipboard", placement: "left")
+ = text_field_tag 'created-personal-access-token', @new_personal_access_token, readonly: true, class: "form-control js-select-on-focus", 'aria-describedby' => "created-personal-access-token-help-block"
+ = clipboard_button(text: @new_personal_access_token, title: "Copy personal access token to clipboard", placement: "left")
%span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again.
%hr
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index a9431cc4956..c5e3a7945bd 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -2,7 +2,7 @@
- if defined?(@merge_request) && @merge_request.discussion_locked?
.issuable-note-warning
- = icon('lock', class: 'icon')
+ = sprite_icon('lock', size: 16, css_class: 'icon')
%span
= _('This merge request is locked.')
= _('Only project members can comment.')
@@ -25,7 +25,7 @@
= markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" })
= markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" })
= markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })
- %button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } }
+ %button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: "Go full screen", data: { container: "body" } }
= sprite_icon("screen-full")
.md-write-holder
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index c1842527480..86510b8ab93 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -14,7 +14,7 @@
%td.branch-commit
- if can?(current_user, :read_build, job)
- = link_to project_job_url(job.project, job) do
+ = link_to project_job_path(job.project, job) do
%span.build-link ##{job.id}
- else
%span.build-link ##{job.id}
diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/projects/clusters/_advanced_settings.html.haml
index 2b3095eb94b..7032b892029 100644
--- a/app/views/projects/clusters/_advanced_settings.html.haml
+++ b/app/views/projects/clusters/_advanced_settings.html.haml
@@ -2,14 +2,14 @@
- if @cluster.managed?
.append-bottom-20
%label.append-bottom-10
- = s_('ClusterIntegration|Google Container Engine')
+ = s_('ClusterIntegration|Google Kubernetes Engine')
%p
- - link_gke = link_to(s_('ClusterIntegration|Google Container Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
+ - link_gke = link_to(s_('ClusterIntegration|Google Kubernetes Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
.well.form-group
%label.text-danger
= s_('ClusterIntegration|Remove cluster integration')
%p
- = s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Container Engine.')
- = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Container Engine"})
+ = s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine.')
+ = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Kubernetes Engine"})
diff --git a/app/views/projects/clusters/_banner.html.haml b/app/views/projects/clusters/_banner.html.haml
index a1cc66eac92..76a66fb92a2 100644
--- a/app/views/projects/clusters/_banner.html.haml
+++ b/app/views/projects/clusters/_banner.html.haml
@@ -2,14 +2,14 @@
.settings-content
.hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' }
- = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine')
+ = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine')
%p.js-error-reason
.hidden.js-cluster-creating.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' }
- = s_('ClusterIntegration|Cluster is being created on Google Container Engine...')
+ = s_('ClusterIntegration|Cluster is being created on Google Kubernetes Engine...')
.hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' }
- = s_('ClusterIntegration|Cluster was successfully created on Google Container Engine. Refresh the page to see cluster\'s details')
+ = s_('ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster\'s details')
%p
- if @cluster.enabled?
diff --git a/app/views/projects/clusters/_cluster.html.haml b/app/views/projects/clusters/_cluster.html.haml
new file mode 100644
index 00000000000..18ca01d2d49
--- /dev/null
+++ b/app/views/projects/clusters/_cluster.html.haml
@@ -0,0 +1,22 @@
+.gl-responsive-table-row
+ .table-section.section-30
+ .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Cluster")
+ .table-mobile-content
+ = link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster)
+ .table-section.section-30
+ .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment pattern")
+ .table-mobile-content= cluster.environment_scope
+ .table-section.section-30
+ .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Project namespace")
+ .table-mobile-content= cluster.platform_kubernetes&.actual_namespace
+ .table-section.section-10
+ .table-mobile-header{ role: "rowheader" }
+ .table-mobile-content
+ %button{ type: "button",
+ class: "js-toggle-cluster-list project-feature-toggle #{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
+ "aria-label": s_("ClusterIntegration|Toggle Cluster"),
+ disabled: !cluster.can_toggle_cluster?,
+ data: { "enabled-text": s_("ClusterIntegration|Active"),
+ "disabled-text": s_("ClusterIntegration|Inactive"),
+ endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } }
+ = icon("spinner spin", class: "loading-icon")
diff --git a/app/views/projects/clusters/_dropdown.html.haml b/app/views/projects/clusters/_dropdown.html.haml
index 39188c7ca27..e36dd900f8d 100644
--- a/app/views/projects/clusters/_dropdown.html.haml
+++ b/app/views/projects/clusters/_dropdown.html.haml
@@ -7,6 +7,6 @@
= icon('chevron-down')
%ul.dropdown-menu.clusters-dropdown-menu.dropdown-menu-full-width
%li
- = link_to(s_('ClusterIntegration|Create cluster on Google Container Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project))
+ = link_to(s_('ClusterIntegration|Create cluster on Google Kubernetes Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project))
%li
= link_to(s_('ClusterIntegration|Add an existing cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project))
diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/projects/clusters/_empty_state.html.haml
new file mode 100644
index 00000000000..e629cc58b06
--- /dev/null
+++ b/app/views/projects/clusters/_empty_state.html.haml
@@ -0,0 +1,12 @@
+.row.empty-state
+ .col-xs-12
+ .svg-content= image_tag 'illustrations/clusters_empty.svg'
+ .col-xs-12.text-center
+ .text-content
+ %h4= s_('ClusterIntegration|Integrate cluster automation')
+ - link_to_help_page = link_to(s_('ClusterIntegration|Learn more about Clusters'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ %p= s_('ClusterIntegration|Clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page}
+
+ %p
+ = link_to s_('ClusterIntegration|Add cluster'), new_project_cluster_path(@project), class: 'btn btn-success'
+
diff --git a/app/views/projects/clusters/_enabled.html.haml b/app/views/projects/clusters/_enabled.html.haml
index f4d261df8f5..70c677f7856 100644
--- a/app/views/projects/clusters/_enabled.html.haml
+++ b/app/views/projects/clusters/_enabled.html.haml
@@ -5,12 +5,11 @@
= field.hidden_field :enabled, { class: 'js-toggle-input'}
%button{ type: 'button',
- class: "js-toggle-cluster project-feature-toggle #{'checked' unless !@cluster.enabled?} #{'disabled' unless can?(current_user, :update_cluster, @cluster)}",
- 'aria-label': s_('ClusterIntegration|Toggle Cluster'),
+ class: "js-toggle-cluster project-feature-toggle #{'is-checked' unless !@cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
+ "aria-label": s_("ClusterIntegration|Toggle Cluster"),
disabled: !can?(current_user, :update_cluster, @cluster),
- data: { 'enabled-text': 'Enabled', 'disabled-text': 'Disabled' } }
+ data: { "enabled-text": s_("ClusterIntegration|Active"), "disabled-text": s_("ClusterIntegration|Inactive"), } }
- if can?(current_user, :update_cluster, @cluster)
.form-group
= field.submit _('Save'), class: 'btn btn-success'
-
diff --git a/app/views/projects/clusters/_tabs.html.haml b/app/views/projects/clusters/_tabs.html.haml
new file mode 100644
index 00000000000..c8120e806fa
--- /dev/null
+++ b/app/views/projects/clusters/_tabs.html.haml
@@ -0,0 +1,16 @@
+.top-area.scrolling-tabs-container.inner-page-scroll-tabs
+ .fade-left= icon("angle-left")
+ .fade-right= icon("angle-right")
+ %ul.nav-links.scrolling-tabs
+ %li{ class: ('active' if @scope == 'active') }>
+ = link_to project_clusters_path(@project, scope: :active), class: "js-active-tab" do
+ = s_("ClusterIntegration|Active")
+ %span.badge= @active_count
+ %li{ class: ('active' if @scope == 'inactive') }>
+ = link_to project_clusters_path(@project, scope: :inactive), class: "js-inactive-tab" do
+ = s_("ClusterIntegration|Inactive")
+ %span.badge= @inactive_count
+ %li{ class: ('active' if @scope.nil? || @scope == 'all') }>
+ = link_to project_clusters_path(@project), class: "js-all-tab" do
+ = s_("ClusterIntegration|All")
+ %span.badge= @all_count
diff --git a/app/views/projects/clusters/gcp/_header.html.haml b/app/views/projects/clusters/gcp/_header.html.haml
index cddb53c2688..f23d5b80e4f 100644
--- a/app/views/projects/clusters/gcp/_header.html.haml
+++ b/app/views/projects/clusters/gcp/_header.html.haml
@@ -4,11 +4,11 @@
= s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:')
%ul
%li
- - link_to_container_engine = link_to(s_('ClusterIntegration|access to Google Container Engine'), 'https://console.cloud.google.com', target: '_blank', rel: 'noopener noreferrer')
- = s_('ClusterIntegration|Your account must have %{link_to_container_engine}').html_safe % { link_to_container_engine: link_to_container_engine }
+ - link_to_kubernetes_engine = link_to(s_('ClusterIntegration|access to Google Kubernetes Engine'), 'https://console.cloud.google.com', target: '_blank', rel: 'noopener noreferrer')
+ = s_('ClusterIntegration|Your account must have %{link_to_kubernetes_engine}').html_safe % { link_to_kubernetes_engine: link_to_kubernetes_engine }
%li
- - link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/container-engine/docs/quickstart', target: '_blank', rel: 'noopener noreferrer')
+ - link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/kubernetes-engine/docs/quickstart', target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters').html_safe % { link_to_requirements: link_to_requirements }
%li
- - link_to_container_project = link_to(s_('ClusterIntegration|Google Container Engine project'), target: '_blank', rel: 'noopener noreferrer')
+ - link_to_container_project = link_to(s_('ClusterIntegration|Google Kubernetes Engine project'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project }
diff --git a/app/views/projects/clusters/gcp/login.html.haml b/app/views/projects/clusters/gcp/login.html.haml
index 790ba61fd86..e97ce01893a 100644
--- a/app/views/projects/clusters/gcp/login.html.haml
+++ b/app/views/projects/clusters/gcp/login.html.haml
@@ -5,7 +5,7 @@
.col-sm-4
= render 'projects/clusters/sidebar'
.col-sm-8
- = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Container Engine')
+ = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Kubernetes Engine')
= render 'header'
.row
.col-sm-8.col-sm-offset-4.signin-with-google
diff --git a/app/views/projects/clusters/gcp/new.html.haml b/app/views/projects/clusters/gcp/new.html.haml
index 9a79480c82f..8d92fb1e320 100644
--- a/app/views/projects/clusters/gcp/new.html.haml
+++ b/app/views/projects/clusters/gcp/new.html.haml
@@ -5,6 +5,6 @@
.col-sm-4
= render 'projects/clusters/sidebar'
.col-sm-8
- = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Container Engine')
+ = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Kubernetes Engine')
= render 'header'
= render 'form'
diff --git a/app/views/projects/clusters/index.html.haml b/app/views/projects/clusters/index.html.haml
new file mode 100644
index 00000000000..104e39b0e06
--- /dev/null
+++ b/app/views/projects/clusters/index.html.haml
@@ -0,0 +1,24 @@
+- breadcrumb_title "Clusters"
+- page_title "Clusters"
+
+.clusters-container
+ - if !@clusters.empty?
+ = render "tabs"
+ .ci-table.js-clusters-list
+ .gl-responsive-table-row.table-row-header{ role: "row" }
+ .table-section.section-30{ role: "rowheader" }
+ = s_("ClusterIntegration|Cluster")
+ .table-section.section-30{ role: "rowheader" }
+ = s_("ClusterIntegration|Environment pattern")
+ .table-section.section-30{ role: "rowheader" }
+ = s_("ClusterIntegration|Project namespace")
+ .table-section.section-10{ role: "rowheader" }
+ - @clusters.each do |cluster|
+ = render "cluster", cluster: cluster.present(current_user: current_user)
+ = paginate @clusters, theme: "gitlab"
+ - elsif @scope == 'all'
+ = render "empty_state"
+ - else
+ = render "tabs"
+ .prepend-top-20.text-center
+ = s_("ClusterIntegration|There are no clusters to show")
diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml
index 2e5bc34f64a..ddd13f8ea96 100644
--- a/app/views/projects/clusters/new.html.haml
+++ b/app/views/projects/clusters/new.html.haml
@@ -7,7 +7,7 @@
.col-sm-8
%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up cluster integration')
- %p= s_('ClusterIntegration|Create a new cluster on Google Engine right from GitLab')
+ %p= s_('ClusterIntegration|Create a new cluster on Google Kubernetes Engine right from GitLab')
= link_to s_('ClusterIntegration|Create on GKE'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
%p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster')
= link_to s_('ClusterIntegration|Add an existing cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20'
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
index d23efe4d9aa..fe6dacf1f0d 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/projects/clusters/show.html.haml
@@ -1,5 +1,6 @@
- @content_class = "limit-container-width" unless fluid_layout
-- breadcrumb_title "Cluster"
+- add_to_breadcrumbs "Clusters", project_clusters_path(@project)
+- breadcrumb_title @cluster.id
- page_title _("Cluster")
- expanded = Rails.env.test?
@@ -28,7 +29,6 @@
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p= s_('ClusterIntegration|See and edit the details for your cluster')
-
.settings-content
- if @cluster.managed?
= render 'projects/clusters/gcp/show'
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 5f607c2ab25..09934c09865 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -47,7 +47,7 @@
%li= link_to s_("DownloadCommit|Email Patches"), project_commit_path(@project, @commit, format: :patch)
%li= link_to s_("DownloadCommit|Plain Diff"), project_commit_path(@project, @commit, format: :diff)
-.commit-box
+.commit-box{ data: { project_path: project_path(@project) } }
%h3.commit-title
= markdown(@commit.title, pipeline: :single_line, author: @commit.author)
- if @commit.description.present?
@@ -80,3 +80,13 @@
- if last_pipeline.duration
in
= time_interval_in_words last_pipeline.duration
+
+ - if @merge_request
+ .well-segment
+ = icon('info-circle fw')
+
+ This commit is part of merge request
+ = succeed '.' do
+ = link_to @merge_request.to_reference, diffs_project_merge_request_path(@project, @merge_request, commit_id: @commit.id)
+
+ Comments created here will be created in the context of that merge request.
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index abb292f8f27..2890e9d2b65 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -6,6 +6,9 @@
- @content_class = limited_container_width
- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
- page_description @commit.description
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('common_vue')
+ = page_specific_javascript_bundle_tag('diff_notes')
.container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box"
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 1b91a94a9f8..618a6355d23 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -1,7 +1,18 @@
-- ref = local_assigns.fetch(:ref)
-
-- cache_key = [project.full_path, commit.id, current_application_settings, @path.presence, current_controller?(:commits), I18n.locale]
-- cache_key.push(commit.status(ref)) if commit.status(ref)
+- view_details = local_assigns.fetch(:view_details, false)
+- merge_request = local_assigns.fetch(:merge_request, nil)
+- project = local_assigns.fetch(:project) { merge_request&.project }
+- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
+
+- link = commit_path(project, commit, merge_request: merge_request)
+- cache_key = [project.full_path,
+ commit.id,
+ current_application_settings,
+ @path.presence,
+ current_controller?(:commits),
+ merge_request&.iid,
+ view_details,
+ commit.status(ref),
+ I18n.locale].compact
= cache(cache_key, expires_in: 1.day) do
%li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
@@ -11,7 +22,7 @@
.commit-detail
.commit-content
- = link_to_markdown_field(commit, :title, project_commit_path(project, commit.id), class: "commit-row-message item-title")
+ = link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title")
%span.commit-row-message.visible-xs-inline
&middot;
= commit.short_id
@@ -31,8 +42,7 @@
- commit_text = _('%{commit_author_link} committed %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago }
#{ commit_text.html_safe }
-
- .commit-actions.hidden-xs
+ .commit-actions.flex-row.hidden-xs
- if request.xhr?
= render partial: 'projects/commit/signature', object: commit.signature
- else
@@ -41,6 +51,9 @@
- if commit.status(ref)
= render_commit_status(commit, ref: ref)
- = link_to commit.short_id, project_commit_path(project, commit), class: "commit-sha btn btn-transparent btn-link"
+ = link_to commit.short_id, link, class: "commit-sha btn btn-transparent btn-link"
= clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard"))
= link_to_browse_code(project, commit)
+
+ - if view_details && merge_request
+ = link_to "View details", project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "btn btn-default"
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index d14897428d0..ac6852751be 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -1,4 +1,7 @@
-- ref = local_assigns.fetch(:ref)
+- merge_request = local_assigns.fetch(:merge_request, nil)
+- project = local_assigns.fetch(:project) { merge_request&.project }
+- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
+
- commits, hidden = limited_commits(@commits)
- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits|
@@ -8,7 +11,7 @@
%li.commits-row{ data: { day: day } }
%ul.content-list.commit-list.flex-list
- = render partial: 'projects/commits/commit', collection: commits, locals: { project: project, ref: ref }
+ = render partial: 'projects/commits/commit', collection: commits, locals: { project: project, ref: ref, merge_request: merge_request }
- if hidden > 0
%li.alert.alert-warning
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index ef305120525..ab371521840 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -3,7 +3,7 @@
- page_title _("Commits"), @ref
= content_for :meta_tags do
- = auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
+ = auto_discovery_link_tag(:atom, project_commits_path(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
.js-project-commits-show{ 'data-commits-limit' => @limit }
%div{ class: container_class }
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index e0aedcac5e1..56cf16c3778 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -19,4 +19,6 @@
"empty-loading-svg-path": image_path('illustrations/monitoring/loading'),
"empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect'),
"additional-metrics": additional_metrics_project_environment_path(@project, @environment, format: :json),
+ "project-path": project_path(@project),
+ "tags-path": project_tags_path(@project),
"has-metrics": "#{@environment.has_metrics?}", deployment_endpoint: project_environment_deployments_path(@project, @environment, format: :json) } }
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 1eccc0509bd..9779c1985d5 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -14,4 +14,4 @@
notes_path: notes_url,
last_fetched_at: Time.now.to_i,
noteable_data: serialize_issuable(@issue),
- current_user_data: UserSerializer.new.represent(current_user).to_json } }
+ current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } }
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 48410ffee21..2f7aece7440 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -12,8 +12,8 @@
- can_update_issue = can?(current_user, :update_issue, @issue)
- can_report_spam = @issue.submittable_as_spam_by?(current_user)
-.clearfix.detail-page-header
- .issuable-header
+.detail-page-header
+ .detail-page-header-body
.issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) }
= icon('check', class: "hidden-sm hidden-md hidden-lg")
%span.hidden-xs
@@ -22,9 +22,6 @@
= icon('circle-o', class: "hidden-sm hidden-md hidden-lg")
%span.hidden-xs Open
- %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
- = icon('angle-double-left')
-
.issuable-meta
- if @issue.confidential
.issuable-warning-icon.inline= sprite_icon('eye-slash', size: 16, css_class: 'icon')
@@ -32,7 +29,10 @@
.issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon')
= issuable_meta(@issue, @project, "Issue")
- .issuable-actions.js-issuable-actions
+ %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
+ = icon('angle-double-left')
+
+ .detail-page-header-actions.js-issuable-actions
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
Options
@@ -74,10 +74,10 @@
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago')
- #merge-requests{ data: { url: referenced_merge_requests_project_issue_url(@project, @issue) } }
+ #merge-requests{ data: { url: referenced_merge_requests_project_issue_path(@project, @issue) } }
// This element is filled in using JavaScript.
- #related-branches{ data: { url: related_branches_project_issue_url(@project, @issue) } }
+ #related-branches{ data: { url: related_branches_project_issue_path(@project, @issue) } }
// This element is filled in using JavaScript.
.content-block.emoji-block
diff --git a/app/views/projects/merge_requests/_commits.html.haml b/app/views/projects/merge_requests/_commits.html.haml
index 11793919ff7..b414518b597 100644
--- a/app/views/projects/merge_requests/_commits.html.haml
+++ b/app/views/projects/merge_requests/_commits.html.haml
@@ -5,4 +5,4 @@
= custom_icon ('illustration_no_commits')
- else
%ol#commits-list.list-unstyled
- = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch
+ = render "projects/commits/commits", merge_request: @merge_request
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 75b3db7e505..135f9ab0aff 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -4,22 +4,22 @@
.alert.alert-danger
%p The source project of this merge request has been removed.
-.clearfix.detail-page-header
- .issuable-header
+.detail-page-header
+ .detail-page-header-body
.issuable-status-box.status-box{ class: status_box_class(@merge_request) }
= icon(@merge_request.state_icon_name, class: "hidden-sm hidden-md hidden-lg")
%span.hidden-xs
= @merge_request.state_human_name
- %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
- = icon('angle-double-left')
-
.issuable-meta
- if @merge_request.discussion_locked?
.issuable-warning-icon.inline= sprite_icon('lock', size: 16, css_class: 'icon')
= issuable_meta(@merge_request, @project, "Merge request")
- .issuable-actions.js-issuable-actions
+ %a.btn.btn-default.pull-right.visible-xs-block.gutter-toggle.issuable-gutter-toggle.js-sidebar-toggle{ href: "#" }
+ = icon('angle-double-left')
+
+ .detail-page-header-actions.js-issuable-actions
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
Options
diff --git a/app/views/projects/merge_requests/diffs/_commit_widget.html.haml b/app/views/projects/merge_requests/diffs/_commit_widget.html.haml
new file mode 100644
index 00000000000..2e5594f8cbe
--- /dev/null
+++ b/app/views/projects/merge_requests/diffs/_commit_widget.html.haml
@@ -0,0 +1,5 @@
+- if @commit
+ .info-well.hidden-xs.prepend-top-default
+ .well-segment
+ %ul.blob-commit-info
+ = render 'projects/commits/commit', commit: @commit, merge_request: @merge_request, view_details: true
diff --git a/app/views/projects/merge_requests/diffs/_different_base.html.haml b/app/views/projects/merge_requests/diffs/_different_base.html.haml
new file mode 100644
index 00000000000..0e57066f9c9
--- /dev/null
+++ b/app/views/projects/merge_requests/diffs/_different_base.html.haml
@@ -0,0 +1,11 @@
+- if @merge_request_diff && different_base?(@start_version, @merge_request_diff)
+ .mr-version-controls
+ .content-block
+ = icon('info-circle')
+ Selected versions have different base commits.
+ Changes will include
+ = link_to project_compare_path(@project, from: @start_version.base_commit_sha, to: @merge_request_diff.base_commit_sha) do
+ new commits
+ from
+ = succeed '.' do
+ %code.ref-name= @merge_request.target_branch
diff --git a/app/views/projects/merge_requests/diffs/_diffs.html.haml b/app/views/projects/merge_requests/diffs/_diffs.html.haml
index 0d30d6da68f..60c91024b23 100644
--- a/app/views/projects/merge_requests/diffs/_diffs.html.haml
+++ b/app/views/projects/merge_requests/diffs/_diffs.html.haml
@@ -1,5 +1,18 @@
-- if @merge_request_diff.collected? || @merge_request_diff.overflow?
- = render 'projects/merge_requests/diffs/versions'
- = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, merge_request: true
-- elsif @merge_request_diff.empty?
- .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
+= render 'projects/merge_requests/diffs/version_controls'
+= render 'projects/merge_requests/diffs/different_base'
+= render 'projects/merge_requests/diffs/not_all_comments_displayed'
+= render 'projects/merge_requests/diffs/commit_widget'
+
+- if @merge_request_diff&.empty?
+ .nothing-here-block
+ = image_tag 'illustrations/merge_request_changes_empty.svg'
+ = succeed '.' do
+ No changes between
+ %span.ref-name= @merge_request.source_branch
+ and
+ %span.ref-name= @merge_request.target_branch
+ %p= link_to 'Create commit', project_new_blob_path(@project, @merge_request.source_branch), class: 'btn btn-save'
+- else
+ - diff_viewable = @merge_request_diff ? @merge_request_diff.collected? || @merge_request_diff.overflow? : true
+ - if diff_viewable
+ = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, merge_request: true
diff --git a/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml b/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml
new file mode 100644
index 00000000000..529fbb8547a
--- /dev/null
+++ b/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml
@@ -0,0 +1,17 @@
+- if @commit || @start_version || (@merge_request_diff && !@merge_request_diff.latest?)
+ .mr-version-controls
+ .content-block.comments-disabled-notif.clearfix
+ = icon('info-circle')
+ = succeed '.' do
+ - if @commit
+ Only comments from the following commit are shown below
+ - else
+ Not all comments are displayed because you're
+ - if @start_version
+ comparing two versions of the diff
+ - else
+ viewing an old version of the diff
+ .pull-right
+ = link_to diffs_project_merge_request_path(@merge_request.project, @merge_request), class: 'btn btn-sm' do
+ Show latest version
+ = "of the diff" if @commit
diff --git a/app/views/projects/merge_requests/diffs/_versions.html.haml b/app/views/projects/merge_requests/diffs/_version_controls.html.haml
index 9f7152b9824..1c26f0405d2 100644
--- a/app/views/projects/merge_requests/diffs/_versions.html.haml
+++ b/app/views/projects/merge_requests/diffs/_version_controls.html.haml
@@ -1,4 +1,4 @@
-- if @merge_request_diffs.size > 1
+- if @merge_request_diff && @merge_request_diffs.size > 1
.mr-version-controls
.mr-version-menus-container.content-block
Changes between
@@ -71,27 +71,3 @@
(base)
%div
%strong.commit-sha= short_sha(@merge_request_diff.base_commit_sha)
-
- - if different_base?(@start_version, @merge_request_diff)
- .content-block
- = icon('info-circle')
- Selected versions have different base commits.
- Changes will include
- = link_to project_compare_path(@project, from: @start_version.base_commit_sha, to: @merge_request_diff.base_commit_sha) do
- new commits
- from
- = succeed '.' do
- %code= @merge_request.target_branch
-
- - if @start_version || !@merge_request_diff.latest?
- .comments-disabled-notif.content-block
- = icon('info-circle')
- Not all comments are displayed because you're
- - if @start_version
- comparing two versions
- - else
- viewing an old version
- of the diff.
-
- .pull-right
- = link_to 'Show latest version', diffs_project_merge_request_path(@project, @merge_request), class: 'btn btn-sm'
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index d88e3d794d3..abff702fd9d 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -8,7 +8,7 @@
= webpack_bundle_tag('common_vue')
= webpack_bundle_tag('diff_notes')
-.merge-request{ 'data-mr-action': "#{j params[:tab].presence || 'show'}", 'data-url' => merge_request_path(@merge_request, format: :json), 'data-project-path' => project_path(@merge_request.project) }
+.merge-request{ data: { mr_action: j(params[:tab].presence || 'show'), url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project) } }
= render "projects/merge_requests/mr_title"
.merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
@@ -38,21 +38,21 @@
.nav-links.scrolling-tabs
%ul.merge-request-tabs
%li.notes-tab
- = link_to project_merge_request_path(@project, @merge_request), data: { target: 'div#notes', action: 'show', toggle: 'tab' } do
+ = tab_link_for @merge_request, :show, force_link: @commit.present? do
Discussion
%span.badge= @merge_request.related_notes.user.count
- if @merge_request.source_project
%li.commits-tab
- = link_to commits_project_merge_request_path(@project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
+ = tab_link_for @merge_request, :commits do
Commits
%span.badge= @commits_count
- if @pipelines.any?
%li.pipelines-tab
- = link_to pipelines_project_merge_request_path(@project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do
+ = tab_link_for @merge_request, :pipelines do
Pipelines
%span.badge.js-pipelines-mr-count= @pipelines.size
%li.diffs-tab
- = link_to diffs_project_merge_request_path(@project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
+ = tab_link_for @merge_request, :diffs do
Changes
%span.badge= @merge_request.diff_size
#resolve-count-app.line-resolve-all-container.prepend-top-10{ "v-cloak" => true }
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 0a7880ce4cd..2f56630c22e 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -18,6 +18,8 @@
A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), #{link_to 'among other things', help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank'}.
%p
All features are enabled when you create a project, but you can disable the ones you don’t need in the project settings.
+ .md
+ = brand_new_project_guidelines
.col-lg-9.js-toggle-container
%ul.nav-links.gitlab-tabs{ role: 'tablist' }
%li.active{ role: 'presentation' }
@@ -84,7 +86,7 @@
= icon('bug', text: 'Fogbugz')
%div
- if gitea_import_enabled?
- = link_to new_import_gitea_url, class: 'btn import_gitea' do
+ = link_to new_import_gitea_path, class: 'btn import_gitea' do
= custom_icon('go_logo')
Gitea
%div
diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml
index 4961835f12a..5ea653ccad5 100644
--- a/app/views/projects/notes/_actions.html.haml
+++ b/app/views/projects/notes/_actions.html.haml
@@ -3,7 +3,7 @@
%span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project.") }
= issuable_first_contribution_icon
- if access.nonzero?
- %span.note-role.note-role-access= Gitlab::Access.human_access(access)
+ %span.note-role.user-access-role= Gitlab::Access.human_access(access)
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml
index ee4fa663b9f..c63e716180c 100644
--- a/app/views/projects/pipelines_settings/_show.html.haml
+++ b/app/views/projects/pipelines_settings/_show.html.haml
@@ -6,46 +6,35 @@
%h5 Auto DevOps (Beta)
%p
Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.
- This will happen starting with the next event (e.g.: push) that occurs to the project.
= link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md')
- message = auto_devops_warning_message(@project)
- if message
%p.settings-message.text-center
= message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
- .radio.js-auto-devops-enable-radio-wrapper
+ .radio
= form.label :enabled_true do
- = form.radio_button :enabled, 'true', class: 'js-auto-devops-enable-radio'
+ = form.radio_button :enabled, 'true'
%strong Enable Auto DevOps
%br
%span.descr
The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project.
- - if show_run_auto_devops_pipeline_checkbox_for_explicit_setting?(@project)
- .checkbox.hide.js-run-auto-devops-pipeline-checkbox-wrapper
- = label_tag 'project[run_auto_devops_pipeline_explicit]' do
- = check_box_tag 'project[run_auto_devops_pipeline_explicit]', true, false, class: 'js-run-auto-devops-pipeline-checkbox'
- = s_('ProjectSettings|Immediately run a pipeline on the default branch')
- .radio.js-auto-devops-enable-radio-wrapper
+ .radio
= form.label :enabled_false do
- = form.radio_button :enabled, 'false', class: 'js-auto-devops-enable-radio'
+ = form.radio_button :enabled, 'false'
%strong Disable Auto DevOps
%br
%span.descr
An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continuous Integration and Delivery.
- .radio.js-auto-devops-enable-radio-wrapper
+ .radio
= form.label :enabled_ do
- = form.radio_button :enabled, '', class: 'js-auto-devops-enable-radio'
+ = form.radio_button :enabled, ''
%strong Instance default (#{current_application_settings.auto_devops_enabled? ? 'enabled' : 'disabled'})
%br
%span.descr
Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>.
- - if show_run_auto_devops_pipeline_checkbox_for_instance_setting?(@project)
- .checkbox.hide.js-run-auto-devops-pipeline-checkbox-wrapper
- = label_tag 'project[run_auto_devops_pipeline_implicit]' do
- = check_box_tag 'project[run_auto_devops_pipeline_implicit]', true, false, class: 'js-run-auto-devops-pipeline-checkbox'
- = s_('ProjectSettings|Immediately run a pipeline on the default branch')
%p
You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
diff --git a/app/views/projects/tree/_old_tree_header.html.haml b/app/views/projects/tree/_old_tree_header.html.haml
index 3a43dde8052..7f636b7e0e8 100644
--- a/app/views/projects/tree/_old_tree_header.html.haml
+++ b/app/views/projects/tree/_old_tree_header.html.haml
@@ -1,3 +1,8 @@
+- if on_top_of_branch?
+ - addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown' }
+- 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
= link_to project_tree_path(@project, @ref) do
@@ -8,13 +13,10 @@
- if current_user
%li
- - if !on_top_of_branch?
- %span.btn.add-to-tree.disabled.has-tooltip{ title: _("You can only add files when you are on a branch"), data: { container: 'body' } }
- = icon('plus')
- - else
- %span.dropdown
- %a.dropdown-toggle.btn.add-to-tree{ href: '#', "data-toggle" => "dropdown", "data-target" => ".add-to-tree-dropdown" }
- = icon('plus')
+ %a.btn.add-to-tree{ addtotree_toggle_attributes }
+ = sprite_icon('plus', size: 16, css_class: 'pull-left')
+ = sprite_icon('arrow-down', size: 16, css_class: 'pull-left')
+ - if on_top_of_branch?
.add-to-tree-dropdown
%ul.dropdown-menu
- if can_edit_tree?
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index fba08092351..1cba4fc6c41 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -3,7 +3,7 @@
.git-clone-holder.input-group
.input-group-btn
- if allowed_protocols_present?
- .clone-dropdown-btn.btn.btn-static
+ .clone-dropdown-btn.btn
%span
= enabled_project_button(project, enabled_protocol)
- else
diff --git a/app/views/shared/_outdated_browser.html.haml b/app/views/shared/_outdated_browser.html.haml
index c06d1ffa59b..8ddb1b2bc99 100644
--- a/app/views/shared/_outdated_browser.html.haml
+++ b/app/views/shared/_outdated_browser.html.haml
@@ -1,7 +1,8 @@
- if outdated_browser?
- .browser-alert
- GitLab may not work properly because you are using an outdated web browser.
- %br
- Please install a
- = link_to 'supported web browser', help_page_url('install/requirements', anchor: 'supported-web-browsers')
- for a better experience.
+ .flash-container
+ .flash-alert.text-center
+ GitLab may not work properly because you are using an outdated web browser.
+ %br
+ Please install a
+ = link_to 'supported web browser', help_page_path('install/requirements', anchor: 'supported-web-browsers')
+ for a better experience.
diff --git a/app/views/shared/_recaptcha_form.html.haml b/app/views/shared/_recaptcha_form.html.haml
new file mode 100644
index 00000000000..0e816870f15
--- /dev/null
+++ b/app/views/shared/_recaptcha_form.html.haml
@@ -0,0 +1,19 @@
+- resource_name = spammable.class.model_name.singular
+- humanized_resource_name = spammable.class.model_name.human.downcase
+- script = local_assigns.fetch(:script, true)
+- has_submit = local_assigns.fetch(:has_submit, true)
+
+= form_for resource_name, method: :post, html: { class: 'recaptcha-form js-recaptcha-form' } do |f|
+ .recaptcha
+ - params[resource_name].each do |field, value|
+ = hidden_field(resource_name, field, value: value)
+ = hidden_field_tag(:spam_log_id, spammable.spam_log.id)
+ = hidden_field_tag(:recaptcha_verification, true)
+ = recaptcha_tags script: script, callback: 'recaptchaDialogCallback'
+
+ -# Yields a block with given extra params.
+ = yield
+
+ - if has_submit
+ .row-content-block.footer-block
+ = f.submit "Submit #{humanized_resource_name}", class: 'btn btn-create'
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 321d8767d08..90395600d4e 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -1,19 +1,7 @@
-- group_member = local_assigns[:group_member]
-- full_name = true unless local_assigns[:full_name] == false
-- group_name = full_name ? group.full_name : group.name
-- css_class = '' unless local_assigns[:css_class]
-- css_class += " no-description" if group.description.blank?
-
-%li.group-row{ class: css_class }
- - if group_member
- .controls.hidden-xs
- - if can?(current_user, :admin_group, group)
- = link_to edit_group_path(group), class: "btn" do
- = sprite_icon('settings')
-
- = link_to leave_group_group_members_path(group), data: { confirm: leave_confirmation_message(group) }, method: :delete, class: "btn", title: s_("GroupsTree|Leave this group") do
- = icon('sign-out')
+- user = local_assigns.fetch(:user, current_user)
+- access = user&.max_member_access_for_group(group.id)
+%li.group-row{ class: ('no-description' if group.description.blank?) }
.stats
%span
= icon('bookmark')
@@ -30,11 +18,10 @@
= link_to group do
= group_icon(group, class: "avatar s40 hidden-xs")
.title
- = link_to group_name, group, class: 'group-name'
+ = link_to group.full_name, group, class: 'group-name'
- - if group_member
- as
- %span= group_member.human_access
+ - if access&.nonzero?
+ %span.user-access-role= Gitlab::Access.human_access(access)
- if group.description.present?
.description
diff --git a/app/views/shared/groups/_list.html.haml b/app/views/shared/groups/_list.html.haml
index aec8ecd1714..f50a6bd4d6a 100644
--- a/app/views/shared/groups/_list.html.haml
+++ b/app/views/shared/groups/_list.html.haml
@@ -1,6 +1,8 @@
- if groups.any?
+ - user = local_assigns[:user]
+
%ul.content-list
- groups.each_with_index do |group, i|
- = render "shared/groups/group", group: group
+ = render "shared/groups/group", group: group, user: user
- else
.nothing-here-block= s_("GroupsEmptyState|No groups found")
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index c6e18108c7a..e11f778adf5 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -27,7 +27,7 @@
- elsif discussion_locked
.disabled-comment.text-center.prepend-top-default
%span.issuable-note-warning
- %span.icon= sprite_icon('lock', size: 14)
+ = sprite_icon('lock', size: 16, css_class: 'icon')
%span
This
= issuable.class.to_s.titleize.downcase
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 0bedfea3502..e1da05d8f08 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -5,18 +5,20 @@
- forks = false unless local_assigns[:forks] == true
- ci = false unless local_assigns[:ci] == true
- skip_namespace = false unless local_assigns[:skip_namespace] == true
+- user = local_assigns[:user]
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true
- remote = false unless local_assigns[:remote] == true
-- load_pipeline_status(projects)
.js-projects-list-holder
- if any_projects?(projects)
+ - load_pipeline_status(projects)
+
%ul.projects-list
- projects.each_with_index do |project, i|
- css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil
= render "shared/projects/project", project: project, skip_namespace: skip_namespace,
avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar,
- forks: forks, show_last_commit_as_description: show_last_commit_as_description
+ forks: forks, show_last_commit_as_description: show_last_commit_as_description, user: user
- if @private_forks_count && @private_forks_count > 0
%li.project-row.private-forks-notice
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 98bfc7c4d36..003f5fa52eb 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -3,6 +3,8 @@
- forks = false unless local_assigns[:forks] == true
- ci = false unless local_assigns[:ci] == true
- skip_namespace = false unless local_assigns[:skip_namespace] == true
+- user = local_assigns[:user]
+- access = user&.max_member_access_for_project(project.id) unless user.nil?
- css_class = '' unless local_assigns[:css_class]
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
@@ -21,14 +23,19 @@
.project-details
%h3.prepend-top-0.append-bottom-0
= link_to project_path(project), class: 'text-plain' do
- %span.project-full-name
+ %span.project-full-name><
%span.namespace-name
- if project.namespace && !skip_namespace
= project.namespace.human_name
\/
- %span.project-name
+ %span.project-name<
= project.name
+ - if access&.nonzero?
+ -# haml-lint:disable UnnecessaryStringOutput
+ = ' ' # prevent haml from eating the space between elements
+ %span.user-access-role= Gitlab::Access.human_access(access)
+
- if show_last_commit_as_description
.description.prepend-top-5
= link_to_markdown(project.commit.title, project_commit_path(project, project.commit), class: "commit-row-message")
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index 119d189f21d..12df79a28c7 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -1,14 +1,15 @@
-.detail-page-header.clearfix
- .snippet-box.has-tooltip.inline.append-right-5{ title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: "body" } }
- %span.sr-only
- = visibility_level_label(@snippet.visibility_level)
- = visibility_level_icon(@snippet.visibility_level, fw: false)
- %span.creator
- Authored
- = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
- by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")}
+.detail-page-header
+ .detail-page-header-body
+ .snippet-box.has-tooltip.inline.append-right-5{ title: snippet_visibility_level_description(@snippet.visibility_level, @snippet), data: { container: "body" } }
+ %span.sr-only
+ = visibility_level_label(@snippet.visibility_level)
+ = visibility_level_icon(@snippet.visibility_level, fw: false)
+ %span.creator
+ Authored
+ = time_ago_with_tooltip(@snippet.created_at, placement: 'bottom', html_class: 'snippet_updated_ago')
+ by #{link_to_member(@project, @snippet.author, size: 24, author_class: "author item-title", avatar_class: "hidden-xs")}
- .snippet-actions
+ .detail-page-header-actions
- if @snippet.project_id?
= render "projects/snippets/actions"
- else
diff --git a/app/workers/update_head_pipeline_for_merge_request_worker.rb b/app/workers/update_head_pipeline_for_merge_request_worker.rb
new file mode 100644
index 00000000000..0a2e9b63578
--- /dev/null
+++ b/app/workers/update_head_pipeline_for_merge_request_worker.rb
@@ -0,0 +1,15 @@
+class UpdateHeadPipelineForMergeRequestWorker
+ include ApplicationWorker
+
+ sidekiq_options queue: 'pipeline_default'
+
+ def perform(merge_request_id)
+ merge_request = MergeRequest.find(merge_request_id)
+ pipeline = Ci::Pipeline.where(project: merge_request.source_project, ref: merge_request.source_branch).last
+
+ return unless pipeline && pipeline.latest?
+ raise ArgumentError, 'merge request sha does not equal pipeline sha' if merge_request.diff_head_sha != pipeline.sha
+
+ merge_request.update_attribute(:head_pipeline_id, pipeline.id)
+ end
+end
diff --git a/bin/storage_check b/bin/storage_check
new file mode 100755
index 00000000000..5a818732bd1
--- /dev/null
+++ b/bin/storage_check
@@ -0,0 +1,11 @@
+#!/usr/bin/env ruby
+
+require 'optparse'
+require 'net/http'
+require 'json'
+require 'socket'
+require 'logger'
+
+require_relative '../lib/gitlab/storage_check'
+
+Gitlab::StorageCheck::CLI.start!(ARGV)
diff --git a/changelogs/unreleased/15774-fix-39233-500-in-merge-request.yml b/changelogs/unreleased/15774-fix-39233-500-in-merge-request.yml
new file mode 100644
index 00000000000..1179b3f20e6
--- /dev/null
+++ b/changelogs/unreleased/15774-fix-39233-500-in-merge-request.yml
@@ -0,0 +1,5 @@
+---
+title: 'fix #39233 - 500 in merge request'
+merge_request: 15774
+author: Martin Nowak
+type: fixed
diff --git a/changelogs/unreleased/22680-unlabel-slash-command-limit-autocomplete-to-applied-labels.yml b/changelogs/unreleased/22680-unlabel-slash-command-limit-autocomplete-to-applied-labels.yml
new file mode 100644
index 00000000000..6d7f8655282
--- /dev/null
+++ b/changelogs/unreleased/22680-unlabel-slash-command-limit-autocomplete-to-applied-labels.yml
@@ -0,0 +1,5 @@
+---
+title: Limit autocomplete menu to applied labels
+merge_request: 11110
+author: Vitaliy @blackst0ne Klachkov
+type: added
diff --git a/changelogs/unreleased/29483-no-feedback-when-checking-on-checklist-if-potential-spam-was-detected.yml b/changelogs/unreleased/29483-no-feedback-when-checking-on-checklist-if-potential-spam-was-detected.yml
new file mode 100644
index 00000000000..6bfcc5e70de
--- /dev/null
+++ b/changelogs/unreleased/29483-no-feedback-when-checking-on-checklist-if-potential-spam-was-detected.yml
@@ -0,0 +1,5 @@
+---
+title: Add recaptcha modal to issue updates detected as spam
+merge_request: 15408
+author:
+type: fixed
diff --git a/changelogs/unreleased/35385-allow-git-pull-push-on-project-redirects.yml b/changelogs/unreleased/35385-allow-git-pull-push-on-project-redirects.yml
new file mode 100644
index 00000000000..31450287caf
--- /dev/null
+++ b/changelogs/unreleased/35385-allow-git-pull-push-on-project-redirects.yml
@@ -0,0 +1,5 @@
+---
+title: Allow git pull/push on group/user/project redirects
+merge_request: 15670
+author:
+type: added
diff --git a/changelogs/unreleased/37354-pipelines-update.yml b/changelogs/unreleased/37354-pipelines-update.yml
new file mode 100644
index 00000000000..2b6ddfe95ed
--- /dev/null
+++ b/changelogs/unreleased/37354-pipelines-update.yml
@@ -0,0 +1,5 @@
+---
+title: Make sure head pippeline always corresponds with the head sha of an MR
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/38032-deploy-markers-should-be-more-verbose.yml b/changelogs/unreleased/38032-deploy-markers-should-be-more-verbose.yml
new file mode 100644
index 00000000000..a1f28b3ba0f
--- /dev/null
+++ b/changelogs/unreleased/38032-deploy-markers-should-be-more-verbose.yml
@@ -0,0 +1,5 @@
+---
+title: Changed the deploy markers on the prometheus dashboard to be more verbose
+merge_request: 38032
+author:
+type: changed
diff --git a/changelogs/unreleased/38869-templates.yml b/changelogs/unreleased/38869-templates.yml
new file mode 100644
index 00000000000..957b5f27bd0
--- /dev/null
+++ b/changelogs/unreleased/38869-templates.yml
@@ -0,0 +1,5 @@
+---
+title: Remove template selector from global namespace
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/39364-in-issue-board-url-doesn-t-take-in-account-hostname-settings.yml b/changelogs/unreleased/39364-in-issue-board-url-doesn-t-take-in-account-hostname-settings.yml
new file mode 100644
index 00000000000..9793c6a8e9c
--- /dev/null
+++ b/changelogs/unreleased/39364-in-issue-board-url-doesn-t-take-in-account-hostname-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Change boards page boards_data absolute urls to paths
+merge_request: 15703
+author:
+type: fixed
diff --git a/changelogs/unreleased/40031-include-assset_sync-gem.yml b/changelogs/unreleased/40031-include-assset_sync-gem.yml
new file mode 100644
index 00000000000..93ce565b32c
--- /dev/null
+++ b/changelogs/unreleased/40031-include-assset_sync-gem.yml
@@ -0,0 +1,5 @@
+---
+title: Add assets_sync gem to Gemfile
+merge_request: 15734
+author:
+type: added
diff --git a/changelogs/unreleased/40508-snippets-zen-mode.yml b/changelogs/unreleased/40508-snippets-zen-mode.yml
new file mode 100644
index 00000000000..c205218575b
--- /dev/null
+++ b/changelogs/unreleased/40508-snippets-zen-mode.yml
@@ -0,0 +1,5 @@
+---
+title: Init zen mode in snippets pages
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/40555-replace-absolute-urls-with-related-branches-to-avoid-hostname.yml b/changelogs/unreleased/40555-replace-absolute-urls-with-related-branches-to-avoid-hostname.yml
new file mode 100644
index 00000000000..4f0eaf8472f
--- /dev/null
+++ b/changelogs/unreleased/40555-replace-absolute-urls-with-related-branches-to-avoid-hostname.yml
@@ -0,0 +1,6 @@
+---
+title: Fix related branches/Merge requests failing to load when the hostname setting
+ is changed
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/40573-rename-gke-as-kubernetes-engine.yml b/changelogs/unreleased/40573-rename-gke-as-kubernetes-engine.yml
new file mode 100644
index 00000000000..afbb869bdbb
--- /dev/null
+++ b/changelogs/unreleased/40573-rename-gke-as-kubernetes-engine.yml
@@ -0,0 +1,5 @@
+---
+title: Rename GKE as Kubernetes Engine
+merge_request: 15608
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/40715-updateendpoint-undefined-on-issue-page.yml b/changelogs/unreleased/40715-updateendpoint-undefined-on-issue-page.yml
new file mode 100644
index 00000000000..0328a693354
--- /dev/null
+++ b/changelogs/unreleased/40715-updateendpoint-undefined-on-issue-page.yml
@@ -0,0 +1,5 @@
+---
+title: Fix updateEndpoint undefined error for issue_show app root
+merge_request: 15698
+author:
+type: fixed
diff --git a/changelogs/unreleased/admin-welcome-new-group-link.yml b/changelogs/unreleased/admin-welcome-new-group-link.yml
new file mode 100644
index 00000000000..9c939a0a3ad
--- /dev/null
+++ b/changelogs/unreleased/admin-welcome-new-group-link.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed admin welcome screen new group path
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/anchor-issue-references.yml b/changelogs/unreleased/anchor-issue-references.yml
new file mode 100644
index 00000000000..78896427417
--- /dev/null
+++ b/changelogs/unreleased/anchor-issue-references.yml
@@ -0,0 +1,6 @@
+---
+title: Fix false positive issue references in merge requests caused by header anchor
+ links.
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/bvl-circuitbreaker-process.yml b/changelogs/unreleased/bvl-circuitbreaker-process.yml
new file mode 100644
index 00000000000..595dd13f724
--- /dev/null
+++ b/changelogs/unreleased/bvl-circuitbreaker-process.yml
@@ -0,0 +1,5 @@
+---
+title: Monitor NFS shards for circuitbreaker in a separate process
+merge_request: 15426
+author:
+type: changed
diff --git a/changelogs/unreleased/commit-title-wrapping.yml b/changelogs/unreleased/commit-title-wrapping.yml
new file mode 100644
index 00000000000..65c28b82b8b
--- /dev/null
+++ b/changelogs/unreleased/commit-title-wrapping.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed long commit links not wrapping correctly
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/deploy-keys-loading-icon.yml b/changelogs/unreleased/deploy-keys-loading-icon.yml
new file mode 100644
index 00000000000..e3cb5bc6924
--- /dev/null
+++ b/changelogs/unreleased/deploy-keys-loading-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed deploy keys remove button loading state not resetting
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/dm-commit-diff-discussions-in-mr-context.yml b/changelogs/unreleased/dm-commit-diff-discussions-in-mr-context.yml
new file mode 100644
index 00000000000..1f8b42ea21f
--- /dev/null
+++ b/changelogs/unreleased/dm-commit-diff-discussions-in-mr-context.yml
@@ -0,0 +1,5 @@
+---
+title: Make diff notes created on a commit in a merge request to persist a rebase.
+merge_request: 12148
+author:
+type: added
diff --git a/changelogs/unreleased/docs-add-why-do-i-get-signed-out-authentication-section.yml b/changelogs/unreleased/docs-add-why-do-i-get-signed-out-authentication-section.yml
new file mode 100644
index 00000000000..bc245880ed0
--- /dev/null
+++ b/changelogs/unreleased/docs-add-why-do-i-get-signed-out-authentication-section.yml
@@ -0,0 +1,5 @@
+---
+title: Add docs for why you might be signed out when using the Remember me token
+merge_request: 15756
+author:
+type: other
diff --git a/changelogs/unreleased/feature-custom-text-for-new-projects.yml b/changelogs/unreleased/feature-custom-text-for-new-projects.yml
new file mode 100644
index 00000000000..905d76dab33
--- /dev/null
+++ b/changelogs/unreleased/feature-custom-text-for-new-projects.yml
@@ -0,0 +1,5 @@
+---
+title: Add custom brand text on new project pages
+merge_request: 15541
+author: Markus Koller
+type: changed
diff --git a/changelogs/unreleased/feature-sm-34834-missing-dependency-should-fail-job-2.yml b/changelogs/unreleased/feature-sm-34834-missing-dependency-should-fail-job-2.yml
new file mode 100644
index 00000000000..ab85b8ee515
--- /dev/null
+++ b/changelogs/unreleased/feature-sm-34834-missing-dependency-should-fail-job-2.yml
@@ -0,0 +1,5 @@
+---
+title: Fail jobs if its dependency is missing
+merge_request: 14009
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-event-target-author-preloading.yml b/changelogs/unreleased/fix-event-target-author-preloading.yml
new file mode 100644
index 00000000000..c6154cc0835
--- /dev/null
+++ b/changelogs/unreleased/fix-event-target-author-preloading.yml
@@ -0,0 +1,5 @@
+---
+title: Fix N+1 query when displaying events
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/fix-new-project-guidelines-styling.yml b/changelogs/unreleased/fix-new-project-guidelines-styling.yml
new file mode 100644
index 00000000000..a97f5c485d4
--- /dev/null
+++ b/changelogs/unreleased/fix-new-project-guidelines-styling.yml
@@ -0,0 +1,5 @@
+---
+title: Use Markdown styling for new project guidelines
+merge_request: 15785
+author: Markus Koller
+type: fixed
diff --git a/changelogs/unreleased/fj-40752-forks-api-not-using-services.yml b/changelogs/unreleased/fj-40752-forks-api-not-using-services.yml
new file mode 100644
index 00000000000..cd7b87596e6
--- /dev/null
+++ b/changelogs/unreleased/fj-40752-forks-api-not-using-services.yml
@@ -0,0 +1,5 @@
+---
+title: Using appropiate services in the API for managing forks
+merge_request: 15709
+author:
+type: fixed
diff --git a/changelogs/unreleased/merge-request-lock-icon-size-fix.yml b/changelogs/unreleased/merge-request-lock-icon-size-fix.yml
new file mode 100644
index 00000000000..09c059a3011
--- /dev/null
+++ b/changelogs/unreleased/merge-request-lock-icon-size-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed merge request lock icon size
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/mk-add-old-attachments-to-uploads-table.yml b/changelogs/unreleased/mk-add-old-attachments-to-uploads-table.yml
new file mode 100644
index 00000000000..499543ef883
--- /dev/null
+++ b/changelogs/unreleased/mk-add-old-attachments-to-uploads-table.yml
@@ -0,0 +1,5 @@
+---
+title: Add untracked files to uploads table
+merge_request: 15270
+author:
+type: other
diff --git a/changelogs/unreleased/outdated-browser-position-fix.yml b/changelogs/unreleased/outdated-browser-position-fix.yml
new file mode 100644
index 00000000000..801e45a28b3
--- /dev/null
+++ b/changelogs/unreleased/outdated-browser-position-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed outdated browser flash positioning
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/patch-24.yml b/changelogs/unreleased/patch-24.yml
new file mode 100644
index 00000000000..a670eb3ab56
--- /dev/null
+++ b/changelogs/unreleased/patch-24.yml
@@ -0,0 +1,5 @@
+---
+title: Fix graph notes number duplication.
+merge_request: 15696
+author: Vladislav Kaverin
+type: fixed
diff --git a/changelogs/unreleased/perform-sql-matching-of-tags.yml b/changelogs/unreleased/perform-sql-matching-of-tags.yml
new file mode 100644
index 00000000000..39f8a867a4d
--- /dev/null
+++ b/changelogs/unreleased/perform-sql-matching-of-tags.yml
@@ -0,0 +1,5 @@
+---
+title: Perform SQL matching of Build&Runner tags to greatly speed-up job picking
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/throttle-touching-of-objects.yml b/changelogs/unreleased/throttle-touching-of-objects.yml
new file mode 100644
index 00000000000..0a57bea7c83
--- /dev/null
+++ b/changelogs/unreleased/throttle-touching-of-objects.yml
@@ -0,0 +1,5 @@
+---
+title: Throttle the number of UPDATEs triggered by touch
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/update_mr_changes_empty_page.yml b/changelogs/unreleased/update_mr_changes_empty_page.yml
new file mode 100644
index 00000000000..bae73c21e8f
--- /dev/null
+++ b/changelogs/unreleased/update_mr_changes_empty_page.yml
@@ -0,0 +1,5 @@
+---
+title: Update empty state page of merge request 'changes' tab
+merge_request: 15611
+author: Vitaliy @blackst0ne Klachkov
+type: added
diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb
index 43b1e943897..eb7959e4da6 100644
--- a/config/initializers/7_prometheus_metrics.rb
+++ b/config/initializers/7_prometheus_metrics.rb
@@ -11,14 +11,7 @@ Prometheus::Client.configure do |config|
config.multiprocess_files_dir ||= Rails.root.join('tmp/prometheus_multiproc_dir')
end
- config.pid_provider = -> do
- worker_id = Prometheus::Client::Support::Unicorn.worker_id
- if worker_id.nil?
- "process_pid_#{Process.pid}"
- else
- "worker_id_#{worker_id}"
- end
- end
+ config.pid_provider = Prometheus::Client::Support::Unicorn.method(:worker_pid_provider)
end
Gitlab::Application.configure do |config|
diff --git a/config/initializers/asset_sync.rb b/config/initializers/asset_sync.rb
new file mode 100644
index 00000000000..db8500f6231
--- /dev/null
+++ b/config/initializers/asset_sync.rb
@@ -0,0 +1,31 @@
+AssetSync.configure do |config|
+ # Disable the asset_sync gem by default. If it is enabled, but not configured,
+ # asset_sync will cause the build to fail.
+ config.enabled = if ENV.has_key?('ASSET_SYNC_ENABLED')
+ ENV['ASSET_SYNC_ENABLED'] == 'true'
+ else
+ false
+ end
+
+ # Pulled from https://github.com/AssetSync/asset_sync/blob/v2.2.0/lib/asset_sync/engine.rb#L15-L40
+ # This allows us to disable asset_sync by default and configure through environment variables
+ # Updates to asset_sync gem should be checked
+ config.fog_provider = ENV['FOG_PROVIDER'] if ENV.has_key?('FOG_PROVIDER')
+ config.fog_directory = ENV['FOG_DIRECTORY'] if ENV.has_key?('FOG_DIRECTORY')
+ config.fog_region = ENV['FOG_REGION'] if ENV.has_key?('FOG_REGION')
+
+ config.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID'] if ENV.has_key?('AWS_ACCESS_KEY_ID')
+ config.aws_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY'] if ENV.has_key?('AWS_SECRET_ACCESS_KEY')
+ config.aws_reduced_redundancy = ENV['AWS_REDUCED_REDUNDANCY'] == true if ENV.has_key?('AWS_REDUCED_REDUNDANCY')
+
+ config.rackspace_username = ENV['RACKSPACE_USERNAME'] if ENV.has_key?('RACKSPACE_USERNAME')
+ config.rackspace_api_key = ENV['RACKSPACE_API_KEY'] if ENV.has_key?('RACKSPACE_API_KEY')
+
+ config.google_storage_access_key_id = ENV['GOOGLE_STORAGE_ACCESS_KEY_ID'] if ENV.has_key?('GOOGLE_STORAGE_ACCESS_KEY_ID')
+ config.google_storage_secret_access_key = ENV['GOOGLE_STORAGE_SECRET_ACCESS_KEY'] if ENV.has_key?('GOOGLE_STORAGE_SECRET_ACCESS_KEY')
+
+ config.existing_remote_files = ENV['ASSET_SYNC_EXISTING_REMOTE_FILES'] || "keep"
+
+ config.gzip_compression = (ENV['ASSET_SYNC_GZIP_COMPRESSION'] == 'true') if ENV.has_key?('ASSET_SYNC_GZIP_COMPRESSION')
+ config.manifest = (ENV['ASSET_SYNC_MANIFEST'] == 'true') if ENV.has_key?('ASSET_SYNC_MANIFEST')
+end
diff --git a/config/routes.rb b/config/routes.rb
index 4f27fea0e92..016140e0ede 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -42,6 +42,7 @@ Rails.application.routes.draw do
scope path: '-' do
get 'liveness' => 'health#liveness'
get 'readiness' => 'health#readiness'
+ post 'storage_check' => 'health#storage_check'
resources :metrics, only: [:index]
mount Peek::Railtie => '/peek'
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index c0748231813..e22fb440abc 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -97,7 +97,7 @@ namespace :admin do
resource :appearances, only: [:show, :create, :update], path: 'appearance' do
member do
- get :preview
+ get :preview_sign_in
delete :logo
delete :header_logos
end
diff --git a/config/routes/group.rb b/config/routes/group.rb
index db99e10bb9a..976837a246d 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -49,6 +49,12 @@ constraints(GroupUrlConstrainer.new) do
post :resend_invite, on: :member
delete :leave, on: :collection
end
+
+ resources :uploads, only: [:create] do
+ collection do
+ get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
+ end
+ end
end
scope(path: '*id',
diff --git a/db/migrate/20171103000000_set_uploads_path_size_for_mysql.rb b/db/migrate/20171103000000_set_uploads_path_size_for_mysql.rb
new file mode 100644
index 00000000000..1fbe505f804
--- /dev/null
+++ b/db/migrate/20171103000000_set_uploads_path_size_for_mysql.rb
@@ -0,0 +1,25 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class SetUploadsPathSizeForMysql < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ # We need at least 297 at the moment. For more detail on that number, see:
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/40168#what-is-the-expected-correct-behavior
+ #
+ # Rails + PostgreSQL `string` is equivalent to a `text` field, but
+ # Rails + MySQL `string` is `varchar(255)` by default. Also, note that we
+ # have an upper limit because with a unique index, MySQL has a max key
+ # length of 3072 bytes which seems to correspond to `varchar(1024)`.
+ change_column :uploads, :path, :string, limit: 511
+ end
+
+ def down
+ # It was unspecified, which is varchar(255) by default in Rails for MySQL.
+ change_column :uploads, :path, :string
+ end
+end
diff --git a/db/migrate/20171122131600_add_new_project_guidelines_to_appearances.rb b/db/migrate/20171122131600_add_new_project_guidelines_to_appearances.rb
new file mode 100644
index 00000000000..f141c442d97
--- /dev/null
+++ b/db/migrate/20171122131600_add_new_project_guidelines_to_appearances.rb
@@ -0,0 +1,12 @@
+class AddNewProjectGuidelinesToAppearances < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ change_table :appearances do |t|
+ t.text :new_project_guidelines
+ t.text :new_project_guidelines_html
+ end
+ end
+end
diff --git a/db/migrate/20171123094802_add_circuitbreaker_check_interval_to_application_settings.rb b/db/migrate/20171123094802_add_circuitbreaker_check_interval_to_application_settings.rb
new file mode 100644
index 00000000000..213d46018fc
--- /dev/null
+++ b/db/migrate/20171123094802_add_circuitbreaker_check_interval_to_application_settings.rb
@@ -0,0 +1,20 @@
+class AddCircuitbreakerCheckIntervalToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :application_settings,
+ :circuitbreaker_check_interval,
+ :integer,
+ default: 1
+ end
+
+ def down
+ remove_column :application_settings,
+ :circuitbreaker_check_interval
+ end
+end
diff --git a/db/migrate/20171204204233_add_permanent_to_redirect_route.rb b/db/migrate/20171204204233_add_permanent_to_redirect_route.rb
new file mode 100644
index 00000000000..f3ae471201e
--- /dev/null
+++ b/db/migrate/20171204204233_add_permanent_to_redirect_route.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPermanentToRedirectRoute < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ add_column(:redirect_routes, :permanent, :boolean)
+ end
+
+ def down
+ remove_column(:redirect_routes, :permanent)
+ end
+end
diff --git a/db/migrate/20171206221519_add_permanent_index_to_redirect_route.rb b/db/migrate/20171206221519_add_permanent_index_to_redirect_route.rb
new file mode 100644
index 00000000000..33ce7e1aa68
--- /dev/null
+++ b/db/migrate/20171206221519_add_permanent_index_to_redirect_route.rb
@@ -0,0 +1,19 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPermanentIndexToRedirectRoute < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:redirect_routes, :permanent)
+ end
+
+ def down
+ remove_concurrent_index(:redirect_routes, :permanent) if index_exists?(:redirect_routes, :permanent)
+ end
+end
diff --git a/db/post_migrate/20171103140253_track_untracked_uploads.rb b/db/post_migrate/20171103140253_track_untracked_uploads.rb
new file mode 100644
index 00000000000..548a94d2d38
--- /dev/null
+++ b/db/post_migrate/20171103140253_track_untracked_uploads.rb
@@ -0,0 +1,21 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class TrackUntrackedUploads < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+ MIGRATION = 'PrepareUntrackedUploads'
+
+ def up
+ BackgroundMigrationWorker.perform_async(MIGRATION)
+ end
+
+ def down
+ if table_exists?(:untracked_files_for_uploads)
+ drop_table :untracked_files_for_uploads
+ end
+ end
+end
diff --git a/db/post_migrate/20171123101020_update_circuitbreaker_defaults.rb b/db/post_migrate/20171123101020_update_circuitbreaker_defaults.rb
new file mode 100644
index 00000000000..8e1c9e6d6bb
--- /dev/null
+++ b/db/post_migrate/20171123101020_update_circuitbreaker_defaults.rb
@@ -0,0 +1,34 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class UpdateCircuitbreakerDefaults < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ class ApplicationSetting < ActiveRecord::Base; end
+
+ def up
+ change_column_default :application_settings,
+ :circuitbreaker_failure_count_threshold,
+ 3
+ change_column_default :application_settings,
+ :circuitbreaker_storage_timeout,
+ 15
+
+ ApplicationSetting.update_all(circuitbreaker_failure_count_threshold: 3,
+ circuitbreaker_storage_timeout: 15)
+ end
+
+ def down
+ change_column_default :application_settings,
+ :circuitbreaker_failure_count_threshold,
+ 160
+ change_column_default :application_settings,
+ :circuitbreaker_storage_timeout,
+ 30
+
+ ApplicationSetting.update_all(circuitbreaker_failure_count_threshold: 160,
+ circuitbreaker_storage_timeout: 30)
+ end
+end
diff --git a/db/post_migrate/20171123101046_remove_old_circuitbreaker_config.rb b/db/post_migrate/20171123101046_remove_old_circuitbreaker_config.rb
new file mode 100644
index 00000000000..e646d4d3224
--- /dev/null
+++ b/db/post_migrate/20171123101046_remove_old_circuitbreaker_config.rb
@@ -0,0 +1,26 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveOldCircuitbreakerConfig < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ remove_column :application_settings,
+ :circuitbreaker_backoff_threshold
+ remove_column :application_settings,
+ :circuitbreaker_failure_wait_time
+ end
+
+ def down
+ add_column :application_settings,
+ :circuitbreaker_backoff_threshold,
+ :integer,
+ default: 80
+ add_column :application_settings,
+ :circuitbreaker_failure_wait_time,
+ :integer,
+ default: 30
+ end
+end
diff --git a/db/post_migrate/20171205190711_reschedule_fork_network_creation_caller.rb b/db/post_migrate/20171205190711_reschedule_fork_network_creation_caller.rb
new file mode 100644
index 00000000000..30ff5173192
--- /dev/null
+++ b/db/post_migrate/20171205190711_reschedule_fork_network_creation_caller.rb
@@ -0,0 +1,27 @@
+class RescheduleForkNetworkCreationCaller < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ MIGRATION = 'PopulateForkNetworksRange'.freeze
+ BATCH_SIZE = 100
+ DELAY_INTERVAL = 15.seconds
+
+ disable_ddl_transaction!
+
+ class ForkedProjectLink < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'forked_project_links'
+ end
+
+ def up
+ say 'Populating the `fork_networks` based on existing `forked_project_links`'
+
+ queue_background_migration_jobs_by_range_at_intervals(ForkedProjectLink, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
+ end
+
+ def down
+ # nothing
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 8e5bcc8b51d..c0a141885ad 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20171124150326) do
+ActiveRecord::Schema.define(version: 20171206221519) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -36,6 +36,8 @@ ActiveRecord::Schema.define(version: 20171124150326) do
t.datetime_with_timezone "updated_at", null: false
t.text "description_html"
t.integer "cached_markdown_version"
+ t.text "new_project_guidelines"
+ t.text "new_project_guidelines_html"
end
create_table "application_settings", force: :cascade do |t|
@@ -133,12 +135,10 @@ ActiveRecord::Schema.define(version: 20171124150326) do
t.boolean "hashed_storage_enabled", default: false, null: false
t.boolean "project_export_enabled", default: true, null: false
t.boolean "auto_devops_enabled", default: false, null: false
- t.integer "circuitbreaker_failure_count_threshold", default: 160
- t.integer "circuitbreaker_failure_wait_time", default: 30
+ t.integer "circuitbreaker_failure_count_threshold", default: 3
t.integer "circuitbreaker_failure_reset_time", default: 1800
- t.integer "circuitbreaker_storage_timeout", default: 30
+ t.integer "circuitbreaker_storage_timeout", default: 15
t.integer "circuitbreaker_access_retries", default: 3
- t.integer "circuitbreaker_backoff_threshold", default: 80
t.boolean "throttle_unauthenticated_enabled", default: false, null: false
t.integer "throttle_unauthenticated_requests_per_period", default: 3600, null: false
t.integer "throttle_unauthenticated_period_in_seconds", default: 3600, null: false
@@ -148,6 +148,7 @@ ActiveRecord::Schema.define(version: 20171124150326) do
t.boolean "throttle_authenticated_web_enabled", default: false, null: false
t.integer "throttle_authenticated_web_requests_per_period", default: 7200, null: false
t.integer "throttle_authenticated_web_period_in_seconds", default: 3600, null: false
+ t.integer "circuitbreaker_check_interval", default: 1, null: false
t.boolean "password_authentication_enabled_for_web"
t.boolean "password_authentication_enabled_for_git", default: true
t.integer "gitaly_timeout_default", default: 55, null: false
@@ -1525,10 +1526,12 @@ ActiveRecord::Schema.define(version: 20171124150326) do
t.string "path", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.boolean "permanent"
end
add_index "redirect_routes", ["path"], name: "index_redirect_routes_on_path", unique: true, using: :btree
add_index "redirect_routes", ["path"], name: "index_redirect_routes_on_path_text_pattern_ops", using: :btree, opclasses: {"path"=>"varchar_pattern_ops"}
+ add_index "redirect_routes", ["permanent"], name: "index_redirect_routes_on_permanent", using: :btree
add_index "redirect_routes", ["source_type", "source_id"], name: "index_redirect_routes_on_source_type_and_source_id", using: :btree
create_table "releases", force: :cascade do |t|
@@ -1735,7 +1738,7 @@ ActiveRecord::Schema.define(version: 20171124150326) do
create_table "uploads", force: :cascade do |t|
t.integer "size", limit: 8, null: false
- t.string "path", null: false
+ t.string "path", limit: 511, null: false
t.string "checksum", limit: 64
t.integer "model_id"
t.string "model_type"
diff --git a/doc/README.md b/doc/README.md
index d4119d35162..11d52001440 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -13,13 +13,14 @@ GitLab offers the most scalable Git-based fully integrated platform for software
- **GitLab Community Edition (CE)** is an [opensource product](https://gitlab.com/gitlab-org/gitlab-ce/),
self-hosted, free to use. Every feature available in GitLab CE is also available on GitLab Enterprise Edition (Starter and Premium) and GitLab.com.
- **GitLab Enterprise Edition (EE)** is an [opencore product](https://gitlab.com/gitlab-org/gitlab-ee/),
-self-hosted, fully featured solution of GitLab, available under distinct [subscriptions](https://about.gitlab.com/products/): **GitLab Enterprise Edition Starter (EES)** and **GitLab Enterprise Edition Premium (EEP)**.
+self-hosted, fully featured solution of GitLab, available under distinct [subscriptions](https://about.gitlab.com/products/): **GitLab Enterprise Edition Starter (EES)**, **GitLab Enterprise Edition Premium (EEP)**, and **GitLab Enterprise Edition Ultimate (EEU)**.
- **GitLab.com**: SaaS GitLab solution, with [free and paid subscriptions](https://about.gitlab.com/gitlab-com/). GitLab.com is hosted by GitLab, Inc., and administrated by GitLab (users don't have access to admin settings).
> **GitLab EE** contains all features available in **GitLab CE**,
plus premium features available in each version: **Enterprise Edition Starter**
-(**EES**) and **Enterprise Edition Premium** (**EEP**). Everything available in
-**EES** is also available in **EEP**.
+(**EES**), **Enterprise Edition Premium** (**EEP**), and **Enterprise Edition Ultimate**
+(**EEU**). Everything available in **EES** is also available in **EEP**. Every feature
+available in **EEP** is also available in **EEU**.
----
@@ -32,8 +33,8 @@ Shortcuts to GitLab's most visited docs:
| [Using Docker images](ci/docker/using_docker_images.md) | [GitLab Pages](user/project/pages/index.md) |
- [User documentation](user/index.md)
-- [Administrator documentation](#administrator-documentation)
-- [Technical Articles](articles/index.md)
+- [Administrator documentation](administration/index.md)
+- [Contributor documentation](#contributor-documentation)
## Getting started with GitLab
@@ -133,82 +134,24 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i
## Administrator documentation
-Learn how to administer your GitLab instance. Regular users don't
-have access to GitLab administration tools and settings.
+[Administration documentation](administration/index.md) applies to admin users of GitLab
+self-hosted instances:
-### Install, update, upgrade, migrate
+- GitLab Community Edition
+- GitLab [Enterprise Editions](https://about.gitlab.com/gitlab-ee/)
+ - Enterprise Edition Starter (EES)
+ - Enterprise Edition Premium (EEP)
+ - Enterprise Edition Ultimate (EEU)
-- [Install](install/README.md): Requirements, directory structures and installation from source.
-- [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/): Integrate [Mattermost](https://about.mattermost.com/) with your GitLab installation.
-- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md): If you have an old GitLab installation (older than 8.0), follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
-- [Restart GitLab](administration/restart_gitlab.md): Learn how to restart GitLab and its components.
-- [Update](update/README.md): Update guides to upgrade your installation.
-
-### User permissions
-
-- [Access restrictions](user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols): Define which Git access protocols can be used to talk to GitLab
-- [Authentication/Authorization](topics/authentication/index.md#gitlab-administrators): Enforce 2FA, configure external authentication with LDAP, SAML, CAS and additional Omniauth providers.
-
-### Features
-
-- [Container Registry](administration/container_registry.md): Configure Docker Registry with GitLab.
-- [Custom Git hooks](administration/custom_hooks.md): Custom Git hooks (on the filesystem) for when webhooks aren't enough.
-- [Git LFS configuration](workflow/lfs/lfs_administration.md): Learn how to use LFS under GitLab.
-- [GitLab Pages configuration](administration/pages/index.md): Configure GitLab Pages.
-- [High Availability](administration/high_availability/README.md): Configure multiple servers for scaling or high availability.
-- [User cohorts](user/admin_area/user_cohorts.md): View user activity over time.
-- [Web terminals](administration/integration/terminal.md): Provide terminal access to environments from within GitLab.
-- GitLab CI
- - [CI admin settings](user/admin_area/settings/continuous_integration.md): Define max artifacts size and expiration time.
-
-### Integrations
-
-- [Integrations](integration/README.md): How to integrate with systems such as JIRA, Redmine, Twitter.
-- [Mattermost](user/project/integrations/mattermost.md): Set up GitLab with Mattermost.
-
-### Monitoring
-
-- [GitLab performance monitoring with InfluxDB](administration/monitoring/performance/introduction.md): Configure GitLab and InfluxDB for measuring performance metrics.
-- [GitLab performance monitoring with Prometheus](administration/monitoring/prometheus/index.md): Configure GitLab and Prometheus for measuring performance metrics.
-- [Monitoring uptime](user/admin_area/monitoring/health_check.md): Check the server status using the health check endpoint.
-- [Monitoring GitHub imports](administration/monitoring/github_imports.md)
-
-### Performance
-
-- [Housekeeping](administration/housekeeping.md): Keep your Git repository tidy and fast.
-- [Operations](administration/operations.md): Keeping GitLab up and running.
-- [Polling](administration/polling.md): Configure how often the GitLab UI polls for updates.
-- [Request Profiling](administration/monitoring/performance/request_profiling.md): Get a detailed profile on slow requests.
-- [Performance Bar](administration/monitoring/performance/performance_bar.md): Get performance information for the current page.
-
-### Customization
-
-- [Adjust your instance's timezone](workflow/timezone.md): Customize the default time zone of GitLab.
-- [Environment variables](administration/environment_variables.md): Supported environment variables that can be used to override their defaults values in order to configure GitLab.
-- [Header logo](customization/branded_page_and_email_header.md): Change the logo on the overall page and email header.
-- [Issue closing pattern](administration/issue_closing_pattern.md): Customize how to close an issue from commit messages.
-- [Libravatar](customization/libravatar.md): Use Libravatar instead of Gravatar for user avatars.
-- [Welcome message](customization/welcome_message.md): Add a custom welcome message to the sign-in page.
-
-### Admin tools
-
-- [Gitaly](administration/gitaly/index.md): Configuring Gitaly, GitLab's Git repository storage service
-- [Raketasks](raketasks/README.md): Backups, maintenance, automatic webhook setup and the importing of projects.
- - [Backup and restore](raketasks/backup_restore.md): Backup and restore your GitLab instance.
-- [Reply by email](administration/reply_by_email.md): Allow users to comment on issues and merge requests by replying to notification emails.
-- [Repository checks](administration/repository_checks.md): Periodic Git repository checks.
-- [Repository storage paths](administration/repository_storage_paths.md): Manage the paths used to store repositories.
-- [Security](security/README.md): Learn what you can do to further secure your GitLab instance.
-- [System hooks](system_hooks/system_hooks.md): Notifications when users, projects and keys are changed.
-
-### Troubleshooting
-
-- [Debugging tips](administration/troubleshooting/debug.md): Tips to debug problems when things go wrong
-- [Log system](administration/logs.md): Where to look for logs.
-- [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md): Debug when Sidekiq appears hung and is not processing jobs.
+Learn how to install, configure, update, upgrade, integrate, and maintain your own instance.
+Regular users don't have access to GitLab administration tools and settings.
## Contributor documentation
+GitLab Community Edition is [opensource](https://gitlab.com/gitlab-org/gitlab-ce/)
+and Enterprise Editions are [opencore](https://gitlab.com/gitlab-org/gitlab-ee/).
+Learn how to contribute to GitLab:
+
- [Development](development/README.md): All styleguides and explanations how to contribute.
- [Legal](legal/README.md): Contributor license agreements.
- [Writing documentation](development/writing_documentation.md): Contributing to GitLab Docs.
diff --git a/doc/administration/index.md b/doc/administration/index.md
new file mode 100644
index 00000000000..c8d28d8485a
--- /dev/null
+++ b/doc/administration/index.md
@@ -0,0 +1,121 @@
+# Administrator documentation
+
+Learn how to administer your GitLab instance (Community Edition and
+[Enterprise Editions](https://about.gitlab.com/gitlab-ee/)).
+Regular users don't have access to GitLab administration tools and settings.
+
+GitLab.com is administered by GitLab, Inc., therefore, only GitLab team members have
+access to its admin configurations. If you're a GitLab.com user, please check the
+[user documentation](../user/index.html).
+
+## Installing and maintaining GitLab
+
+Learn how to install, configure, update, and maintain your GitLab instance.
+
+### Installing GitLab
+
+- [Install](../install/README.md): Requirements, directory structures, and installation methods.
+- [High Availability](high_availability/README.md): Configure multiple servers for scaling or high availability.
+
+### Configuring GitLab
+
+- [Adjust your instance's timezone](../workflow/timezone.md): Customize the default time zone of GitLab.
+- [Header logo](../customization/branded_page_and_email_header.md): Change the logo on all pages and email headers.
+- [Welcome message](../customization/welcome_message.md): Add a custom welcome message to the sign-in page.
+- [System hooks](../system_hooks/system_hooks.md): Notifications when users, projects and keys are changed.
+- [Security](../security/README.md): Learn what you can do to further secure your GitLab instance.
+- [Usage statistics, version check, and usage ping](../user/admin_area/settings/usage_statistics.md): Enable or disable information about your instance to be sent to GitLab, Inc.
+- [Polling](polling.md): Configure how often the GitLab UI polls for updates.
+- [GitLab Pages configuration](pages/index.md): Enable and configure GitLab Pages.
+- [GitLab Pages configuration for installations from the source](pages/source.md): Enable and configure GitLab Pages on
+[source installations](../install/installation.md#installation-from-source).
+- [Environment variables](environment_variables.md): Supported environment variables that can be used to override their defaults values in order to configure GitLab.
+
+### Maintaining GitLab
+
+- [Raketasks](../raketasks/README.md): Perform various tasks for maintenance, backups, automatic webhooks setup, etc.
+ - [Backup and restore](../raketasks/backup_restore.md): Backup and restore your GitLab instance.
+- [Operations](operations.md): Keeping GitLab up and running (clean up Redis sessions, moving repositories, Sidekiq Job throttling, Sidekiq MemoryKiller, Unicorn).
+- [Restart GitLab](restart_gitlab.md): Learn how to restart GitLab and its components.
+
+#### Updating GitLab
+
+- [GitLab versions and maintenance policy](../policy/maintenance.md): Understand GitLab versions and releases (Major, Minor, Patch, Security), as well as update recommendations.
+- [Update GitLab](../update/README.md): Update guides to upgrade your installation to a new version.
+- [Downtimeless updates](../update/README.md#upgrading-without-downtime): Upgrade to a newer major, minor, or patch version of GitLab without taking your GitLab instance offline.
+- [Migrate your GitLab CI/CD data to another version of GitLab](../migrate_ci_to_ce/README.md): If you have an old GitLab installation (older than 8.0), follow this guide to migrate your existing GitLab CI/CD data to another version of GitLab.
+
+### Upgrading or downgrading GitLab
+
+- [Upgrade from GitLab CE to GitLab EE](../update/README.md#upgrading-between-editions): learn how to upgrade GitLab Community Edition to GitLab Enterprise Editions.
+- [Downgrade from GitLab EE to GitLab CE](../downgrade_ee_to_ce/README.md): Learn how to downgrade GitLab Enterprise Editions to Community Edition.
+
+### GitLab platform integrations
+
+- [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/): Integrate with [Mattermost](https://about.mattermost.com/), an open source, private cloud workplace for web messaging.
+- [PlantUML](integration/plantuml.md): Create simple diagrams in AsciiDoc and Markdown documents
+created in snippets, wikis, and repos.
+- [Web terminals](integration/terminal.md): Provide terminal access to your applications deployed to Kubernetes from within GitLab's CI/CD [environments](../ci/environments.md#web-terminals).
+
+## User settings and permissions
+
+- [Libravatar](../customization/libravatar.md): Use Libravatar instead of Gravatar for user avatars.
+- [Sign-up restrictions](../user/admin_area/settings/sign_up_restrictions.md): block email addresses of specific domains, or whitelist only specific domains.
+- [Access restrictions](../user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols): Define which Git access protocols can be used to talk to GitLab (SSH, HTTP, HTTPS).
+- [Authentication/Authorization](../topics/authentication/index.md#gitlab-administrators): Enforce 2FA, configure external authentication with LDAP, SAML, CAS and additional Omniauth providers.
+- [Reply by email](reply_by_email.md): Allow users to comment on issues and merge requests by replying to notification emails.
+ - [Postfix for Reply by email](reply_by_email_postfix_setup.md): Set up a basic Postfix mail
+server with IMAP authentication on Ubuntu, to be used with Reply by email.
+- [User Cohorts](../user/admin_area/user_cohorts.md): Display the monthly cohorts of new users and their activities over time.
+
+## Project settings
+
+- [Container Registry](container_registry.md): Configure Container Registry with GitLab.
+- [Issue closing pattern](issue_closing_pattern.md): Customize how to close an issue from commit messages.
+- [Gitaly](gitaly/index.md): Configuring Gitaly, GitLab's Git repository storage service.
+- [Default labels](../user/admin_area/labels.html): Create labels that will be automatically added to every new project.
+
+### Repository settings
+
+- [Repository checks](repository_checks.md): Periodic Git repository checks.
+- [Repository storage paths](repository_storage_paths.md): Manage the paths used to store repositories.
+- [Repository storage rake tasks](raketasks/storage.md): A collection of rake tasks to list and migrate existing projects and attachments associated with it from Legacy storage to Hashed storage.
+
+## Continuous Integration settings
+
+- [Enable/disable GitLab CI/CD](../ci/enable_or_disable_ci.md#site-wide-admin-setting): Enable or disable GitLab CI/CD for your instance.
+- [GitLab CI/CD admin settings](../user/admin_area/settings/continuous_integration.md): Define max artifacts size and expiration time.
+- [Job artifacts](job_artifacts.md): Enable, disable, and configure job artifacts (a set of files and directories which are outputted by a job when it completes successfully).
+- [Artifacts size and expiration](../user/admin_area/settings/continuous_integration.md#maximum-artifacts-size): Define maximum artifacts limits and expiration date.
+- [Register Shared and specific Runners](../ci/runners/README.md#registering-a-shared-runner): Learn how to register and configure Shared and specific Runners to your own instance.
+- [Shared Runners pipelines quota](../user/admin_area/settings/continuous_integration.md#shared-runners-pipeline-minutes-quota): Limit the usage of pipeline minutes for Shared Runners.
+- [Enable/disable Auto DevOps](../topics/autodevops/index.md#enabling-auto-devops): Enable or disable Auto DevOps for your instance.
+
+## Git configuration options
+
+- [Custom Git hooks](custom_hooks.md): Custom Git hooks (on the filesystem) for when webhooks aren't enough.
+- [Git LFS configuration](../workflow/lfs/lfs_administration.md): Learn how to configure LFS for GitLab.
+- [Housekeeping](housekeeping.md): Keep your Git repositories tidy and fast.
+
+## Monitoring GitLab
+
+- [Monitoring uptime](../user/admin_area/monitoring/health_check.md): Check the server status using the health check endpoint.
+ - [IP whitelist](monitoring/ip_whitelist.md): Monitor endpoints that provide health check information when probed.
+- [Monitoring GitHub imports](monitoring/github_imports.md): GitLab's GitHub Importer displays Prometheus metrics to monitor the health and progress of the importer.
+- [Conversational Development (ConvDev) Index](../user/admin_area/monitoring/convdev.md): Provides an overview of your entire instance's feature usage.
+
+### Performance Monitoring
+
+- [GitLab Performance Monitoring](monitoring/performance/gitlab_configuration.md): Enable GitLab Performance Monitoring.
+- [GitLab performance monitoring with InfluxDB](monitoring/performance/introduction.md): Configure GitLab and InfluxDB for measuring performance metrics.
+ - [InfluxDB Schema](monitoring/performance/influxdb_schema.md): Measurements stored in InfluxDB.
+- [GitLab performance monitoring with Prometheus](monitoring/prometheus/index.md): Configure GitLab and Prometheus for measuring performance metrics.
+- [GitLab performance monitoring with Grafana](monitoring/prometheus/index.md): Configure GitLab to visualize time series metrics through graphs and dashboards.
+- [Request Profiling](monitoring/performance/request_profiling.md): Get a detailed profile on slow requests.
+- [Performance Bar](monitoring/performance/performance_bar.md): Get performance information for the current page.
+
+## Troubleshooting
+
+- [Debugging tips](troubleshooting/debug.md): Tips to debug problems when things go wrong
+- [Log system](logs.md): Where to look for logs.
+- [Sidekiq Troubleshooting](troubleshooting/sidekiq.md): Debug when Sidekiq appears hung and is not processing jobs.
diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md
index 86b436d89dd..33f8a69c249 100644
--- a/doc/administration/job_artifacts.md
+++ b/doc/administration/job_artifacts.md
@@ -128,6 +128,45 @@ steps below.
1. Save the file and [restart GitLab][] for the changes to take effect.
+## Validation for dependencies
+
+> Introduced in GitLab 10.3.
+
+To disable [the dependencies validation](../ci/yaml/README.md#when-a-dependent-job-will-fail),
+you can flip the feature flag from a Rails console.
+
+---
+
+**In Omnibus installations:**
+
+1. Enter the Rails console:
+
+ ```sh
+ sudo gitlab-rails console
+ ```
+
+1. Flip the switch and disable it:
+
+ ```ruby
+ Feature.enable('ci_disable_validates_dependencies')
+ ```
+---
+
+**In installations from source:**
+
+1. Enter the Rails console:
+
+ ```sh
+ cd /home/git/gitlab
+ RAILS_ENV=production sudo -u git -H bundle exec rails console
+ ```
+
+1. Flip the switch and disable it:
+
+ ```ruby
+ Feature.enable('ci_disable_validates_dependencies')
+ ```
+
## Set the maximum file size of the artifacts
Provided the artifacts are enabled, you can change the maximum file size of the
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 0c63b0b59a7..7d47aaac299 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -58,6 +58,9 @@ Before proceeding with the Pages configuration, you will need to:
so that your users don't have to bring their own.
1. (Only for custom domains) Have a **secondary IP**.
+NOTE: **Note:**
+If your GitLab instance and the Pages daemon are deployed in a private network or behind a firewall, your GitLab Pages websites will only be accessible to devices/users that have access to the private network.
+
### DNS configuration
GitLab Pages expect to run on their own virtual host. In your DNS server/provider
diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md
index 5b6ee354887..136192191f9 100644
--- a/doc/administration/raketasks/maintenance.md
+++ b/doc/administration/raketasks/maintenance.md
@@ -58,7 +58,9 @@ Runs the following rake tasks:
It will check that each component was setup according to the installation guide and suggest fixes for issues found.
-You may also have a look at our [Trouble Shooting Guide](https://github.com/gitlabhq/gitlab-public-wiki/wiki/Trouble-Shooting-Guide).
+You may also have a look at our Trouble Shooting Guides:
+- [Trouble Shooting Guide (GitLab)](http://docs.gitlab.com/ee/README.html#troubleshooting)
+- [Trouble Shooting Guide (Omnibus Gitlab)](http://docs.gitlab.com/omnibus/README.html#troubleshooting)
**Omnibus Installation**
diff --git a/doc/api/protected_branches.md b/doc/api/protected_branches.md
index 81fe854060a..950ead52560 100644
--- a/doc/api/protected_branches.md
+++ b/doc/api/protected_branches.md
@@ -136,7 +136,7 @@ DELETE /projects/:id/protected_branches/:name
```
```bash
-curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/5/protected_branches/*-stable'
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/5/protected_branches/*-stable'
```
| Attribute | Type | Required | Description |
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 22fb2baa8ec..0e4758cda2d 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -70,10 +70,9 @@ PUT /application/settings
| `akismet_api_key` | string | no | API key for akismet spam protection |
| `akismet_enabled` | boolean | no | Enable or disable akismet spam protection |
| `circuitbreaker_access_retries | integer | no | The number of attempts GitLab will make to access a storage. |
-| `circuitbreaker_backoff_threshold | integer | no | The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host. |
+| `circuitbreaker_check_interval` | integer | no | Number of seconds in between storage checks. |
| `circuitbreaker_failure_count_threshold` | integer | no | The number of failures of after which GitLab will completely prevent access to the storage. |
| `circuitbreaker_failure_reset_time` | integer | no | Time in seconds GitLab will keep storage failure information. When no failures occur during this time, the failure information is reset. |
-| `circuitbreaker_failure_wait_time` | integer | no | Time in seconds GitLab will block access to a failing storage to allow it to recover. |
| `circuitbreaker_storage_timeout` | integer | no | Seconds to wait for a storage access attempt |
| `clientside_sentry_dsn` | string | no | Required if `clientside_sentry_dsn` is enabled |
| `clientside_sentry_enabled` | boolean | no | Enable Sentry error reporting for the client side |
diff --git a/doc/ci/autodeploy/quick_start_guide.md b/doc/ci/autodeploy/quick_start_guide.md
index f76c2a2cf31..cc6c9ec0e0a 100644
--- a/doc/ci/autodeploy/quick_start_guide.md
+++ b/doc/ci/autodeploy/quick_start_guide.md
@@ -11,11 +11,11 @@ We made a minimal [Ruby application](https://gitlab.com/gitlab-examples/minimal-
Let’s start by forking our sample application. Go to [the project page](https://gitlab.com/gitlab-examples/minimal-ruby-app) and press the `Fork` button. Soon you should have a project under your namespace with the necessary files.
-## Setup your own cluster on Google Container Engine
+## Setup your own cluster on Google Kubernetes Engine
If you do not already have a Google Cloud account, create one at https://console.cloud.google.com.
-Visit the [`Container Engine`](https://console.cloud.google.com/kubernetes/list) tab and create a new cluster. You can change the name and leave the rest of the default settings. Once you have your cluster running, you need to connect to the cluster by following the Google interface.
+Visit the [`Kubernetes Engine`](https://console.cloud.google.com/kubernetes/list) tab and create a new cluster. You can change the name and leave the rest of the default settings. Once you have your cluster running, you need to connect to the cluster by following the Google interface.
## Connect to Kubernetes cluster
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index f40d2c5e347..32464cbb259 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1153,6 +1153,20 @@ deploy:
script: make deploy
```
+#### When a dependent job will fail
+
+> Introduced in GitLab 10.3.
+
+If the artifacts of the job that is set as a dependency have been
+[expired](#artifacts-expire_in) or
+[erased](../../user/project/pipelines/job_artifacts.md#erasing-artifacts), then
+the dependent job will fail.
+
+NOTE: **Note:**
+You can ask your administrator to
+[flip this switch](../../administration/job_artifacts.md#validation-for-dependencies)
+and bring back the old behavior.
+
### before_script and after_script
It's possible to overwrite the globally defined `before_script` and `after_script`:
diff --git a/doc/customization/new_project_page.md b/doc/customization/new_project_page.md
new file mode 100644
index 00000000000..148bf9512c6
--- /dev/null
+++ b/doc/customization/new_project_page.md
@@ -0,0 +1,20 @@
+# Customizing the new project page
+
+It is possible to add a markdown-formatted message to your GitLab
+new project page.
+
+By default, the new project page shows a sidebar with general information:
+
+![](new_project_page/default_new_project_page.png)
+
+## Changing the appearance of the new project page
+
+Navigate to the **Admin** area and go to the **Appearance** page.
+
+Fill in your project guidelines:
+
+![](new_project_page/appearance_settings.png)
+
+After saving the page, your new project page will show the guidelines in the sidebar, below the general information:
+
+![](new_project_page/custom_new_project_page.png)
diff --git a/doc/customization/new_project_page/appearance_settings.png b/doc/customization/new_project_page/appearance_settings.png
new file mode 100644
index 00000000000..08eea684e14
--- /dev/null
+++ b/doc/customization/new_project_page/appearance_settings.png
Binary files differ
diff --git a/doc/customization/new_project_page/custom_new_project_page.png b/doc/customization/new_project_page/custom_new_project_page.png
new file mode 100644
index 00000000000..662c715f193
--- /dev/null
+++ b/doc/customization/new_project_page/custom_new_project_page.png
Binary files differ
diff --git a/doc/customization/new_project_page/default_new_project_page.png b/doc/customization/new_project_page/default_new_project_page.png
new file mode 100644
index 00000000000..4a0bcf09903
--- /dev/null
+++ b/doc/customization/new_project_page/default_new_project_page.png
Binary files differ
diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md
index fa564d83785..96968c1e3ab 100644
--- a/doc/install/kubernetes/gitlab_chart.md
+++ b/doc/install/kubernetes/gitlab_chart.md
@@ -1,7 +1,7 @@
# GitLab Helm Chart
> **Note**:
* This chart is deprecated, and is being replaced by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). For more information on available charts, please see our [overview](index.md#chart-overview).
-* These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
+* These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview).
@@ -243,7 +243,7 @@ controller. For `nginx-ingress` you can check the
on how to add the annotation to the `controller.service.annotations` array.
>**Note:**
-When using the `nginx-ingress` controller on Google Container Engine (GKE), and using the `external-traffic` annotation,
+When using the `nginx-ingress` controller on Google Kubernetes Engine (GKE), and using the `external-traffic` annotation,
you will need to additionally set the `controller.kind` to be DaemonSet. Otherwise only pods running on the same node
as the nginx controller will be able to reach GitLab. This may result in pods within your cluster not being able to reach GitLab.
See the [Kubernetes documentation](https://kubernetes.io/docs/tutorials/services/source-ip/#source-ip-for-services-with-typeloadbalancer) and
diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md
index 6659c3cf7b2..5a5f8d67ff5 100644
--- a/doc/install/kubernetes/gitlab_omnibus.md
+++ b/doc/install/kubernetes/gitlab_omnibus.md
@@ -1,7 +1,7 @@
# GitLab-Omnibus Helm Chart
> **Note:**
* This Helm chart is in beta, and will be deprecated by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md).
-* These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
+* These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
This work is based partially on: https://github.com/lwolf/kubernetes-gitlab/. GitLab would like to thank Sergey Nuzhdin for his work.
@@ -72,7 +72,7 @@ Other common configuration options:
- `baseIP`: the desired [external IP address](#external-ip-recommended)
- `gitlab`: Choose the [desired edition](https://about.gitlab.com/products), either `ee` or `ce`. `ce` is the default.
- `gitlabEELicense`: For Enterprise Edition, the [license](https://docs.gitlab.com/ee/user/admin_area/license.html) can be installed directly via the Chart
-- `provider`: Optimizes the deployment for a cloud provider. The default is `gke` for [Google Container Engine](https://cloud.google.com/container-engine/), with `acs` also supported for the [Azure Container Service](https://azure.microsoft.com/en-us/services/container-service/).
+- `provider`: Optimizes the deployment for a cloud provider. The default is `gke` for [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/), with `acs` also supported for the [Azure Container Service](https://azure.microsoft.com/en-us/services/container-service/).
For additional configuration options, consult the [values.yaml](https://gitlab.com/charts/charts.gitlab.io/blob/master/charts/gitlab-omnibus/values.yaml).
diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md
index 5e0d7493b61..ca9c95aeced 100644
--- a/doc/install/kubernetes/gitlab_runner_chart.md
+++ b/doc/install/kubernetes/gitlab_runner_chart.md
@@ -1,6 +1,6 @@
# GitLab Runner Helm Chart
> **Note:**
-These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
+These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
The `gitlab-runner` Helm chart deploys a GitLab Runner instance into your
Kubernetes cluster.
diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md
index dd350820c18..0932e1eee3a 100644
--- a/doc/install/kubernetes/index.md
+++ b/doc/install/kubernetes/index.md
@@ -1,5 +1,5 @@
# Installing GitLab on Kubernetes
-> **Note**: These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
+> **Note**: These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
The easiest method to deploy GitLab on [Kubernetes](https://kubernetes.io/) is
to take advantage of GitLab's Helm charts. [Helm] is a package
diff --git a/doc/integration/slash_commands.md b/doc/integration/slash_commands.md
index aa52b5415cf..36a8844e953 100644
--- a/doc/integration/slash_commands.md
+++ b/doc/integration/slash_commands.md
@@ -17,6 +17,9 @@ Taking the trigger term as `project-name`, the commands are:
| `/project-name issue search <query>` | Shows up to 5 issues matching `<query>` |
| `/project-name deploy <from> to <to>` | Deploy from the `<from>` environment to the `<to>` environment |
+Note that if you are using the [GitLab Slack application](https://docs.gitlab.com/ee/user/project/integrations/gitlab_slack_application.html) for
+your GitLab.com projects, you need to [add the `gitlab` keyword at the beginning of the command](https://docs.gitlab.com/ee/user/project/integrations/gitlab_slack_application.html#usage).
+
## Issue commands
It is possible to create new issue, display issue details and search up to 5 issues.
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index 597c98fbf6b..1f30909b0aa 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -6,6 +6,7 @@ This page gathers all the resources for the topic **Authentication** within GitL
- [SSH](../../ssh/README.md)
- [Two-Factor Authentication (2FA)](../../user/profile/account/two_factor_authentication.md#two-factor-authentication)
+- [Why do I keep getting signed out?](../../user/profile/index.md#why-do-i-keep-getting-signed-out)
- **Articles:**
- [Support for Universal 2nd Factor Authentication - YubiKeys](https://about.gitlab.com/2016/06/22/gitlab-adds-support-for-u2f/)
- [Security Webcast with Yubico](https://about.gitlab.com/2016/08/31/gitlab-and-yubico-security-webcast/)
diff --git a/doc/topics/autodevops/img/auto_devops_settings.png b/doc/topics/autodevops/img/auto_devops_settings.png
index b572cc5b855..067c9da3fdc 100644
--- a/doc/topics/autodevops/img/auto_devops_settings.png
+++ b/doc/topics/autodevops/img/auto_devops_settings.png
Binary files differ
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 914217772b8..d100b431721 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -129,8 +129,6 @@ full use of Auto DevOps. If this is your fist time, we recommend you follow the
1. Go to your project's **Settings > CI/CD > General pipelines settings** and
find the Auto DevOps section
1. Select "Enable Auto DevOps"
-1. After selecting an option to enable Auto DevOps, a checkbox will appear below
- so you can immediately run a pipeline on the default branch
1. Optionally, but recommended, add in the [base domain](#auto-devops-base-domain)
that will be used by Kubernetes to deploy your application
1. Hit **Save changes** for the changes to take effect
diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
index ffe05519d7b..4858735ee86 100644
--- a/doc/topics/autodevops/quick_start_guide.md
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -23,12 +23,12 @@ page](https://gitlab.com/auto-devops-examples/minimal-ruby-app) and press the
**Fork** button. Soon you should have a project under your namespace with the
necessary files.
-## Setup your own cluster on Google Container Engine
+## Setup your own cluster on Google Kubernetes Engine
If you do not already have a Google Cloud account, create one at
https://console.cloud.google.com.
-Visit the [**Container Engine**](https://console.cloud.google.com/kubernetes/list)
+Visit the [**Kubernetes Engine**](https://console.cloud.google.com/kubernetes/list)
tab and create a new cluster. You can change the name and leave the rest of the
default settings. Once you have your cluster running, you need to connect to the
cluster by following the Google interface.
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index b9532bf897f..4fa83388d0c 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -23,25 +23,26 @@ The following table depicts the various user permission levels in a project.
|---------------------------------------|---------|------------|-------------|----------|--------|
| Create new issue | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| Create confidential issue | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
-| View confidential issues | (✓) [^2] | ✓ | ✓ | ✓ | ✓ |
+| View confidential issues | (✓) [^2] | ✓ | ✓ | ✓ | ✓ |
| Leave comments | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| Lock discussions (issues and merge requests) | | | | ✓ | ✓ |
| See a list of jobs | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
-| See a job log | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
+| See a job log | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
| Download and browse job artifacts | ✓ [^3] | ✓ | ✓ | ✓ | ✓ |
| View wiki pages | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| Pull project code | [^1] | ✓ | ✓ | ✓ | ✓ |
| Download project | [^1] | ✓ | ✓ | ✓ | ✓ |
+| Assign issues and merge requests | | ✓ | ✓ | ✓ | ✓ |
+| Label issues and merge requests | | ✓ | ✓ | ✓ | ✓ |
| Create code snippets | | ✓ | ✓ | ✓ | ✓ |
| Manage issue tracker | | ✓ | ✓ | ✓ | ✓ |
| Manage labels | | ✓ | ✓ | ✓ | ✓ |
| See a commit status | | ✓ | ✓ | ✓ | ✓ |
| See a container registry | | ✓ | ✓ | ✓ | ✓ |
| See environments | | ✓ | ✓ | ✓ | ✓ |
+| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ |
| Create new environments | | | ✓ | ✓ | ✓ |
-| Use environment terminals | | | | ✓ | ✓ |
| Stop environments | | | ✓ | ✓ | ✓ |
-| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ |
| Manage/Accept merge requests | | | ✓ | ✓ | ✓ |
| Create new merge request | | | ✓ | ✓ | ✓ |
| Create new branches | | | ✓ | ✓ | ✓ |
@@ -55,10 +56,11 @@ The following table depicts the various user permission levels in a project.
| Update a container registry | | | ✓ | ✓ | ✓ |
| Remove a container registry image | | | ✓ | ✓ | ✓ |
| Create/edit/delete project milestones | | | ✓ | ✓ | ✓ |
+| Use environment terminals | | | | ✓ | ✓ |
| Add new team members | | | | ✓ | ✓ |
| Push to protected branches | | | | ✓ | ✓ |
| Enable/disable branch protection | | | | ✓ | ✓ |
-| Turn on/off protected branch push for devs| | | | ✓ | ✓ |
+| Turn on/off protected branch push for devs| | | | ✓ | ✓ |
| Enable/disable tag protections | | | | ✓ | ✓ |
| Rewrite/remove Git tags | | | | ✓ | ✓ |
| Edit project | | | | ✓ | ✓ |
@@ -69,14 +71,15 @@ The following table depicts the various user permission levels in a project.
| Manage variables | | | | ✓ | ✓ |
| Manage pages | | | | ✓ | ✓ |
| Manage pages domains and certificates | | | | ✓ | ✓ |
+| Manage clusters | | | | ✓ | ✓ |
+| Edit comments (posted by any user) | | | | ✓ | ✓ |
| Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ |
| Remove project | | | | | ✓ |
| Delete issues | | | | | ✓ |
+| Remove pages | | | | | ✓ |
| Force push to protected branches [^4] | | | | | |
| Remove protected branches [^4] | | | | | |
-| Remove pages | | | | | ✓ |
-| Manage clusters | | | | ✓ | ✓ |
## Project features permissions
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 5fcc0501dc1..04e615330ce 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -1,8 +1,32 @@
# User account
-When logged into their GitLab account, users can customize their
+When signed into their GitLab account, users can customize their
experience according to the best approach to their cases.
+## Signing in
+
+There are several ways to sign into your GitLab account.
+See the [authentication topic](../../topics/authentication/index.md) for more details.
+
+### Why do I keep getting signed out?
+
+When signing in to the main GitLab application, a `_gitlab_session` cookie is
+set. `_gitlab_session` is cleared client-side when you close your browser
+and expires after "Application settings -> Session duration (minutes)"/`session_expire_delay`
+(defaults to `10080` minutes = 7 days).
+
+When signing in to the main GitLab application, you can also check the
+"Remember me" option which sets the `remember_user_token`
+cookie (via [`devise`](https://github.com/plataformatec/devise)).
+`remember_user_token` expires after
+`config/initializers/devise.rb` -> `config.remember_for` (defaults to 2 weeks).
+
+When the `_gitlab_session` expires or isn't available, GitLab uses the `remember_user_token`
+to get you a new `_gitlab_session` and keep you signed in through browser restarts.
+
+After your `remember_user_token` expires and your `_gitlab_session` is cleared/expired,
+you will be asked to sign in again to verify your identity (which is for security reasons).
+
## Username
Your `username` is a unique [`namespace`](../group/index.md#namespaces)
diff --git a/doc/user/project/index.md b/doc/user/project/index.md
index 97d0d529886..5d91aef5735 100644
--- a/doc/user/project/index.md
+++ b/doc/user/project/index.md
@@ -64,7 +64,7 @@ common actions on issues or merge requests
- [Pipeline settings](pipelines/settings.md): Set up Git strategy (choose the default way your repository is fetched from GitLab in a job),
timeout (defines the maximum amount of time in minutes that a job is able run), custom path for `.gitlab-ci.yml`, test coverage parsing, pipeline's visibility, and much more
- [GKE cluster integration](clusters/index.md): Connecting your GitLab project
- with Google Container Engine
+ with Google Kubernetes Engine
- [GitLab Pages](pages/index.md): Build, test, and deploy your static
website with GitLab Pages
diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md
index abe6b4cbd8e..8404d789de6 100644
--- a/doc/user/project/pages/index.md
+++ b/doc/user/project/pages/index.md
@@ -1,49 +1,78 @@
-# GitLab Pages documentation
-
-With GitLab Pages you can create static websites for your GitLab projects,
-groups, or user accounts. You can use any static website generator: Jekyll,
-Middleman, Hexo, Hugo, Pelican, you name it! Connect as many customs domains
-as you like and bring your own TLS certificate to secure them.
-
-Here's some info we've gathered to get you started.
-
-## General info
-
-- [Product webpage](https://pages.gitlab.io)
-- ["We're bringing GitLab Pages to CE"](https://about.gitlab.com/2016/12/24/were-bringing-gitlab-pages-to-community-edition/)
-- [Pages group - templates](https://gitlab.com/pages)
-- [General user documentation](introduction.md)
-- [Admin documentation - Set GitLab Pages on your own GitLab instance](../../../administration/pages/index.md)
-- ["We are changing the IP of GitLab Pages on GitLab.com"](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/)
-
-## Getting started
-
-- **GitLab Pages from A to Z**
- - [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md)
- - [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md)
- - [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md)
- - [Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md)
-- **Static Site Generators - Blog posts series**
- - [SSGs part 1: Static vs dynamic websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/)
- - [SSGs part 2: Modern static site generators](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/)
- - [SSGs part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
-- **Secure GitLab Pages custom domain with SSL/TLS certificates**
- - [Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/)
- - [CloudFlare](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/)
- - [StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/)
-- **General**
- - [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) a comprehensive step-by-step guide
- - [Posting to your GitLab Pages blog from iOS](https://about.gitlab.com/2016/08/19/posting-to-your-gitlab-pages-blog-from-ios/)
-
-## Video tutorials
-
-- [How to publish a website with GitLab Pages on GitLab.com: from a forked project](https://youtu.be/TWqh9MtT4Bg)
-- [How to Enable GitLab Pages for GitLab CE and EE (for Admins only)](https://youtu.be/dD8c7WNcc6s)
+# GitLab Pages
+
+With GitLab Pages you can host your website at no cost.
+
+Your files live in a GitLab project's [repository](../repository/index.md),
+from which you can deploy [static websites](#explore-gitlab-pages).
+GitLab Pages supports all static site generators (SSGs).
+
+## Getting Started
+
+Follow the steps below to get your website live. They shouldn't take more than
+5 minutes to complete:
+
+- 1. [Fork](../../../gitlab-basics/fork-project.md#how-to-fork-a-project) an [example project](https://gitlab.com/pages)
+- 2. Change a file to trigger a GitLab CI/CD pipeline
+- 3. Visit your project's **Settings > Pages** to see your **website link**, and click on it. Bam! Your website is live.
+
+_Further steps (optional):_
+
+- 4. Remove the [fork relationship](getting_started_part_two.md#fork-a-project-to-get-started-from) (_You don't need the relationship unless you intent to contribute back to the example project you forked from_).
+- 5. Make it a [user/group website](getting_started_part_one.md#user-and-group-websites)
+
+**Watch a video with the steps above: https://www.youtube.com/watch?v=TWqh9MtT4Bg**
+
+_Advanced options:_
+
+- [Use a custom domain](getting_started_part_three.md#adding-your-custom-domain-to-gitlab-pages)
+- Apply [SSL/TLS certification](getting_started_part_three.md#ssl-tls-certificates) to your custom domain
+
+## Explore GitLab Pages
+
+With GitLab Pages you can create [static websites](getting_started_part_one.md#what-you-need-to-know-before-getting-started)
+for your GitLab projects, groups, or user accounts. You can use any static
+website generator: Jekyll, Middleman, Hexo, Hugo, Pelican, you name it!
+Connect as many custom domains as you like and bring your own TLS certificate
+to secure them.
+
+Read the following tutorials to know more about:
+
+- [Static websites and GitLab Pages domains](getting_started_part_one.md)
+- [Forking projects and creating new ones from scratch, URLs and baseurls](getting_started_part_two.md)
+- [Custom domains and subdomains, DNS records, SSL/TLS certificates](getting_started_part_three.md)
+- [How to create your own `.gitlab-ci.yml` for your site](getting_started_part_four.md)
+- [Technical aspects, custom 404 pages, limitations](introduction.md)
+- [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) (outdated)
+
+_Blog posts series about Static Site Generators (SSGs):_
+
+- [SSGs part 1: Static vs dynamic websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/)
+- [SSGs part 2: Modern static site generators](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/)
+- [SSGs part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
+
+_Blog posts for securing GitLab Pages custom domains with SSL/TLS certificates:_
+
+- [CloudFlare](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/)
+- [Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/) (outdated)
+- [StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/) (deprecated)
## Advanced use
-- **Blog Posts**
- - [GitLab CI: Run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/)
- - [GitLab CI: Deployment & environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
- - [Building a new GitLab docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
- - [Publish code coverage reports with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/)
+- [Posting to your GitLab Pages blog from iOS](https://about.gitlab.com/2016/08/19/posting-to-your-gitlab-pages-blog-from-ios/)
+- [GitLab CI: Run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/)
+- [GitLab CI: Deployment & environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
+- [Building a new GitLab docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
+- [Publish code coverage reports with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/)
+
+## Admin GitLab Pages for CE and EE
+
+Enable and configure GitLab Pages on your own instance (GitLab Community Edition and Enterprise Editions) with
+the [admin guide](../../../administration/pages/index.md).
+
+**Watch the video: https://www.youtube.com/watch?v=dD8c7WNcc6s**
+
+## More information about GitLab Pages
+
+- For an overview, visit the [feature webpage](https://about.gitlab.com/features/pages/)
+- Announcement (2016-12-24): ["We're bringing GitLab Pages to CE"](https://about.gitlab.com/2016/12/24/were-bringing-gitlab-pages-to-community-edition/)
+- Announcement (2017-03-06): ["We are changing the IP of GitLab Pages on GitLab.com"](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/)
diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md
index f9a268fb789..402989f4508 100644
--- a/doc/user/project/pipelines/job_artifacts.md
+++ b/doc/user/project/pipelines/job_artifacts.md
@@ -44,7 +44,7 @@ the artifacts will be kept forever.
For more examples on artifacts, follow the [artifacts reference in
`.gitlab-ci.yml`](../../../ci/yaml/README.md#artifacts).
-## Browsing job artifacts
+## Browsing artifacts
>**Note:**
With GitLab 9.2, PDFs, images, videos and other formats can be previewed
@@ -77,7 +77,7 @@ one HTML file that you can view directly online when
---
-## Downloading job artifacts
+## Downloading artifacts
If you need to download the whole archive, there are buttons in various places
inside GitLab that make that possible.
@@ -102,7 +102,7 @@ inside GitLab that make that possible.
![Job artifacts browser](img/job_artifacts_browser.png)
-## Downloading the latest job artifacts
+## Downloading the latest artifacts
It is possible to download the latest artifacts of a job via a well known URL
so you can use it for scripting purposes.
@@ -163,6 +163,18 @@ information in the UI.
![Latest artifacts button](img/job_latest_artifacts_browser.png)
+## Erasing artifacts
+
+DANGER: **Warning:**
+This is a destructive action that leads to data loss. Use with caution.
+
+If you have at least Developer [permissions](../../permissions.md#gitlab-ci-cd-permissions)
+on the project, you can erase a single job via the UI which will also remove the
+artifacts and the job's trace.
+
+1. Navigate to a job's page.
+1. Click the trash icon at the top right of the job's trace.
+1. Confirm the deletion.
[expiry date]: ../../../ci/yaml/README.md#artifacts-expire_in
[ce-14399]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14399
diff --git a/lib/api/circuit_breakers.rb b/lib/api/circuit_breakers.rb
index 118883f5ea5..598c76f6168 100644
--- a/lib/api/circuit_breakers.rb
+++ b/lib/api/circuit_breakers.rb
@@ -41,7 +41,7 @@ module API
detail 'This feature was introduced in GitLab 9.5'
end
delete do
- Gitlab::Git::Storage::CircuitBreaker.reset_all!
+ Gitlab::Git::Storage::FailureInfo.reset_all!
end
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 62ee20bf7de..d96e7f2770f 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -16,10 +16,13 @@ module API
class UserBasic < UserSafe
expose :state
+
expose :avatar_url do |user, options|
user.avatar_url(only_path: false)
end
+ expose :avatar_path, if: ->(user, options) { options.fetch(:only_path, false) && user.avatar_path }
+
expose :web_url do |user, options|
Gitlab::Routing.url_helpers.user_url(user)
end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 451121a4cea..ccaaeca10d4 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -4,6 +4,7 @@ module API
before { authenticate_by_gitlab_shell_token! }
helpers ::API::Helpers::InternalHelpers
+ helpers ::Gitlab::Identifier
namespace 'internal' do
# Check if git command is allowed to project
@@ -176,17 +177,25 @@ module API
post '/post_receive' do
status 200
-
PostReceive.perform_async(params[:gl_repository], params[:identifier],
params[:changes])
broadcast_message = BroadcastMessage.current&.last&.message
reference_counter_decreased = Gitlab::ReferenceCounter.new(params[:gl_repository]).decrease
- {
+ output = {
merge_request_urls: merge_request_urls,
broadcast_message: broadcast_message,
reference_counter_decreased: reference_counter_decreased
}
+
+ project = Gitlab::GlRepository.parse(params[:gl_repository]).first
+ user = identify(params[:identifier])
+ redirect_message = Gitlab::Checks::ProjectMoved.fetch_redirect_message(user.id, project.id)
+ if redirect_message
+ output[:redirected_message] = redirect_message
+ end
+
+ output
end
end
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 14a4fc6f025..fa222bf2b1c 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -367,15 +367,16 @@ module API
post ":id/fork/:forked_from_id" do
authenticated_as_admin!
- forked_from_project = find_project!(params[:forked_from_id])
- not_found!("Source Project") unless forked_from_project
+ fork_from_project = find_project!(params[:forked_from_id])
- if user_project.forked_from_project.nil?
- user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id)
+ not_found!("Source Project") unless fork_from_project
- ::Projects::ForksCountService.new(forked_from_project).refresh_cache
+ result = ::Projects::ForkService.new(fork_from_project, current_user).execute(user_project)
+
+ if result
+ present user_project.reload, with: Entities::Project
else
- render_api_error!("Project already forked", 409)
+ render_api_error!("Project already forked", 409) if user_project.forked?
end
end
@@ -383,11 +384,11 @@ module API
delete ":id/fork" do
authorize! :remove_fork_project, user_project
- if user_project.forked?
- destroy_conditionally!(user_project.forked_project_link)
- else
- not_modified!
+ result = destroy_conditionally!(user_project) do
+ ::Projects::UnlinkForkService.new(user_project, current_user).execute
end
+
+ result ? status(204) : not_modified!
end
desc 'Share the project with a group' do
diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb
index b5021e8a712..614822509f0 100644
--- a/lib/api/protected_branches.rb
+++ b/lib/api/protected_branches.rb
@@ -39,10 +39,10 @@ module API
end
params do
requires :name, type: String, desc: 'The name of the protected branch'
- optional :push_access_level, type: Integer, default: Gitlab::Access::MASTER,
+ optional :push_access_level, type: Integer,
values: ProtectedRefAccess::ALLOWED_ACCESS_LEVELS,
desc: 'Access levels allowed to push (defaults: `40`, master access level)'
- optional :merge_access_level, type: Integer, default: Gitlab::Access::MASTER,
+ optional :merge_access_level, type: Integer,
values: ProtectedRefAccess::ALLOWED_ACCESS_LEVELS,
desc: 'Access levels allowed to merge (defaults: `40`, master access level)'
end
@@ -52,15 +52,13 @@ module API
conflict!("Protected branch '#{params[:name]}' already exists")
end
- protected_branch_params = {
- name: params[:name],
- push_access_levels_attributes: [{ access_level: params[:push_access_level] }],
- merge_access_levels_attributes: [{ access_level: params[:merge_access_level] }]
- }
+ # Replace with `declared(params)` after updating to grape v1.0.2
+ # See https://github.com/ruby-grape/grape/pull/1710
+ # and https://gitlab.com/gitlab-org/gitlab-ce/issues/40843
+ declared_params = params.slice("name", "push_access_level", "merge_access_level", "allowed_to_push", "allowed_to_merge")
- service_args = [user_project, current_user, protected_branch_params]
-
- protected_branch = ::ProtectedBranches::CreateService.new(*service_args).execute
+ api_service = ::ProtectedBranches::ApiService.new(user_project, current_user, declared_params)
+ protected_branch = api_service.create
if protected_branch.persisted?
present protected_branch, with: Entities::ProtectedBranch, project: user_project
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index b6d273b98c2..2a04c03919d 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -193,12 +193,9 @@ module Backup
end
def empty_repo?(project_or_wiki)
- project_or_wiki.repository.expire_exists_cache # protect backups from stale cache
- project_or_wiki.repository.empty_repo?
- rescue => e
- progress.puts "Ignoring repository error and continuing backing up project: #{display_repo_path(project_or_wiki)} - #{e.message}".color(:orange)
-
- false
+ # Protect against stale caches
+ project_or_wiki.repository.expire_emptiness_caches
+ project_or_wiki.repository.empty?
end
def repository_storage_paths_args
diff --git a/lib/banzai/cross_project_reference.rb b/lib/banzai/cross_project_reference.rb
index e2b57adf611..d8fb7705b2a 100644
--- a/lib/banzai/cross_project_reference.rb
+++ b/lib/banzai/cross_project_reference.rb
@@ -11,7 +11,7 @@ module Banzai
# ref - String reference.
#
# Returns a Project, or nil if the reference can't be found
- def project_from_ref(ref)
+ def parent_from_ref(ref)
return context[:project] unless ref
Project.find_by_full_path(ref)
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index 8975395aff1..e7e6a90b5fd 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -82,9 +82,9 @@ module Banzai
end
end
- def project_from_ref_cached(ref)
- cached_call(:banzai_project_refs, ref) do
- project_from_ref(ref)
+ def from_ref_cached(ref)
+ cached_call("banzai_#{parent_type}_refs".to_sym, ref) do
+ parent_from_ref(ref)
end
end
@@ -153,15 +153,20 @@ module Banzai
# have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
def object_link_filter(text, pattern, link_content: nil, link_reference: false)
references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches|
- project_path = full_project_path(namespace_ref, project_ref)
- project = project_from_ref_cached(project_path)
+ parent_path = if parent_type == :group
+ full_group_path(namespace_ref)
+ else
+ full_project_path(namespace_ref, project_ref)
+ end
- if project
+ parent = from_ref_cached(parent_path)
+
+ if parent
object =
if link_reference
- find_object_from_link_cached(project, id)
+ find_object_from_link_cached(parent, id)
else
- find_object_cached(project, id)
+ find_object_cached(parent, id)
end
end
@@ -169,13 +174,13 @@ module Banzai
title = object_link_title(object)
klass = reference_class(object_sym)
- data = data_attributes_for(link_content || match, project, object, link: !!link_content)
+ data = data_attributes_for(link_content || match, parent, object, link: !!link_content)
url =
if matches.names.include?("url") && matches[:url]
matches[:url]
else
- url_for_object_cached(object, project)
+ url_for_object_cached(object, parent)
end
content = link_content || object_link_text(object, matches)
@@ -224,17 +229,24 @@ module Banzai
# Returns a Hash containing all object references (e.g. issue IDs) per the
# project they belong to.
- def references_per_project
- @references_per_project ||= begin
+ def references_per_parent
+ @references_per ||= {}
+
+ @references_per[parent_type] ||= begin
refs = Hash.new { |hash, key| hash[key] = Set.new }
regex = Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern)
nodes.each do |node|
node.to_html.scan(regex) do
- project_path = full_project_path($~[:namespace], $~[:project])
+ path = if parent_type == :project
+ full_project_path($~[:namespace], $~[:project])
+ else
+ full_group_path($~[:group])
+ end
+
symbol = $~[object_sym]
- refs[project_path] << symbol if object_class.reference_valid?(symbol)
+ refs[path] << symbol if object_class.reference_valid?(symbol)
end
end
@@ -244,35 +256,41 @@ module Banzai
# Returns a Hash containing referenced projects grouped per their full
# path.
- def projects_per_reference
- @projects_per_reference ||= begin
+ def parent_per_reference
+ @per_reference ||= {}
+
+ @per_reference[parent_type] ||= begin
refs = Set.new
- references_per_project.each do |project_ref, _|
- refs << project_ref
+ references_per_parent.each do |ref, _|
+ refs << ref
end
- find_projects_for_paths(refs.to_a).index_by(&:full_path)
+ find_for_paths(refs.to_a).index_by(&:full_path)
end
end
- def projects_relation_for_paths(paths)
- Project.where_full_path_in(paths).includes(:namespace)
+ def relation_for_paths(paths)
+ klass = parent_type.to_s.camelize.constantize
+ result = klass.where_full_path_in(paths)
+ return result if parent_type == :group
+
+ result.includes(:namespace) if parent_type == :project
end
# Returns projects for the given paths.
- def find_projects_for_paths(paths)
+ def find_for_paths(paths)
if RequestStore.active?
- cache = project_refs_cache
+ cache = refs_cache
to_query = paths - cache.keys
unless to_query.empty?
- projects = projects_relation_for_paths(to_query)
+ records = relation_for_paths(to_query)
found = []
- projects.each do |project|
- ref = project.full_path
- get_or_set_cache(cache, ref) { project }
+ records.each do |record|
+ ref = record.full_path
+ get_or_set_cache(cache, ref) { record }
found << ref
end
@@ -284,33 +302,37 @@ module Banzai
cache.slice(*paths).values.compact
else
- projects_relation_for_paths(paths)
+ relation_for_paths(paths)
end
end
- def current_project_path
- return unless project
-
- @current_project_path ||= project.full_path
+ def current_parent_path
+ @current_parent_path ||= parent&.full_path
end
def current_project_namespace_path
- return unless project
-
- @current_project_namespace_path ||= project.namespace.full_path
+ @current_project_namespace_path ||= project&.namespace&.full_path
end
private
def full_project_path(namespace, project_ref)
- return current_project_path unless project_ref
+ return current_parent_path unless project_ref
namespace_ref = namespace || current_project_namespace_path
"#{namespace_ref}/#{project_ref}"
end
- def project_refs_cache
- RequestStore[:banzai_project_refs] ||= {}
+ def refs_cache
+ RequestStore["banzai_#{parent_type}_refs".to_sym] ||= {}
+ end
+
+ def parent_type
+ :project
+ end
+
+ def parent
+ parent_type == :project ? project : group
end
end
end
diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb
index 714e0319025..eedb95197aa 100644
--- a/lib/banzai/filter/commit_reference_filter.rb
+++ b/lib/banzai/filter/commit_reference_filter.rb
@@ -22,10 +22,30 @@ module Banzai
end
end
+ def referenced_merge_request_commit_shas
+ return [] unless noteable.is_a?(MergeRequest)
+
+ @referenced_merge_request_commit_shas ||= begin
+ referenced_shas = references_per_parent.values.reduce(:|).to_a
+ noteable.all_commit_shas.select do |sha|
+ referenced_shas.any? { |ref| Gitlab::Git.shas_eql?(sha, ref) }
+ end
+ end
+ end
+
def url_for_object(commit, project)
h = Gitlab::Routing.url_helpers
- h.project_commit_url(project, commit,
- only_path: context[:only_path])
+
+ if referenced_merge_request_commit_shas.include?(commit.id)
+ h.diffs_project_merge_request_url(project,
+ noteable,
+ commit_id: commit.id,
+ only_path: only_path?)
+ else
+ h.project_commit_url(project,
+ commit,
+ only_path: only_path?)
+ end
end
def object_link_text_extras(object, matches)
@@ -38,6 +58,16 @@ module Banzai
extras
end
+
+ private
+
+ def noteable
+ context[:noteable]
+ end
+
+ def only_path?
+ context[:only_path]
+ end
end
end
end
diff --git a/lib/banzai/filter/epic_reference_filter.rb b/lib/banzai/filter/epic_reference_filter.rb
new file mode 100644
index 00000000000..265924abe24
--- /dev/null
+++ b/lib/banzai/filter/epic_reference_filter.rb
@@ -0,0 +1,12 @@
+module Banzai
+ module Filter
+ # The actual filter is implemented in the EE mixin
+ class EpicReferenceFilter < IssuableReferenceFilter
+ self.reference_type = :epic
+
+ def self.object_class
+ Epic
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/issuable_reference_filter.rb b/lib/banzai/filter/issuable_reference_filter.rb
new file mode 100644
index 00000000000..7addf09be73
--- /dev/null
+++ b/lib/banzai/filter/issuable_reference_filter.rb
@@ -0,0 +1,31 @@
+module Banzai
+ module Filter
+ class IssuableReferenceFilter < AbstractReferenceFilter
+ def records_per_parent
+ @records_per_project ||= {}
+
+ @records_per_project[object_class.to_s.underscore] ||= begin
+ hash = Hash.new { |h, k| h[k] = {} }
+
+ parent_per_reference.each do |path, parent|
+ record_ids = references_per_parent[path]
+
+ parent_records(parent, record_ids).each do |record|
+ hash[parent][record.iid.to_i] = record
+ end
+ end
+
+ hash
+ end
+ end
+
+ def find_object(parent, iid)
+ records_per_parent[parent][iid]
+ end
+
+ def parent_from_ref(ref)
+ parent_per_reference[ref || current_parent_path]
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index ce1ab977d3b..6877cae8c55 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -8,46 +8,24 @@ module Banzai
# When external issues tracker like Jira is activated we should not
# use issue reference pattern, but we should still be able
# to reference issues from other GitLab projects.
- class IssueReferenceFilter < AbstractReferenceFilter
+ class IssueReferenceFilter < IssuableReferenceFilter
self.reference_type = :issue
def self.object_class
Issue
end
- def find_object(project, iid)
- issues_per_project[project][iid]
- end
-
def url_for_object(issue, project)
IssuesHelper.url_for_issue(issue.iid, project, only_path: context[:only_path], internal: true)
end
- def project_from_ref(ref)
- projects_per_reference[ref || current_project_path]
- end
-
- # Returns a Hash containing the issues per Project instance.
- def issues_per_project
- @issues_per_project ||= begin
- hash = Hash.new { |h, k| h[k] = {} }
-
- projects_per_reference.each do |path, project|
- issue_ids = references_per_project[path]
- issues = project.issues.where(iid: issue_ids.to_a)
-
- issues.each do |issue|
- hash[project][issue.iid.to_i] = issue
- end
- end
-
- hash
- end
- end
-
def projects_relation_for_paths(paths)
super(paths).includes(:gitlab_issue_tracker_service)
end
+
+ def parent_records(parent, ids)
+ parent.issues.where(iid: ids.to_a)
+ end
end
end
end
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
index 5364984c9d3..d5360ad8f68 100644
--- a/lib/banzai/filter/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -33,7 +33,7 @@ module Banzai
end
def find_label(project_ref, label_id, label_name)
- project = project_from_ref(project_ref)
+ project = parent_from_ref(project_ref)
return unless project
label_params = label_params(label_id, label_name)
@@ -66,7 +66,7 @@ module Banzai
def object_link_text(object, matches)
project_path = full_project_path(matches[:namespace], matches[:project])
- project_from_ref = project_from_ref_cached(project_path)
+ project_from_ref = from_ref_cached(project_path)
reference = project_from_ref.to_human_reference(project)
label_suffix = " <i>in #{reference}</i>" if reference.present?
diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb
index 0eab865ac04..b3cfa97d0e0 100644
--- a/lib/banzai/filter/merge_request_reference_filter.rb
+++ b/lib/banzai/filter/merge_request_reference_filter.rb
@@ -4,48 +4,19 @@ module Banzai
# to merge requests that do not exist are ignored.
#
# This filter supports cross-project references.
- class MergeRequestReferenceFilter < AbstractReferenceFilter
+ class MergeRequestReferenceFilter < IssuableReferenceFilter
self.reference_type = :merge_request
def self.object_class
MergeRequest
end
- def find_object(project, iid)
- merge_requests_per_project[project][iid]
- end
-
def url_for_object(mr, project)
h = Gitlab::Routing.url_helpers
h.project_merge_request_url(project, mr,
only_path: context[:only_path])
end
- def project_from_ref(ref)
- projects_per_reference[ref || current_project_path]
- end
-
- # Returns a Hash containing the merge_requests per Project instance.
- def merge_requests_per_project
- @merge_requests_per_project ||= begin
- hash = Hash.new { |h, k| h[k] = {} }
-
- projects_per_reference.each do |path, project|
- merge_request_ids = references_per_project[path]
-
- merge_requests = project.merge_requests
- .where(iid: merge_request_ids.to_a)
- .includes(target_project: :namespace)
-
- merge_requests.each do |merge_request|
- hash[project][merge_request.iid.to_i] = merge_request
- end
- end
-
- hash
- end
- end
-
def object_link_text_extras(object, matches)
extras = super
@@ -61,6 +32,12 @@ module Banzai
extras
end
+
+ def parent_records(parent, ids)
+ parent.merge_requests
+ .where(iid: ids.to_a)
+ .includes(target_project: :namespace)
+ end
end
end
end
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
index bb5da310e09..2a6b0964ac5 100644
--- a/lib/banzai/filter/milestone_reference_filter.rb
+++ b/lib/banzai/filter/milestone_reference_filter.rb
@@ -38,7 +38,7 @@ module Banzai
def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name)
project_path = full_project_path(namespace_ref, project_ref)
- project = project_from_ref(project_path)
+ project = parent_from_ref(project_path)
return unless project
diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb
index 47151626208..97244159985 100644
--- a/lib/banzai/filter/table_of_contents_filter.rb
+++ b/lib/banzai/filter/table_of_contents_filter.rb
@@ -32,6 +32,7 @@ module Banzai
.gsub(PUNCTUATION_REGEXP, '') # remove punctuation
.tr(' ', '-') # replace spaces with dash
.squeeze('-') # replace multiple dashes with one
+ .gsub(/\A(\d+)\z/, 'anchor-\1') # digits-only hrefs conflict with issue refs
uniq = headers[id] > 0 ? "-#{headers[id]}" : ''
headers[id] += 1
diff --git a/lib/banzai/filter/upload_link_filter.rb b/lib/banzai/filter/upload_link_filter.rb
index 09844931be5..d64f9ac4eb6 100644
--- a/lib/banzai/filter/upload_link_filter.rb
+++ b/lib/banzai/filter/upload_link_filter.rb
@@ -8,7 +8,7 @@ module Banzai
#
class UploadLinkFilter < HTML::Pipeline::Filter
def call
- return doc unless project
+ return doc unless project || group
doc.xpath('descendant-or-self::a[starts-with(@href, "/uploads/")]').each do |el|
process_link_attr el.attribute('href')
@@ -28,13 +28,27 @@ module Banzai
end
def build_url(uri)
- File.join(Gitlab.config.gitlab.url, project.full_path, uri)
+ base_path = Gitlab.config.gitlab.url
+
+ if group
+ urls = Gitlab::Routing.url_helpers
+ # we need to get last 2 parts of the uri which are secret and filename
+ uri_parts = uri.split(File::SEPARATOR)
+ file_path = urls.show_group_uploads_path(group, uri_parts[-2], uri_parts[-1])
+ File.join(base_path, file_path)
+ else
+ File.join(base_path, project.full_path, uri)
+ end
end
def project
context[:project]
end
+ def group
+ context[:group]
+ end
+
# Ensure that a :project key exists in context
#
# Note that while the key might exist, its value could be nil!
diff --git a/lib/banzai/issuable_extractor.rb b/lib/banzai/issuable_extractor.rb
index cbabf9156de..49603d0b363 100644
--- a/lib/banzai/issuable_extractor.rb
+++ b/lib/banzai/issuable_extractor.rb
@@ -28,8 +28,8 @@ module Banzai
issue_parser = Banzai::ReferenceParser::IssueParser.new(project, user)
merge_request_parser = Banzai::ReferenceParser::MergeRequestParser.new(project, user)
- issuables_for_nodes = issue_parser.issues_for_nodes(nodes).merge(
- merge_request_parser.merge_requests_for_nodes(nodes)
+ issuables_for_nodes = issue_parser.records_for_nodes(nodes).merge(
+ merge_request_parser.records_for_nodes(nodes)
)
# The project for the issue/MR might be pending for deletion!
diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb
index ecb3affbba5..2691be81623 100644
--- a/lib/banzai/object_renderer.rb
+++ b/lib/banzai/object_renderer.rb
@@ -17,11 +17,11 @@ module Banzai
# project - A Project to use for redacting Markdown.
# user - The user viewing the Markdown/HTML documents, if any.
- # context - A Hash containing extra attributes to use during redaction
+ # redaction_context - A Hash containing extra attributes to use during redaction
def initialize(project, user = nil, redaction_context = {})
@project = project
@user = user
- @redaction_context = redaction_context
+ @redaction_context = base_context.merge(redaction_context)
end
# Renders and redacts an Array of objects.
@@ -73,19 +73,19 @@ module Banzai
# Returns a Banzai context for the given object and attribute.
def context_for(object, attribute)
- base_context.merge(object.banzai_render_context(attribute))
+ @redaction_context.merge(object.banzai_render_context(attribute))
end
def base_context
- @base_context ||= @redaction_context.merge(
+ {
current_user: user,
project: project,
skip_redaction: true
- )
+ }
end
def save_options
- return {} unless base_context[:xhtml]
+ return {} unless @redaction_context[:xhtml]
{ save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML }
end
diff --git a/lib/banzai/reference_parser/epic_parser.rb b/lib/banzai/reference_parser/epic_parser.rb
new file mode 100644
index 00000000000..08b8a4c9a0f
--- /dev/null
+++ b/lib/banzai/reference_parser/epic_parser.rb
@@ -0,0 +1,12 @@
+module Banzai
+ module ReferenceParser
+ # The actual parser is implemented in the EE mixin
+ class EpicParser < IssuableParser
+ self.reference_type = :epic
+
+ def records_for_nodes(_nodes)
+ {}
+ end
+ end
+ end
+end
diff --git a/lib/banzai/reference_parser/issuable_parser.rb b/lib/banzai/reference_parser/issuable_parser.rb
new file mode 100644
index 00000000000..3953867eb83
--- /dev/null
+++ b/lib/banzai/reference_parser/issuable_parser.rb
@@ -0,0 +1,25 @@
+module Banzai
+ module ReferenceParser
+ class IssuableParser < BaseParser
+ def nodes_visible_to_user(user, nodes)
+ records = records_for_nodes(nodes)
+
+ nodes.select do |node|
+ issuable = records[node]
+
+ issuable && can_read_reference?(user, issuable)
+ end
+ end
+
+ def referenced_by(nodes)
+ records = records_for_nodes(nodes)
+
+ nodes.map { |node| records[node] }.compact.uniq
+ end
+
+ def can_read_reference?(user, issuable)
+ can?(user, "read_#{issuable.class.to_s.underscore}".to_sym, issuable)
+ end
+ end
+ end
+end
diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb
index e0a8ca653cb..38d4e3f3e44 100644
--- a/lib/banzai/reference_parser/issue_parser.rb
+++ b/lib/banzai/reference_parser/issue_parser.rb
@@ -1,10 +1,10 @@
module Banzai
module ReferenceParser
- class IssueParser < BaseParser
+ class IssueParser < IssuableParser
self.reference_type = :issue
def nodes_visible_to_user(user, nodes)
- issues = issues_for_nodes(nodes)
+ issues = records_for_nodes(nodes)
readable_issues = Ability
.issues_readable_by_user(issues.values, user).to_set
@@ -14,13 +14,7 @@ module Banzai
end
end
- def referenced_by(nodes)
- issues = issues_for_nodes(nodes)
-
- nodes.map { |node| issues[node] }.compact.uniq
- end
-
- def issues_for_nodes(nodes)
+ def records_for_nodes(nodes)
@issues_for_nodes ||= grouped_objects_for_nodes(
nodes,
Issue.all.includes(
diff --git a/lib/banzai/reference_parser/merge_request_parser.rb b/lib/banzai/reference_parser/merge_request_parser.rb
index 75cbc7fdac4..a370ff5b5b3 100644
--- a/lib/banzai/reference_parser/merge_request_parser.rb
+++ b/lib/banzai/reference_parser/merge_request_parser.rb
@@ -1,25 +1,9 @@
module Banzai
module ReferenceParser
- class MergeRequestParser < BaseParser
+ class MergeRequestParser < IssuableParser
self.reference_type = :merge_request
- def nodes_visible_to_user(user, nodes)
- merge_requests = merge_requests_for_nodes(nodes)
-
- nodes.select do |node|
- merge_request = merge_requests[node]
-
- merge_request && can?(user, :read_merge_request, merge_request.project)
- end
- end
-
- def referenced_by(nodes)
- merge_requests = merge_requests_for_nodes(nodes)
-
- nodes.map { |node| merge_requests[node] }.compact.uniq
- end
-
- def merge_requests_for_nodes(nodes)
+ def records_for_nodes(nodes)
@merge_requests_for_nodes ||= grouped_objects_for_nodes(
nodes,
MergeRequest.includes(
@@ -40,10 +24,6 @@ module Banzai
self.class.data_attribute
)
end
-
- def can_read_reference?(user, ref_project, node)
- can?(user, :read_merge_request, ref_project)
- end
end
end
end
diff --git a/lib/gitlab/background_migration/populate_untracked_uploads.rb b/lib/gitlab/background_migration/populate_untracked_uploads.rb
new file mode 100644
index 00000000000..81e95e5832d
--- /dev/null
+++ b/lib/gitlab/background_migration/populate_untracked_uploads.rb
@@ -0,0 +1,259 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # This class processes a batch of rows in `untracked_files_for_uploads` by
+ # adding each file to the `uploads` table if it does not exist.
+ class PopulateUntrackedUploads # rubocop:disable Metrics/ClassLength
+ # This class is responsible for producing the attributes necessary to
+ # track an uploaded file in the `uploads` table.
+ class UntrackedFile < ActiveRecord::Base # rubocop:disable Metrics/ClassLength, Metrics/LineLength
+ self.table_name = 'untracked_files_for_uploads'
+
+ # Ends with /:random_hex/:filename
+ FILE_UPLOADER_PATH = %r{/\h+/[^/]+\z}
+ FULL_PATH_CAPTURE = %r{\A(.+)#{FILE_UPLOADER_PATH}}
+
+ # These regex patterns are tested against a relative path, relative to
+ # the upload directory.
+ # For convenience, if there exists a capture group in the pattern, then
+ # it indicates the model_id.
+ PATH_PATTERNS = [
+ {
+ pattern: %r{\A-/system/appearance/logo/(\d+)/},
+ uploader: 'AttachmentUploader',
+ model_type: 'Appearance'
+ },
+ {
+ pattern: %r{\A-/system/appearance/header_logo/(\d+)/},
+ uploader: 'AttachmentUploader',
+ model_type: 'Appearance'
+ },
+ {
+ pattern: %r{\A-/system/note/attachment/(\d+)/},
+ uploader: 'AttachmentUploader',
+ model_type: 'Note'
+ },
+ {
+ pattern: %r{\A-/system/user/avatar/(\d+)/},
+ uploader: 'AvatarUploader',
+ model_type: 'User'
+ },
+ {
+ pattern: %r{\A-/system/group/avatar/(\d+)/},
+ uploader: 'AvatarUploader',
+ model_type: 'Namespace'
+ },
+ {
+ pattern: %r{\A-/system/project/avatar/(\d+)/},
+ uploader: 'AvatarUploader',
+ model_type: 'Project'
+ },
+ {
+ pattern: FILE_UPLOADER_PATH,
+ uploader: 'FileUploader',
+ model_type: 'Project'
+ }
+ ].freeze
+
+ def to_h
+ @upload_hash ||= {
+ path: upload_path,
+ uploader: uploader,
+ model_type: model_type,
+ model_id: model_id,
+ size: file_size,
+ checksum: checksum
+ }
+ end
+
+ def upload_path
+ # UntrackedFile#path is absolute, but Upload#path depends on uploader
+ @upload_path ||=
+ if uploader == 'FileUploader'
+ # Path relative to project directory in uploads
+ matchd = path_relative_to_upload_dir.match(FILE_UPLOADER_PATH)
+ matchd[0].sub(%r{\A/}, '') # remove leading slash
+ else
+ path
+ end
+ end
+
+ def uploader
+ matching_pattern_map[:uploader]
+ end
+
+ def model_type
+ matching_pattern_map[:model_type]
+ end
+
+ def model_id
+ return @model_id if defined?(@model_id)
+
+ pattern = matching_pattern_map[:pattern]
+ matchd = path_relative_to_upload_dir.match(pattern)
+
+ # If something is captured (matchd[1] is not nil), it is a model_id
+ # Only the FileUploader pattern will not match an ID
+ @model_id = matchd[1] ? matchd[1].to_i : file_uploader_model_id
+ end
+
+ def file_size
+ File.size(absolute_path)
+ end
+
+ def checksum
+ Digest::SHA256.file(absolute_path).hexdigest
+ end
+
+ private
+
+ def matching_pattern_map
+ @matching_pattern_map ||= PATH_PATTERNS.find do |path_pattern_map|
+ path_relative_to_upload_dir.match(path_pattern_map[:pattern])
+ end
+
+ unless @matching_pattern_map
+ raise "Unknown upload path pattern \"#{path}\""
+ end
+
+ @matching_pattern_map
+ end
+
+ def file_uploader_model_id
+ matchd = path_relative_to_upload_dir.match(FULL_PATH_CAPTURE)
+ not_found_msg = <<~MSG
+ Could not capture project full_path from a FileUploader path:
+ "#{path_relative_to_upload_dir}"
+ MSG
+ raise not_found_msg unless matchd
+
+ full_path = matchd[1]
+ project = Project.find_by_full_path(full_path)
+ return nil unless project
+
+ project.id
+ end
+
+ # Not including a leading slash
+ def path_relative_to_upload_dir
+ upload_dir = Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR # rubocop:disable Metrics/LineLength
+ base = %r{\A#{Regexp.escape(upload_dir)}/}
+ @path_relative_to_upload_dir ||= path.sub(base, '')
+ end
+
+ def absolute_path
+ File.join(CarrierWave.root, path)
+ end
+ end
+
+ # This class is used to query the `uploads` table.
+ class Upload < ActiveRecord::Base
+ self.table_name = 'uploads'
+ end
+
+ def perform(start_id, end_id)
+ return unless migrate?
+
+ files = UntrackedFile.where(id: start_id..end_id)
+ processed_files = insert_uploads_if_needed(files)
+ processed_files.delete_all
+
+ drop_temp_table_if_finished
+ end
+
+ private
+
+ def migrate?
+ UntrackedFile.table_exists? && Upload.table_exists?
+ end
+
+ def insert_uploads_if_needed(files)
+ filtered_files, error_files = filter_error_files(files)
+ filtered_files = filter_existing_uploads(filtered_files)
+ filtered_files = filter_deleted_models(filtered_files)
+ insert(filtered_files)
+
+ processed_files = files.where.not(id: error_files.map(&:id))
+ processed_files
+ end
+
+ def filter_error_files(files)
+ files.partition do |file|
+ begin
+ file.to_h
+ true
+ rescue => e
+ msg = <<~MSG
+ Error parsing path "#{file.path}":
+ #{e.message}
+ #{e.backtrace.join("\n ")}
+ MSG
+ Rails.logger.error(msg)
+ false
+ end
+ end
+ end
+
+ def filter_existing_uploads(files)
+ paths = files.map(&:upload_path)
+ existing_paths = Upload.where(path: paths).pluck(:path).to_set
+
+ files.reject do |file|
+ existing_paths.include?(file.upload_path)
+ end
+ end
+
+ # There are files on disk that are not in the uploads table because their
+ # model was deleted, and we don't delete the files on disk.
+ def filter_deleted_models(files)
+ ids = deleted_model_ids(files)
+
+ files.reject do |file|
+ ids[file.model_type].include?(file.model_id)
+ end
+ end
+
+ def deleted_model_ids(files)
+ ids = {
+ 'Appearance' => [],
+ 'Namespace' => [],
+ 'Note' => [],
+ 'Project' => [],
+ 'User' => []
+ }
+
+ # group model IDs by model type
+ files.each do |file|
+ ids[file.model_type] << file.model_id
+ end
+
+ ids.each do |model_type, model_ids|
+ model_class = Object.const_get(model_type)
+ found_ids = model_class.where(id: model_ids.uniq).pluck(:id)
+ deleted_ids = ids[model_type] - found_ids
+ ids[model_type] = deleted_ids
+ end
+
+ ids
+ end
+
+ def insert(files)
+ rows = files.map do |file|
+ file.to_h.merge(created_at: 'NOW()')
+ end
+
+ Gitlab::Database.bulk_insert('uploads',
+ rows,
+ disable_quote: :created_at)
+ end
+
+ def drop_temp_table_if_finished
+ if UntrackedFile.all.empty?
+ UntrackedFile.connection.drop_table(:untracked_files_for_uploads,
+ if_exists: true)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/prepare_untracked_uploads.rb b/lib/gitlab/background_migration/prepare_untracked_uploads.rb
new file mode 100644
index 00000000000..476c46341ae
--- /dev/null
+++ b/lib/gitlab/background_migration/prepare_untracked_uploads.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # This class finds all non-hashed uploaded file paths and saves them to a
+ # `untracked_files_for_uploads` table.
+ class PrepareUntrackedUploads # rubocop:disable Metrics/ClassLength
+ # For bulk_queue_background_migration_jobs_by_range
+ include Database::MigrationHelpers
+
+ FIND_BATCH_SIZE = 500
+ RELATIVE_UPLOAD_DIR = "uploads".freeze
+ ABSOLUTE_UPLOAD_DIR = "#{CarrierWave.root}/#{RELATIVE_UPLOAD_DIR}".freeze
+ FOLLOW_UP_MIGRATION = 'PopulateUntrackedUploads'.freeze
+ START_WITH_CARRIERWAVE_ROOT_REGEX = %r{\A#{CarrierWave.root}/}
+ EXCLUDED_HASHED_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/@hashed/*".freeze
+ EXCLUDED_TMP_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/tmp/*".freeze
+
+ # This class is used to iterate over batches of
+ # `untracked_files_for_uploads` rows.
+ class UntrackedFile < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'untracked_files_for_uploads'
+ end
+
+ def perform
+ ensure_temporary_tracking_table_exists
+
+ # Since Postgres < 9.5 does not have ON CONFLICT DO NOTHING, and since
+ # doing inserts-if-not-exists without ON CONFLICT DO NOTHING would be
+ # slow, start with an empty table for Postgres < 9.5.
+ # That way we can do bulk inserts at ~30x the speed of individual
+ # inserts (~20 minutes worth of inserts at GitLab.com scale instead of
+ # ~10 hours).
+ # In all other cases, installations will get both bulk inserts and the
+ # ability for these jobs to retry without having to clear and reinsert.
+ clear_untracked_file_paths unless can_bulk_insert_and_ignore_duplicates?
+
+ store_untracked_file_paths
+
+ schedule_populate_untracked_uploads_jobs
+ end
+
+ private
+
+ def ensure_temporary_tracking_table_exists
+ table_name = :untracked_files_for_uploads
+ unless UntrackedFile.connection.table_exists?(table_name)
+ UntrackedFile.connection.create_table table_name do |t|
+ t.string :path, limit: 600, null: false
+ t.index :path, unique: true
+ end
+ end
+ end
+
+ def clear_untracked_file_paths
+ UntrackedFile.delete_all
+ end
+
+ def store_untracked_file_paths
+ return unless Dir.exist?(ABSOLUTE_UPLOAD_DIR)
+
+ each_file_batch(ABSOLUTE_UPLOAD_DIR, FIND_BATCH_SIZE) do |file_paths|
+ insert_file_paths(file_paths)
+ end
+ end
+
+ def each_file_batch(search_dir, batch_size, &block)
+ cmd = build_find_command(search_dir)
+
+ Open3.popen2(*cmd) do |stdin, stdout, status_thread|
+ yield_paths_in_batches(stdout, batch_size, &block)
+
+ raise "Find command failed" unless status_thread.value.success?
+ end
+ end
+
+ def yield_paths_in_batches(stdout, batch_size, &block)
+ paths = []
+
+ stdout.each_line("\0") do |line|
+ paths << line.chomp("\0").sub(START_WITH_CARRIERWAVE_ROOT_REGEX, '')
+
+ if paths.size >= batch_size
+ yield(paths)
+ paths = []
+ end
+ end
+
+ yield(paths)
+ end
+
+ def build_find_command(search_dir)
+ cmd = %W[find -L #{search_dir}
+ -type f
+ ! ( -path #{EXCLUDED_HASHED_UPLOADS_PATH} -prune )
+ ! ( -path #{EXCLUDED_TMP_UPLOADS_PATH} -prune )
+ -print0]
+
+ ionice = which_ionice
+ cmd = %W[#{ionice} -c Idle] + cmd if ionice
+
+ log_msg = "PrepareUntrackedUploads find command: \"#{cmd.join(' ')}\""
+ Rails.logger.info log_msg
+
+ cmd
+ end
+
+ def which_ionice
+ Gitlab::Utils.which('ionice')
+ rescue StandardError
+ # In this case, returning false is relatively safe,
+ # even though it isn't very nice
+ false
+ end
+
+ def insert_file_paths(file_paths)
+ sql = insert_sql(file_paths)
+
+ ActiveRecord::Base.connection.execute(sql)
+ end
+
+ def insert_sql(file_paths)
+ if postgresql_pre_9_5?
+ "INSERT INTO #{table_columns_and_values_for_insert(file_paths)};"
+ elsif postgresql?
+ "INSERT INTO #{table_columns_and_values_for_insert(file_paths)}"\
+ " ON CONFLICT DO NOTHING;"
+ else # MySQL
+ "INSERT IGNORE INTO"\
+ " #{table_columns_and_values_for_insert(file_paths)};"
+ end
+ end
+
+ def table_columns_and_values_for_insert(file_paths)
+ values = file_paths.map do |file_path|
+ ActiveRecord::Base.send(:sanitize_sql_array, ['(?)', file_path]) # rubocop:disable GitlabSecurity/PublicSend, Metrics/LineLength
+ end.join(', ')
+
+ "#{UntrackedFile.table_name} (path) VALUES #{values}"
+ end
+
+ def postgresql?
+ @postgresql ||= Gitlab::Database.postgresql?
+ end
+
+ def can_bulk_insert_and_ignore_duplicates?
+ !postgresql_pre_9_5?
+ end
+
+ def postgresql_pre_9_5?
+ @postgresql_pre_9_5 ||= postgresql? &&
+ Gitlab::Database.version.to_f < 9.5
+ end
+
+ def schedule_populate_untracked_uploads_jobs
+ bulk_queue_background_migration_jobs_by_range(
+ UntrackedFile, FOLLOW_UP_MIGRATION)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/checks/project_moved.rb b/lib/gitlab/checks/project_moved.rb
new file mode 100644
index 00000000000..3a1c0a3455e
--- /dev/null
+++ b/lib/gitlab/checks/project_moved.rb
@@ -0,0 +1,65 @@
+module Gitlab
+ module Checks
+ class ProjectMoved
+ REDIRECT_NAMESPACE = "redirect_namespace".freeze
+
+ def initialize(project, user, redirected_path, protocol)
+ @project = project
+ @user = user
+ @redirected_path = redirected_path
+ @protocol = protocol
+ end
+
+ def self.fetch_redirect_message(user_id, project_id)
+ redirect_key = redirect_message_key(user_id, project_id)
+
+ Gitlab::Redis::SharedState.with do |redis|
+ message = redis.get(redirect_key)
+ redis.del(redirect_key)
+ message
+ end
+ end
+
+ def add_redirect_message
+ Gitlab::Redis::SharedState.with do |redis|
+ key = self.class.redirect_message_key(user.id, project.id)
+ redis.setex(key, 5.minutes, redirect_message)
+ end
+ end
+
+ def redirect_message(rejected: false)
+ <<~MESSAGE.strip_heredoc
+ Project '#{redirected_path}' was moved to '#{project.full_path}'.
+
+ Please update your Git remote:
+
+ #{remote_url_message(rejected)}
+ MESSAGE
+ end
+
+ def permanent_redirect?
+ RedirectRoute.permanent.exists?(path: redirected_path)
+ end
+
+ private
+
+ attr_reader :project, :redirected_path, :protocol, :user
+
+ def self.redirect_message_key(user_id, project_id)
+ "#{REDIRECT_NAMESPACE}:#{user_id}:#{project_id}"
+ end
+
+ def remote_url_message(rejected)
+ if rejected
+ "git remote set-url origin #{url} and try again."
+ else
+ "git remote set-url origin #{url}"
+ end
+ end
+
+ def url
+ protocol == 'ssh' ? project.ssh_url_to_repo : project.http_url_to_repo
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/base.rb b/lib/gitlab/ci/pipeline/chain/base.rb
index 8d82e1b288d..efed19da21c 100644
--- a/lib/gitlab/ci/pipeline/chain/base.rb
+++ b/lib/gitlab/ci/pipeline/chain/base.rb
@@ -3,14 +3,13 @@ module Gitlab
module Pipeline
module Chain
class Base
- attr_reader :pipeline, :project, :current_user
+ attr_reader :pipeline, :command
+
+ delegate :project, :current_user, to: :command
def initialize(pipeline, command)
@pipeline = pipeline
@command = command
-
- @project = command.project
- @current_user = command.current_user
end
def perform!
diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb
index a126dded1ae..70732d26bbd 100644
--- a/lib/gitlab/ci/pipeline/chain/build.rb
+++ b/lib/gitlab/ci/pipeline/chain/build.rb
@@ -3,20 +3,18 @@ module Gitlab
module Pipeline
module Chain
class Build < Chain::Base
- include Chain::Helpers
-
def perform!
@pipeline.assign_attributes(
source: @command.source,
- project: @project,
- ref: ref,
- sha: sha,
- before_sha: before_sha,
- tag: tag_exists?,
+ project: @command.project,
+ ref: @command.ref,
+ sha: @command.sha,
+ before_sha: @command.before_sha,
+ tag: @command.tag_exists?,
trigger_requests: Array(@command.trigger_request),
- user: @current_user,
+ user: @command.current_user,
pipeline_schedule: @command.schedule,
- protected: protected_ref?
+ protected: @command.protected_ref?
)
@pipeline.set_config_source
@@ -25,32 +23,6 @@ module Gitlab
def break?
false
end
-
- private
-
- def ref
- @ref ||= Gitlab::Git.ref_name(origin_ref)
- end
-
- def sha
- @project.commit(origin_sha || origin_ref).try(:id)
- end
-
- def origin_ref
- @command.origin_ref
- end
-
- def origin_sha
- @command.checkout_sha || @command.after_sha
- end
-
- def before_sha
- @command.checkout_sha || @command.before_sha || Gitlab::Git::BLANK_SHA
- end
-
- def protected_ref?
- @project.protected_for?(ref)
- end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
new file mode 100644
index 00000000000..7b19b10e05b
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -0,0 +1,61 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ Command = Struct.new(
+ :source, :project, :current_user,
+ :origin_ref, :checkout_sha, :after_sha, :before_sha,
+ :trigger_request, :schedule,
+ :ignore_skip_ci, :save_incompleted,
+ :seeds_block
+ ) do
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(**params)
+ params.each do |key, value|
+ self[key] = value
+ end
+ end
+
+ def branch_exists?
+ strong_memoize(:is_branch) do
+ project.repository.branch_exists?(ref)
+ end
+ end
+
+ def tag_exists?
+ strong_memoize(:is_tag) do
+ project.repository.tag_exists?(ref)
+ end
+ end
+
+ def ref
+ strong_memoize(:ref) do
+ Gitlab::Git.ref_name(origin_ref)
+ end
+ end
+
+ def sha
+ strong_memoize(:sha) do
+ project.commit(origin_sha || origin_ref).try(:id)
+ end
+ end
+
+ def origin_sha
+ checkout_sha || after_sha
+ end
+
+ def before_sha
+ self[:before_sha] || checkout_sha || Gitlab::Git::BLANK_SHA
+ end
+
+ def protected_ref?
+ strong_memoize(:protected_ref) do
+ project.protected_for?(ref)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/create.rb b/lib/gitlab/ci/pipeline/chain/create.rb
index d5e17a123df..d19a2519803 100644
--- a/lib/gitlab/ci/pipeline/chain/create.rb
+++ b/lib/gitlab/ci/pipeline/chain/create.rb
@@ -17,11 +17,27 @@ module Gitlab
end
rescue ActiveRecord::RecordInvalid => e
error("Failed to persist the pipeline: #{e}")
+ ensure
+ if pipeline.builds.where(stage_id: nil).any?
+ invalid_builds_counter.increment(node: hostname)
+ end
end
def break?
!pipeline.persisted?
end
+
+ private
+
+ def invalid_builds_counter
+ @counter ||= Gitlab::Metrics
+ .counter(:gitlab_ci_invalid_builds_total,
+ 'Invalid builds without stage assigned counter')
+ end
+
+ def hostname
+ @hostname ||= Socket.gethostname
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/helpers.rb b/lib/gitlab/ci/pipeline/chain/helpers.rb
index 02d81286f21..bf1380a1da9 100644
--- a/lib/gitlab/ci/pipeline/chain/helpers.rb
+++ b/lib/gitlab/ci/pipeline/chain/helpers.rb
@@ -3,18 +3,6 @@ module Gitlab
module Pipeline
module Chain
module Helpers
- def branch_exists?
- return @is_branch if defined?(@is_branch)
-
- @is_branch = project.repository.branch_exists?(pipeline.ref)
- end
-
- def tag_exists?
- return @is_tag if defined?(@is_tag)
-
- @is_tag = project.repository.tag_exists?(pipeline.ref)
- end
-
def error(message)
pipeline.errors.add(:base, message)
end
diff --git a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
index 4913a604079..13c6fedd831 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
@@ -14,7 +14,7 @@ module Gitlab
unless allowed_to_trigger_pipeline?
if can?(current_user, :create_pipeline, project)
- return error("Insufficient permissions for protected ref '#{pipeline.ref}'")
+ return error("Insufficient permissions for protected ref '#{command.ref}'")
else
return error('Insufficient permissions to create a new pipeline')
end
@@ -29,7 +29,7 @@ module Gitlab
if current_user
allowed_to_create?
else # legacy triggers don't have a corresponding user
- !project.protected_for?(@pipeline.ref)
+ !@command.protected_ref?
end
end
@@ -38,10 +38,10 @@ module Gitlab
access = Gitlab::UserAccess.new(current_user, project: project)
- if branch_exists?
- access.can_update_branch?(@pipeline.ref)
- elsif tag_exists?
- access.can_create_tag?(@pipeline.ref)
+ if @command.branch_exists?
+ access.can_update_branch?(@command.ref)
+ elsif @command.tag_exists?
+ access.can_create_tag?(@command.ref)
else
true # Allow it for now and we'll reject when we check ref existence
end
diff --git a/lib/gitlab/ci/pipeline/chain/validate/repository.rb b/lib/gitlab/ci/pipeline/chain/validate/repository.rb
index 70a4cfdbdea..9699c24e5b6 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/repository.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/repository.rb
@@ -7,14 +7,11 @@ module Gitlab
include Chain::Helpers
def perform!
- unless branch_exists? || tag_exists?
+ unless @command.branch_exists? || @command.tag_exists?
return error('Reference not found')
end
- ## TODO, we check commit in the service, that is why
- # there is no repository access here.
- #
- unless pipeline.sha
+ unless @command.sha
return error('Commit not found')
end
end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index cd7b4c043da..e51794fef99 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -50,6 +50,10 @@ module Gitlab
postgresql? && version.to_f >= 9.3
end
+ def self.replication_slots_supported?
+ postgresql? && version.to_f >= 9.4
+ end
+
def self.nulls_last_order(field, direction = 'ASC')
order = "#{field} #{direction}"
@@ -116,15 +120,21 @@ module Gitlab
# values.
# return_ids - When set to true the return value will be an Array of IDs of
# the inserted rows, this only works on PostgreSQL.
- def self.bulk_insert(table, rows, return_ids: false)
+ # disable_quote - A key or an Array of keys to exclude from quoting (You
+ # become responsible for protection from SQL injection for
+ # these keys!)
+ def self.bulk_insert(table, rows, return_ids: false, disable_quote: [])
return if rows.empty?
keys = rows.first.keys
columns = keys.map { |key| connection.quote_column_name(key) }
return_ids = false if mysql?
+ disable_quote = Array(disable_quote).to_set
tuples = rows.map do |row|
- row.values_at(*keys).map { |value| connection.quote(value) }
+ keys.map do |k|
+ disable_quote.include?(k) ? row[k] : connection.quote(row[k])
+ end
end
sql = <<-EOF
diff --git a/lib/gitlab/diff/diff_refs.rb b/lib/gitlab/diff/diff_refs.rb
index c98eefbce25..88e0db830f6 100644
--- a/lib/gitlab/diff/diff_refs.rb
+++ b/lib/gitlab/diff/diff_refs.rb
@@ -13,9 +13,9 @@ module Gitlab
def ==(other)
other.is_a?(self.class) &&
- shas_equal?(base_sha, other.base_sha) &&
- shas_equal?(start_sha, other.start_sha) &&
- shas_equal?(head_sha, other.head_sha)
+ Git.shas_eql?(base_sha, other.base_sha) &&
+ Git.shas_eql?(start_sha, other.start_sha) &&
+ Git.shas_eql?(head_sha, other.head_sha)
end
alias_method :eql?, :==
@@ -47,22 +47,6 @@ module Gitlab
CompareService.new(project, head_sha).execute(project, start_sha, straight: straight)
end
end
-
- private
-
- def shas_equal?(sha1, sha2)
- return true if sha1 == sha2
- return false if sha1.nil? || sha2.nil?
- return false unless sha1.class == sha2.class
-
- length = [sha1.length, sha2.length].min
-
- # If either of the shas is below the minimum length, we cannot be sure
- # that they actually refer to the same commit because of hash collision.
- return false if length < Commit::MIN_SHA_LENGTH
-
- sha1[0, length] == sha2[0, length]
- end
end
end
end
diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb
index 2d7b57120a6..54783a07919 100644
--- a/lib/gitlab/diff/inline_diff.rb
+++ b/lib/gitlab/diff/inline_diff.rb
@@ -70,7 +70,7 @@ module Gitlab
def find_changed_line_pairs(lines)
# Prefixes of all diff lines, indicating their types
# For example: `" - + -+ ---+++ --+ -++"`
- line_prefixes = lines.each_with_object("") { |line, s| s << line[0] }.gsub(/[^ +-]/, ' ')
+ line_prefixes = lines.each_with_object("") { |line, s| s << (line[0] || ' ') }.gsub(/[^ +-]/, ' ')
changed_line_pairs = []
line_prefixes.scan(LINE_PAIRS_PATTERN) do
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index 1f31cdbc96d..1f7c35cafaa 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -70,6 +70,18 @@ module Gitlab
def diff_line_code(file_path, new_line_position, old_line_position)
"#{Digest::SHA1.hexdigest(file_path)}_#{old_line_position}_#{new_line_position}"
end
+
+ def shas_eql?(sha1, sha2)
+ return false if sha1.nil? || sha2.nil?
+ return false unless sha1.class == sha2.class
+
+ # If either of the shas is below the minimum length, we cannot be sure
+ # that they actually refer to the same commit because of hash collision.
+ length = [sha1.length, sha2.length].min
+ return false if length < Gitlab::Git::Commit::MIN_SHA_LENGTH
+
+ sha1[0, length] == sha2[0, length]
+ end
end
end
end
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index c85dcfa0475..e90b158fb34 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -6,6 +6,7 @@ module Gitlab
attr_accessor :raw_commit, :head
+ MIN_SHA_LENGTH = 7
SERIALIZE_KEYS = [
:id, :message, :parent_ids,
:authored_date, :author_name, :author_email,
@@ -213,11 +214,17 @@ module Gitlab
end
def shas_with_signatures(repository, shas)
- shas.select do |sha|
- begin
- Rugged::Commit.extract_signature(repository.rugged, sha)
- rescue Rugged::OdbError
- false
+ GitalyClient.migrate(:filter_shas_with_signatures) do |is_enabled|
+ if is_enabled
+ Gitlab::GitalyClient::CommitService.new(repository).filter_shas_with_signatures(shas)
+ else
+ shas.select do |sha|
+ begin
+ Rugged::Commit.extract_signature(repository.rugged, sha)
+ rescue Rugged::OdbError
+ false
+ end
+ end
end
end
end
diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb
index e36d5410431..7e8fe173056 100644
--- a/lib/gitlab/git/operation_service.rb
+++ b/lib/gitlab/git/operation_service.rb
@@ -83,7 +83,7 @@ module Gitlab
Gitlab::Git.check_namespace!(start_repository)
start_repository = RemoteRepository.new(start_repository) unless start_repository.is_a?(RemoteRepository)
- start_branch_name = nil if start_repository.empty_repo?
+ start_branch_name = nil if start_repository.empty?
if start_branch_name && !start_repository.branch_exists?(start_branch_name)
raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.relative_path}"
diff --git a/lib/gitlab/git/remote_repository.rb b/lib/gitlab/git/remote_repository.rb
index 3685aa20669..6bd6e58feeb 100644
--- a/lib/gitlab/git/remote_repository.rb
+++ b/lib/gitlab/git/remote_repository.rb
@@ -24,10 +24,12 @@ module Gitlab
@path = repository.path
end
- def empty_repo?
+ def empty?
# We will override this implementation in gitaly-ruby because we cannot
# use '@repository' there.
- @repository.empty_repo?
+ #
+ # Caches and memoization used on the Rails side
+ !@repository.exists? || @repository.empty?
end
def commit_id(revision)
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index eab04bcac65..73889328f36 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -75,9 +75,6 @@ module Gitlab
@attributes = Gitlab::Git::Attributes.new(path)
end
- delegate :empty?,
- to: :rugged
-
def ==(other)
path == other.path
end
@@ -206,6 +203,13 @@ module Gitlab
end
end
+ # Git repository can contains some hidden refs like:
+ # /refs/notes/*
+ # /refs/git-as-svn/*
+ # /refs/pulls/*
+ # This refs by default not visible in project page and not cloned to client side.
+ alias_method :has_visible_content?, :has_local_branches?
+
def has_local_branches_rugged?
rugged.branches.each(:local).any? do |ref|
begin
@@ -776,24 +780,21 @@ module Gitlab
end
def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
- OperationService.new(user, self).with_branch(
- branch_name,
- start_branch_name: start_branch_name,
- start_repository: start_repository
- ) do |start_commit|
-
- Gitlab::Git.check_namespace!(commit, start_repository)
-
- revert_tree_id = check_revert_content(commit, start_commit.sha)
- raise CreateTreeError unless revert_tree_id
-
- committer = user_to_committer(user)
+ gitaly_migrate(:revert) do |is_enabled|
+ args = {
+ user: user,
+ commit: commit,
+ branch_name: branch_name,
+ message: message,
+ start_branch_name: start_branch_name,
+ start_repository: start_repository
+ }
- create_commit(message: message,
- author: committer,
- committer: committer,
- tree: revert_tree_id,
- parents: [start_commit.sha])
+ if is_enabled
+ gitaly_operations_client.user_revert(args)
+ else
+ rugged_revert(args)
+ end
end
end
@@ -1007,7 +1008,7 @@ module Gitlab
Gitlab::Git.check_namespace!(start_repository)
start_repository = RemoteRepository.new(start_repository) unless start_repository.is_a?(RemoteRepository)
- return yield nil if start_repository.empty_repo?
+ return yield nil if start_repository.empty?
if start_repository.same_repository?(self)
yield commit(start_branch_name)
@@ -1123,24 +1124,8 @@ module Gitlab
Gitlab::Git::Commit.find(self, ref)
end
- # Refactoring aid; allows us to copy code from app/models/repository.rb
- def empty_repo?
- !exists? || !has_visible_content?
- end
-
- #
- # Git repository can contains some hidden refs like:
- # /refs/notes/*
- # /refs/git-as-svn/*
- # /refs/pulls/*
- # This refs by default not visible in project page and not cloned to client side.
- #
- # This method return true if repository contains some content visible in project page.
- #
- def has_visible_content?
- return @has_visible_content if defined?(@has_visible_content)
-
- @has_visible_content = has_local_branches?
+ def empty?
+ !has_visible_content?
end
# Like all public `Gitlab::Git::Repository` methods, this method is part
@@ -1175,9 +1160,15 @@ module Gitlab
end
def fsck
- output, status = run_git(%W[--git-dir=#{path} fsck], nice: true)
+ gitaly_migrate(:git_fsck) do |is_enabled|
+ msg, status = if is_enabled
+ gitaly_fsck
+ else
+ shell_fsck
+ end
- raise GitError.new("Could not fsck repository:\n#{output}") unless status.zero?
+ raise GitError.new("Could not fsck repository: #{msg}") unless status.zero?
+ end
end
def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:)
@@ -1325,6 +1316,14 @@ module Gitlab
File.write(File.join(worktree_info_path, 'sparse-checkout'), files)
end
+ def gitaly_fsck
+ gitaly_repository_client.fsck
+ end
+
+ def shell_fsck
+ run_git(%W[--git-dir=#{path} fsck], nice: true)
+ end
+
def rugged_fetch_source_branch(source_repository, source_branch, local_ref)
with_repo_branch_commit(source_repository, source_branch) do |commit|
if commit
@@ -1769,6 +1768,28 @@ module Gitlab
end
end
+ def rugged_revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
+ OperationService.new(user, self).with_branch(
+ branch_name,
+ start_branch_name: start_branch_name,
+ start_repository: start_repository
+ ) do |start_commit|
+
+ Gitlab::Git.check_namespace!(commit, start_repository)
+
+ revert_tree_id = check_revert_content(commit, start_commit.sha)
+ raise CreateTreeError unless revert_tree_id
+
+ committer = user_to_committer(user)
+
+ create_commit(message: message,
+ author: committer,
+ committer: committer,
+ tree: revert_tree_id,
+ parents: [start_commit.sha])
+ end
+ end
+
def gitaly_add_branch(branch_name, user, target)
gitaly_operation_client.user_create_branch(branch_name, user, target)
rescue GRPC::FailedPrecondition => ex
diff --git a/lib/gitlab/git/storage/checker.rb b/lib/gitlab/git/storage/checker.rb
new file mode 100644
index 00000000000..de63cb4b40c
--- /dev/null
+++ b/lib/gitlab/git/storage/checker.rb
@@ -0,0 +1,98 @@
+module Gitlab
+ module Git
+ module Storage
+ class Checker
+ include CircuitBreakerSettings
+
+ attr_reader :storage_path, :storage, :hostname, :logger
+
+ def self.check_all(logger = Rails.logger)
+ threads = Gitlab.config.repositories.storages.keys.map do |storage_name|
+ Thread.new do
+ Thread.current[:result] = new(storage_name, logger).check_with_lease
+ end
+ end
+
+ threads.map do |thread|
+ thread.join
+ thread[:result]
+ end
+ end
+
+ def initialize(storage, logger = Rails.logger)
+ @storage = storage
+ config = Gitlab.config.repositories.storages[@storage]
+ @storage_path = config['path']
+ @logger = logger
+
+ @hostname = Gitlab::Environment.hostname
+ end
+
+ def check_with_lease
+ lease_key = "storage_check:#{cache_key}"
+ lease = Gitlab::ExclusiveLease.new(lease_key, timeout: storage_timeout)
+ result = { storage: storage, success: nil }
+
+ if uuid = lease.try_obtain
+ result[:success] = check
+
+ Gitlab::ExclusiveLease.cancel(lease_key, uuid)
+ else
+ logger.warn("#{hostname}: #{storage}: Skipping check, previous check still running")
+ end
+
+ result
+ end
+
+ def check
+ if Gitlab::Git::Storage::ForkedStorageCheck.storage_available?(storage_path, storage_timeout, access_retries)
+ track_storage_accessible
+ true
+ else
+ track_storage_inaccessible
+ logger.error("#{hostname}: #{storage}: Not accessible.")
+ false
+ end
+ end
+
+ private
+
+ def track_storage_inaccessible
+ first_failure = current_failure_info.first_failure || Time.now
+ last_failure = Time.now
+
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.pipelined do
+ redis.hset(cache_key, :first_failure, first_failure.to_i)
+ redis.hset(cache_key, :last_failure, last_failure.to_i)
+ redis.hincrby(cache_key, :failure_count, 1)
+ redis.expire(cache_key, failure_reset_time)
+ maintain_known_keys(redis)
+ end
+ end
+ end
+
+ def track_storage_accessible
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.pipelined do
+ redis.hset(cache_key, :first_failure, nil)
+ redis.hset(cache_key, :last_failure, nil)
+ redis.hset(cache_key, :failure_count, 0)
+ maintain_known_keys(redis)
+ end
+ end
+ end
+
+ def maintain_known_keys(redis)
+ expire_time = Time.now.to_i + failure_reset_time
+ redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, expire_time, cache_key)
+ redis.zremrangebyscore(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, '-inf', Time.now.to_i)
+ end
+
+ def current_failure_info
+ FailureInfo.load(cache_key)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/storage/circuit_breaker.rb b/lib/gitlab/git/storage/circuit_breaker.rb
index 4328c0ea29b..898bb1b65be 100644
--- a/lib/gitlab/git/storage/circuit_breaker.rb
+++ b/lib/gitlab/git/storage/circuit_breaker.rb
@@ -4,22 +4,11 @@ module Gitlab
class CircuitBreaker
include CircuitBreakerSettings
- FailureInfo = Struct.new(:last_failure, :failure_count)
-
attr_reader :storage,
- :hostname,
- :storage_path
-
- delegate :last_failure, :failure_count, to: :failure_info
-
- def self.reset_all!
- Gitlab::Git::Storage.redis.with do |redis|
- all_storage_keys = redis.zrange(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, -1)
- redis.del(*all_storage_keys) unless all_storage_keys.empty?
- end
+ :hostname
- RequestStore.delete(:circuitbreaker_cache)
- end
+ delegate :last_failure, :failure_count, :no_failures?,
+ to: :failure_info
def self.for_storage(storage)
cached_circuitbreakers = RequestStore.fetch(:circuitbreaker_cache) do
@@ -46,9 +35,6 @@ module Gitlab
def initialize(storage, hostname)
@storage = storage
@hostname = hostname
-
- config = Gitlab.config.repositories.storages[@storage]
- @storage_path = config['path']
end
def perform
@@ -65,15 +51,6 @@ module Gitlab
failure_count > failure_count_threshold
end
- def backing_off?
- return false if no_failures?
-
- recent_failure = last_failure > failure_wait_time.seconds.ago
- too_many_failures = failure_count > backoff_threshold
-
- recent_failure && too_many_failures
- end
-
private
# The circuitbreaker can be enabled for the entire fleet using a Feature
@@ -86,88 +63,13 @@ module Gitlab
end
def failure_info
- @failure_info ||= get_failure_info
- end
-
- # Memoizing the `storage_available` call means we only do it once per
- # request when the storage is available.
- #
- # When the storage appears not available, and the memoized value is `false`
- # we might want to try again.
- def storage_available?
- return @storage_available if @storage_available
-
- if @storage_available = Gitlab::Git::Storage::ForkedStorageCheck
- .storage_available?(storage_path, storage_timeout, access_retries)
- track_storage_accessible
- else
- track_storage_inaccessible
- end
-
- @storage_available
+ @failure_info ||= FailureInfo.load(cache_key)
end
def check_storage_accessible!
if circuit_broken?
raise Gitlab::Git::Storage::CircuitOpen.new("Circuit for #{storage} is broken", failure_reset_time)
end
-
- if backing_off?
- raise Gitlab::Git::Storage::Failing.new("Backing off access to #{storage}", failure_wait_time)
- end
-
- unless storage_available?
- raise Gitlab::Git::Storage::Inaccessible.new("#{storage} not accessible", failure_wait_time)
- end
- end
-
- def no_failures?
- last_failure.blank? && failure_count == 0
- end
-
- def track_storage_inaccessible
- @failure_info = FailureInfo.new(Time.now, failure_count + 1)
-
- Gitlab::Git::Storage.redis.with do |redis|
- redis.pipelined do
- redis.hset(cache_key, :last_failure, last_failure.to_i)
- redis.hincrby(cache_key, :failure_count, 1)
- redis.expire(cache_key, failure_reset_time)
- maintain_known_keys(redis)
- end
- end
- end
-
- def track_storage_accessible
- @failure_info = FailureInfo.new(nil, 0)
-
- Gitlab::Git::Storage.redis.with do |redis|
- redis.pipelined do
- redis.hset(cache_key, :last_failure, nil)
- redis.hset(cache_key, :failure_count, 0)
- maintain_known_keys(redis)
- end
- end
- end
-
- def maintain_known_keys(redis)
- expire_time = Time.now.to_i + failure_reset_time
- redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, expire_time, cache_key)
- redis.zremrangebyscore(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, '-inf', Time.now.to_i)
- end
-
- def get_failure_info
- last_failure, failure_count = Gitlab::Git::Storage.redis.with do |redis|
- redis.hmget(cache_key, :last_failure, :failure_count)
- end
-
- last_failure = Time.at(last_failure.to_i) if last_failure.present?
-
- FailureInfo.new(last_failure, failure_count.to_i)
- end
-
- def cache_key
- @cache_key ||= "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage}:#{hostname}"
end
end
end
diff --git a/lib/gitlab/git/storage/circuit_breaker_settings.rb b/lib/gitlab/git/storage/circuit_breaker_settings.rb
index 257fe8cd8f0..c9e225f187d 100644
--- a/lib/gitlab/git/storage/circuit_breaker_settings.rb
+++ b/lib/gitlab/git/storage/circuit_breaker_settings.rb
@@ -6,10 +6,6 @@ module Gitlab
application_settings.circuitbreaker_failure_count_threshold
end
- def failure_wait_time
- application_settings.circuitbreaker_failure_wait_time
- end
-
def failure_reset_time
application_settings.circuitbreaker_failure_reset_time
end
@@ -22,8 +18,12 @@ module Gitlab
application_settings.circuitbreaker_access_retries
end
- def backoff_threshold
- application_settings.circuitbreaker_backoff_threshold
+ def check_interval
+ application_settings.circuitbreaker_check_interval
+ end
+
+ def cache_key
+ @cache_key ||= "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage}:#{hostname}"
end
private
diff --git a/lib/gitlab/git/storage/failure_info.rb b/lib/gitlab/git/storage/failure_info.rb
new file mode 100644
index 00000000000..387279c110d
--- /dev/null
+++ b/lib/gitlab/git/storage/failure_info.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ module Git
+ module Storage
+ class FailureInfo
+ attr_accessor :first_failure, :last_failure, :failure_count
+
+ def self.reset_all!
+ Gitlab::Git::Storage.redis.with do |redis|
+ all_storage_keys = redis.zrange(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, -1)
+ redis.del(*all_storage_keys) unless all_storage_keys.empty?
+ end
+
+ RequestStore.delete(:circuitbreaker_cache)
+ end
+
+ def self.load(cache_key)
+ first_failure, last_failure, failure_count = Gitlab::Git::Storage.redis.with do |redis|
+ redis.hmget(cache_key, :first_failure, :last_failure, :failure_count)
+ end
+
+ last_failure = Time.at(last_failure.to_i) if last_failure.present?
+ first_failure = Time.at(first_failure.to_i) if first_failure.present?
+
+ new(first_failure, last_failure, failure_count.to_i)
+ end
+
+ def initialize(first_failure, last_failure, failure_count)
+ @first_failure = first_failure
+ @last_failure = last_failure
+ @failure_count = failure_count
+ end
+
+ def no_failures?
+ first_failure.blank? && last_failure.blank? && failure_count == 0
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/storage/null_circuit_breaker.rb b/lib/gitlab/git/storage/null_circuit_breaker.rb
index a12d52d295f..261c936c689 100644
--- a/lib/gitlab/git/storage/null_circuit_breaker.rb
+++ b/lib/gitlab/git/storage/null_circuit_breaker.rb
@@ -11,6 +11,9 @@ module Gitlab
# These will always have nil values
attr_reader :storage_path
+ delegate :last_failure, :failure_count, :no_failures?,
+ to: :failure_info
+
def initialize(storage, hostname, error: nil)
@storage = storage
@hostname = hostname
@@ -29,16 +32,17 @@ module Gitlab
false
end
- def last_failure
- circuit_broken? ? Time.now : nil
- end
-
- def failure_count
- circuit_broken? ? failure_count_threshold : 0
- end
-
def failure_info
- Gitlab::Git::Storage::CircuitBreaker::FailureInfo.new(last_failure, failure_count)
+ @failure_info ||=
+ if circuit_broken?
+ Gitlab::Git::Storage::FailureInfo.new(Time.now,
+ Time.now,
+ failure_count_threshold)
+ else
+ Gitlab::Git::Storage::FailureInfo.new(nil,
+ nil,
+ 0)
+ end
end
end
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 8998c4b1a83..56f6febe86d 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -102,18 +102,15 @@ module Gitlab
end
def check_project_moved!
- return unless redirected_path
+ return if redirected_path.nil?
- url = protocol == 'ssh' ? project.ssh_url_to_repo : project.http_url_to_repo
- message = <<-MESSAGE.strip_heredoc
- Project '#{redirected_path}' was moved to '#{project.full_path}'.
+ project_moved = Checks::ProjectMoved.new(project, user, redirected_path, protocol)
- Please update your Git remote and try again:
-
- git remote set-url origin #{url}
- MESSAGE
-
- raise ProjectMovedError, message
+ if project_moved.permanent_redirect?
+ project_moved.add_redirect_message
+ else
+ raise ProjectMovedError, project_moved.redirect_message(rejected: true)
+ end
end
def check_command_disabled!(cmd)
@@ -166,7 +163,7 @@ module Gitlab
end
if Gitlab::Database.read_only?
- raise UnauthorizedError, ERROR_MESSAGES[:cannot_push_to_read_only]
+ raise UnauthorizedError, push_to_read_only_message
end
if deploy_key
@@ -280,5 +277,9 @@ module Gitlab
UserAccess.new(user, project: project)
end
end
+
+ def push_to_read_only_message
+ ERROR_MESSAGES[:cannot_push_to_read_only]
+ end
end
end
diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb
index 98f1f45b338..1c9477e84b2 100644
--- a/lib/gitlab/git_access_wiki.rb
+++ b/lib/gitlab/git_access_wiki.rb
@@ -19,10 +19,14 @@ module Gitlab
end
if Gitlab::Database.read_only?
- raise UnauthorizedError, ERROR_MESSAGES[:read_only]
+ raise UnauthorizedError, push_to_read_only_message
end
true
end
+
+ def push_to_read_only_message
+ ERROR_MESSAGES[:read_only]
+ end
end
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index f27cd800bdd..b753ac46291 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -27,7 +27,7 @@ module Gitlab
end
SERVER_VERSION_FILE = 'GITALY_SERVER_VERSION'.freeze
- MAXIMUM_GITALY_CALLS = 30
+ MAXIMUM_GITALY_CALLS = 35
CLIENT_NAME = (Sidekiq.server? ? 'gitlab-sidekiq' : 'gitlab-web').freeze
MUTEX = Mutex.new
@@ -336,6 +336,12 @@ module Gitlab
s.dup.force_encoding(Encoding::ASCII_8BIT)
end
+ def self.binary_stringio(s)
+ io = StringIO.new(s || '')
+ io.set_encoding(Encoding::ASCII_8BIT)
+ io
+ end
+
def self.encode_repeated(a)
Google::Protobuf::RepeatedField.new(:bytes, a.map { |s| self.encode(s) } )
end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 34807d280e5..7985f5b5457 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -250,6 +250,26 @@ module Gitlab
consume_commits_response(response)
end
+ def filter_shas_with_signatures(shas)
+ request = Gitaly::FilterShasWithSignaturesRequest.new(repository: @gitaly_repo)
+
+ enum = Enumerator.new do |y|
+ shas.each_slice(20) do |revs|
+ request.shas = GitalyClient.encode_repeated(revs)
+
+ y.yield request
+
+ request = Gitaly::FilterShasWithSignaturesRequest.new
+ end
+ end
+
+ response = GitalyClient.call(@repository.storage, :commit_service, :filter_shas_with_signatures, enum)
+
+ response.flat_map do |msg|
+ msg.shas.map { |sha| EncodingHelper.encode!(sha) }
+ end
+ end
+
private
def call_commit_diff(request_params, options = {})
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index 51de6a9a75d..400a4af363b 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -124,7 +124,31 @@ module Gitlab
end
def user_cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
- request = Gitaly::UserCherryPickRequest.new(
+ call_cherry_pick_or_revert(:cherry_pick,
+ user: user,
+ commit: commit,
+ branch_name: branch_name,
+ message: message,
+ start_branch_name: start_branch_name,
+ start_repository: start_repository)
+ end
+
+ def user_revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
+ call_cherry_pick_or_revert(:revert,
+ user: user,
+ commit: commit,
+ branch_name: branch_name,
+ message: message,
+ start_branch_name: start_branch_name,
+ start_repository: start_repository)
+ end
+
+ private
+
+ def call_cherry_pick_or_revert(rpc, user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
+ request_class = "Gitaly::User#{rpc.to_s.camelcase}Request".constantize
+
+ request = request_class.new(
repository: @gitaly_repo,
user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
commit: commit.to_gitaly_commit,
@@ -137,11 +161,15 @@ module Gitlab
response = GitalyClient.call(
@repository.storage,
:operation_service,
- :user_cherry_pick,
+ :"user_#{rpc}",
request,
remote_storage: start_repository.storage
)
+ handle_cherry_pick_or_revert_response(response)
+ end
+
+ def handle_cherry_pick_or_revert_response(response)
if response.pre_receive_error.presence
raise Gitlab::Git::HooksService::PreReceiveError, response.pre_receive_error
elsif response.commit_error.presence
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index b9e606592d7..a477d618f63 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -87,6 +87,17 @@ module Gitlab
response.result
end
+
+ def fsck
+ request = Gitaly::FsckRequest.new(repository: @gitaly_repo)
+ response = GitalyClient.call(@storage, :repository_service, :fsck, request)
+
+ if response.error.empty?
+ return "", 0
+ else
+ return response.error.b, 1
+ end
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb
index c8f065f5881..337d225d081 100644
--- a/lib/gitlab/gitaly_client/wiki_service.rb
+++ b/lib/gitlab/gitaly_client/wiki_service.rb
@@ -18,12 +18,11 @@ module Gitlab
commit_details: gitaly_commit_details(commit_details)
)
- strio = StringIO.new(content)
+ strio = GitalyClient.binary_stringio(content)
enum = Enumerator.new do |y|
until strio.eof?
- chunk = strio.read(MAX_MSG_SIZE)
- request.content = GitalyClient.encode(chunk)
+ request.content = strio.read(MAX_MSG_SIZE)
y.yield request
@@ -46,12 +45,11 @@ module Gitlab
commit_details: gitaly_commit_details(commit_details)
)
- strio = StringIO.new(content)
+ strio = GitalyClient.binary_stringio(content)
enum = Enumerator.new do |y|
until strio.eof?
- chunk = strio.read(MAX_MSG_SIZE)
- request.content = GitalyClient.encode(chunk)
+ request.content = strio.read(MAX_MSG_SIZE)
y.yield request
diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb
index 94678b6ec40..3f3f10596c5 100644
--- a/lib/gitlab/identifier.rb
+++ b/lib/gitlab/identifier.rb
@@ -2,9 +2,8 @@
# key-13 or user-36 or last commit
module Gitlab
module Identifier
- def identify(identifier, project, newrev)
+ def identify(identifier, project = nil, newrev = nil)
if identifier.blank?
- # Local push from gitlab
identify_using_commit(project, newrev)
elsif identifier =~ /\Auser-\d+\Z/
# git push over http
@@ -17,6 +16,8 @@ module Gitlab
# Tries to identify a user based on a commit SHA.
def identify_using_commit(project, ref)
+ return if project.nil? && ref.nil?
+
commit = project.commit(ref)
return if !commit || !commit.author_email
diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb
index 436a9e9550d..f4901be9581 100644
--- a/lib/gitlab/metrics/samplers/ruby_sampler.rb
+++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb
@@ -32,7 +32,7 @@ module Gitlab
def init_metrics
metrics = {}
- metrics[:sampler_duration] = Metrics.histogram(with_prefix(:sampler_duration, :seconds), 'Sampler time', {})
+ metrics[:sampler_duration] = Metrics.histogram(with_prefix(:sampler_duration, :seconds), 'Sampler time', { worker: nil })
metrics[:total_time] = Metrics.gauge(with_prefix(:gc, :time_total), 'Total GC time', labels, :livesum)
GC.stat.keys.each do |key|
metrics[key] = Metrics.gauge(with_prefix(:gc, key), to_doc_string(key), labels, :livesum)
@@ -100,9 +100,9 @@ module Gitlab
worker_no = ::Prometheus::Client::Support::Unicorn.worker_id
if worker_no
- { unicorn: worker_no }
+ { worker: worker_no }
else
- { unicorn: 'master' }
+ { worker: 'master' }
end
end
end
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index bc836dcc08d..9ff82d628c0 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -1,7 +1,7 @@
module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor < Banzai::ReferenceExtractor
- REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range directly_addressed_user).freeze
+ REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range directly_addressed_user epic).freeze
attr_accessor :project, :current_user, :author
def initialize(project, current_user = nil)
diff --git a/lib/gitlab/storage_check.rb b/lib/gitlab/storage_check.rb
new file mode 100644
index 00000000000..fe81513c9ec
--- /dev/null
+++ b/lib/gitlab/storage_check.rb
@@ -0,0 +1,11 @@
+require_relative 'storage_check/cli'
+require_relative 'storage_check/gitlab_caller'
+require_relative 'storage_check/option_parser'
+require_relative 'storage_check/response'
+
+module Gitlab
+ module StorageCheck
+ ENDPOINT = '/-/storage_check'.freeze
+ Options = Struct.new(:target, :token, :interval, :dryrun)
+ end
+end
diff --git a/lib/gitlab/storage_check/cli.rb b/lib/gitlab/storage_check/cli.rb
new file mode 100644
index 00000000000..04bf1bf1d26
--- /dev/null
+++ b/lib/gitlab/storage_check/cli.rb
@@ -0,0 +1,69 @@
+module Gitlab
+ module StorageCheck
+ class CLI
+ def self.start!(args)
+ runner = new(Gitlab::StorageCheck::OptionParser.parse!(args))
+ runner.start_loop
+ end
+
+ attr_reader :logger, :options
+
+ def initialize(options)
+ @options = options
+ @logger = Logger.new(STDOUT)
+ end
+
+ def start_loop
+ logger.info "Checking #{options.target} every #{options.interval} seconds"
+
+ if options.dryrun
+ logger.info "Dryrun, exiting..."
+ return
+ end
+
+ begin
+ loop do
+ response = GitlabCaller.new(options).call!
+ log_response(response)
+ update_settings(response)
+
+ sleep options.interval
+ end
+ rescue Interrupt
+ logger.info "Ending storage-check"
+ end
+ end
+
+ def update_settings(response)
+ previous_interval = options.interval
+
+ if response.valid?
+ options.interval = response.check_interval || previous_interval
+ end
+
+ if previous_interval != options.interval
+ logger.info "Interval changed: #{options.interval} seconds"
+ end
+ end
+
+ def log_response(response)
+ unless response.valid?
+ return logger.error("Invalid response checking nfs storage: #{response.http_response.inspect}")
+ end
+
+ if response.responsive_shards.any?
+ logger.debug("Responsive shards: #{response.responsive_shards.join(', ')}")
+ end
+
+ warnings = []
+ if response.skipped_shards.any?
+ warnings << "Skipped shards: #{response.skipped_shards.join(', ')}"
+ end
+ if response.failing_shards.any?
+ warnings << "Failing shards: #{response.failing_shards.join(', ')}"
+ end
+ logger.warn(warnings.join(' - ')) if warnings.any?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/storage_check/gitlab_caller.rb b/lib/gitlab/storage_check/gitlab_caller.rb
new file mode 100644
index 00000000000..44952b68844
--- /dev/null
+++ b/lib/gitlab/storage_check/gitlab_caller.rb
@@ -0,0 +1,39 @@
+require 'excon'
+
+module Gitlab
+ module StorageCheck
+ class GitlabCaller
+ def initialize(options)
+ @options = options
+ end
+
+ def call!
+ Gitlab::StorageCheck::Response.new(get_response)
+ rescue Errno::ECONNREFUSED, Excon::Error
+ # Server not ready, treated as invalid response.
+ Gitlab::StorageCheck::Response.new(nil)
+ end
+
+ def get_response
+ scheme, *other_parts = URI.split(@options.target)
+ socket_path = if scheme == 'unix'
+ other_parts.compact.join
+ end
+
+ connection = Excon.new(@options.target, socket: socket_path)
+ connection.post(path: Gitlab::StorageCheck::ENDPOINT,
+ headers: headers)
+ end
+
+ def headers
+ @headers ||= begin
+ headers = {}
+ headers['Content-Type'] = headers['Accept'] = 'application/json'
+ headers['TOKEN'] = @options.token if @options.token
+
+ headers
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/storage_check/option_parser.rb b/lib/gitlab/storage_check/option_parser.rb
new file mode 100644
index 00000000000..66ed7906f97
--- /dev/null
+++ b/lib/gitlab/storage_check/option_parser.rb
@@ -0,0 +1,39 @@
+module Gitlab
+ module StorageCheck
+ class OptionParser
+ def self.parse!(args)
+ # Start out with some defaults
+ options = Gitlab::StorageCheck::Options.new(nil, nil, 1, false)
+
+ parser = ::OptionParser.new do |opts|
+ opts.banner = "Usage: bin/storage_check [options]"
+
+ opts.on('-t=string', '--target string', 'URL or socket to trigger storage check') do |value|
+ options.target = value
+ end
+
+ opts.on('-T=string', '--token string', 'Health token to use') { |value| options.token = value }
+
+ opts.on('-i=n', '--interval n', ::OptionParser::DecimalInteger, 'Seconds between checks') do |value|
+ options.interval = value
+ end
+
+ opts.on('-d', '--dryrun', "Output what will be performed, but don't start the process") do |value|
+ options.dryrun = value
+ end
+ end
+ parser.parse!(args)
+
+ unless options.target
+ raise ::OptionParser::InvalidArgument.new('Provide a URI to provide checks')
+ end
+
+ if URI.parse(options.target).scheme.nil?
+ raise ::OptionParser::InvalidArgument.new('Add the scheme to the target, `unix://`, `https://` or `http://` are supported')
+ end
+
+ options
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/storage_check/response.rb b/lib/gitlab/storage_check/response.rb
new file mode 100644
index 00000000000..326ab236e3e
--- /dev/null
+++ b/lib/gitlab/storage_check/response.rb
@@ -0,0 +1,77 @@
+require 'json'
+
+module Gitlab
+ module StorageCheck
+ class Response
+ attr_reader :http_response
+
+ def initialize(http_response)
+ @http_response = http_response
+ end
+
+ def valid?
+ @http_response && (200...299).cover?(@http_response.status) &&
+ @http_response.headers['Content-Type'].include?('application/json') &&
+ parsed_response
+ end
+
+ def check_interval
+ return nil unless parsed_response
+
+ parsed_response['check_interval']
+ end
+
+ def responsive_shards
+ divided_results[:responsive_shards]
+ end
+
+ def skipped_shards
+ divided_results[:skipped_shards]
+ end
+
+ def failing_shards
+ divided_results[:failing_shards]
+ end
+
+ private
+
+ def results
+ return [] unless parsed_response
+
+ parsed_response['results']
+ end
+
+ def divided_results
+ return @divided_results if @divided_results
+
+ @divided_results = {}
+ @divided_results[:responsive_shards] = []
+ @divided_results[:skipped_shards] = []
+ @divided_results[:failing_shards] = []
+
+ results.each do |info|
+ name = info['storage']
+
+ case info['success']
+ when true
+ @divided_results[:responsive_shards] << name
+ when false
+ @divided_results[:failing_shards] << name
+ else
+ @divided_results[:skipped_shards] << name
+ end
+ end
+
+ @divided_results
+ end
+
+ def parsed_response
+ return @parsed_response if defined?(@parsed_response)
+
+ @parsed_response = JSON.parse(@http_response.body)
+ rescue JSON::JSONError
+ @parsed_response = nil
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index abb3d3a02c3..b3baaf036d8 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -46,5 +46,22 @@ module Gitlab
def random_string
Random.rand(Float::MAX.to_i).to_s(36)
end
+
+ # See: http://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby
+ # Cross-platform way of finding an executable in the $PATH.
+ #
+ # which('ruby') #=> /usr/bin/ruby
+ def which(cmd, env = ENV)
+ exts = env['PATHEXT'] ? env['PATHEXT'].split(';') : ['']
+
+ env['PATH'].split(File::PATH_SEPARATOR).each do |path|
+ exts.each do |ext|
+ exe = File.join(path, "#{cmd}#{ext}")
+ return exe if File.executable?(exe) && !File.directory?(exe)
+ end
+ end
+
+ nil
+ end
end
end
diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po
index 8e688dede89..374164cbe65 100644
--- a/locale/bg/gitlab.po
+++ b/locale/bg/gitlab.po
@@ -504,13 +504,13 @@ msgstr ""
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
msgstr ""
-msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Copy cluster name"
@@ -519,7 +519,7 @@ msgstr ""
msgid "ClusterIntegration|Create cluster"
msgstr ""
-msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
@@ -528,10 +528,10 @@ msgstr ""
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine project"
+msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
@@ -585,7 +585,7 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
@@ -594,13 +594,13 @@ msgstr ""
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
-msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
msgstr ""
msgid "ClusterIntegration|Zone"
msgstr ""
-msgid "ClusterIntegration|access to Google Container Engine"
+msgid "ClusterIntegration|access to Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|cluster"
diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po
index ef730d91c75..a79a7d1a353 100644
--- a/locale/de/gitlab.po
+++ b/locale/de/gitlab.po
@@ -504,13 +504,13 @@ msgstr ""
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
msgstr ""
-msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Copy cluster name"
@@ -519,7 +519,7 @@ msgstr ""
msgid "ClusterIntegration|Create cluster"
msgstr ""
-msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
@@ -528,10 +528,10 @@ msgstr ""
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine project"
+msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
@@ -585,7 +585,7 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
@@ -594,13 +594,13 @@ msgstr ""
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
-msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
msgstr ""
msgid "ClusterIntegration|Zone"
msgstr ""
-msgid "ClusterIntegration|access to Google Container Engine"
+msgid "ClusterIntegration|access to Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|cluster"
diff --git a/locale/eo/gitlab.po b/locale/eo/gitlab.po
index 0a1b379b3d3..f7be343c4e1 100644
--- a/locale/eo/gitlab.po
+++ b/locale/eo/gitlab.po
@@ -504,13 +504,13 @@ msgstr ""
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
msgstr ""
-msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Copy cluster name"
@@ -519,7 +519,7 @@ msgstr ""
msgid "ClusterIntegration|Create cluster"
msgstr ""
-msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
@@ -528,10 +528,10 @@ msgstr ""
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine project"
+msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
@@ -585,7 +585,7 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
@@ -594,13 +594,13 @@ msgstr ""
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
-msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
msgstr ""
msgid "ClusterIntegration|Zone"
msgstr ""
-msgid "ClusterIntegration|access to Google Container Engine"
+msgid "ClusterIntegration|access to Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|cluster"
diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po
index 31ff4e08592..c35a3503019 100644
--- a/locale/es/gitlab.po
+++ b/locale/es/gitlab.po
@@ -504,13 +504,13 @@ msgstr ""
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
msgstr ""
-msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Copy cluster name"
@@ -519,7 +519,7 @@ msgstr ""
msgid "ClusterIntegration|Create cluster"
msgstr ""
-msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
@@ -528,10 +528,10 @@ msgstr ""
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine project"
+msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
@@ -585,7 +585,7 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
@@ -594,13 +594,13 @@ msgstr ""
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
-msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
msgstr ""
msgid "ClusterIntegration|Zone"
msgstr ""
-msgid "ClusterIntegration|access to Google Container Engine"
+msgid "ClusterIntegration|access to Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|cluster"
diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po
index 41fa86451f5..a0e523339db 100644
--- a/locale/fr/gitlab.po
+++ b/locale/fr/gitlab.po
@@ -504,14 +504,14 @@ msgstr "L'intégration du cluster est activée pour ce projet."
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
msgstr "L'intégration de cluster est activée pour ce projet. La désactivation de cette intégration n’affectera pas votre cluster, il coupera temporairement la connexion de GitLab à celui-ci."
-msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
-msgstr "Le cluster est en cours de création sur Google Container Engine…"
+msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
+msgstr "Le cluster est en cours de création sur Google Kubernetes Engine…"
msgid "ClusterIntegration|Cluster name"
msgstr "Nom du cluster"
-msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
-msgstr "Le cluster a été correctement créé sur Google Container Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
+msgstr "Le cluster a été correctement créé sur Google Kubernetes Engine"
msgid "ClusterIntegration|Copy cluster name"
msgstr "Copier le nom du cluster"
@@ -519,8 +519,8 @@ msgstr "Copier le nom du cluster"
msgid "ClusterIntegration|Create cluster"
msgstr "Créer le cluster"
-msgid "ClusterIntegration|Create new cluster on Google Container Engine"
-msgstr "Créer un nouveau cluster sur Google Container Engine"
+msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
+msgstr "Créer un nouveau cluster sur Google Kubernetes Engine"
msgid "ClusterIntegration|Enable cluster integration"
msgstr "Activer l’intégration du cluster"
@@ -528,11 +528,11 @@ msgstr "Activer l’intégration du cluster"
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr "ID de projet Google Cloud Platform"
-msgid "ClusterIntegration|Google Container Engine"
-msgstr "Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine"
+msgstr "Google Kubernetes Engine"
-msgid "ClusterIntegration|Google Container Engine project"
-msgstr "Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine project"
+msgstr "Google Kubernetes Engine"
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr "En savoir plus sur %{link_to_documentation}"
@@ -585,8 +585,8 @@ msgstr "Voir les zones"
msgid "ClusterIntegration|Something went wrong on our end."
msgstr "Un problème est survenu de notre côté."
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
-msgstr "Un problème est survenu lors de la création de votre cluster sur Google Container Engine."
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine"
+msgstr "Un problème est survenu lors de la création de votre cluster sur Google Kubernetes Engine."
msgid "ClusterIntegration|Toggle Cluster"
msgstr "Activer/désactiver le cluster"
@@ -594,14 +594,14 @@ msgstr "Activer/désactiver le cluster"
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "Avec un cluster associé à ce projet, vous pouvez utiliser des applications de revue, déployer vos applications, exécuter vos pipelines et bien plus encore, de manière très simple."
-msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
-msgstr "Votre compte doit disposer de %{link_to_container_engine}"
+msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
+msgstr "Votre compte doit disposer de %{link_to_kubernetes_engine}"
msgid "ClusterIntegration|Zone"
msgstr "Zone"
-msgid "ClusterIntegration|access to Google Container Engine"
-msgstr "Accéder à Google Container Engine"
+msgid "ClusterIntegration|access to Google Kubernetes Engine"
+msgstr "Accéder à Google Kubernetes Engine"
msgid "ClusterIntegration|cluster"
msgstr "cluster"
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 32afb7b06e4..3ebc7859232 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-22 16:40+0300\n"
-"PO-Revision-Date: 2017-10-22 16:40+0300\n"
+"POT-Creation-Date: 2017-12-05 20:31+0100\n"
+"PO-Revision-Date: 2017-12-05 20:31+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -36,6 +36,11 @@ msgstr[1] ""
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr ""
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -53,9 +58,18 @@ msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts
msgstr[0] ""
msgstr[1] ""
+msgid "%{text} is available"
+msgstr ""
+
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] ""
@@ -100,9 +114,6 @@ msgstr ""
msgid "Add License"
msgstr ""
-msgid "Add an SSH key to your profile to pull or push via SSH."
-msgstr ""
-
msgid "Add new directory"
msgstr ""
@@ -115,6 +126,12 @@ msgstr ""
msgid "All"
msgstr ""
+msgid "An error occurred when toggling the notification subscription"
+msgstr ""
+
+msgid "An error occurred while fetching sidebar data"
+msgstr ""
+
msgid "An error occurred. Please try again."
msgstr ""
@@ -124,6 +141,12 @@ msgstr ""
msgid "Applications"
msgstr ""
+msgid "Apr"
+msgstr ""
+
+msgid "April"
+msgstr ""
+
msgid "Archived project! Repository is read-only"
msgstr ""
@@ -151,6 +174,12 @@ msgstr ""
msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr ""
+msgid "Aug"
+msgstr ""
+
+msgid "August"
+msgstr ""
+
msgid "Authentication Log"
msgstr ""
@@ -184,6 +213,9 @@ msgstr ""
msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
+msgid "Available"
+msgstr ""
+
msgid "Branch"
msgid_plural "Branches"
msgstr[0] ""
@@ -195,6 +227,12 @@ msgstr ""
msgid "Branch has changed"
msgstr ""
+msgid "Branch is already taken"
+msgstr ""
+
+msgid "Branch name"
+msgstr ""
+
msgid "BranchSwitcherPlaceholder|Search branches"
msgstr ""
@@ -330,6 +368,12 @@ msgstr ""
msgid "Chat"
msgstr ""
+msgid "Checking %{text} availability…"
+msgstr ""
+
+msgid "Checking branch availability..."
+msgstr ""
+
msgid "Cherry-pick this commit"
msgstr ""
@@ -399,7 +443,40 @@ msgstr ""
msgid "Cluster"
msgstr ""
-msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}"
+msgstr ""
+
+msgid "ClusterIntegration|API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Active"
+msgstr ""
+
+msgid "ClusterIntegration|Add an existing cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Add cluster"
+msgstr ""
+
+msgid "ClusterIntegration|All"
+msgstr ""
+
+msgid "ClusterIntegration|Applications"
+msgstr ""
+
+msgid "ClusterIntegration|CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
+msgstr ""
+
+msgid "ClusterIntegration|Choose how to set up cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster"
msgstr ""
msgid "ClusterIntegration|Cluster details"
@@ -417,57 +494,138 @@ msgstr ""
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
msgstr ""
-msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster's details"
+msgstr ""
+
+msgid "ClusterIntegration|Clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}"
+msgstr ""
+
+msgid "ClusterIntegration|Copy API URL"
+msgstr ""
+
+msgid "ClusterIntegration|Copy CA Certificate"
+msgstr ""
+
+msgid "ClusterIntegration|Copy Token"
msgstr ""
msgid "ClusterIntegration|Copy cluster name"
msgstr ""
+msgid "ClusterIntegration|Create a new cluster on Google Engine right from GitLab"
+msgstr ""
+
msgid "ClusterIntegration|Create cluster"
msgstr ""
-msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgid "ClusterIntegration|Create cluster on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Create on GKE"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
msgstr ""
+msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Enter the details for your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Environment pattern"
+msgstr ""
+
+msgid "ClusterIntegration|GKE pricing"
+msgstr ""
+
+msgid "ClusterIntegration|GitLab Runner"
+msgstr ""
+
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Google Kubernetes Engine project"
+msgstr ""
+
+msgid "ClusterIntegration|Helm Tiller"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine project"
+msgid "ClusterIntegration|Inactive"
+msgstr ""
+
+msgid "ClusterIntegration|Ingress"
+msgstr ""
+
+msgid "ClusterIntegration|Install"
+msgstr ""
+
+msgid "ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}"
+msgstr ""
+
+msgid "ClusterIntegration|Installed"
+msgstr ""
+
+msgid "ClusterIntegration|Installing"
+msgstr ""
+
+msgid "ClusterIntegration|Integrate cluster automation"
msgstr ""
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
+msgid "ClusterIntegration|Learn more about Clusters"
+msgstr ""
+
msgid "ClusterIntegration|Machine type"
msgstr ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr ""
-msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
+msgid "ClusterIntegration|Manage cluster integration on your GitLab project"
msgstr ""
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
+msgid "ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate"
+msgstr ""
+
+msgid "ClusterIntegration|Note:"
+msgstr ""
+
msgid "ClusterIntegration|Number of nodes"
msgstr ""
+msgid "ClusterIntegration|Please enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters"
+msgstr ""
+
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
+msgid "ClusterIntegration|Problem setting up the cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Problem setting up the clusters list"
+msgstr ""
+
+msgid "ClusterIntegration|Project ID"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace"
+msgstr ""
+
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
@@ -480,7 +638,13 @@ msgstr ""
msgid "ClusterIntegration|Remove integration"
msgstr ""
-msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Container Engine."
+msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine."
+msgstr ""
+
+msgid "ClusterIntegration|Request to begin installing failed"
+msgstr ""
+
+msgid "ClusterIntegration|Save changes"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your cluster"
@@ -495,33 +659,57 @@ msgstr ""
msgid "ClusterIntegration|See zones"
msgstr ""
+msgid "ClusterIntegration|Service token"
+msgstr ""
+
+msgid "ClusterIntegration|Show"
+msgstr ""
+
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine"
+msgstr ""
+
+msgid "ClusterIntegration|Something went wrong while installing %{title}"
+msgstr ""
+
+msgid "ClusterIntegration|There are no clusters to show"
+msgstr ""
+
+msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
+msgid "ClusterIntegration|Token"
+msgstr ""
+
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
-msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
msgstr ""
msgid "ClusterIntegration|Zone"
msgstr ""
-msgid "ClusterIntegration|access to Google Container Engine"
+msgid "ClusterIntegration|access to Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|cluster"
msgstr ""
+msgid "ClusterIntegration|documentation"
+msgstr ""
+
msgid "ClusterIntegration|help page"
msgstr ""
+msgid "ClusterIntegration|installing applications"
+msgstr ""
+
msgid "ClusterIntegration|meets the requirements"
msgstr ""
@@ -617,6 +805,15 @@ msgstr ""
msgid "Contributors"
msgstr ""
+msgid "ContributorsPage|Building repository graph."
+msgstr ""
+
+msgid "ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits."
+msgstr ""
+
+msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
+msgstr ""
+
msgid "Copy URL to clipboard"
msgstr ""
@@ -635,12 +832,21 @@ msgstr ""
msgid "Create empty bare repository"
msgstr ""
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr ""
msgid "Create new branch"
msgstr ""
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr ""
@@ -698,6 +904,12 @@ msgstr ""
msgid "DashboardProjects|Personal"
msgstr ""
+msgid "Dec"
+msgstr ""
+
+msgid "December"
+msgstr ""
+
msgid "Define a custom pattern with cron syntax"
msgstr ""
@@ -766,6 +978,60 @@ msgstr ""
msgid "Emails"
msgstr ""
+msgid "Environments|An error occurred while fetching the environments."
+msgstr ""
+
+msgid "Environments|An error occurred while making the request."
+msgstr ""
+
+msgid "Environments|Commit"
+msgstr ""
+
+msgid "Environments|Deployment"
+msgstr ""
+
+msgid "Environments|Environment"
+msgstr ""
+
+msgid "Environments|Environments"
+msgstr ""
+
+msgid "Environments|Environments are places where code gets deployed, such as staging or production."
+msgstr ""
+
+msgid "Environments|Job"
+msgstr ""
+
+msgid "Environments|New environment"
+msgstr ""
+
+msgid "Environments|No deployments yet"
+msgstr ""
+
+msgid "Environments|Open"
+msgstr ""
+
+msgid "Environments|Re-deploy"
+msgstr ""
+
+msgid "Environments|Read more about environments"
+msgstr ""
+
+msgid "Environments|Rollback"
+msgstr ""
+
+msgid "Environments|Show all"
+msgstr ""
+
+msgid "Environments|Updated"
+msgstr ""
+
+msgid "Environments|You don't have any environments right now."
+msgstr ""
+
+msgid "Error occurred when toggling the notification subscription"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -805,6 +1071,15 @@ msgstr ""
msgid "Failed to remove the pipeline schedule"
msgstr ""
+msgid "Feb"
+msgstr ""
+
+msgid "February"
+msgstr ""
+
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr ""
@@ -981,6 +1256,24 @@ msgstr ""
msgid "Issues"
msgstr ""
+msgid "Jan"
+msgstr ""
+
+msgid "January"
+msgstr ""
+
+msgid "Jul"
+msgstr ""
+
+msgid "July"
+msgstr ""
+
+msgid "Jun"
+msgstr ""
+
+msgid "June"
+msgstr ""
+
msgid "LFSStatus|Disabled"
msgstr ""
@@ -1048,9 +1341,18 @@ msgstr ""
msgid "Login"
msgstr ""
+msgid "Mar"
+msgstr ""
+
+msgid "March"
+msgstr ""
+
msgid "Maximum git storage failures"
msgstr ""
+msgid "May"
+msgstr ""
+
msgid "Median"
msgstr ""
@@ -1092,6 +1394,9 @@ msgstr ""
msgid "New branch"
msgstr ""
+msgid "New branch unavailable"
+msgstr ""
+
msgid "New directory"
msgstr ""
@@ -1131,6 +1436,12 @@ msgstr ""
msgid "No schedules"
msgstr ""
+msgid "No time spent"
+msgstr ""
+
+msgid "None"
+msgstr ""
+
msgid "Not available"
msgstr ""
@@ -1194,6 +1505,24 @@ msgstr ""
msgid "Notifications"
msgstr ""
+msgid "Nov"
+msgstr ""
+
+msgid "November"
+msgstr ""
+
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
+msgid "Oct"
+msgstr ""
+
+msgid "October"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr ""
@@ -1431,6 +1760,12 @@ msgstr ""
msgid "ProjectNetworkGraph|Graph"
msgstr ""
+msgid "ProjectSettings|Immediately run a pipeline on the default branch"
+msgstr ""
+
+msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript"
+msgstr ""
+
msgid "Projects"
msgstr ""
@@ -1455,6 +1790,39 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "PrometheusService|By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server."
+msgstr ""
+
+msgid "PrometheusService|Finding and configuring metrics..."
+msgstr ""
+
+msgid "PrometheusService|Metrics"
+msgstr ""
+
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
+msgstr ""
+
+msgid "PrometheusService|Missing environment variable"
+msgstr ""
+
+msgid "PrometheusService|Monitored"
+msgstr ""
+
+msgid "PrometheusService|More information"
+msgstr ""
+
+msgid "PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment."
+msgstr ""
+
+msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
+msgstr ""
+
+msgid "PrometheusService|Prometheus monitoring"
+msgstr ""
+
+msgid "PrometheusService|View environments"
+msgstr ""
+
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr ""
@@ -1563,6 +1931,12 @@ msgstr ""
msgid "Select target branch"
msgstr ""
+msgid "Sep"
+msgstr ""
+
+msgid "September"
+msgstr ""
+
msgid "Service Templates"
msgstr ""
@@ -1601,7 +1975,7 @@ msgstr ""
msgid "Something went wrong on our end."
msgstr ""
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}"
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -1700,9 +2074,15 @@ msgstr ""
msgid "SortOptions|Start soon"
msgstr ""
+msgid "Source"
+msgstr ""
+
msgid "Source code"
msgstr ""
+msgid "Source is not available"
+msgstr ""
+
msgid "Spam Logs"
msgstr ""
@@ -1721,9 +2101,15 @@ msgstr ""
msgid "Start the Runner!"
msgstr ""
+msgid "Stopped"
+msgstr ""
+
msgid "Subgroups"
msgstr ""
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr ""
@@ -1738,12 +2124,84 @@ msgstr[1] ""
msgid "Tags"
msgstr ""
+msgid "TagsPage|Browse commits"
+msgstr ""
+
+msgid "TagsPage|Browse files"
+msgstr ""
+
+msgid "TagsPage|Can't find HEAD commit for this tag"
+msgstr ""
+
+msgid "TagsPage|Cancel"
+msgstr ""
+
+msgid "TagsPage|Create tag"
+msgstr ""
+
+msgid "TagsPage|Delete tag"
+msgstr ""
+
+msgid "TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "TagsPage|Edit release notes"
+msgstr ""
+
+msgid "TagsPage|Existing branch name, tag, or commit SHA"
+msgstr ""
+
+msgid "TagsPage|Filter by tag name"
+msgstr ""
+
+msgid "TagsPage|New Tag"
+msgstr ""
+
+msgid "TagsPage|New tag"
+msgstr ""
+
+msgid "TagsPage|Optionally, add a message to the tag."
+msgstr ""
+
+msgid "TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page."
+msgstr ""
+
+msgid "TagsPage|Release notes"
+msgstr ""
+
+msgid "TagsPage|Repository has no tags yet."
+msgstr ""
+
+msgid "TagsPage|Sort by"
+msgstr ""
+
+msgid "TagsPage|Tags"
+msgstr ""
+
+msgid "TagsPage|Tags give the ability to mark specific points in history as being important"
+msgstr ""
+
+msgid "TagsPage|This tag has no release notes."
+msgstr ""
+
+msgid "TagsPage|Use git tag command to add a new one:"
+msgstr ""
+
+msgid "TagsPage|Write your release notes or drag files here..."
+msgstr ""
+
+msgid "TagsPage|protected"
+msgstr ""
+
msgid "Target Branch"
msgstr ""
msgid "Team"
msgstr ""
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+msgstr ""
+
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr ""
@@ -1756,6 +2214,12 @@ msgstr ""
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr ""
+msgid "The number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
msgstr ""
@@ -1976,6 +2440,9 @@ msgstr ""
msgid "Total Time"
msgstr ""
+msgid "Total issue time spent"
+msgstr ""
+
msgid "Total test time for all commits/merges"
msgstr ""
@@ -1988,6 +2455,9 @@ msgstr ""
msgid "Unstar"
msgstr ""
+msgid "Unsubscribe"
+msgstr ""
+
msgid "Upload New File"
msgstr ""
@@ -2189,6 +2659,9 @@ msgstr ""
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr ""
+msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
+msgstr ""
+
msgid "Your comment will not be visible to the public."
msgstr ""
@@ -2201,6 +2674,9 @@ msgstr ""
msgid "Your projects"
msgstr ""
+msgid "branch name"
+msgstr ""
+
msgid "day"
msgid_plural "days"
msgstr[0] ""
@@ -2223,5 +2699,8 @@ msgstr ""
msgid "personal access token"
msgstr ""
+msgid "source"
+msgstr ""
+
msgid "username"
msgstr ""
diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po
index 7a132aeb238..8a987129452 100644
--- a/locale/it/gitlab.po
+++ b/locale/it/gitlab.po
@@ -504,13 +504,13 @@ msgstr ""
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
msgstr ""
-msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Copy cluster name"
@@ -519,7 +519,7 @@ msgstr ""
msgid "ClusterIntegration|Create cluster"
msgstr ""
-msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
@@ -528,10 +528,10 @@ msgstr ""
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine project"
+msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
@@ -585,7 +585,7 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
@@ -594,13 +594,13 @@ msgstr ""
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
-msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
msgstr ""
msgid "ClusterIntegration|Zone"
msgstr ""
-msgid "ClusterIntegration|access to Google Container Engine"
+msgid "ClusterIntegration|access to Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|cluster"
diff --git a/locale/ja/gitlab.po b/locale/ja/gitlab.po
index 9045ae26c4a..8d93a936be9 100644
--- a/locale/ja/gitlab.po
+++ b/locale/ja/gitlab.po
@@ -497,13 +497,13 @@ msgstr ""
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
msgstr ""
-msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Copy cluster name"
@@ -512,7 +512,7 @@ msgstr ""
msgid "ClusterIntegration|Create cluster"
msgstr ""
-msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
@@ -521,10 +521,10 @@ msgstr ""
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine project"
+msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
@@ -578,7 +578,7 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
@@ -587,13 +587,13 @@ msgstr ""
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
-msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
msgstr ""
msgid "ClusterIntegration|Zone"
msgstr ""
-msgid "ClusterIntegration|access to Google Container Engine"
+msgid "ClusterIntegration|access to Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|cluster"
diff --git a/locale/ko/gitlab.po b/locale/ko/gitlab.po
index ab74d4cbeae..d6c1ff2deeb 100644
--- a/locale/ko/gitlab.po
+++ b/locale/ko/gitlab.po
@@ -497,13 +497,13 @@ msgstr ""
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
msgstr ""
-msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Copy cluster name"
@@ -512,7 +512,7 @@ msgstr ""
msgid "ClusterIntegration|Create cluster"
msgstr ""
-msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
@@ -521,10 +521,10 @@ msgstr ""
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine project"
+msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
@@ -578,7 +578,7 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
@@ -587,13 +587,13 @@ msgstr ""
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
-msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
msgstr ""
msgid "ClusterIntegration|Zone"
msgstr ""
-msgid "ClusterIntegration|access to Google Container Engine"
+msgid "ClusterIntegration|access to Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|cluster"
diff --git a/locale/nl_NL/gitlab.po b/locale/nl_NL/gitlab.po
index 7e33af9f747..68d1f809bb4 100644
--- a/locale/nl_NL/gitlab.po
+++ b/locale/nl_NL/gitlab.po
@@ -504,13 +504,13 @@ msgstr ""
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
msgstr ""
-msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Copy cluster name"
@@ -519,7 +519,7 @@ msgstr ""
msgid "ClusterIntegration|Create cluster"
msgstr ""
-msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
@@ -528,10 +528,10 @@ msgstr ""
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine project"
+msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
@@ -585,7 +585,7 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
@@ -594,13 +594,13 @@ msgstr ""
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
-msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
msgstr ""
msgid "ClusterIntegration|Zone"
msgstr ""
-msgid "ClusterIntegration|access to Google Container Engine"
+msgid "ClusterIntegration|access to Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|cluster"
diff --git a/locale/pl_PL/gitlab.po b/locale/pl_PL/gitlab.po
index fb4a8af7217..c48909540b1 100644
--- a/locale/pl_PL/gitlab.po
+++ b/locale/pl_PL/gitlab.po
@@ -511,13 +511,13 @@ msgstr ""
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
msgstr ""
-msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Copy cluster name"
@@ -526,7 +526,7 @@ msgstr ""
msgid "ClusterIntegration|Create cluster"
msgstr ""
-msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
@@ -535,10 +535,10 @@ msgstr ""
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine project"
+msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
@@ -592,7 +592,7 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
@@ -601,13 +601,13 @@ msgstr ""
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
-msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
msgstr ""
msgid "ClusterIntegration|Zone"
msgstr ""
-msgid "ClusterIntegration|access to Google Container Engine"
+msgid "ClusterIntegration|access to Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|cluster"
diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po
index fa335bc819d..78e0967c3bc 100644
--- a/locale/pt_BR/gitlab.po
+++ b/locale/pt_BR/gitlab.po
@@ -504,14 +504,14 @@ msgstr "Integração do cluster está ativada nesse projeto."
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
msgstr "Integração do cluster está ativada para esse projeto. Desabilitar a integração não afetará seu cluster, mas desligará temporariamente a conexão do Gitlab com ele."
-msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
-msgstr "O cluster está sendo criado no Google Container Engine..."
+msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
+msgstr "O cluster está sendo criado no Google Kubernetes Engine..."
msgid "ClusterIntegration|Cluster name"
msgstr "Nome do cluster"
-msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
-msgstr "O cluster foi criado com sucesso no Google Container Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
+msgstr "O cluster foi criado com sucesso no Google Kubernetes Engine"
msgid "ClusterIntegration|Copy cluster name"
msgstr "Copiar nome do cluster"
@@ -519,8 +519,8 @@ msgstr "Copiar nome do cluster"
msgid "ClusterIntegration|Create cluster"
msgstr "Criar cluster"
-msgid "ClusterIntegration|Create new cluster on Google Container Engine"
-msgstr "Criar novo cluster no Google Container Engine"
+msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
+msgstr "Criar novo cluster no Google Kubernetes Engine"
msgid "ClusterIntegration|Enable cluster integration"
msgstr "Ativar integração com o cluster"
@@ -528,11 +528,11 @@ msgstr "Ativar integração com o cluster"
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr "ID do projeto no Google Cloud Platform"
-msgid "ClusterIntegration|Google Container Engine"
-msgstr "Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine"
+msgstr "Google Kubernetes Engine"
-msgid "ClusterIntegration|Google Container Engine project"
-msgstr "Projeto no Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine project"
+msgstr "Projeto no Google Kubernetes Engine"
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr "Leia mais sobre %{link_to_documentation}"
@@ -585,8 +585,8 @@ msgstr "Ver zonas"
msgid "ClusterIntegration|Something went wrong on our end."
msgstr "Alguma coisa deu errado do nosso lado."
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
-msgstr "Algo deu errado ao criar seu cluster no Google Container Engine"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine"
+msgstr "Algo deu errado ao criar seu cluster no Google Kubernetes Engine"
msgid "ClusterIntegration|Toggle Cluster"
msgstr "Alternar cluster"
@@ -594,14 +594,14 @@ msgstr "Alternar cluster"
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "Com um cluster associado à esse projeto, você pode usar revisão de apps, fazer deploy de suas aplicações, rodar suas pipelines e muito mais de um jeito simples."
-msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
-msgstr "Sua conta precisa ter %{link_to_container_engine}"
+msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
+msgstr "Sua conta precisa ter %{link_to_kubernetes_engine}"
msgid "ClusterIntegration|Zone"
msgstr "Zona"
-msgid "ClusterIntegration|access to Google Container Engine"
-msgstr "Acesso ao Google Container Engine"
+msgid "ClusterIntegration|access to Google Kubernetes Engine"
+msgstr "Acesso ao Google Kubernetes Engine"
msgid "ClusterIntegration|cluster"
msgstr "cluster"
diff --git a/locale/ru/gitlab.po b/locale/ru/gitlab.po
index 9111f1eb3d2..b25a5d1e75b 100644
--- a/locale/ru/gitlab.po
+++ b/locale/ru/gitlab.po
@@ -511,14 +511,14 @@ msgstr "Ð˜Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ ÐºÐ»Ð°Ñтеров включена Ð´Ð»Ñ Ñтог
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
msgstr "Ð”Ð»Ñ Ñтого проекта включена Ð¸Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ ÐºÐ»Ð°Ñтеров. Отключение интеграции не повлиÑет на клаÑтер, но Ñоединение Ñ GitLab будет временно отключено."
-msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
-msgstr "СоздаетÑÑ ÐºÐ»Ð°Ñтер в Google Container Engine..."
+msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
+msgstr "СоздаетÑÑ ÐºÐ»Ð°Ñтер в Google Kubernetes Engine..."
msgid "ClusterIntegration|Cluster name"
msgstr "Ðазвание клаÑтера"
-msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
-msgstr "КлаÑтер был уÑпешно Ñоздан в Google Container Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
+msgstr "КлаÑтер был уÑпешно Ñоздан в Google Kubernetes Engine"
msgid "ClusterIntegration|Copy cluster name"
msgstr "Копировать название клаÑтера"
@@ -526,8 +526,8 @@ msgstr "Копировать название клаÑтера"
msgid "ClusterIntegration|Create cluster"
msgstr "Создать клаÑтер"
-msgid "ClusterIntegration|Create new cluster on Google Container Engine"
-msgstr "Создать новый клаÑтер в Google Container Engine"
+msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
+msgstr "Создать новый клаÑтер в Google Kubernetes Engine"
msgid "ClusterIntegration|Enable cluster integration"
msgstr "Включить интеграцию Ñ ÐºÐ»Ð°Ñтерами"
@@ -535,11 +535,11 @@ msgstr "Включить интеграцию Ñ ÐºÐ»Ð°Ñтерами"
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr "Идентификатор проекта в Google Cloud Platform"
-msgid "ClusterIntegration|Google Container Engine"
-msgstr "Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine"
+msgstr "Google Kubernetes Engine"
-msgid "ClusterIntegration|Google Container Engine project"
-msgstr "Проект Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine project"
+msgstr "Проект Google Kubernetes Engine"
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr "Узнайте больше на %{link_to_documentation}"
@@ -592,8 +592,8 @@ msgstr "См. зоны"
msgid "ClusterIntegration|Something went wrong on our end."
msgstr " У Ð½Ð°Ñ Ñ‡Ñ‚Ð¾-то пошло не так."
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
-msgstr "Что-то пошло не так во Ð²Ñ€ÐµÐ¼Ñ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ ÐºÐ»Ð°Ñтера в Google Container Engine"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine"
+msgstr "Что-то пошло не так во Ð²Ñ€ÐµÐ¼Ñ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ ÐºÐ»Ð°Ñтера в Google Kubernetes Engine"
msgid "ClusterIntegration|Toggle Cluster"
msgstr "Переключить КлаÑтер"
@@ -601,14 +601,14 @@ msgstr "Переключить КлаÑтер"
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "ЕÑли привÑзать клаÑтер к Ñтому проекту, вы Ñ Ð»Ñ‘Ð³ÐºÐ¾Ñтью Ñможете иÑпользовать Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ñ€ÐµÐ²ÑŒÑŽ, развертывать ваши приложениÑ, запуÑкать Ñборочные линии и многое другое."
-msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
-msgstr "Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ должна иметь %{link_to_container_engine}"
+msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
+msgstr "Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ должна иметь %{link_to_kubernetes_engine}"
msgid "ClusterIntegration|Zone"
msgstr "Зона"
-msgid "ClusterIntegration|access to Google Container Engine"
-msgstr "доÑтуп к Google Container Engine"
+msgid "ClusterIntegration|access to Google Kubernetes Engine"
+msgstr "доÑтуп к Google Kubernetes Engine"
msgid "ClusterIntegration|cluster"
msgstr "клаÑтер"
diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po
index 73dfe949ded..53054bdaa27 100644
--- a/locale/uk/gitlab.po
+++ b/locale/uk/gitlab.po
@@ -511,14 +511,14 @@ msgstr "Ð†Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ Ñ–Ð· клаÑтером увімкнена Ð´Ð»Ñ Ñ
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
msgstr "Ð”Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту увімкнена Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ Ñ–Ð· клаÑтером. Ð’Ð¸ÐºÐ½ÐµÐ½Ð½Ñ Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ— не вплине на клаÑтер, але з'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ GitLab з ним буде тимчаÑово розірване."
-msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
-msgstr "СтворюєтьÑÑ ÐºÐ»Ð°Ñтер в Google Container Engine..."
+msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
+msgstr "СтворюєтьÑÑ ÐºÐ»Ð°Ñтер в Google Kubernetes Engine..."
msgid "ClusterIntegration|Cluster name"
msgstr "Ім'Ñ ÐºÐ»Ð°Ñтера"
-msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
-msgstr "КлаÑтер був уÑпішно Ñтворений в Google Container Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
+msgstr "КлаÑтер був уÑпішно Ñтворений в Google Kubernetes Engine"
msgid "ClusterIntegration|Copy cluster name"
msgstr "Копіювати назву клаÑтера"
@@ -526,8 +526,8 @@ msgstr "Копіювати назву клаÑтера"
msgid "ClusterIntegration|Create cluster"
msgstr "Створити клаÑтер"
-msgid "ClusterIntegration|Create new cluster on Google Container Engine"
-msgstr "Створити новий клаÑтер в Google Container Engine"
+msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
+msgstr "Створити новий клаÑтер в Google Kubernetes Engine"
msgid "ClusterIntegration|Enable cluster integration"
msgstr "Увімкнути інтеграцію із клаÑтерами"
@@ -535,11 +535,11 @@ msgstr "Увімкнути інтеграцію із клаÑтерами"
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr "Ідентифікатор проекту в Google Cloud Platform"
-msgid "ClusterIntegration|Google Container Engine"
-msgstr "Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine"
+msgstr "Google Kubernetes Engine"
-msgid "ClusterIntegration|Google Container Engine project"
-msgstr "Проект Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine project"
+msgstr "Проект Google Kubernetes Engine"
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr "ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про %{link_to_documentation}"
@@ -592,8 +592,8 @@ msgstr "ПереглÑнути зони"
msgid "ClusterIntegration|Something went wrong on our end."
msgstr "ЩоÑÑŒ пішло не так з нашого боку."
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
-msgstr "ЩоÑÑŒ пішло не так під Ñ‡Ð°Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ ÐºÐ»Ð°Ñтера в Google Container Engine"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine"
+msgstr "ЩоÑÑŒ пішло не так під Ñ‡Ð°Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ ÐºÐ»Ð°Ñтера в Google Kubernetes Engine"
msgid "ClusterIntegration|Toggle Cluster"
msgstr "Переключити КлаÑтер"
@@ -601,14 +601,14 @@ msgstr "Переключити КлаÑтер"
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "За допомогою підключеного до цього проекту клаÑтера, ви можете викориÑтовувати Review Apps, розгортати ваші проекти, запуÑкати конвеєри збірки та багато іншого."
-msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
-msgstr "Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð¿Ð¾Ð²Ð¸Ð½ÐµÐ½ мати %{link_to_container_engine}"
+msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
+msgstr "Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð¿Ð¾Ð²Ð¸Ð½ÐµÐ½ мати %{link_to_kubernetes_engine}"
msgid "ClusterIntegration|Zone"
msgstr "Зона"
-msgid "ClusterIntegration|access to Google Container Engine"
-msgstr "доÑтуп до Google Container Engine"
+msgid "ClusterIntegration|access to Google Kubernetes Engine"
+msgstr "доÑтуп до Google Kubernetes Engine"
msgid "ClusterIntegration|cluster"
msgstr "клаÑтер"
diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po
index 3a08b6c20a2..e1bc9219908 100644
--- a/locale/zh_CN/gitlab.po
+++ b/locale/zh_CN/gitlab.po
@@ -497,14 +497,14 @@ msgstr "此项目已å¯ç”¨é›†ç¾¤é›†æˆã€‚"
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
msgstr "此项目已å¯ç”¨é›†ç¾¤é›†æˆã€‚ç¦ç”¨æ­¤é›†æˆä¸ä¼šå½±å“您的集群,它åªä¼šæš‚时关闭 GitLab 的连接。"
-msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
-msgstr "集群正在 Google Container Engine 上创建..."
+msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
+msgstr "集群正在 Google Kubernetes Engine 上创建..."
msgid "ClusterIntegration|Cluster name"
msgstr "集群å称"
-msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
-msgstr "集群已在 Google Container Engine 上æˆåŠŸåˆ›å»º"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
+msgstr "集群已在 Google Kubernetes Engine 上æˆåŠŸåˆ›å»º"
msgid "ClusterIntegration|Copy cluster name"
msgstr "å¤åˆ¶é›†ç¾¤å称"
@@ -512,8 +512,8 @@ msgstr "å¤åˆ¶é›†ç¾¤å称"
msgid "ClusterIntegration|Create cluster"
msgstr "创建集群"
-msgid "ClusterIntegration|Create new cluster on Google Container Engine"
-msgstr "在 Google Container Engine 上创建新集群"
+msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
+msgstr "在 Google Kubernetes Engine 上创建新集群"
msgid "ClusterIntegration|Enable cluster integration"
msgstr "å¯ç”¨é›†ç¾¤é›†æˆ"
@@ -521,11 +521,11 @@ msgstr "å¯ç”¨é›†ç¾¤é›†æˆ"
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr "Google 云平å°é¡¹ç›®ID"
-msgid "ClusterIntegration|Google Container Engine"
-msgstr "Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine"
+msgstr "Google Kubernetes Engine"
-msgid "ClusterIntegration|Google Container Engine project"
-msgstr "Google Container Engine 项目"
+msgid "ClusterIntegration|Google Kubernetes Engine project"
+msgstr "Google Kubernetes Engine 项目"
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr "了解详细%{link_to_documentation}"
@@ -578,8 +578,8 @@ msgstr "查看区域"
msgid "ClusterIntegration|Something went wrong on our end."
msgstr "å‘生了内部错误"
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
-msgstr "在 Google Container Engine 上创建集群时å‘生错误"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine"
+msgstr "在 Google Kubernetes Engine 上创建集群时å‘生错误"
msgid "ClusterIntegration|Toggle Cluster"
msgstr "切æ¢é›†ç¾¤"
@@ -587,14 +587,14 @@ msgstr "切æ¢é›†ç¾¤"
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "使用与此项目关è”的集群,您å¯ä»¥ä½¿ç”¨å®¡é˜…应用程åºï¼Œéƒ¨ç½²åº”用程åºï¼Œè¿è¡Œæµæ°´çº¿ç­‰ç­‰ã€‚"
-msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
-msgstr "您的å¸æˆ·å¿…须有%{link_to_container_engine}"
+msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
+msgstr "您的å¸æˆ·å¿…须有%{link_to_kubernetes_engine}"
msgid "ClusterIntegration|Zone"
msgstr "区域"
-msgid "ClusterIntegration|access to Google Container Engine"
-msgstr "访问 Google Container Engine"
+msgid "ClusterIntegration|access to Google Kubernetes Engine"
+msgstr "访问 Google Kubernetes Engine"
msgid "ClusterIntegration|cluster"
msgstr "集群"
diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po
index 30d5b0c1416..b851809fc7c 100644
--- a/locale/zh_HK/gitlab.po
+++ b/locale/zh_HK/gitlab.po
@@ -497,13 +497,13 @@ msgstr ""
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
msgstr ""
-msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
msgstr ""
msgid "ClusterIntegration|Cluster name"
msgstr ""
-msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Copy cluster name"
@@ -512,7 +512,7 @@ msgstr ""
msgid "ClusterIntegration|Create cluster"
msgstr ""
-msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration"
@@ -521,10 +521,10 @@ msgstr ""
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr ""
-msgid "ClusterIntegration|Google Container Engine project"
+msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr ""
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
@@ -578,7 +578,7 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong on our end."
msgstr ""
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|Toggle Cluster"
@@ -587,13 +587,13 @@ msgstr ""
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
-msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
+msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
msgstr ""
msgid "ClusterIntegration|Zone"
msgstr ""
-msgid "ClusterIntegration|access to Google Container Engine"
+msgid "ClusterIntegration|access to Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|cluster"
diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po
index 6531330074a..b6d4ed27487 100644
--- a/locale/zh_TW/gitlab.po
+++ b/locale/zh_TW/gitlab.po
@@ -497,13 +497,13 @@ msgstr "此專案已經啟用å¢é›†æ•´åˆ"
msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it."
msgstr "此專案已啟用å¢é›†æ•´åˆã€‚ç¦æ­¢å¢é›†æ•´åˆä¸æœƒå½±éŸ¿æ‚¨çš„å¢é›†ï¼Œå®ƒåªæ˜¯æš«æ™‚關閉 GitLab 的連接。"
-msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
+msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..."
msgstr "在 Google 容器引擎中建立新的å¢é›†"
msgid "ClusterIntegration|Cluster name"
msgstr "å¢é›†å稱"
-msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
+msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine"
msgstr "在 Google 容器引擎上æˆåŠŸå»ºç«‹å¢é›†"
msgid "ClusterIntegration|Copy cluster name"
@@ -512,7 +512,7 @@ msgstr "複製å¢é›†å稱"
msgid "ClusterIntegration|Create cluster"
msgstr "建立å¢é›†"
-msgid "ClusterIntegration|Create new cluster on Google Container Engine"
+msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine"
msgstr "在 Google 容器引擎中建立新的å¢é›†"
msgid "ClusterIntegration|Enable cluster integration"
@@ -521,10 +521,10 @@ msgstr "å•Ÿå‹•å¢é›†æ•´åˆ"
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr "Google 雲端專案 ID"
-msgid "ClusterIntegration|Google Container Engine"
+msgid "ClusterIntegration|Google Kubernetes Engine"
msgstr "Google 容器引擎"
-msgid "ClusterIntegration|Google Container Engine project"
+msgid "ClusterIntegration|Google Kubernetes Engine project"
msgstr "Google 容器引擎專案"
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
@@ -578,8 +578,8 @@ msgstr "查看å€åŸŸ"
msgid "ClusterIntegration|Something went wrong on our end."
msgstr "內部發生了錯誤"
-msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
-msgstr "在 Google Container Engine 上建立å¢é›†æ™‚發生了錯誤"
+msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine"
+msgstr "在 Google Kubernetes Engine 上建立å¢é›†æ™‚發生了錯誤"
msgid "ClusterIntegration|Toggle Cluster"
msgstr "å¢é›†é–‹é—œ"
@@ -587,14 +587,14 @@ msgstr "å¢é›†é–‹é—œ"
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "當å¢é›†é€£çµåˆ°æ­¤å°ˆæ¡ˆï¼Œä½ å¯ä»¥ä½¿ç”¨è¤‡é–±æ‡‰ç”¨ (review apps),部署你的應用程å¼ï¼ŒåŸ·è¡Œä½ çš„æµæ°´ç·š (pipelines),還有更多容易上手的方å¼å¯ä»¥ä½¿ç”¨ã€‚"
-msgid "ClusterIntegration|Your account must have %{link_to_container_engine}"
-msgstr "您的帳號必須有 %{link_to_container_engine}"
+msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
+msgstr "您的帳號必須有 %{link_to_kubernetes_engine}"
msgid "ClusterIntegration|Zone"
msgstr "å€åŸŸ"
-msgid "ClusterIntegration|access to Google Container Engine"
-msgstr "å­˜å– Google Container Engine"
+msgid "ClusterIntegration|access to Google Kubernetes Engine"
+msgstr "å­˜å– Google Kubernetes Engine"
msgid "ClusterIntegration|cluster"
msgstr "å¢é›†"
diff --git a/qa/qa/page/group/new.rb b/qa/qa/page/group/new.rb
index cb743a7bf11..53fdaaed078 100644
--- a/qa/qa/page/group/new.rb
+++ b/qa/qa/page/group/new.rb
@@ -4,6 +4,7 @@ module QA
class New < Page::Base
def set_path(path)
fill_in 'group_path', with: path
+ fill_in 'group_name', with: path
end
def set_description(description)
diff --git a/spec/bin/storage_check_spec.rb b/spec/bin/storage_check_spec.rb
new file mode 100644
index 00000000000..02f6fcb6e3a
--- /dev/null
+++ b/spec/bin/storage_check_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe 'bin/storage_check' do
+ it 'is executable' do
+ command = %w[bin/storage_check -t unix://the/path/to/a/unix-socket.sock -i 10 -d]
+ expected_output = 'Checking unix://the/path/to/a/unix-socket.sock every 10 seconds'
+
+ output, status = Gitlab::Popen.popen(command, Rails.root.to_s)
+
+ expect(status).to eq(0)
+ expect(output).to include(expected_output)
+ end
+end
diff --git a/spec/controllers/admin/health_check_controller_spec.rb b/spec/controllers/admin/health_check_controller_spec.rb
index 0b8e0c8a065..d15ee0021d9 100644
--- a/spec/controllers/admin/health_check_controller_spec.rb
+++ b/spec/controllers/admin/health_check_controller_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Admin::HealthCheckController, broken_storage: true do
+describe Admin::HealthCheckController do
let(:admin) { create(:admin) }
before do
@@ -17,7 +17,7 @@ describe Admin::HealthCheckController, broken_storage: true do
describe 'POST reset_storage_health' do
it 'resets all storage health information' do
- expect(Gitlab::Git::Storage::CircuitBreaker).to receive(:reset_all!)
+ expect(Gitlab::Git::Storage::FailureInfo).to receive(:reset_all!)
post :reset_storage_health
end
diff --git a/spec/controllers/groups/uploads_controller_spec.rb b/spec/controllers/groups/uploads_controller_spec.rb
new file mode 100644
index 00000000000..67a11e56e94
--- /dev/null
+++ b/spec/controllers/groups/uploads_controller_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+
+describe Groups::UploadsController do
+ let(:model) { create(:group, :public) }
+ let(:params) do
+ { group_id: model }
+ end
+
+ it_behaves_like 'handle uploads'
+end
diff --git a/spec/controllers/health_controller_spec.rb b/spec/controllers/health_controller_spec.rb
index 9e9cf4f2c1f..95946def5f9 100644
--- a/spec/controllers/health_controller_spec.rb
+++ b/spec/controllers/health_controller_spec.rb
@@ -14,6 +14,48 @@ describe HealthController do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
end
+ describe '#storage_check' do
+ before do
+ allow(Gitlab::RequestContext).to receive(:client_ip).and_return(whitelisted_ip)
+ end
+
+ subject { post :storage_check }
+
+ it 'checks all the configured storages' do
+ expect(Gitlab::Git::Storage::Checker).to receive(:check_all).and_call_original
+
+ subject
+ end
+
+ it 'returns the check interval' do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'true')
+ stub_application_setting(circuitbreaker_check_interval: 10)
+
+ subject
+
+ expect(json_response['check_interval']).to eq(10)
+ end
+
+ context 'with failing storages', :broken_storage do
+ before do
+ stub_storage_settings(
+ broken: { path: 'tmp/tests/non-existent-repositories' }
+ )
+ end
+
+ it 'includes the failure information' do
+ subject
+
+ expected_results = [
+ { 'storage' => 'broken', 'success' => false },
+ { 'storage' => 'default', 'success' => true }
+ ]
+
+ expect(json_response['results']).to eq(expected_results)
+ end
+ end
+ end
+
describe '#readiness' do
shared_context 'endpoint responding with readiness data' do
let(:request_params) { {} }
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
index 84cde33d944..d6ccb92c54b 100644
--- a/spec/controllers/projects/boards_controller_spec.rb
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -14,6 +14,12 @@ describe Projects::BoardsController do
expect { list_boards }.to change(project.boards, :count).by(1)
end
+ it 'sets boards_endpoint instance variable to a boards path' do
+ list_boards
+
+ expect(assigns(:boards_endpoint)).to eq project_boards_path(project)
+ end
+
context 'when format is HTML' do
it 'renders template' do
list_boards
@@ -59,6 +65,12 @@ describe Projects::BoardsController do
describe 'GET show' do
let!(:board) { create(:board, project: project) }
+ it 'sets boards_endpoint instance variable to a boards path' do
+ read_board board: board
+
+ expect(assigns(:boards_endpoint)).to eq project_boards_path(project)
+ end
+
context 'when format is HTML' do
it 'renders template' do
read_board board: board
diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb
index bb5ef7bb365..ee7928beb7e 100644
--- a/spec/controllers/projects/clusters/gcp_controller_spec.rb
+++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb
@@ -143,9 +143,9 @@ describe Projects::Clusters::GcpController do
expect(ClusterProvisionWorker).to receive(:perform_async)
expect { go }.to change { Clusters::Cluster.count }
.and change { Clusters::Providers::Gcp.count }
- expect(response).to redirect_to(project_cluster_path(project, project.cluster))
- expect(project.cluster).to be_gcp
- expect(project.cluster).to be_kubernetes
+ expect(response).to redirect_to(project_cluster_path(project, project.clusters.first))
+ expect(project.clusters.first).to be_gcp
+ expect(project.clusters.first).to be_kubernetes
end
end
end
diff --git a/spec/controllers/projects/clusters/user_controller_spec.rb b/spec/controllers/projects/clusters/user_controller_spec.rb
index 22005e0dc66..913976d187f 100644
--- a/spec/controllers/projects/clusters/user_controller_spec.rb
+++ b/spec/controllers/projects/clusters/user_controller_spec.rb
@@ -64,7 +64,9 @@ describe Projects::Clusters::UserController do
expect(ClusterProvisionWorker).to receive(:perform_async)
expect { go }.to change { Clusters::Cluster.count }
.and change { Clusters::Platforms::Kubernetes.count }
- expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ expect(response).to redirect_to(project_cluster_path(project, project.clusters.first))
+ expect(project.clusters.first).to be_user
+ expect(project.clusters.first).to be_kubernetes
end
end
end
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index 66e67652dad..280b7e4d8b9 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -15,14 +15,72 @@ describe Projects::ClustersController do
sign_in(user)
end
- context 'when project has a cluster' do
- let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ context 'when project has one or more clusters' do
+ let(:project) { create(:project) }
+ let!(:enabled_cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ let!(:disabled_cluster) { create(:cluster, :disabled, :provided_by_gcp, projects: [project]) }
+ it 'lists available clusters' do
+ go
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:index)
+ expect(assigns(:clusters)).to match_array([enabled_cluster, disabled_cluster])
+ end
+
+ it 'assigns counters to correct values' do
+ go
+
+ expect(assigns(:active_count)).to eq(1)
+ expect(assigns(:inactive_count)).to eq(1)
+ end
+
+ context 'when page is specified' do
+ let(:last_page) { project.clusters.page.total_pages }
- it { expect(go).to redirect_to(project_cluster_path(project, project.cluster)) }
+ before do
+ allow(Clusters::Cluster).to receive(:paginates_per).and_return(1)
+ create_list(:cluster, 2, :provided_by_gcp, projects: [project])
+ get :index, namespace_id: project.namespace, project_id: project, page: last_page
+ end
+
+ it 'redirects to the page' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:clusters).current_page).to eq(last_page)
+ end
+ end
+
+ context 'when only enabled clusters are requested' do
+ it 'returns only enabled clusters' do
+ get :index, namespace_id: project.namespace, project_id: project, scope: 'active'
+ expect(assigns(:clusters)).to all(have_attributes(enabled: true))
+ end
+ end
+
+ context 'when only disabled clusters are requested' do
+ it 'returns only disabled clusters' do
+ get :index, namespace_id: project.namespace, project_id: project, scope: 'inactive'
+ expect(assigns(:clusters)).to all(have_attributes(enabled: false))
+ end
+ end
end
context 'when project does not have a cluster' do
- it { expect(go).to redirect_to(new_project_cluster_path(project)) }
+ let(:project) { create(:project) }
+
+ it 'returns an empty state page' do
+ go
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:index, partial: :empty_state)
+ expect(assigns(:clusters)).to eq([])
+ end
+
+ it 'assigns counters to zero' do
+ go
+
+ expect(assigns(:active_count)).to eq(0)
+ expect(assigns(:inactive_count)).to eq(0)
+ end
end
end
@@ -146,7 +204,7 @@ describe Projects::ClustersController do
go
cluster.reload
- expect(response).to redirect_to(project_cluster_path(project, project.cluster))
+ expect(response).to redirect_to(project_cluster_path(project, cluster))
expect(flash[:notice]).to eq('Cluster was successfully updated.')
expect(cluster.enabled).to be_falsey
end
@@ -180,28 +238,77 @@ describe Projects::ClustersController do
sign_in(user)
end
- context 'when changing parameters' do
- let(:params) do
- {
- cluster: {
- enabled: false,
- name: 'my-new-cluster-name',
- platform_kubernetes_attributes: {
- namespace: 'my-namespace'
+ context 'when format is json' do
+ context 'when changing parameters' do
+ context 'when valid parameters are used' do
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: 'my-new-cluster-name',
+ platform_kubernetes_attributes: {
+ namespace: 'my-namespace'
+ }
+ }
}
- }
- }
+ end
+
+ it "updates and redirects back to show page" do
+ go_json
+
+ cluster.reload
+ expect(response).to have_http_status(:no_content)
+ expect(cluster.enabled).to be_falsey
+ expect(cluster.name).to eq('my-new-cluster-name')
+ expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
+ end
+ end
+
+ context 'when invalid parameters are used' do
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ platform_kubernetes_attributes: {
+ namespace: 'my invalid namespace #@'
+ }
+ }
+ }
+ end
+
+ it "rejects changes" do
+ go_json
+
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
end
+ end
- it "updates and redirects back to show page" do
- go
+ context 'when format is html' do
+ context 'when update enabled' do
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: 'my-new-cluster-name',
+ platform_kubernetes_attributes: {
+ namespace: 'my-namespace'
+ }
+ }
+ }
+ end
- cluster.reload
- expect(response).to redirect_to(project_cluster_path(project, project.cluster))
- expect(flash[:notice]).to eq('Cluster was successfully updated.')
- expect(cluster.enabled).to be_falsey
- expect(cluster.name).to eq('my-new-cluster-name')
- expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
+ it "updates and redirects back to show page" do
+ go
+
+ cluster.reload
+ expect(response).to redirect_to(project_cluster_path(project, cluster))
+ expect(flash[:notice]).to eq('Cluster was successfully updated.')
+ expect(cluster.enabled).to be_falsey
+ expect(cluster.name).to eq('my-new-cluster-name')
+ expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
+ end
end
end
end
@@ -228,6 +335,13 @@ describe Projects::ClustersController do
project_id: project,
id: cluster)
end
+
+ def go_json
+ put :update, params.merge(namespace_id: project.namespace,
+ project_id: project,
+ id: cluster,
+ format: :json)
+ end
end
describe 'DELETE destroy' do
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index fd90c0d8bad..694c64ae1ad 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -132,6 +132,22 @@ describe Projects::CommitController do
expect(response).to be_success
end
end
+
+ context 'in the context of a merge_request' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:commit) { merge_request.commits.first }
+
+ it 'prepare diff notes in the context of the merge request' do
+ go(id: commit.id, merge_request_iid: merge_request.iid)
+
+ expect(assigns(:new_diff_note_attrs)).to eq({
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id,
+ commit_id: commit.id
+ })
+ expect(response).to be_ok
+ end
+ end
end
describe 'GET branches' do
@@ -323,7 +339,7 @@ describe Projects::CommitController do
context 'when the commit does not exist' do
before do
- diff_for_path(id: commit.id.succ, old_path: existing_path, new_path: existing_path)
+ diff_for_path(id: commit.id.reverse, old_path: existing_path, new_path: existing_path)
end
it 'returns a 404' do
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 4dbbaecdd6d..c5d08cb0b9d 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -272,6 +272,20 @@ describe Projects::IssuesController do
expect(response).to have_http_status(:ok)
expect(issue.reload.title).to eq('New title')
end
+
+ context 'when Akismet is enabled and the issue is identified as spam' do
+ before do
+ stub_application_setting(recaptcha_enabled: true)
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true)
+ end
+
+ it 'renders json with recaptcha_html' do
+ subject
+
+ expect(JSON.parse(response.body)).to have_key('recaptcha_html')
+ end
+ end
end
context 'when user does not have access to update issue' do
@@ -504,17 +518,16 @@ describe Projects::IssuesController do
expect(spam_logs.first.recaptcha_verified).to be_falsey
end
- it 'renders json errors' do
+ it 'renders recaptcha_html json response' do
update_issue
- expect(json_response)
- .to eql("errors" => ["Your issue has been recognized as spam. Please, change the content or solve the reCAPTCHA to proceed."])
+ expect(json_response).to have_key('recaptcha_html')
end
- it 'returns 422 status' do
+ it 'returns 200 status' do
update_issue
- expect(response).to have_gitlab_http_status(422)
+ expect(response).to have_gitlab_http_status(200)
end
end
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index 18a70bec103..ba97ccfbbd4 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -100,7 +100,8 @@ describe Projects::MergeRequests::DiffsController do
expect(assigns(:diff_notes_disabled)).to be_falsey
expect(assigns(:new_diff_note_attrs)).to eq(noteable_type: 'MergeRequest',
- noteable_id: merge_request.id)
+ noteable_id: merge_request.id,
+ commit_id: nil)
end
it 'only renders the diffs for the path given' do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index bfdad85c082..51d5d6a52b3 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -324,12 +324,12 @@ describe Projects::MergeRequestsController do
end
context 'when the pipeline succeeds is passed' do
- def merge_when_pipeline_succeeds
- post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1')
+ let!(:head_pipeline) do
+ create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch, head_pipeline_of: merge_request)
end
- before do
- create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch, head_pipeline_of: merge_request)
+ def merge_when_pipeline_succeeds
+ post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1')
end
it 'returns :merge_when_pipeline_succeeds' do
@@ -354,6 +354,18 @@ describe Projects::MergeRequestsController do
project.update_column(:only_allow_merge_if_pipeline_succeeds, true)
end
+ context 'and head pipeline is not the current one' do
+ before do
+ head_pipeline.update(sha: 'not_current_sha')
+ end
+
+ it 'returns :failed' do
+ merge_when_pipeline_succeeds
+
+ expect(json_response).to eq('status' => 'failed')
+ end
+ end
+
it 'returns :merge_when_pipeline_succeeds' do
merge_when_pipeline_succeeds
diff --git a/spec/controllers/projects/pipelines_settings_controller_spec.rb b/spec/controllers/projects/pipelines_settings_controller_spec.rb
index b2d83a02290..1cc488bef32 100644
--- a/spec/controllers/projects/pipelines_settings_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_settings_controller_spec.rb
@@ -16,14 +16,13 @@ describe Projects::PipelinesSettingsController do
patch :update,
namespace_id: project.namespace.to_param,
project_id: project,
- project: { auto_devops_attributes: params,
- run_auto_devops_pipeline_implicit: 'false',
- run_auto_devops_pipeline_explicit: auto_devops_pipeline }
+ project: {
+ auto_devops_attributes: params
+ }
end
context 'when updating the auto_devops settings' do
let(:params) { { enabled: '', domain: 'mepmep.md' } }
- let(:auto_devops_pipeline) { 'false' }
it 'redirects to the settings page' do
subject
@@ -44,7 +43,9 @@ describe Projects::PipelinesSettingsController do
end
context 'when run_auto_devops_pipeline is true' do
- let(:auto_devops_pipeline) { 'true' }
+ before do
+ expect_any_instance_of(Projects::UpdateService).to receive(:run_auto_devops_pipeline?).and_return(true)
+ end
it 'queues a CreatePipelineWorker' do
expect(CreatePipelineWorker).to receive(:perform_async).with(project.id, user.id, project.default_branch, :web, any_args)
@@ -54,7 +55,9 @@ describe Projects::PipelinesSettingsController do
end
context 'when run_auto_devops_pipeline is not true' do
- let(:auto_devops_pipeline) { 'false' }
+ before do
+ expect_any_instance_of(Projects::UpdateService).to receive(:run_auto_devops_pipeline?).and_return(false)
+ end
it 'does not queue a CreatePipelineWorker' do
expect(CreatePipelineWorker).not_to receive(:perform_async).with(project.id, user.id, :web, any_args)
diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb
index c2550b1efa7..d572085661d 100644
--- a/spec/controllers/projects/uploads_controller_spec.rb
+++ b/spec/controllers/projects/uploads_controller_spec.rb
@@ -1,247 +1,10 @@
-require('spec_helper')
+require 'spec_helper'
describe Projects::UploadsController do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') }
- let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
-
- describe "POST #create" do
- before do
- sign_in(user)
- project.team << [user, :developer]
- end
-
- context "without params['file']" do
- it "returns an error" do
- post :create,
- namespace_id: project.namespace.to_param,
- project_id: project,
- format: :json
- expect(response).to have_gitlab_http_status(422)
- end
- end
-
- context 'with valid image' do
- before do
- post :create,
- namespace_id: project.namespace.to_param,
- project_id: project,
- file: jpg,
- format: :json
- end
-
- it 'returns a content with original filename, new link, and correct type.' do
- expect(response.body).to match '\"alt\":\"rails_sample\"'
- expect(response.body).to match "\"url\":\"/uploads"
- end
-
- # NOTE: This is as close as we're getting to an Integration test for this
- # behavior. We're avoiding a proper Feature test because those should be
- # testing things entirely user-facing, which the Upload model is very much
- # not.
- it 'creates a corresponding Upload record' do
- upload = Upload.last
-
- aggregate_failures do
- expect(upload).to exist
- expect(upload.model).to eq project
- end
- end
- end
-
- context 'with valid non-image file' do
- before do
- post :create,
- namespace_id: project.namespace.to_param,
- project_id: project,
- file: txt,
- format: :json
- end
-
- it 'returns a content with original filename, new link, and correct type.' do
- expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
- expect(response.body).to match "\"url\":\"/uploads"
- end
- end
+ let(:model) { create(:project, :public) }
+ let(:params) do
+ { namespace_id: model.namespace.to_param, project_id: model }
end
- describe "GET #show" do
- let(:go) do
- get :show,
- namespace_id: project.namespace.to_param,
- project_id: project,
- secret: "123456",
- filename: "image.jpg"
- end
-
- context "when the project is public" do
- before do
- project.update_attribute(:visibility_level, Project::PUBLIC)
- end
-
- context "when not signed in" do
- context "when the file exists" do
- before do
- allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
- allow(jpg).to receive(:exists?).and_return(true)
- end
-
- it "responds with status 200" do
- go
-
- expect(response).to have_gitlab_http_status(200)
- end
- end
-
- context "when the file doesn't exist" do
- it "responds with status 404" do
- go
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- context "when signed in" do
- before do
- sign_in(user)
- end
-
- context "when the file exists" do
- before do
- allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
- allow(jpg).to receive(:exists?).and_return(true)
- end
-
- it "responds with status 200" do
- go
-
- expect(response).to have_gitlab_http_status(200)
- end
- end
-
- context "when the file doesn't exist" do
- it "responds with status 404" do
- go
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
- end
-
- context "when the project is private" do
- before do
- project.update_attribute(:visibility_level, Project::PRIVATE)
- end
-
- context "when not signed in" do
- context "when the file exists" do
- before do
- allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
- allow(jpg).to receive(:exists?).and_return(true)
- end
-
- context "when the file is an image" do
- before do
- allow_any_instance_of(FileUploader).to receive(:image?).and_return(true)
- end
-
- it "responds with status 200" do
- go
-
- expect(response).to have_gitlab_http_status(200)
- end
- end
-
- context "when the file is not an image" do
- it "redirects to the sign in page" do
- go
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
- end
-
- context "when the file doesn't exist" do
- it "redirects to the sign in page" do
- go
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
- end
-
- context "when signed in" do
- before do
- sign_in(user)
- end
-
- context "when the user has access to the project" do
- before do
- project.team << [user, :master]
- end
-
- context "when the file exists" do
- before do
- allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
- allow(jpg).to receive(:exists?).and_return(true)
- end
-
- it "responds with status 200" do
- go
-
- expect(response).to have_gitlab_http_status(200)
- end
- end
-
- context "when the file doesn't exist" do
- it "responds with status 404" do
- go
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- context "when the user doesn't have access to the project" do
- context "when the file exists" do
- before do
- allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
- allow(jpg).to receive(:exists?).and_return(true)
- end
-
- context "when the file is an image" do
- before do
- allow_any_instance_of(FileUploader).to receive(:image?).and_return(true)
- end
-
- it "responds with status 200" do
- go
-
- expect(response).to have_gitlab_http_status(200)
- end
- end
-
- context "when the file is not an image" do
- it "responds with status 404" do
- go
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- context "when the file doesn't exist" do
- it "responds with status 404" do
- go
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
- end
- end
- end
+ it_behaves_like 'handle uploads'
end
diff --git a/spec/factories/appearances.rb b/spec/factories/appearances.rb
index cf2a2b76bcb..860973024c9 100644
--- a/spec/factories/appearances.rb
+++ b/spec/factories/appearances.rb
@@ -4,5 +4,6 @@ FactoryGirl.define do
factory :appearance do
title "MepMep"
description "This is my Community Edition instance"
+ new_project_guidelines "Custom project guidelines"
end
end
diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb
index 81866845a20..9e73a19e856 100644
--- a/spec/factories/clusters/clusters.rb
+++ b/spec/factories/clusters/clusters.rb
@@ -28,5 +28,9 @@ FactoryGirl.define do
provider_type :gcp
provider_gcp factory: [:cluster_provider_gcp, :creating]
end
+
+ trait :disabled do
+ enabled false
+ end
end
end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index ab4ae123429..471bfb3213a 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -63,13 +63,19 @@ FactoryGirl.define do
factory :diff_note_on_commit, traits: [:on_commit], class: DiffNote do
association :project, :repository
+
+ transient do
+ line_number 14
+ diff_refs { project.commit(commit_id).try(:diff_refs) }
+ end
+
position do
Gitlab::Diff::Position.new(
old_path: "files/ruby/popen.rb",
new_path: "files/ruby/popen.rb",
old_line: nil,
- new_line: 14,
- diff_refs: project.commit(commit_id).try(:diff_refs)
+ new_line: line_number,
+ diff_refs: diff_refs
)
end
end
diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb
index 3222c41c3d8..e18f1a6bd4a 100644
--- a/spec/factories/uploads.rb
+++ b/spec/factories/uploads.rb
@@ -4,5 +4,21 @@ FactoryGirl.define do
path { "uploads/-/system/project/avatar/avatar.jpg" }
size 100.kilobytes
uploader "AvatarUploader"
+
+ trait :personal_snippet do
+ model { build(:personal_snippet) }
+ uploader "PersonalFileUploader"
+ end
+
+ trait :issuable_upload do
+ path { "#{SecureRandom.hex}/myfile.jpg" }
+ uploader "FileUploader"
+ end
+
+ trait :namespace_upload do
+ path { "#{SecureRandom.hex}/myfile.jpg" }
+ model { build(:group) }
+ uploader "NamespaceFileUploader"
+ end
end
end
diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb
index 5f3a37c1dcc..d91dcf76191 100644
--- a/spec/features/admin/admin_appearance_spec.rb
+++ b/spec/features/admin/admin_appearance_spec.rb
@@ -9,6 +9,7 @@ feature 'Admin Appearance' do
fill_in 'appearance_title', with: 'MyCompany'
fill_in 'appearance_description', with: 'dev server'
+ fill_in 'appearance_new_project_guidelines', with: 'Custom project guidelines'
click_button 'Save'
expect(current_path).to eq admin_appearances_path
@@ -16,21 +17,39 @@ feature 'Admin Appearance' do
expect(page).to have_field('appearance_title', with: 'MyCompany')
expect(page).to have_field('appearance_description', with: 'dev server')
+ expect(page).to have_field('appearance_new_project_guidelines', with: 'Custom project guidelines')
expect(page).to have_content 'Last edit'
end
- scenario 'Preview appearance' do
+ scenario 'Preview sign-in page appearance' do
sign_in(create(:admin))
visit admin_appearances_path
- click_link "Preview"
+ click_link "Sign-in page"
- expect_page_has_custom_appearance(appearance)
+ expect_custom_sign_in_appearance(appearance)
+ end
+
+ scenario 'Preview new project page appearance' do
+ sign_in(create(:admin))
+
+ visit admin_appearances_path
+ click_link "New project page"
+
+ expect_custom_new_project_appearance(appearance)
end
scenario 'Custom sign-in page' do
visit new_user_session_path
- expect_page_has_custom_appearance(appearance)
+
+ expect_custom_sign_in_appearance(appearance)
+ end
+
+ scenario 'Custom new project page' do
+ sign_in create(:user)
+ visit new_project_path
+
+ expect_custom_new_project_appearance(appearance)
end
scenario 'Appearance logo' do
@@ -57,11 +76,15 @@ feature 'Admin Appearance' do
expect(page).not_to have_css(header_logo_selector)
end
- def expect_page_has_custom_appearance(appearance)
+ def expect_custom_sign_in_appearance(appearance)
expect(page).to have_content appearance.title
expect(page).to have_content appearance.description
end
+ def expect_custom_new_project_appearance(appearance)
+ expect(page).to have_content appearance.new_project_guidelines
+ end
+
def logo_selector
'//img[data-src^="/uploads/-/system/appearance/logo"]'
end
diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb
index 4430fc15501..ac3392b49f9 100644
--- a/spec/features/admin/admin_health_check_spec.rb
+++ b/spec/features/admin/admin_health_check_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature "Admin Health Check", :feature, :broken_storage do
+feature "Admin Health Check", :feature do
include StubENV
before do
@@ -36,6 +36,7 @@ feature "Admin Health Check", :feature, :broken_storage do
context 'when services are up' do
before do
+ stub_storage_settings({}) # Hide the broken storage
visit admin_health_check_path
end
@@ -56,10 +57,8 @@ feature "Admin Health Check", :feature, :broken_storage do
end
end
- context 'with repository storage failures' do
+ context 'with repository storage failures', :broken_storage do
before do
- # Track a failure
- Gitlab::Git::Storage::CircuitBreaker.for_storage('broken').perform { nil } rescue nil
visit admin_health_check_path
end
@@ -67,9 +66,10 @@ feature "Admin Health Check", :feature, :broken_storage do
hostname = Gitlab::Environment.hostname
maximum_failures = Gitlab::CurrentSettings.current_application_settings
.circuitbreaker_failure_count_threshold
+ number_of_failures = maximum_failures + 1
- expect(page).to have_content('broken: failed storage access attempt on host:')
- expect(page).to have_content("#{hostname}: 1 of #{maximum_failures} failures.")
+ expect(page).to have_content("broken: #{number_of_failures} failed storage access attempts:")
+ expect(page).to have_content("#{hostname}: #{number_of_failures} of #{maximum_failures} failures.")
end
it 'allows resetting storage failures' do
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 95d637265e0..c31b636d67f 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -220,6 +220,89 @@ feature 'GFM autocomplete', :js do
end
end
+ # This context has jsut one example in each contexts in order to improve spec performance.
+ context 'labels' do
+ let!(:backend) { create(:label, project: project, title: 'backend') }
+ let!(:bug) { create(:label, project: project, title: 'bug') }
+ let!(:feature_proposal) { create(:label, project: project, title: 'feature proposal') }
+
+ context 'when no labels are assigned' do
+ it 'shows labels' do
+ note = find('#note-body')
+
+ # It should show all the labels on "~".
+ type(note, '~')
+ expect_labels(shown: [backend, bug, feature_proposal])
+
+ # It should show all the labels on "/label ~".
+ type(note, '/label ~')
+ expect_labels(shown: [backend, bug, feature_proposal])
+
+ # It should show all the labels on "/relabel ~".
+ type(note, '/relabel ~')
+ expect_labels(shown: [backend, bug, feature_proposal])
+
+ # It should show no labels on "/unlabel ~".
+ type(note, '/unlabel ~')
+ expect_labels(not_shown: [backend, bug, feature_proposal])
+ end
+ end
+
+ context 'when some labels are assigned' do
+ before do
+ issue.labels << [backend]
+ end
+
+ it 'shows labels' do
+ note = find('#note-body')
+
+ # It should show all the labels on "~".
+ type(note, '~')
+ expect_labels(shown: [backend, bug, feature_proposal])
+
+ # It should show only unset labels on "/label ~".
+ type(note, '/label ~')
+ expect_labels(shown: [bug, feature_proposal], not_shown: [backend])
+
+ # It should show all the labels on "/relabel ~".
+ type(note, '/relabel ~')
+ expect_labels(shown: [backend, bug, feature_proposal])
+
+ # It should show only set labels on "/unlabel ~".
+ type(note, '/unlabel ~')
+ expect_labels(shown: [backend], not_shown: [bug, feature_proposal])
+ end
+ end
+
+ context 'when all labels are assigned' do
+ before do
+ issue.labels << [backend, bug, feature_proposal]
+ end
+
+ it 'shows labels' do
+ note = find('#note-body')
+
+ # It should show all the labels on "~".
+ type(note, '~')
+ expect_labels(shown: [backend, bug, feature_proposal])
+
+ # It should show no labels on "/label ~".
+ type(note, '/label ~')
+ expect_labels(not_shown: [backend, bug, feature_proposal])
+
+ # It should show all the labels on "/relabel ~".
+ type(note, '/relabel ~')
+ expect_labels(shown: [backend, bug, feature_proposal])
+
+ # It should show all the labels on "/unlabel ~".
+ type(note, '/unlabel ~')
+ expect_labels(shown: [backend, bug, feature_proposal])
+ end
+ end
+ end
+
+ private
+
def expect_to_wrap(should_wrap, item, note, value)
expect(item).to have_content(value)
expect(item).not_to have_content("\"#{value}\"")
@@ -232,4 +315,27 @@ feature 'GFM autocomplete', :js do
expect(note.value).not_to include("\"#{value}\"")
end
end
+
+ def expect_labels(shown: nil, not_shown: nil)
+ page.within('.atwho-container') do
+ if shown
+ expect(page).to have_selector('.atwho-view li', count: shown.size)
+ shown.each { |label| expect(page).to have_content(label.title) }
+ end
+
+ if not_shown
+ expect(page).not_to have_selector('.atwho-view li') unless shown
+ not_shown.each { |label| expect(page).not_to have_content(label.title) }
+ end
+ end
+ end
+
+ # `note` is a textarea where the given text should be typed.
+ # We don't want to find it each time this function gets called.
+ def type(note, text)
+ page.within('.timeline-content-form') do
+ note.set('')
+ note.native.send_keys(text)
+ end
+ end
end
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index cc1b187ff54..e285befc66f 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -207,8 +207,9 @@ describe 'GitLab Markdown' do
before do
@feat = MarkdownFeature.new
- # `markdown` helper expects a `@project` variable
+ # `markdown` helper expects a `@project` and `@group` variable
@project = @feat.project
+ @group = @feat.group
end
context 'default pipeline' do
diff --git a/spec/features/merge_requests/pipelines_spec.rb b/spec/features/merge_requests/pipelines_spec.rb
index a3fcc27cab0..307c860eac4 100644
--- a/spec/features/merge_requests/pipelines_spec.rb
+++ b/spec/features/merge_requests/pipelines_spec.rb
@@ -20,10 +20,14 @@ feature 'Pipelines for Merge Requests', :js do
end
before do
- visit project_merge_request_path(project, merge_request)
+ merge_request.update_attribute(:head_pipeline_id, pipeline.id)
end
scenario 'user visits merge request pipelines tab' do
+ visit project_merge_request_path(project, merge_request)
+
+ expect(page.find('.ci-widget')).to have_content('pending')
+
page.within('.merge-request-tabs') do
click_link('Pipelines')
end
@@ -31,6 +35,15 @@ feature 'Pipelines for Merge Requests', :js do
expect(page).to have_selector('.stage-cell')
end
+
+ scenario 'pipeline sha does not equal last commit sha' do
+ pipeline.update_attribute(:sha, '19e2e9b4ef76b422ce1154af39a91323ccc57434')
+ visit project_merge_request_path(project, merge_request)
+ wait_for_requests
+
+ expect(page.find('.ci-widget')).to have_content(
+ 'Could not connect to the CI server. Please check your settings and try again')
+ end
end
context 'without pipelines' do
diff --git a/spec/features/merge_requests/versions_spec.rb b/spec/features/merge_requests/versions_spec.rb
index 29f95039af8..482f2e51c8b 100644
--- a/spec/features/merge_requests/versions_spec.rb
+++ b/spec/features/merge_requests/versions_spec.rb
@@ -6,18 +6,47 @@ feature 'Merge Request versions', :js do
let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
let!(:merge_request_diff2) { merge_request.merge_request_diffs.create(head_commit_sha: nil) }
let!(:merge_request_diff3) { merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+ let!(:params) { Hash.new }
before do
sign_in(create(:admin))
- visit diffs_project_merge_request_path(project, merge_request)
+ visit diffs_project_merge_request_path(project, merge_request, params)
end
- it 'show the latest version of the diff' do
- page.within '.mr-version-dropdown' do
- expect(page).to have_content 'latest version'
+ shared_examples 'allows commenting' do |file_id:, line_code:, comment:|
+ it do
+ diff_file_selector = ".diff-file[id='#{file_id}']"
+ line_code = "#{file_id}_#{line_code}"
+
+ page.within(diff_file_selector) do
+ find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").hover
+ find(".line_holder[id='#{line_code}'] button").click
+
+ page.within("form[data-line-code='#{line_code}']") do
+ fill_in "note[note]", with: comment
+ find(".js-comment-button").click
+ end
+
+ wait_for_requests
+
+ expect(page).to have_content(comment)
+ end
end
+ end
- expect(page).to have_content '8 changed files'
+ describe 'compare with the latest version' do
+ it 'show the latest version of the diff' do
+ page.within '.mr-version-dropdown' do
+ expect(page).to have_content 'latest version'
+ end
+
+ expect(page).to have_content '8 changed files'
+ end
+
+ it_behaves_like 'allows commenting',
+ file_id: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44',
+ line_code: '1_1',
+ comment: 'Typo, please fix.'
end
describe 'switch between versions' do
@@ -62,24 +91,10 @@ feature 'Merge Request versions', :js do
expect(page).to have_css(".diffs .notes[data-discussion-id='#{outdated_diff_note.discussion_id}']")
end
- it 'allows commenting' do
- diff_file_selector = ".diff-file[id='7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44']"
- line_code = '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_2_2'
-
- page.within(diff_file_selector) do
- find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").hover
- find(".line_holder[id='#{line_code}'] button").click
-
- page.within("form[data-line-code='#{line_code}']") do
- fill_in "note[note]", with: "Typo, please fix"
- find(".js-comment-button").click
- end
-
- wait_for_requests
-
- expect(page).to have_content("Typo, please fix")
- end
- end
+ it_behaves_like 'allows commenting',
+ file_id: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44',
+ line_code: '2_2',
+ comment: 'Typo, please fix.'
end
describe 'compare with older version' do
@@ -132,25 +147,6 @@ feature 'Merge Request versions', :js do
expect(page).to have_css(".diffs .notes[data-discussion-id='#{outdated_diff_note.discussion_id}']")
end
- it 'allows commenting' do
- diff_file_selector = ".diff-file[id='7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44']"
- line_code = '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_4_4'
-
- page.within(diff_file_selector) do
- find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").hover
- find(".line_holder[id='#{line_code}'] button").click
-
- page.within("form[data-line-code='#{line_code}']") do
- fill_in "note[note]", with: "Typo, please fix"
- find(".js-comment-button").click
- end
-
- wait_for_requests
-
- expect(page).to have_content("Typo, please fix")
- end
- end
-
it 'show diff between new and old version' do
expect(page).to have_content '4 changed files with 15 additions and 6 deletions'
end
@@ -162,6 +158,11 @@ feature 'Merge Request versions', :js do
end
expect(page).to have_content '8 changed files'
end
+
+ it_behaves_like 'allows commenting',
+ file_id: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44',
+ line_code: '4_4',
+ comment: 'Typo, please fix.'
end
describe 'compare with same version' do
@@ -210,4 +211,24 @@ feature 'Merge Request versions', :js do
expect(page).to have_content '0 changed files'
end
end
+
+ describe 'scoped in a commit' do
+ let(:params) { { commit_id: '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' } }
+
+ before do
+ wait_for_requests
+ end
+
+ it 'should only show diffs from the commit' do
+ diff_commit_ids = find_all('.diff-file [data-commit-id]').map {|diff| diff['data-commit-id']}
+
+ expect(diff_commit_ids).not_to be_empty
+ expect(diff_commit_ids).to all(eq(params[:commit_id]))
+ end
+
+ it_behaves_like 'allows commenting',
+ file_id: '2f6fcd96b88b36ce98c38da085c795a27d92a3dd',
+ line_code: '6_6',
+ comment: 'Typo, please fix.'
+ end
end
diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb
index 2bad3b02250..3ee094c216e 100644
--- a/spec/features/merge_requests/widget_spec.rb
+++ b/spec/features/merge_requests/widget_spec.rb
@@ -63,6 +63,18 @@ describe 'Merge request', :js do
expect(page).to have_selector('.accept-merge-request')
expect(find('.accept-merge-request')['disabled']).not_to be(true)
end
+
+ it 'allows me to merge, see cherry-pick modal and load branches list' do
+ wait_for_requests
+ click_button 'Merge'
+
+ wait_for_requests
+ click_link 'Cherry-pick'
+ page.find('.js-project-refs-dropdown').click
+ wait_for_requests
+
+ expect(page.all('.js-cherry-pick-form .dropdown-content li').size).to be > 1
+ end
end
context 'view merge request with external CI service' do
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index 8a0da669147..67b8901f8fb 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -24,6 +24,7 @@ feature 'Gcp Cluster', :js do
before do
visit project_clusters_path(project)
+ click_link 'Add cluster'
click_link 'Create on GKE'
end
@@ -45,15 +46,15 @@ feature 'Gcp Cluster', :js do
end
it 'user sees a cluster details page and creation status' do
- expect(page).to have_content('Cluster is being created on Google Container Engine...')
+ expect(page).to have_content('Cluster is being created on Google Kubernetes Engine...')
Clusters::Cluster.last.provider.make_created!
- expect(page).to have_content('Cluster was successfully created on Google Container Engine')
+ expect(page).to have_content('Cluster was successfully created on Google Kubernetes Engine')
end
it 'user sees a error if something worng during creation' do
- expect(page).to have_content('Cluster is being created on Google Container Engine...')
+ expect(page).to have_content('Cluster is being created on Google Kubernetes Engine...')
Clusters::Cluster.last.provider.make_errored!('Something wrong!')
@@ -116,7 +117,7 @@ feature 'Gcp Cluster', :js do
it 'user sees creation form with the successful message' do
expect(page).to have_content('Cluster integration was successfully removed.')
- expect(page).to have_link('Create on GKE')
+ expect(page).to have_link('Add cluster')
end
end
end
@@ -126,6 +127,7 @@ feature 'Gcp Cluster', :js do
before do
visit project_clusters_path(project)
+ click_link 'Add cluster'
click_link 'Create on GKE'
end
diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb
index e97ba88f2f4..414f4acba86 100644
--- a/spec/features/projects/clusters/user_spec.rb
+++ b/spec/features/projects/clusters/user_spec.rb
@@ -16,6 +16,7 @@ feature 'User Cluster', :js do
before do
visit project_clusters_path(project)
+ click_link 'Add cluster'
click_link 'Add an existing cluster'
end
@@ -94,7 +95,7 @@ feature 'User Cluster', :js do
it 'user sees creation form with the successful message' do
expect(page).to have_content('Cluster integration was successfully removed.')
- expect(page).to have_link('Add an existing cluster')
+ expect(page).to have_link('Add cluster')
end
end
end
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index 4243c4fd266..008bdf2044b 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -14,12 +14,78 @@ feature 'Clusters', :js do
context 'when user does not have a cluster and visits cluster index page' do
before do
visit project_clusters_path(project)
+ end
- click_link 'Create on GKE'
+ it 'sees empty state' do
+ expect(page).to have_link('Add cluster')
+ expect(page).to have_selector('.empty-state')
end
+ end
+
+ context 'when user has a cluster and visits cluster index page' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ before do
+ visit project_clusters_path(project)
+ end
+
+ it 'user sees a table with one cluster' do
+ # One is the header row, the other the cluster row
+ expect(page).to have_selector('.gl-responsive-table-row', count: 2)
+ end
+
+ it 'user sees navigation tabs' do
+ expect(page.find('.js-active-tab').text).to include('Active')
+ expect(page.find('.js-active-tab .badge').text).to include('1')
+
+ expect(page.find('.js-inactive-tab').text).to include('Inactive')
+ expect(page.find('.js-inactive-tab .badge').text).to include('0')
+
+ expect(page.find('.js-all-tab').text).to include('All')
+ expect(page.find('.js-all-tab .badge').text).to include('1')
+ end
+
+ context 'inline update of cluster' do
+ it 'user can update cluster' do
+ expect(page).to have_selector('.js-toggle-cluster-list')
+ end
+
+ context 'with sucessfull request' do
+ it 'user sees updated cluster' do
+ expect do
+ page.find('.js-toggle-cluster-list').click
+ wait_for_requests
+ end.to change { cluster.reload.enabled }
+
+ expect(page).not_to have_selector('.is-checked')
+ expect(cluster.reload).not_to be_enabled
+ end
+ end
+
+ context 'with failed request' do
+ it 'user sees not update cluster and error message' do
+ expect_any_instance_of(Clusters::UpdateService).to receive(:execute).and_call_original
+ allow_any_instance_of(Clusters::Cluster).to receive(:valid?) { false }
+
+ page.find('.js-toggle-cluster-list').click
+
+ expect(page).to have_content('Something went wrong on our end.')
+ expect(page).to have_selector('.is-checked')
+ expect(cluster.reload).to be_enabled
+ end
+ end
+ end
+
+ context 'when user clicks on a cluster' do
+ before do
+ click_link cluster.name
+ end
- it 'user sees a new page' do
- expect(page).to have_button('Create cluster')
+ it 'user sees a cluster details page' do
+ expect(page).to have_button('Save')
+ expect(page.find(:css, '.cluster-name').value).to eq(cluster.name)
+ end
end
end
end
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 951456763dc..033c45a60bf 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -177,7 +177,7 @@ describe 'Edit Project Settings' do
click_button "Save changes"
end
- expect(find(".sharing-permissions")).to have_selector(".project-feature-toggle.disabled", count: 2)
+ expect(find(".sharing-permissions")).to have_selector(".project-feature-toggle.is-disabled", count: 2)
end
it "shows empty features project homepage" do
@@ -272,10 +272,10 @@ describe 'Edit Project Settings' do
end
def toggle_feature_off(feature_name)
- find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle.checked").click
+ find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle.is-checked").click
end
def toggle_feature_on(feature_name)
- find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle:not(.checked)").click
+ find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle:not(.is-checked)").click
end
end
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index a012db8fd27..0257cd157c9 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -32,7 +32,7 @@ feature 'issuable templates', :js do
message: 'added issue template',
branch_name: 'master')
visit project_issue_path project, issue
- page.within('.content .issuable-actions') do
+ page.within('.js-issuable-actions') do
click_on 'Edit'
end
fill_in :'issuable-title', with: 'test issue title'
@@ -77,7 +77,7 @@ feature 'issuable templates', :js do
message: 'added issue template',
branch_name: 'master')
visit project_issue_path project, issue
- page.within('.content .issuable-actions') do
+ page.within('.js-issuable-actions') do
click_on 'Edit'
end
fill_in :'issuable-title', with: 'test issue title'
diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb
index eb8e7265dd3..561f08cba00 100644
--- a/spec/features/projects/settings/pipelines_settings_spec.rb
+++ b/spec/features/projects/settings/pipelines_settings_spec.rb
@@ -59,107 +59,6 @@ feature "Pipelines settings" do
expect(project.auto_devops).to be_present
expect(project.auto_devops).not_to be_enabled
end
-
- describe 'Immediately run pipeline checkbox option', :js do
- context 'when auto devops is set to instance default (enabled)' do
- before do
- stub_application_setting(auto_devops_enabled: true)
- project.create_auto_devops!(enabled: nil)
- visit project_settings_ci_cd_path(project)
- end
-
- it 'does not show checkboxes on page-load' do
- expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false)
- end
-
- it 'selecting explicit disabled hides all checkboxes' do
- page.choose('project_auto_devops_attributes_enabled_false')
-
- expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false)
- end
-
- it 'selecting explicit enabled hides all checkboxes because we are already enabled' do
- page.choose('project_auto_devops_attributes_enabled_true')
-
- expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false)
- end
- end
-
- context 'when auto devops is set to instance default (disabled)' do
- before do
- stub_application_setting(auto_devops_enabled: false)
- project.create_auto_devops!(enabled: nil)
- visit project_settings_ci_cd_path(project)
- end
-
- it 'does not show checkboxes on page-load' do
- expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false)
- end
-
- it 'selecting explicit disabled hides all checkboxes' do
- page.choose('project_auto_devops_attributes_enabled_false')
-
- expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false)
- end
-
- it 'selecting explicit enabled shows a checkbox' do
- page.choose('project_auto_devops_attributes_enabled_true')
-
- expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper:not(.hide)', count: 1)
- end
- end
-
- context 'when auto devops is set to explicit disabled' do
- before do
- stub_application_setting(auto_devops_enabled: true)
- project.create_auto_devops!(enabled: false)
- visit project_settings_ci_cd_path(project)
- end
-
- it 'does not show checkboxes on page-load' do
- expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 2, visible: false)
- end
-
- it 'selecting explicit enabled shows a checkbox' do
- page.choose('project_auto_devops_attributes_enabled_true')
-
- expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper:not(.hide)', count: 1)
- end
-
- it 'selecting instance default (enabled) shows a checkbox' do
- page.choose('project_auto_devops_attributes_enabled_')
-
- expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper:not(.hide)', count: 1)
- end
- end
-
- context 'when auto devops is set to explicit enabled' do
- before do
- stub_application_setting(auto_devops_enabled: false)
- project.create_auto_devops!(enabled: true)
- visit project_settings_ci_cd_path(project)
- end
-
- it 'does not have any checkboxes' do
- expect(page).not_to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper', visible: false)
- end
- end
-
- context 'when master contains a .gitlab-ci.yml file' do
- let(:project) { create(:project, :repository) }
-
- before do
- project.repository.create_file(user, '.gitlab-ci.yml', "script: ['test']", message: 'test', branch_name: project.default_branch)
- stub_application_setting(auto_devops_enabled: true)
- project.create_auto_devops!(enabled: false)
- visit project_settings_ci_cd_path(project)
- end
-
- it 'does not have any checkboxes' do
- expect(page).not_to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper', visible: false)
- end
- end
- end
end
end
end
diff --git a/spec/features/projects/snippets_spec.rb b/spec/features/projects/snippets_spec.rb
index 1cfbbb4cb62..0fa7ca9afd4 100644
--- a/spec/features/projects/snippets_spec.rb
+++ b/spec/features/projects/snippets_spec.rb
@@ -39,6 +39,11 @@ describe 'Project snippets', :js do
expect(page).to have_selector('.atwho-view')
end
+
+ it 'should have zen mode' do
+ find('.js-zen-enter').click()
+ expect(page).to have_selector('.fullscreen')
+ end
end
end
end
diff --git a/spec/finders/clusters_finder_spec.rb b/spec/finders/clusters_finder_spec.rb
new file mode 100644
index 00000000000..c10efac2432
--- /dev/null
+++ b/spec/finders/clusters_finder_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe ClustersFinder do
+ let(:project) { create(:project) }
+ set(:user) { create(:user) }
+
+ describe '#execute' do
+ let(:enabled_cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ let(:disabled_cluster) { create(:cluster, :disabled, :provided_by_gcp, projects: [project]) }
+
+ subject { described_class.new(project, user, scope).execute }
+
+ context 'when scope is all' do
+ let(:scope) { :all }
+
+ it { is_expected.to match_array([enabled_cluster, disabled_cluster]) }
+ end
+
+ context 'when scope is active' do
+ let(:scope) { :active }
+
+ it { is_expected.to match_array([enabled_cluster]) }
+ end
+
+ context 'when scope is inactive' do
+ let(:scope) { :inactive }
+
+ it { is_expected.to match_array([disabled_cluster]) }
+ end
+ end
+end
diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb
index 7266e1b84d1..5e272af6073 100644
--- a/spec/helpers/auto_devops_helper_spec.rb
+++ b/spec/helpers/auto_devops_helper_spec.rb
@@ -82,104 +82,4 @@ describe AutoDevopsHelper do
it { is_expected.to eq(false) }
end
end
-
- describe '.show_run_auto_devops_pipeline_checkbox_for_instance_setting?' do
- subject { helper.show_run_auto_devops_pipeline_checkbox_for_instance_setting?(project) }
-
- context 'when master contains a .gitlab-ci.yml file' do
- before do
- allow(project.repository).to receive(:gitlab_ci_yml).and_return("script: ['test']")
- end
-
- it { is_expected.to eq(false) }
- end
-
- context 'when auto devops is explicitly enabled' do
- before do
- project.create_auto_devops!(enabled: true)
- end
-
- it { is_expected.to eq(false) }
- end
-
- context 'when auto devops is explicitly disabled' do
- before do
- project.create_auto_devops!(enabled: false)
- end
-
- context 'when auto devops is enabled system-wide' do
- before do
- stub_application_setting(auto_devops_enabled: true)
- end
-
- it { is_expected.to eq(true) }
- end
-
- context 'when auto devops is disabled system-wide' do
- before do
- stub_application_setting(auto_devops_enabled: false)
- end
-
- it { is_expected.to eq(false) }
- end
- end
-
- context 'when auto devops is set to instance setting' do
- before do
- project.create_auto_devops!(enabled: nil)
- end
-
- it { is_expected.to eq(false) }
- end
- end
-
- describe '.show_run_auto_devops_pipeline_checkbox_for_explicit_setting?' do
- subject { helper.show_run_auto_devops_pipeline_checkbox_for_explicit_setting?(project) }
-
- context 'when master contains a .gitlab-ci.yml file' do
- before do
- allow(project.repository).to receive(:gitlab_ci_yml).and_return("script: ['test']")
- end
-
- it { is_expected.to eq(false) }
- end
-
- context 'when auto devops is explicitly enabled' do
- before do
- project.create_auto_devops!(enabled: true)
- end
-
- it { is_expected.to eq(false) }
- end
-
- context 'when auto devops is explicitly disabled' do
- before do
- project.create_auto_devops!(enabled: false)
- end
-
- it { is_expected.to eq(true) }
- end
-
- context 'when auto devops is set to instance setting' do
- before do
- project.create_auto_devops!(enabled: nil)
- end
-
- context 'when auto devops is enabled system-wide' do
- before do
- stub_application_setting(auto_devops_enabled: true)
- end
-
- it { is_expected.to eq(false) }
- end
-
- context 'when auto devops is disabled system-wide' do
- before do
- stub_application_setting(auto_devops_enabled: false)
- end
-
- it { is_expected.to eq(true) }
- end
- end
- end
end
diff --git a/spec/helpers/boards_helper_spec.rb b/spec/helpers/boards_helper_spec.rb
new file mode 100644
index 00000000000..a3c5ab99c87
--- /dev/null
+++ b/spec/helpers/boards_helper_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe BoardsHelper do
+ describe '#board_data' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:board) { create(:board, project: project) }
+
+ before do
+ assign(:board, board)
+ assign(:project, project)
+
+ allow(helper).to receive(:current_user) { user }
+ allow(helper).to receive(:can?).with(user, :admin_list, project).and_return(true)
+ end
+
+ it 'returns a board_lists_path as lists_endpoint' do
+ expect(helper.board_data[:lists_endpoint]).to eq(board_lists_path(board))
+ end
+ end
+end
diff --git a/spec/helpers/button_helper_spec.rb b/spec/helpers/button_helper_spec.rb
index d2c7867febb..fee8df10129 100644
--- a/spec/helpers/button_helper_spec.rb
+++ b/spec/helpers/button_helper_spec.rb
@@ -92,6 +92,34 @@ describe ButtonHelper do
end
end
+ describe 'ssh and http clone buttons' do
+ let(:user) { create(:user) }
+ let(:project) { build_stubbed(:project) }
+
+ def http_button_element
+ element = helper.http_clone_button(project, append_link: false)
+
+ Nokogiri::HTML::DocumentFragment.parse(element).first_element_child
+ end
+
+ def ssh_button_element
+ element = helper.ssh_clone_button(project, append_link: false)
+
+ Nokogiri::HTML::DocumentFragment.parse(element).first_element_child
+ end
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ it 'only shows the title of any of the clone buttons when append_link is false' do
+ expect(http_button_element.text).to eq('HTTP')
+ expect(http_button_element.search('.dropdown-menu-inner-content').first).to eq(nil)
+ expect(ssh_button_element.text).to eq('SSH')
+ expect(ssh_button_element.search('.dropdown-menu-inner-content').first).to eq(nil)
+ end
+ end
+
describe 'clipboard_button' do
let(:user) { create(:user) }
let(:project) { build_stubbed(:project) }
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index fd7900c32f4..3008528e60c 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -1,7 +1,9 @@
require 'spec_helper'
describe MergeRequestsHelper do
+ include ActionView::Helpers::UrlHelper
include ProjectForksHelper
+
describe 'ci_build_details_path' do
let(:project) { create(:project) }
let(:merge_request) { MergeRequest.new }
@@ -41,4 +43,19 @@ describe MergeRequestsHelper do
it { is_expected.to eq([source_title, target_title]) }
end
end
+
+ describe '#tab_link_for' do
+ let(:merge_request) { create(:merge_request, :simple) }
+ let(:options) { Hash.new }
+
+ subject { tab_link_for(merge_request, :show, options) { 'Discussion' } }
+
+ describe 'supports the :force_link option' do
+ let(:options) { { force_link: true } }
+
+ it 'removes the data-toggle attributes' do
+ is_expected.not_to match(/data-toggle="tab"/)
+ end
+ end
+ end
end
diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js
index 6d6e71cc215..f5be9ea0fb2 100644
--- a/spec/javascripts/clusters/clusters_bundle_spec.js
+++ b/spec/javascripts/clusters/clusters_bundle_spec.js
@@ -28,7 +28,7 @@ describe('Clusters', () => {
expect(
cluster.toggleButton.classList,
- ).not.toContain('checked');
+ ).not.toContain('is-checked');
expect(
cluster.toggleInput.getAttribute('value'),
diff --git a/spec/javascripts/clusters/clusters_index_spec.js b/spec/javascripts/clusters/clusters_index_spec.js
new file mode 100644
index 00000000000..0a8b63ed5b4
--- /dev/null
+++ b/spec/javascripts/clusters/clusters_index_spec.js
@@ -0,0 +1,58 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import setClusterTableToggles from '~/clusters/clusters_index';
+import { setTimeout } from 'core-js/library/web/timers';
+
+describe('Clusters table', () => {
+ preloadFixtures('clusters/index_cluster.html.raw');
+ let mock;
+
+ beforeEach(() => {
+ loadFixtures('clusters/index_cluster.html.raw');
+ mock = new MockAdapter(axios);
+ setClusterTableToggles();
+ });
+
+ describe('update cluster', () => {
+ it('renders loading state while request is made', () => {
+ const button = document.querySelector('.js-toggle-cluster-list');
+
+ button.click();
+
+ expect(button.classList).toContain('is-loading');
+ expect(button.getAttribute('disabled')).toEqual('true');
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('shows updated state after sucessfull request', (done) => {
+ mock.onPut().reply(200, {}, {});
+ const button = document.querySelector('.js-toggle-cluster-list');
+ button.click();
+
+ expect(button.classList).toContain('is-loading');
+
+ setTimeout(() => {
+ expect(button.classList).not.toContain('is-loading');
+ expect(button.classList).not.toContain('is-checked');
+ done();
+ }, 0);
+ });
+
+ it('shows inital state after failed request', (done) => {
+ mock.onPut().reply(500, {}, {});
+ const button = document.querySelector('.js-toggle-cluster-list');
+
+ button.click();
+ expect(button.classList).toContain('is-loading');
+
+ setTimeout(() => {
+ expect(button.classList).not.toContain('is-loading');
+ expect(button.classList).toContain('is-checked');
+ done();
+ }, 0);
+ });
+ });
+});
diff --git a/spec/javascripts/deploy_keys/components/action_btn_spec.js b/spec/javascripts/deploy_keys/components/action_btn_spec.js
index 5b93fbc5575..7025c3d836c 100644
--- a/spec/javascripts/deploy_keys/components/action_btn_spec.js
+++ b/spec/javascripts/deploy_keys/components/action_btn_spec.js
@@ -34,7 +34,7 @@ describe('Deploy keys action btn', () => {
setTimeout(() => {
expect(
eventHub.$emit,
- ).toHaveBeenCalledWith('enable.key', deployKey);
+ ).toHaveBeenCalledWith('enable.key', deployKey, jasmine.anything());
done();
});
diff --git a/spec/javascripts/deploy_keys/components/app_spec.js b/spec/javascripts/deploy_keys/components/app_spec.js
index 700897f50b0..0ca9290d3d2 100644
--- a/spec/javascripts/deploy_keys/components/app_spec.js
+++ b/spec/javascripts/deploy_keys/components/app_spec.js
@@ -139,4 +139,18 @@ describe('Deploy keys app component', () => {
it('hasKeys returns true when there are keys', () => {
expect(vm.hasKeys).toEqual(3);
});
+
+ it('resets remove button loading state', (done) => {
+ spyOn(window, 'confirm').and.returnValue(false);
+
+ const btn = vm.$el.querySelector('.btn-warning');
+
+ btn.click();
+
+ Vue.nextTick(() => {
+ expect(btn.querySelector('.fa')).toBeNull();
+
+ done();
+ });
+ });
});
diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb
index 8e74c4f859c..d26ea3febe8 100644
--- a/spec/javascripts/fixtures/clusters.rb
+++ b/spec/javascripts/fixtures/clusters.rb
@@ -31,4 +31,19 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
+
+ context 'rendering non-empty state' do
+ before do
+ cluster
+ end
+
+ it 'clusters/index_cluster.html.raw' do |example|
+ get :index,
+ namespace_id: namespace,
+ project_id: project
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+ end
end
diff --git a/spec/javascripts/image_diff/helpers/utils_helper_spec.js b/spec/javascripts/image_diff/helpers/utils_helper_spec.js
index 56d77a05c4c..31949c39d9c 100644
--- a/spec/javascripts/image_diff/helpers/utils_helper_spec.js
+++ b/spec/javascripts/image_diff/helpers/utils_helper_spec.js
@@ -157,27 +157,19 @@ describe('utilsHelper', () => {
beforeEach(() => {
window.gl = window.gl || (window.gl = {});
glCache = window.gl;
- window.gl.ImageFile = () => {};
fileEl = document.createElement('div');
fileEl.innerHTML = `
<div class="diff-file"></div>
`;
- spyOn(ImageDiff.prototype, 'init').and.callFake(() => {});
spyOn(ReplacedImageDiff.prototype, 'init').and.callFake(() => {});
+ spyOn(ImageDiff.prototype, 'init').and.callFake(() => {});
});
afterEach(() => {
window.gl = glCache;
});
- it('should initialize gl.ImageFile', () => {
- spyOn(window.gl, 'ImageFile');
-
- utilsHelper.initImageDiff(fileEl, false, false);
- expect(gl.ImageFile).toHaveBeenCalled();
- });
-
it('should initialize ImageDiff if js-single-image', () => {
const diffFileEl = fileEl.querySelector('.diff-file');
diffFileEl.innerHTML = `
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index b47a8bf705f..53b8a368d28 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -4,6 +4,7 @@ import '~/render_gfm';
import issuableApp from '~/issue_show/components/app.vue';
import eventHub from '~/issue_show/event_hub';
import issueShowData from '../mock_data';
+import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
function formatText(text) {
return text.trim().replace(/\s\s+/g, ' ');
@@ -55,6 +56,8 @@ describe('Issuable output', () => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
vm.poll.stop();
+
+ vm.$destroy();
});
it('should render a title/description/edited and update title/description/edited on update', (done) => {
@@ -268,6 +271,52 @@ describe('Issuable output', () => {
});
});
+ it('opens recaptcha dialog if update rejected as spam', (done) => {
+ function mockScriptSrc() {
+ const recaptchaChild = vm.$children
+ .find(child => child.$options._componentTag === 'recaptcha-dialog'); // eslint-disable-line no-underscore-dangle
+
+ recaptchaChild.scriptSrc = '//scriptsrc';
+ }
+
+ let modal;
+ const promise = new Promise((resolve) => {
+ resolve({
+ json() {
+ return {
+ recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>',
+ };
+ },
+ });
+ });
+
+ spyOn(vm.service, 'updateIssuable').and.returnValue(promise);
+
+ vm.canUpdate = true;
+ vm.showForm = true;
+
+ vm.$nextTick()
+ .then(() => mockScriptSrc())
+ .then(() => vm.updateIssuable())
+ .then(promise)
+ .then(() => setTimeoutPromise())
+ .then(() => {
+ modal = vm.$el.querySelector('.js-recaptcha-dialog');
+
+ expect(modal.style.display).not.toEqual('none');
+ expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html');
+ expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc');
+ })
+ .then(() => modal.querySelector('.close').click())
+ .then(() => vm.$nextTick())
+ .then(() => {
+ expect(modal.style.display).toEqual('none');
+ expect(document.body.querySelector('.js-recaptcha-script')).toBeNull();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
describe('deleteIssuable', () => {
it('changes URL when deleted', (done) => {
spyOn(gl.utils, 'visitUrl');
diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js
index 163e5cdd062..2e000a1063f 100644
--- a/spec/javascripts/issue_show/components/description_spec.js
+++ b/spec/javascripts/issue_show/components/description_spec.js
@@ -51,6 +51,35 @@ describe('Description component', () => {
});
});
+ it('opens recaptcha dialog if update rejected as spam', (done) => {
+ let modal;
+ const recaptchaChild = vm.$children
+ .find(child => child.$options._componentTag === 'recaptcha-dialog'); // eslint-disable-line no-underscore-dangle
+
+ recaptchaChild.scriptSrc = '//scriptsrc';
+
+ vm.taskListUpdateSuccess({
+ recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>',
+ });
+
+ vm.$nextTick()
+ .then(() => {
+ modal = vm.$el.querySelector('.js-recaptcha-dialog');
+
+ expect(modal.style.display).not.toEqual('none');
+ expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html');
+ expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc');
+ })
+ .then(() => modal.querySelector('.close').click())
+ .then(() => vm.$nextTick())
+ .then(() => {
+ expect(modal.style.display).toEqual('none');
+ expect(document.body.querySelector('.js-recaptcha-script')).toBeNull();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
describe('TaskList', () => {
beforeEach(() => {
vm = mountComponent(DescriptionComponent, Object.assign({}, props, {
@@ -86,6 +115,7 @@ describe('Description component', () => {
dataType: 'issuableType',
fieldName: 'description',
selector: '.detail-page-description',
+ onSuccess: jasmine.any(Function),
});
done();
});
diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js
index 6e89528a3ea..000b53af016 100644
--- a/spec/javascripts/issue_show/components/form_spec.js
+++ b/spec/javascripts/issue_show/components/form_spec.js
@@ -34,7 +34,6 @@ describe('Inline edit form component', () => {
});
it('renders template selector when templates exists', (done) => {
- spyOn(gl, 'IssuableTemplateSelectors');
vm.issuableTemplates = ['test'];
Vue.nextTick(() => {
diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js
index 5e67911d338..20c4caa865d 100644
--- a/spec/javascripts/job_spec.js
+++ b/spec/javascripts/job_spec.js
@@ -28,7 +28,7 @@ describe('Job', () => {
});
it('copies build options', function () {
- expect(this.job.pageUrl).toBe(JOB_URL);
+ expect(this.job.pagePath).toBe(JOB_URL);
expect(this.job.buildStatus).toBe('success');
expect(this.job.buildStage).toBe('test');
expect(this.job.state).toBe('');
diff --git a/spec/javascripts/lib/utils/datefix_spec.js b/spec/javascripts/lib/utils/datefix_spec.js
index e58ac4300ba..a9f3abcf2a4 100644
--- a/spec/javascripts/lib/utils/datefix_spec.js
+++ b/spec/javascripts/lib/utils/datefix_spec.js
@@ -21,7 +21,7 @@ describe('datefix', () => {
describe('pikadayToString', () => {
it('should format a UTC date into yyyy-mm-dd format', () => {
- expect(pikadayToString(new Date('2020-01-29'))).toEqual('2020-01-29');
+ expect(pikadayToString(new Date('2020-01-29:00:00'))).toEqual('2020-01-29');
});
});
});
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index 3ab901da6b6..70ae63ba036 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -63,7 +63,7 @@ import IssuablesHelper from '~/helpers/issuables_helper';
describe('merge request of another user', () => {
beforeEach(() => {
loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
- this.el = document.querySelector('.merge-request .issuable-actions');
+ this.el = document.querySelector('.js-issuable-actions');
const merge = new MergeRequest();
merge.hideCloseButton();
});
@@ -83,7 +83,7 @@ import IssuablesHelper from '~/helpers/issuables_helper';
describe('merge request of current_user', () => {
beforeEach(() => {
loadFixtures('merge_requests/merge_request_of_current_user.html.raw');
- this.el = document.querySelector('.merge-request .issuable-actions');
+ this.el = document.querySelector('.js-issuable-actions');
const merge = new MergeRequest();
merge.hideCloseButton();
});
diff --git a/spec/javascripts/monitoring/graph/deployment_spec.js b/spec/javascripts/monitoring/graph/deployment_spec.js
index dea42d755d4..bf6ada8185e 100644
--- a/spec/javascripts/monitoring/graph/deployment_spec.js
+++ b/spec/javascripts/monitoring/graph/deployment_spec.js
@@ -118,7 +118,7 @@ describe('MonitoringDeployment', () => {
).not.toEqual('display: none;');
});
- it('shows the refText inside a text element with the deploy-info-text class', () => {
+ it('contains date, refs and the "deployed" text', () => {
reducedDeploymentData[0].showDeploymentFlag = true;
const component = createComponent({
showDeployInfo: true,
@@ -129,8 +129,31 @@ describe('MonitoringDeployment', () => {
});
expect(
- component.$el.querySelector('.deploy-info-text').firstChild.nodeValue.trim(),
- ).toEqual(component.refText(reducedDeploymentData[0]));
+ component.$el.querySelectorAll('.deploy-info-text'),
+ ).toContainText('Deployed');
+
+ expect(
+ component.$el.querySelectorAll('.deploy-info-text'),
+ ).toContainText('Wed, May 31');
+
+ expect(
+ component.$el.querySelectorAll('.deploy-info-text'),
+ ).toContainText(component.refText(reducedDeploymentData[0]));
+ });
+
+ it('contains a link to the commit contents', () => {
+ reducedDeploymentData[0].showDeploymentFlag = true;
+ const component = createComponent({
+ showDeployInfo: true,
+ deploymentData: reducedDeploymentData,
+ graphHeight: 300,
+ graphWidth: 440,
+ graphHeightOffset: 120,
+ });
+
+ expect(
+ component.$el.querySelectorAll('.deploy-info-text-link')[0].parentElement.getAttribute('xlink:href'),
+ ).not.toEqual('');
});
it('should contain a hidden gradient', () => {
diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js
index fd79abe241a..b1d69752bad 100644
--- a/spec/javascripts/monitoring/graph_spec.js
+++ b/spec/javascripts/monitoring/graph_spec.js
@@ -4,6 +4,8 @@ import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins';
import eventHub from '~/monitoring/event_hub';
import { deploymentData, convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from './mock_data';
+const tagsPath = 'http://test.host/frontend-fixtures/environments-project/tags';
+const projectPath = 'http://test.host/frontend-fixtures/environments-project';
const createComponent = (propsData) => {
const Component = Vue.extend(Graph);
@@ -25,6 +27,8 @@ describe('Graph', () => {
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
+ tagsPath,
+ projectPath,
});
expect(component.$el.querySelector('.text-center').innerText.trim()).toBe(component.graphData.title);
@@ -37,6 +41,8 @@ describe('Graph', () => {
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
+ tagsPath,
+ projectPath,
});
const transformedHeight = `${component.graphHeight - 100}`;
@@ -50,6 +56,8 @@ describe('Graph', () => {
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
+ tagsPath,
+ projectPath,
});
const viewBoxArray = component.outerViewBox.split(' ');
@@ -65,6 +73,8 @@ describe('Graph', () => {
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
+ tagsPath,
+ projectPath,
});
spyOn(eventHub, '$emit');
@@ -81,6 +91,8 @@ describe('Graph', () => {
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
+ tagsPath,
+ projectPath,
});
expect(component.yAxisLabel).toEqual(component.graphData.y_label);
@@ -98,6 +110,8 @@ describe('Graph', () => {
hoveredDate: new Date('Sun Aug 27 2017 06:11:51 GMT-0500 (CDT)'),
currentDeployXPos: null,
},
+ tagsPath,
+ projectPath,
});
component.positionFlag();
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index 6b34855b8b2..1f4e858e731 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -2430,33 +2430,39 @@ export const deploymentData = [
id: 111,
iid: 3,
sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
+ commitUrl: 'http://test.host/frontend-fixtures/environments-project/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
ref: {
name: 'master'
},
created_at: '2017-05-31T21:23:37.881Z',
tag: false,
+ tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
'last?': true
},
{
id: 110,
iid: 2,
sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
+ commitUrl: 'http://test.host/frontend-fixtures/environments-project/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
ref: {
name: 'master'
},
created_at: '2017-05-30T20:08:04.629Z',
tag: false,
+ tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
'last?': false
},
{
id: 109,
iid: 1,
sha: '6511e58faafaa7ad2228990ec57f19d66f7db7c2',
+ commitUrl: 'http://test.host/frontend-fixtures/environments-project/commit/6511e58faafaa7ad2228990ec57f19d66f7db7c2',
ref: {
name: 'update2-readme'
},
created_at: '2017-05-30T17:42:38.409Z',
tag: false,
+ tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
'last?': false
}
];
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index a2394857b82..fdfc59a6f12 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -191,8 +191,6 @@ import '~/lib/utils/common_utils';
// browsers will not trigger default behavior (form submit, in this
// example) on JavaScript-created keypresses.
expect(submitSpy).not.toHaveBeenTriggered();
- // Does a worse job at capturing the intent of the test, but works.
- expect(enterKeyEvent.isDefaultPrevented()).toBe(true);
});
});
}).call(window);
diff --git a/spec/javascripts/vue_shared/components/popup_dialog_spec.js b/spec/javascripts/vue_shared/components/popup_dialog_spec.js
new file mode 100644
index 00000000000..5c1d2a196f4
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/popup_dialog_spec.js
@@ -0,0 +1,12 @@
+import Vue from 'vue';
+import PopupDialog from '~/vue_shared/components/popup_dialog.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('PopupDialog', () => {
+ it('does not render a primary button if no primaryButtonLabel', () => {
+ const popupDialog = Vue.extend(PopupDialog);
+ const vm = mountComponent(popupDialog);
+
+ expect(vm.$el.querySelector('.js-primary-button')).toBeNull();
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/toggle_button_spec.js b/spec/javascripts/vue_shared/components/toggle_button_spec.js
new file mode 100644
index 00000000000..447d74d4e08
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/toggle_button_spec.js
@@ -0,0 +1,91 @@
+import Vue from 'vue';
+import toggleButton from '~/vue_shared/components/toggle_button.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Toggle Button', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(toggleButton);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('render output', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ value: true,
+ name: 'foo',
+ });
+ });
+
+ it('renders input with provided name', () => {
+ expect(vm.$el.querySelector('input').getAttribute('name')).toEqual('foo');
+ });
+
+ it('renders input with provided value', () => {
+ expect(vm.$el.querySelector('input').getAttribute('value')).toEqual('true');
+ });
+
+ it('renders Enabled and Disabled text data attributes', () => {
+ expect(vm.$el.querySelector('button').getAttribute('data-enabled-text')).toEqual('Enabled');
+ expect(vm.$el.querySelector('button').getAttribute('data-disabled-text')).toEqual('Disabled');
+ });
+ });
+
+ describe('is-checked', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ value: true,
+ });
+
+ spyOn(vm, '$emit');
+ });
+
+ it('renders is checked class', () => {
+ expect(vm.$el.querySelector('button').classList.contains('is-checked')).toEqual(true);
+ });
+
+ it('emits change event when clicked', () => {
+ vm.$el.querySelector('button').click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('change', false);
+ });
+ });
+
+ describe('is-disabled', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ value: true,
+ disabledInput: true,
+ });
+ spyOn(vm, '$emit');
+ });
+
+ it('renders disabled button', () => {
+ expect(vm.$el.querySelector('button').classList.contains('is-disabled')).toEqual(true);
+ });
+
+ it('does not emit change event when clicked', () => {
+ vm.$el.querySelector('button').click();
+
+ expect(vm.$emit).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('is-loading', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ value: true,
+ isLoading: true,
+ });
+ });
+
+ it('renders loading class', () => {
+ expect(vm.$el.querySelector('button').classList.contains('is-loading')).toEqual(true);
+ });
+ });
+});
diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb
index b68301a066a..b68301a066a 100644
--- a/spec/lib/gitlab/backup/manager_spec.rb
+++ b/spec/lib/backup/manager_spec.rb
diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb
new file mode 100644
index 00000000000..6ee3d531d6e
--- /dev/null
+++ b/spec/lib/backup/repository_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe Backup::Repository do
+ let(:progress) { StringIO.new }
+ let!(:project) { create(:project) }
+
+ before do
+ allow(progress).to receive(:puts)
+ allow(progress).to receive(:print)
+
+ allow_any_instance_of(String).to receive(:color) do |string, _color|
+ string
+ end
+
+ allow_any_instance_of(described_class).to receive(:progress).and_return(progress)
+ end
+
+ describe '#dump' do
+ describe 'repo failure' do
+ before do
+ allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0])
+ end
+
+ it 'does not raise error' do
+ expect { described_class.new.dump }.not_to raise_error
+ end
+ end
+ end
+
+ describe '#restore' do
+ describe 'command failure' do
+ before do
+ allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1])
+ end
+
+ it 'shows the appropriate error' do
+ described_class.new.restore
+
+ expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error")
+ end
+ end
+ end
+
+ describe '#empty_repo?' do
+ context 'for a wiki' do
+ let(:wiki) { create(:project_wiki) }
+
+ it 'invalidates the emptiness cache' do
+ expect(wiki.repository).to receive(:expire_emptiness_caches).once
+
+ wiki.empty?
+ end
+
+ context 'wiki repo has content' do
+ let!(:wiki_page) { create(:wiki_page, wiki: wiki) }
+
+ it 'returns true, regardless of bad cache value' do
+ expect(described_class.new.send(:empty_repo?, wiki)).to be(false)
+ end
+ end
+
+ context 'wiki repo does not have content' do
+ it 'returns true, regardless of bad cache value' do
+ expect(described_class.new.send(:empty_repo?, wiki)).to be_truthy
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/cross_project_reference_spec.rb b/spec/lib/banzai/cross_project_reference_spec.rb
index d70749536b8..68ca960caab 100644
--- a/spec/lib/banzai/cross_project_reference_spec.rb
+++ b/spec/lib/banzai/cross_project_reference_spec.rb
@@ -3,20 +3,20 @@ require 'spec_helper'
describe Banzai::CrossProjectReference do
include described_class
- describe '#project_from_ref' do
+ describe '#parent_from_ref' do
context 'when no project was referenced' do
it 'returns the project from context' do
project = double
allow(self).to receive(:context).and_return({ project: project })
- expect(project_from_ref(nil)).to eq project
+ expect(parent_from_ref(nil)).to eq project
end
end
context 'when referenced project does not exist' do
it 'returns nil' do
- expect(project_from_ref('invalid/reference')).to be_nil
+ expect(parent_from_ref('invalid/reference')).to be_nil
end
end
@@ -27,7 +27,7 @@ describe Banzai::CrossProjectReference do
expect(Project).to receive(:find_by_full_path)
.with('cross/reference').and_return(project2)
- expect(project_from_ref('cross/reference')).to eq project2
+ expect(parent_from_ref('cross/reference')).to eq project2
end
end
end
diff --git a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb
index 7c0ba9ee67f..1e82d18d056 100644
--- a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb
@@ -3,67 +3,67 @@ require 'spec_helper'
describe Banzai::Filter::AbstractReferenceFilter do
let(:project) { create(:project) }
- describe '#references_per_project' do
- it 'returns a Hash containing references grouped per project paths' do
+ describe '#references_per_parent' do
+ it 'returns a Hash containing references grouped per parent paths' do
doc = Nokogiri::HTML.fragment("#1 #{project.full_path}#2")
filter = described_class.new(doc, project: project)
expect(filter).to receive(:object_class).exactly(4).times.and_return(Issue)
expect(filter).to receive(:object_sym).twice.and_return(:issue)
- refs = filter.references_per_project
+ refs = filter.references_per_parent
expect(refs).to be_an_instance_of(Hash)
expect(refs[project.full_path]).to eq(Set.new(%w[1 2]))
end
end
- describe '#projects_per_reference' do
- it 'returns a Hash containing projects grouped per project paths' do
+ describe '#parent_per_reference' do
+ it 'returns a Hash containing projects grouped per parent paths' do
doc = Nokogiri::HTML.fragment('')
filter = described_class.new(doc, project: project)
- expect(filter).to receive(:references_per_project)
+ expect(filter).to receive(:references_per_parent)
.and_return({ project.full_path => Set.new(%w[1]) })
- expect(filter.projects_per_reference)
+ expect(filter.parent_per_reference)
.to eq({ project.full_path => project })
end
end
- describe '#find_projects_for_paths' do
+ describe '#find_for_paths' do
let(:doc) { Nokogiri::HTML.fragment('') }
let(:filter) { described_class.new(doc, project: project) }
context 'with RequestStore disabled' do
it 'returns a list of Projects for a list of paths' do
- expect(filter.find_projects_for_paths([project.full_path]))
+ expect(filter.find_for_paths([project.full_path]))
.to eq([project])
end
it "return an empty array for paths that don't exist" do
- expect(filter.find_projects_for_paths(['nonexistent/project']))
+ expect(filter.find_for_paths(['nonexistent/project']))
.to eq([])
end
end
context 'with RequestStore enabled', :request_store do
it 'returns a list of Projects for a list of paths' do
- expect(filter.find_projects_for_paths([project.full_path]))
+ expect(filter.find_for_paths([project.full_path]))
.to eq([project])
end
context "when no project with that path exists" do
it "returns no value" do
- expect(filter.find_projects_for_paths(['nonexistent/project']))
+ expect(filter.find_for_paths(['nonexistent/project']))
.to eq([])
end
it "adds the ref to the project refs cache" do
project_refs_cache = {}
- allow(filter).to receive(:project_refs_cache).and_return(project_refs_cache)
+ allow(filter).to receive(:refs_cache).and_return(project_refs_cache)
- filter.find_projects_for_paths(['nonexistent/project'])
+ filter.find_for_paths(['nonexistent/project'])
expect(project_refs_cache).to eq({ 'nonexistent/project' => nil })
end
@@ -71,11 +71,11 @@ describe Banzai::Filter::AbstractReferenceFilter do
context 'when the project refs cache includes nil values' do
before do
# adds { 'nonexistent/project' => nil } to cache
- filter.project_from_ref_cached('nonexistent/project')
+ filter.from_ref_cached('nonexistent/project')
end
it "return an empty array for paths that don't exist" do
- expect(filter.find_projects_for_paths(['nonexistent/project']))
+ expect(filter.find_for_paths(['nonexistent/project']))
.to eq([])
end
end
@@ -83,12 +83,12 @@ describe Banzai::Filter::AbstractReferenceFilter do
end
end
- describe '#current_project_path' do
- it 'returns the path of the current project' do
+ describe '#current_parent_path' do
+ it 'returns the path of the current parent' do
doc = Nokogiri::HTML.fragment('')
filter = described_class.new(doc, project: project)
- expect(filter.current_project_path).to eq(project.full_path)
+ expect(filter.current_parent_path).to eq(project.full_path)
end
end
end
diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
index 702fcac0c6f..080a5f57da9 100644
--- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
@@ -92,6 +92,18 @@ describe Banzai::Filter::CommitReferenceFilter do
expect(link).not_to match %r(https?://)
expect(link).to eq urls.project_commit_url(project, reference, only_path: true)
end
+
+ context "in merge request context" do
+ let(:noteable) { create(:merge_request, target_project: project, source_project: project) }
+ let(:commit) { noteable.commits.first }
+
+ it 'handles merge request contextual commit references' do
+ url = urls.diffs_project_merge_request_url(project, noteable, commit_id: commit.id)
+ doc = reference_filter("See #{reference}", noteable: noteable)
+
+ expect(doc.css('a').first[:href]).to eq(url)
+ end
+ end
end
context 'cross-project / cross-namespace complete reference' do
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index f70c69ef588..3a5f52ea23f 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -157,6 +157,12 @@ describe Banzai::Filter::IssueReferenceFilter do
expect(doc.text).to eq("Fixed (#{project2.full_path}##{issue.iid}.)")
end
+ it 'includes default classes' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip'
+ end
+
it 'ignores invalid issue IDs on the referenced project' do
exp = act = "Fixed #{invalidate_reference(reference)}"
@@ -201,6 +207,12 @@ describe Banzai::Filter::IssueReferenceFilter do
expect(doc.text).to eq("Fixed (#{project2.path}##{issue.iid}.)")
end
+ it 'includes default classes' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip'
+ end
+
it 'ignores invalid issue IDs on the referenced project' do
exp = act = "Fixed #{invalidate_reference(reference)}"
@@ -245,6 +257,12 @@ describe Banzai::Filter::IssueReferenceFilter do
expect(doc.text).to eq("Fixed (#{project2.path}##{issue.iid}.)")
end
+ it 'includes default classes' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip'
+ end
+
it 'ignores invalid issue IDs on the referenced project' do
exp = act = "Fixed #{invalidate_reference(reference)}"
@@ -269,8 +287,15 @@ describe Banzai::Filter::IssueReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Fixed (#{reference}.)")
+
expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/)
end
+
+ it 'includes default classes' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip'
+ end
end
context 'cross-project reference in link href' do
@@ -291,8 +316,15 @@ describe Banzai::Filter::IssueReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Fixed (#{reference_link}.)")
+
expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
end
+
+ it 'includes default classes' do
+ doc = reference_filter("Fixed (#{reference_link}.)")
+
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip'
+ end
end
context 'cross-project URL in link href' do
@@ -313,8 +345,15 @@ describe Banzai::Filter::IssueReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Fixed (#{reference_link}.)")
+
expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
end
+
+ it 'includes default classes' do
+ doc = reference_filter("Fixed (#{reference_link}.)")
+
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip'
+ end
end
context 'group context' do
@@ -387,19 +426,19 @@ describe Banzai::Filter::IssueReferenceFilter do
end
end
- describe '#issues_per_project' do
+ describe '#records_per_parent' do
context 'using an internal issue tracker' do
it 'returns a Hash containing the issues per project' do
doc = Nokogiri::HTML.fragment('')
filter = described_class.new(doc, project: project)
- expect(filter).to receive(:projects_per_reference)
+ expect(filter).to receive(:parent_per_reference)
.and_return({ project.full_path => project })
- expect(filter).to receive(:references_per_project)
+ expect(filter).to receive(:references_per_parent)
.and_return({ project.full_path => Set.new([issue.iid]) })
- expect(filter.issues_per_project)
+ expect(filter.records_per_parent)
.to eq({ project => { issue.iid => issue } })
end
end
diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
index 85eddde732e..0cfef4ff5bf 100644
--- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
+++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
@@ -65,6 +65,13 @@ describe Banzai::Filter::TableOfContentsFilter do
expect(doc.css('h2 a').first.attr('href')).to eq '#one-1'
end
+ it 'prepends a prefix to digits-only ids' do
+ doc = filter(header(1, "123") + header(2, "1.0"))
+
+ expect(doc.css('h1 a').first.attr('href')).to eq '#anchor-123'
+ expect(doc.css('h2 a').first.attr('href')).to eq '#anchor-10'
+ end
+
it 'supports Unicode' do
doc = filter(header(1, '한글'))
expect(doc.css('h1 a').first.attr('id')).to eq 'user-content-한글'
diff --git a/spec/lib/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb
index 60a88e903ef..76bc0c36ab7 100644
--- a/spec/lib/banzai/filter/upload_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/upload_link_filter_spec.rb
@@ -89,7 +89,35 @@ describe Banzai::Filter::UploadLinkFilter do
end
end
- context 'when project context does not exist' do
+ context 'in group context' do
+ let(:upload_link) { link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg') }
+ let(:group) { create(:group) }
+ let(:filter_context) { { project: nil, group: group } }
+ let(:relative_path) { "groups/#{group.full_path}/-/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" }
+
+ it 'rewrites the link correctly' do
+ doc = raw_filter(upload_link, filter_context)
+
+ expect(doc.at_css('a')['href']).to eq("#{Gitlab.config.gitlab.url}/#{relative_path}")
+ end
+
+ it 'rewrites the link correctly for subgroup' do
+ subgroup = create(:group, parent: group)
+ relative_path = "groups/#{subgroup.full_path}/-/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg"
+
+ doc = raw_filter(upload_link, { project: nil, group: subgroup })
+
+ expect(doc.at_css('a')['href']).to eq("#{Gitlab.config.gitlab.url}/#{relative_path}")
+ end
+
+ it 'does not modify absolute URL' do
+ doc = filter(link('http://example.com'), filter_context)
+
+ expect(doc.at_css('a')['href']).to eq 'http://example.com'
+ end
+ end
+
+ context 'when project or group context does not exist' do
let(:upload_link) { link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg') }
it 'does not raise error' do
diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
index 23dbe2b6238..4cef3bdb24b 100644
--- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb
+++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb
@@ -70,12 +70,12 @@ describe Banzai::ReferenceParser::IssueParser do
end
end
- describe '#issues_for_nodes' do
+ describe '#records_for_nodes' do
it 'returns a Hash containing the issues for a list of nodes' do
link['data-issue'] = issue.id.to_s
nodes = [link]
- expect(subject.issues_for_nodes(nodes)).to eq({ link => issue })
+ expect(subject.records_for_nodes(nodes)).to eq({ link => issue })
end
end
end
diff --git a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb
new file mode 100644
index 00000000000..b80df6956b0
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb
@@ -0,0 +1,510 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq do
+ include TrackUntrackedUploadsHelpers
+
+ subject { described_class.new }
+
+ let!(:untracked_files_for_uploads) { described_class::UntrackedFile }
+ let!(:uploads) { described_class::Upload }
+
+ before do
+ DatabaseCleaner.clean
+ drop_temp_table_if_exists
+ ensure_temporary_tracking_table_exists
+ uploads.delete_all
+ end
+
+ after(:all) do
+ drop_temp_table_if_exists
+ end
+
+ context 'with untracked files and tracked files in untracked_files_for_uploads' do
+ let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) }
+ let!(:user1) { create(:user, :with_avatar) }
+ let!(:user2) { create(:user, :with_avatar) }
+ let!(:project1) { create(:project, :with_avatar) }
+ let!(:project2) { create(:project, :with_avatar) }
+
+ before do
+ UploadService.new(project1, uploaded_file, FileUploader).execute # Markdown upload
+ UploadService.new(project2, uploaded_file, FileUploader).execute # Markdown upload
+
+ # File records created by PrepareUntrackedUploads
+ untracked_files_for_uploads.create!(path: appearance.uploads.first.path)
+ untracked_files_for_uploads.create!(path: appearance.uploads.last.path)
+ untracked_files_for_uploads.create!(path: user1.uploads.first.path)
+ untracked_files_for_uploads.create!(path: user2.uploads.first.path)
+ untracked_files_for_uploads.create!(path: project1.uploads.first.path)
+ untracked_files_for_uploads.create!(path: project2.uploads.first.path)
+ untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project1.full_path}/#{project1.uploads.last.path}")
+ untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/#{project2.uploads.last.path}")
+
+ # Untrack 4 files
+ user2.uploads.delete_all
+ project2.uploads.delete_all # 2 files: avatar and a Markdown upload
+ appearance.uploads.where("path like '%header_logo%'").delete_all
+ end
+
+ it 'adds untracked files to the uploads table' do
+ expect do
+ subject.perform(1, untracked_files_for_uploads.last.id)
+ end.to change { uploads.count }.from(4).to(8)
+
+ expect(user2.uploads.count).to eq(1)
+ expect(project2.uploads.count).to eq(2)
+ expect(appearance.uploads.count).to eq(2)
+ end
+
+ it 'deletes rows after processing them' do
+ expect(subject).to receive(:drop_temp_table_if_finished) # Don't drop the table so we can look at it
+
+ expect do
+ subject.perform(1, untracked_files_for_uploads.last.id)
+ end.to change { untracked_files_for_uploads.count }.from(8).to(0)
+ end
+
+ it 'does not create duplicate uploads of already tracked files' do
+ subject.perform(1, untracked_files_for_uploads.last.id)
+
+ expect(user1.uploads.count).to eq(1)
+ expect(project1.uploads.count).to eq(2)
+ expect(appearance.uploads.count).to eq(2)
+ end
+
+ it 'uses the start and end batch ids [only 1st half]' do
+ ids = untracked_files_for_uploads.all.order(:id).pluck(:id)
+ start_id = ids[0]
+ end_id = ids[3]
+
+ expect do
+ subject.perform(start_id, end_id)
+ end.to change { uploads.count }.from(4).to(6)
+
+ expect(user1.uploads.count).to eq(1)
+ expect(user2.uploads.count).to eq(1)
+ expect(appearance.uploads.count).to eq(2)
+ expect(project1.uploads.count).to eq(2)
+ expect(project2.uploads.count).to eq(0)
+
+ # Only 4 have been either confirmed or added to uploads
+ expect(untracked_files_for_uploads.count).to eq(4)
+ end
+
+ it 'uses the start and end batch ids [only 2nd half]' do
+ ids = untracked_files_for_uploads.all.order(:id).pluck(:id)
+ start_id = ids[4]
+ end_id = ids[7]
+
+ expect do
+ subject.perform(start_id, end_id)
+ end.to change { uploads.count }.from(4).to(6)
+
+ expect(user1.uploads.count).to eq(1)
+ expect(user2.uploads.count).to eq(0)
+ expect(appearance.uploads.count).to eq(1)
+ expect(project1.uploads.count).to eq(2)
+ expect(project2.uploads.count).to eq(2)
+
+ # Only 4 have been either confirmed or added to uploads
+ expect(untracked_files_for_uploads.count).to eq(4)
+ end
+
+ it 'does not drop the temporary tracking table after processing the batch, if there are still untracked rows' do
+ subject.perform(1, untracked_files_for_uploads.last.id - 1)
+
+ expect(ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads)).to be_truthy
+ end
+
+ it 'drops the temporary tracking table after processing the batch, if there are no untracked rows left' do
+ subject.perform(1, untracked_files_for_uploads.last.id)
+
+ expect(ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads)).to be_falsey
+ end
+
+ it 'does not block a whole batch because of one bad path' do
+ untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/._7d37bf4c747916390e596744117d5d1a")
+ expect(untracked_files_for_uploads.count).to eq(9)
+ expect(uploads.count).to eq(4)
+
+ subject.perform(1, untracked_files_for_uploads.last.id)
+
+ expect(untracked_files_for_uploads.count).to eq(1)
+ expect(uploads.count).to eq(8)
+ end
+
+ it 'an unparseable path is shown in error output' do
+ bad_path = "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/._7d37bf4c747916390e596744117d5d1a"
+ untracked_files_for_uploads.create!(path: bad_path)
+
+ expect(Rails.logger).to receive(:error).with(/Error parsing path "#{bad_path}":/)
+
+ subject.perform(1, untracked_files_for_uploads.last.id)
+ end
+ end
+
+ context 'with no untracked files' do
+ it 'does not add to the uploads table (and does not raise error)' do
+ expect do
+ subject.perform(1, 1000)
+ end.not_to change { uploads.count }.from(0)
+ end
+ end
+
+ describe 'upload outcomes for each path pattern' do
+ shared_examples_for 'non_markdown_file' do
+ let!(:expected_upload_attrs) { model.uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') }
+ let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) }
+
+ before do
+ model.uploads.delete_all
+ end
+
+ it 'creates an Upload record' do
+ expect do
+ subject.perform(1, untracked_files_for_uploads.last.id)
+ end.to change { model.reload.uploads.count }.from(0).to(1)
+
+ expect(model.uploads.first.attributes).to include(expected_upload_attrs)
+ end
+ end
+
+ context 'for an appearance logo file path' do
+ let(:model) { create_or_update_appearance(logo: uploaded_file) }
+
+ it_behaves_like 'non_markdown_file'
+ end
+
+ context 'for an appearance header_logo file path' do
+ let(:model) { create_or_update_appearance(header_logo: uploaded_file) }
+
+ it_behaves_like 'non_markdown_file'
+ end
+
+ context 'for a pre-Markdown Note attachment file path' do
+ class Note < ActiveRecord::Base
+ has_many :uploads, as: :model, dependent: :destroy
+ end
+
+ let(:model) { create(:note, :with_attachment) }
+
+ it_behaves_like 'non_markdown_file'
+ end
+
+ context 'for a user avatar file path' do
+ let(:model) { create(:user, :with_avatar) }
+
+ it_behaves_like 'non_markdown_file'
+ end
+
+ context 'for a group avatar file path' do
+ let(:model) { create(:group, :with_avatar) }
+
+ it_behaves_like 'non_markdown_file'
+ end
+
+ context 'for a project avatar file path' do
+ let(:model) { create(:project, :with_avatar) }
+
+ it_behaves_like 'non_markdown_file'
+ end
+
+ context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
+ let(:model) { create(:project) }
+
+ before do
+ # Upload the file
+ UploadService.new(model, uploaded_file, FileUploader).execute
+
+ # Create the untracked_files_for_uploads record
+ untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{model.full_path}/#{model.uploads.first.path}")
+
+ # Save the expected upload attributes
+ @expected_upload_attrs = model.reload.uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum')
+
+ # Untrack the file
+ model.reload.uploads.delete_all
+ end
+
+ it 'creates an Upload record' do
+ expect do
+ subject.perform(1, untracked_files_for_uploads.last.id)
+ end.to change { model.reload.uploads.count }.from(0).to(1)
+
+ expect(model.uploads.first.attributes).to include(@expected_upload_attrs)
+ end
+ end
+ end
+end
+
+describe Gitlab::BackgroundMigration::PopulateUntrackedUploads::UntrackedFile do
+ include TrackUntrackedUploadsHelpers
+
+ let(:upload_class) { Gitlab::BackgroundMigration::PopulateUntrackedUploads::Upload }
+
+ before(:all) do
+ ensure_temporary_tracking_table_exists
+ end
+
+ after(:all) do
+ drop_temp_table_if_exists
+ end
+
+ describe '#upload_path' do
+ def assert_upload_path(file_path, expected_upload_path)
+ untracked_file = create_untracked_file(file_path)
+
+ expect(untracked_file.upload_path).to eq(expected_upload_path)
+ end
+
+ context 'for an appearance logo file path' do
+ it 'returns the file path relative to the CarrierWave root' do
+ assert_upload_path('/-/system/appearance/logo/1/some_logo.jpg', 'uploads/-/system/appearance/logo/1/some_logo.jpg')
+ end
+ end
+
+ context 'for an appearance header_logo file path' do
+ it 'returns the file path relative to the CarrierWave root' do
+ assert_upload_path('/-/system/appearance/header_logo/1/some_logo.jpg', 'uploads/-/system/appearance/header_logo/1/some_logo.jpg')
+ end
+ end
+
+ context 'for a pre-Markdown Note attachment file path' do
+ it 'returns the file path relative to the CarrierWave root' do
+ assert_upload_path('/-/system/note/attachment/1234/some_attachment.pdf', 'uploads/-/system/note/attachment/1234/some_attachment.pdf')
+ end
+ end
+
+ context 'for a user avatar file path' do
+ it 'returns the file path relative to the CarrierWave root' do
+ assert_upload_path('/-/system/user/avatar/1234/avatar.jpg', 'uploads/-/system/user/avatar/1234/avatar.jpg')
+ end
+ end
+
+ context 'for a group avatar file path' do
+ it 'returns the file path relative to the CarrierWave root' do
+ assert_upload_path('/-/system/group/avatar/1234/avatar.jpg', 'uploads/-/system/group/avatar/1234/avatar.jpg')
+ end
+ end
+
+ context 'for a project avatar file path' do
+ it 'returns the file path relative to the CarrierWave root' do
+ assert_upload_path('/-/system/project/avatar/1234/avatar.jpg', 'uploads/-/system/project/avatar/1234/avatar.jpg')
+ end
+ end
+
+ context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
+ it 'returns the file path relative to the project directory in uploads' do
+ project = create(:project)
+ random_hex = SecureRandom.hex
+
+ assert_upload_path("/#{project.full_path}/#{random_hex}/Some file.jpg", "#{random_hex}/Some file.jpg")
+ end
+ end
+ end
+
+ describe '#uploader' do
+ def assert_uploader(file_path, expected_uploader)
+ untracked_file = create_untracked_file(file_path)
+
+ expect(untracked_file.uploader).to eq(expected_uploader)
+ end
+
+ context 'for an appearance logo file path' do
+ it 'returns AttachmentUploader as a string' do
+ assert_uploader('/-/system/appearance/logo/1/some_logo.jpg', 'AttachmentUploader')
+ end
+ end
+
+ context 'for an appearance header_logo file path' do
+ it 'returns AttachmentUploader as a string' do
+ assert_uploader('/-/system/appearance/header_logo/1/some_logo.jpg', 'AttachmentUploader')
+ end
+ end
+
+ context 'for a pre-Markdown Note attachment file path' do
+ it 'returns AttachmentUploader as a string' do
+ assert_uploader('/-/system/note/attachment/1234/some_attachment.pdf', 'AttachmentUploader')
+ end
+ end
+
+ context 'for a user avatar file path' do
+ it 'returns AvatarUploader as a string' do
+ assert_uploader('/-/system/user/avatar/1234/avatar.jpg', 'AvatarUploader')
+ end
+ end
+
+ context 'for a group avatar file path' do
+ it 'returns AvatarUploader as a string' do
+ assert_uploader('/-/system/group/avatar/1234/avatar.jpg', 'AvatarUploader')
+ end
+ end
+
+ context 'for a project avatar file path' do
+ it 'returns AvatarUploader as a string' do
+ assert_uploader('/-/system/project/avatar/1234/avatar.jpg', 'AvatarUploader')
+ end
+ end
+
+ context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
+ it 'returns FileUploader as a string' do
+ project = create(:project)
+
+ assert_uploader("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", 'FileUploader')
+ end
+ end
+ end
+
+ describe '#model_type' do
+ def assert_model_type(file_path, expected_model_type)
+ untracked_file = create_untracked_file(file_path)
+
+ expect(untracked_file.model_type).to eq(expected_model_type)
+ end
+
+ context 'for an appearance logo file path' do
+ it 'returns Appearance as a string' do
+ assert_model_type('/-/system/appearance/logo/1/some_logo.jpg', 'Appearance')
+ end
+ end
+
+ context 'for an appearance header_logo file path' do
+ it 'returns Appearance as a string' do
+ assert_model_type('/-/system/appearance/header_logo/1/some_logo.jpg', 'Appearance')
+ end
+ end
+
+ context 'for a pre-Markdown Note attachment file path' do
+ it 'returns Note as a string' do
+ assert_model_type('/-/system/note/attachment/1234/some_attachment.pdf', 'Note')
+ end
+ end
+
+ context 'for a user avatar file path' do
+ it 'returns User as a string' do
+ assert_model_type('/-/system/user/avatar/1234/avatar.jpg', 'User')
+ end
+ end
+
+ context 'for a group avatar file path' do
+ it 'returns Namespace as a string' do
+ assert_model_type('/-/system/group/avatar/1234/avatar.jpg', 'Namespace')
+ end
+ end
+
+ context 'for a project avatar file path' do
+ it 'returns Project as a string' do
+ assert_model_type('/-/system/project/avatar/1234/avatar.jpg', 'Project')
+ end
+ end
+
+ context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
+ it 'returns Project as a string' do
+ project = create(:project)
+
+ assert_model_type("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", 'Project')
+ end
+ end
+ end
+
+ describe '#model_id' do
+ def assert_model_id(file_path, expected_model_id)
+ untracked_file = create_untracked_file(file_path)
+
+ expect(untracked_file.model_id).to eq(expected_model_id)
+ end
+
+ context 'for an appearance logo file path' do
+ it 'returns the ID as a string' do
+ assert_model_id('/-/system/appearance/logo/1/some_logo.jpg', 1)
+ end
+ end
+
+ context 'for an appearance header_logo file path' do
+ it 'returns the ID as a string' do
+ assert_model_id('/-/system/appearance/header_logo/1/some_logo.jpg', 1)
+ end
+ end
+
+ context 'for a pre-Markdown Note attachment file path' do
+ it 'returns the ID as a string' do
+ assert_model_id('/-/system/note/attachment/1234/some_attachment.pdf', 1234)
+ end
+ end
+
+ context 'for a user avatar file path' do
+ it 'returns the ID as a string' do
+ assert_model_id('/-/system/user/avatar/1234/avatar.jpg', 1234)
+ end
+ end
+
+ context 'for a group avatar file path' do
+ it 'returns the ID as a string' do
+ assert_model_id('/-/system/group/avatar/1234/avatar.jpg', 1234)
+ end
+ end
+
+ context 'for a project avatar file path' do
+ it 'returns the ID as a string' do
+ assert_model_id('/-/system/project/avatar/1234/avatar.jpg', 1234)
+ end
+ end
+
+ context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
+ it 'returns the ID as a string' do
+ project = create(:project)
+
+ assert_model_id("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", project.id)
+ end
+ end
+ end
+
+ describe '#file_size' do
+ context 'for an appearance logo file path' do
+ let(:appearance) { create_or_update_appearance(logo: uploaded_file) }
+ let(:untracked_file) { described_class.create!(path: appearance.uploads.first.path) }
+
+ it 'returns the file size' do
+ expect(untracked_file.file_size).to eq(35255)
+ end
+
+ it 'returns the same thing that CarrierWave would return' do
+ expect(untracked_file.file_size).to eq(appearance.logo.size)
+ end
+ end
+
+ context 'for a project avatar file path' do
+ let(:project) { create(:project, avatar: uploaded_file) }
+ let(:untracked_file) { described_class.create!(path: project.uploads.first.path) }
+
+ it 'returns the file size' do
+ expect(untracked_file.file_size).to eq(35255)
+ end
+
+ it 'returns the same thing that CarrierWave would return' do
+ expect(untracked_file.file_size).to eq(project.avatar.size)
+ end
+ end
+
+ context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do
+ let(:project) { create(:project) }
+ let(:untracked_file) { create_untracked_file("/#{project.full_path}/#{project.uploads.first.path}") }
+
+ before do
+ UploadService.new(project, uploaded_file, FileUploader).execute
+ end
+
+ it 'returns the file size' do
+ expect(untracked_file.file_size).to eq(35255)
+ end
+
+ it 'returns the same thing that CarrierWave would return' do
+ expect(untracked_file.file_size).to eq(project.uploads.first.size)
+ end
+ end
+ end
+
+ def create_untracked_file(path_relative_to_upload_dir)
+ described_class.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}#{path_relative_to_upload_dir}")
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb
new file mode 100644
index 00000000000..cd3f1a45270
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb
@@ -0,0 +1,242 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do
+ include TrackUntrackedUploadsHelpers
+
+ let!(:untracked_files_for_uploads) { described_class::UntrackedFile }
+
+ matcher :be_scheduled_migration do |*expected|
+ match do |migration|
+ BackgroundMigrationWorker.jobs.any? do |job|
+ job['args'] == [migration, expected]
+ end
+ end
+
+ failure_message do |migration|
+ "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!"
+ end
+ end
+
+ before do
+ DatabaseCleaner.clean
+
+ drop_temp_table_if_exists
+ end
+
+ after do
+ drop_temp_table_if_exists
+ end
+
+ around do |example|
+ # Especially important so the follow-up migration does not get run
+ Sidekiq::Testing.fake! do
+ example.run
+ end
+ end
+
+ it 'ensures the untracked_files_for_uploads table exists' do
+ expect do
+ described_class.new.perform
+ end.to change { ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads) }.from(false).to(true)
+ end
+
+ it 'has a path field long enough for really long paths' do
+ described_class.new.perform
+
+ component = 'a' * 255
+
+ long_path = [
+ 'uploads',
+ component, # project.full_path
+ component # filename
+ ].flatten.join('/')
+
+ record = untracked_files_for_uploads.create!(path: long_path)
+ expect(record.reload.path.size).to eq(519)
+ end
+
+ context "test bulk insert with ON CONFLICT DO NOTHING or IGNORE" do
+ around do |example|
+ # If this is CI, we use Postgres 9.2 so this whole context should be
+ # skipped since we're unable to use ON CONFLICT DO NOTHING or IGNORE.
+ if described_class.new.send(:can_bulk_insert_and_ignore_duplicates?)
+ example.run
+ end
+ end
+
+ context 'when files were uploaded before and after hashed storage was enabled' do
+ let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) }
+ let!(:user) { create(:user, :with_avatar) }
+ let!(:project1) { create(:project, :with_avatar) }
+ let(:project2) { create(:project) } # instantiate after enabling hashed_storage
+
+ before do
+ # Markdown upload before enabling hashed_storage
+ UploadService.new(project1, uploaded_file, FileUploader).execute
+
+ stub_application_setting(hashed_storage_enabled: true)
+
+ # Markdown upload after enabling hashed_storage
+ UploadService.new(project2, uploaded_file, FileUploader).execute
+ end
+
+ it 'adds unhashed files to the untracked_files_for_uploads table' do
+ described_class.new.perform
+
+ expect(untracked_files_for_uploads.count).to eq(5)
+ end
+
+ it 'adds files with paths relative to CarrierWave.root' do
+ described_class.new.perform
+ untracked_files_for_uploads.all.each do |file|
+ expect(file.path.start_with?('uploads/')).to be_truthy
+ end
+ end
+
+ it 'does not add hashed files to the untracked_files_for_uploads table' do
+ described_class.new.perform
+
+ hashed_file_path = project2.uploads.where(uploader: 'FileUploader').first.path
+ expect(untracked_files_for_uploads.where("path like '%#{hashed_file_path}%'").exists?).to be_falsey
+ end
+
+ it 'correctly schedules the follow-up background migration jobs' do
+ described_class.new.perform
+
+ expect(described_class::FOLLOW_UP_MIGRATION).to be_scheduled_migration(1, 5)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(1)
+ end
+
+ # E.g. from a previous failed run of this background migration
+ context 'when there is existing data in untracked_files_for_uploads' do
+ before do
+ described_class.new.perform
+ end
+
+ it 'does not error or produce duplicates of existing data' do
+ expect do
+ described_class.new.perform
+ end.not_to change { untracked_files_for_uploads.count }.from(5)
+ end
+ end
+
+ # E.g. The installation is in use at the time of migration, and someone has
+ # just uploaded a file
+ context 'when there are files in /uploads/tmp' do
+ let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') }
+
+ before do
+ FileUtils.touch(tmp_file)
+ end
+
+ after do
+ FileUtils.rm(tmp_file)
+ end
+
+ it 'does not add files from /uploads/tmp' do
+ described_class.new.perform
+
+ expect(untracked_files_for_uploads.count).to eq(5)
+ end
+ end
+ end
+ end
+
+ context 'test bulk insert without ON CONFLICT DO NOTHING or IGNORE' do
+ before do
+ # If this is CI, we use Postgres 9.2 so this stub has no effect.
+ #
+ # If this is being run on Postgres 9.5+ or MySQL, then this stub allows us
+ # to test the bulk insert functionality without ON CONFLICT DO NOTHING or
+ # IGNORE.
+ allow_any_instance_of(described_class).to receive(:postgresql_pre_9_5?).and_return(true)
+ end
+
+ context 'when files were uploaded before and after hashed storage was enabled' do
+ let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) }
+ let!(:user) { create(:user, :with_avatar) }
+ let!(:project1) { create(:project, :with_avatar) }
+ let(:project2) { create(:project) } # instantiate after enabling hashed_storage
+
+ before do
+ # Markdown upload before enabling hashed_storage
+ UploadService.new(project1, uploaded_file, FileUploader).execute
+
+ stub_application_setting(hashed_storage_enabled: true)
+
+ # Markdown upload after enabling hashed_storage
+ UploadService.new(project2, uploaded_file, FileUploader).execute
+ end
+
+ it 'adds unhashed files to the untracked_files_for_uploads table' do
+ described_class.new.perform
+
+ expect(untracked_files_for_uploads.count).to eq(5)
+ end
+
+ it 'adds files with paths relative to CarrierWave.root' do
+ described_class.new.perform
+ untracked_files_for_uploads.all.each do |file|
+ expect(file.path.start_with?('uploads/')).to be_truthy
+ end
+ end
+
+ it 'does not add hashed files to the untracked_files_for_uploads table' do
+ described_class.new.perform
+
+ hashed_file_path = project2.uploads.where(uploader: 'FileUploader').first.path
+ expect(untracked_files_for_uploads.where("path like '%#{hashed_file_path}%'").exists?).to be_falsey
+ end
+
+ it 'correctly schedules the follow-up background migration jobs' do
+ described_class.new.perform
+
+ expect(described_class::FOLLOW_UP_MIGRATION).to be_scheduled_migration(1, 5)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(1)
+ end
+
+ # E.g. from a previous failed run of this background migration
+ context 'when there is existing data in untracked_files_for_uploads' do
+ before do
+ described_class.new.perform
+ end
+
+ it 'does not error or produce duplicates of existing data' do
+ expect do
+ described_class.new.perform
+ end.not_to change { untracked_files_for_uploads.count }.from(5)
+ end
+ end
+
+ # E.g. The installation is in use at the time of migration, and someone has
+ # just uploaded a file
+ context 'when there are files in /uploads/tmp' do
+ let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') }
+
+ before do
+ FileUtils.touch(tmp_file)
+ end
+
+ after do
+ FileUtils.rm(tmp_file)
+ end
+
+ it 'does not add files from /uploads/tmp' do
+ described_class.new.perform
+
+ expect(untracked_files_for_uploads.count).to eq(5)
+ end
+ end
+ end
+ end
+
+ # Very new or lightly-used installations that are running this migration
+ # may not have an upload directory because they have no uploads.
+ context 'when no files were ever uploaded' do
+ it 'does not add to the untracked_files_for_uploads table (and does not raise error)' do
+ described_class.new.perform
+
+ expect(untracked_files_for_uploads.count).to eq(0)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/backup/repository_spec.rb b/spec/lib/gitlab/backup/repository_spec.rb
deleted file mode 100644
index 535cce12780..00000000000
--- a/spec/lib/gitlab/backup/repository_spec.rb
+++ /dev/null
@@ -1,117 +0,0 @@
-require 'spec_helper'
-
-describe Backup::Repository do
- let(:progress) { StringIO.new }
- let!(:project) { create(:project) }
-
- before do
- allow(progress).to receive(:puts)
- allow(progress).to receive(:print)
-
- allow_any_instance_of(String).to receive(:color) do |string, _color|
- string
- end
-
- allow_any_instance_of(described_class).to receive(:progress).and_return(progress)
- end
-
- describe '#dump' do
- describe 'repo failure' do
- before do
- allow_any_instance_of(Repository).to receive(:empty_repo?).and_raise(Rugged::OdbError)
- allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0])
- end
-
- it 'does not raise error' do
- expect { described_class.new.dump }.not_to raise_error
- end
-
- it 'shows the appropriate error' do
- described_class.new.dump
-
- expect(progress).to have_received(:puts).with("Ignoring repository error and continuing backing up project: #{project.full_path} - Rugged::OdbError")
- end
- end
-
- describe 'command failure' do
- before do
- allow_any_instance_of(Repository).to receive(:empty_repo?).and_return(false)
- allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1])
- end
-
- it 'shows the appropriate error' do
- described_class.new.dump
-
- expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error")
- end
- end
- end
-
- describe '#restore' do
- describe 'command failure' do
- before do
- allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1])
- end
-
- it 'shows the appropriate error' do
- described_class.new.restore
-
- expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error")
- end
- end
- end
-
- describe '#empty_repo?' do
- context 'for a wiki' do
- let(:wiki) { create(:project_wiki) }
-
- context 'wiki repo has content' do
- let!(:wiki_page) { create(:wiki_page, wiki: wiki) }
-
- before do
- wiki.repository.exists? # initial cache
- end
-
- context '`repository.exists?` is incorrectly cached as false' do
- before do
- repo = wiki.repository
- repo.send(:cache).expire(:exists?)
- repo.send(:cache).fetch(:exists?) { false }
- repo.send(:instance_variable_set, :@exists, false)
- end
-
- it 'returns false, regardless of bad cache value' do
- expect(described_class.new.send(:empty_repo?, wiki)).to be_falsey
- end
- end
-
- context '`repository.exists?` is correctly cached as true' do
- it 'returns false' do
- expect(described_class.new.send(:empty_repo?, wiki)).to be_falsey
- end
- end
- end
-
- context 'wiki repo does not have content' do
- context '`repository.exists?` is incorrectly cached as true' do
- before do
- repo = wiki.repository
- repo.send(:cache).expire(:exists?)
- repo.send(:cache).fetch(:exists?) { true }
- repo.send(:instance_variable_set, :@exists, true)
- end
-
- it 'returns true, regardless of bad cache value' do
- expect(described_class.new.send(:empty_repo?, wiki)).to be_truthy
- end
- end
-
- context '`repository.exists?` is correctly cached as false' do
- it 'returns true' do
- expect(described_class.new.send(:empty_repo?, wiki)).to be_truthy
- end
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/checks/project_moved_spec.rb b/spec/lib/gitlab/checks/project_moved_spec.rb
new file mode 100644
index 00000000000..fa1575e2177
--- /dev/null
+++ b/spec/lib/gitlab/checks/project_moved_spec.rb
@@ -0,0 +1,81 @@
+require 'rails_helper'
+
+describe Gitlab::Checks::ProjectMoved, :clean_gitlab_redis_shared_state do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ describe '.fetch_redirct_message' do
+ context 'with a redirect message queue' do
+ it 'should return the redirect message' do
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ project_moved.add_redirect_message
+
+ expect(described_class.fetch_redirect_message(user.id, project.id)).to eq(project_moved.redirect_message)
+ end
+
+ it 'should delete the redirect message from redis' do
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ project_moved.add_redirect_message
+
+ expect(Gitlab::Redis::SharedState.with { |redis| redis.get("redirect_namespace:#{user.id}:#{project.id}") }).not_to be_nil
+ described_class.fetch_redirect_message(user.id, project.id)
+ expect(Gitlab::Redis::SharedState.with { |redis| redis.get("redirect_namespace:#{user.id}:#{project.id}") }).to be_nil
+ end
+ end
+
+ context 'with no redirect message queue' do
+ it 'should return nil' do
+ expect(described_class.fetch_redirect_message(1, 2)).to be_nil
+ end
+ end
+ end
+
+ describe '#add_redirect_message' do
+ it 'should queue a redirect message' do
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ expect(project_moved.add_redirect_message).to eq("OK")
+ end
+ end
+
+ describe '#redirect_message' do
+ context 'when the push is rejected' do
+ it 'should return a redirect message telling the user to try again' do
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ message = "Project 'foo/bar' was moved to '#{project.full_path}'." +
+ "\n\nPlease update your Git remote:" +
+ "\n\n git remote set-url origin #{project.http_url_to_repo} and try again.\n"
+
+ expect(project_moved.redirect_message(rejected: true)).to eq(message)
+ end
+ end
+
+ context 'when the push is not rejected' do
+ it 'should return a redirect message' do
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ message = "Project 'foo/bar' was moved to '#{project.full_path}'." +
+ "\n\nPlease update your Git remote:" +
+ "\n\n git remote set-url origin #{project.http_url_to_repo}\n"
+
+ expect(project_moved.redirect_message).to eq(message)
+ end
+ end
+ end
+
+ describe '#permanent_redirect?' do
+ context 'with a permanent RedirectRoute' do
+ it 'should return true' do
+ project.route.create_redirect('foo/bar', permanent: true)
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ expect(project_moved.permanent_redirect?).to be_truthy
+ end
+ end
+
+ context 'without a permanent RedirectRoute' do
+ it 'should return false' do
+ project.route.create_redirect('foo/bar')
+ project_moved = described_class.new(project, user, 'foo/bar', 'http')
+ expect(project_moved.permanent_redirect?).to be_falsy
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
index 0f1d72080c5..3ae7053a995 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
@@ -6,46 +6,81 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
let(:pipeline) { Ci::Pipeline.new }
let(:command) do
- double('command', source: :push,
- origin_ref: 'master',
- checkout_sha: project.commit.id,
- after_sha: nil,
- before_sha: nil,
- trigger_request: nil,
- schedule: nil,
- project: project,
- current_user: user)
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ source: :push,
+ origin_ref: 'master',
+ checkout_sha: project.commit.id,
+ after_sha: nil,
+ before_sha: nil,
+ trigger_request: nil,
+ schedule: nil,
+ project: project,
+ current_user: user)
end
let(:step) { described_class.new(pipeline, command) }
before do
stub_repository_ci_yaml_file(sha: anything)
-
- step.perform!
end
it 'never breaks the chain' do
+ step.perform!
+
expect(step.break?).to be false
end
it 'fills pipeline object with data' do
+ step.perform!
+
expect(pipeline.sha).not_to be_empty
expect(pipeline.sha).to eq project.commit.id
expect(pipeline.ref).to eq 'master'
+ expect(pipeline.tag).to be false
expect(pipeline.user).to eq user
expect(pipeline.project).to eq project
end
it 'sets a valid config source' do
+ step.perform!
+
expect(pipeline.repository_source?).to be true
end
it 'returns a valid pipeline' do
+ step.perform!
+
expect(pipeline).to be_valid
end
it 'does not persist a pipeline' do
+ step.perform!
+
expect(pipeline).not_to be_persisted
end
+
+ context 'when pipeline is running for a tag' do
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ source: :push,
+ origin_ref: 'mytag',
+ checkout_sha: project.commit.id,
+ after_sha: nil,
+ before_sha: nil,
+ trigger_request: nil,
+ schedule: nil,
+ project: project,
+ current_user: user)
+ end
+
+ before do
+ allow_any_instance_of(Repository).to receive(:tag_exists?).with('mytag').and_return(true)
+
+ step.perform!
+ end
+
+ it 'correctly indicated that this is a tagged pipeline' do
+ expect(pipeline).to be_tag
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
new file mode 100644
index 00000000000..75a177d2d1f
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
@@ -0,0 +1,185 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Chain::Command do
+ set(:project) { create(:project, :repository) }
+
+ describe '#initialize' do
+ subject do
+ described_class.new(origin_ref: 'master')
+ end
+
+ it 'properly initialises object from hash' do
+ expect(subject.origin_ref).to eq('master')
+ end
+ end
+
+ context 'handling of origin_ref' do
+ let(:command) { described_class.new(project: project, origin_ref: origin_ref) }
+
+ describe '#branch_exists?' do
+ subject { command.branch_exists? }
+
+ context 'for existing branch' do
+ let(:origin_ref) { 'master' }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'for invalid branch' do
+ let(:origin_ref) { 'something' }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#tag_exists?' do
+ subject { command.tag_exists? }
+
+ context 'for existing ref' do
+ let(:origin_ref) { 'v1.0.0' }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'for invalid ref' do
+ let(:origin_ref) { 'something' }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#ref' do
+ subject { command.ref }
+
+ context 'for regular ref' do
+ let(:origin_ref) { 'master' }
+
+ it { is_expected.to eq('master') }
+ end
+
+ context 'for branch ref' do
+ let(:origin_ref) { 'refs/heads/master' }
+
+ it { is_expected.to eq('master') }
+ end
+
+ context 'for tag ref' do
+ let(:origin_ref) { 'refs/tags/1.0.0' }
+
+ it { is_expected.to eq('1.0.0') }
+ end
+
+ context 'for other refs' do
+ let(:origin_ref) { 'refs/merge-requests/11/head' }
+
+ it { is_expected.to eq('refs/merge-requests/11/head') }
+ end
+ end
+ end
+
+ describe '#sha' do
+ subject { command.sha }
+
+ context 'when invalid checkout_sha is specified' do
+ let(:command) { described_class.new(project: project, checkout_sha: 'aaa') }
+
+ it 'returns empty value' do
+ is_expected.to be_nil
+ end
+ end
+
+ context 'when a valid checkout_sha is specified' do
+ let(:command) { described_class.new(project: project, checkout_sha: project.commit.id) }
+
+ it 'returns checkout_sha' do
+ is_expected.to eq(project.commit.id)
+ end
+ end
+
+ context 'when a valid after_sha is specified' do
+ let(:command) { described_class.new(project: project, after_sha: project.commit.id) }
+
+ it 'returns after_sha' do
+ is_expected.to eq(project.commit.id)
+ end
+ end
+
+ context 'when a valid origin_ref is specified' do
+ let(:command) { described_class.new(project: project, origin_ref: 'HEAD') }
+
+ it 'returns SHA for given ref' do
+ is_expected.to eq(project.commit.id)
+ end
+ end
+ end
+
+ describe '#origin_sha' do
+ subject { command.origin_sha }
+
+ context 'when using checkout_sha and after_sha' do
+ let(:command) { described_class.new(project: project, checkout_sha: 'aaa', after_sha: 'bbb') }
+
+ it 'uses checkout_sha' do
+ is_expected.to eq('aaa')
+ end
+ end
+
+ context 'when using after_sha only' do
+ let(:command) { described_class.new(project: project, after_sha: 'bbb') }
+
+ it 'uses after_sha' do
+ is_expected.to eq('bbb')
+ end
+ end
+ end
+
+ describe '#before_sha' do
+ subject { command.before_sha }
+
+ context 'when using checkout_sha and before_sha' do
+ let(:command) { described_class.new(project: project, checkout_sha: 'aaa', before_sha: 'bbb') }
+
+ it 'uses before_sha' do
+ is_expected.to eq('bbb')
+ end
+ end
+
+ context 'when using checkout_sha only' do
+ let(:command) { described_class.new(project: project, checkout_sha: 'aaa') }
+
+ it 'uses checkout_sha' do
+ is_expected.to eq('aaa')
+ end
+ end
+
+ context 'when checkout_sha and before_sha are empty' do
+ let(:command) { described_class.new(project: project) }
+
+ it 'uses BLANK_SHA' do
+ is_expected.to eq(Gitlab::Git::BLANK_SHA)
+ end
+ end
+ end
+
+ describe '#protected_ref?' do
+ let(:command) { described_class.new(project: project, origin_ref: 'my-branch') }
+
+ subject { command.protected_ref? }
+
+ context 'when a ref is protected' do
+ before do
+ expect_any_instance_of(Project).to receive(:protected_for?).with('my-branch').and_return(true)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when a ref is unprotected' do
+ before do
+ expect_any_instance_of(Project).to receive(:protected_for?).with('my-branch').and_return(false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
index f54e2326b06..1b03227d67b 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb
@@ -10,9 +10,9 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
end
let(:command) do
- double('command', project: project,
- current_user: user,
- seeds_block: nil)
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project,
+ current_user: user, seeds_block: nil)
end
let(:step) { described_class.new(pipeline, command) }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
index e165e0fac2a..eca23694a2b 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Ci::Pipeline::Chain::Sequence do
set(:user) { create(:user) }
let(:pipeline) { build_stubbed(:ci_pipeline) }
- let(:command) { double('command' ) }
+ let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new }
let(:first_step) { spy('first step') }
let(:second_step) { spy('second step') }
let(:sequence) { [first_step, second_step] }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb
index 32bd5de829b..dc13cae961c 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb
@@ -6,10 +6,11 @@ describe Gitlab::Ci::Pipeline::Chain::Skip do
set(:pipeline) { create(:ci_pipeline, project: project) }
let(:command) do
- double('command', project: project,
- current_user: user,
- ignore_skip_ci: false,
- save_incompleted: true)
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project,
+ current_user: user,
+ ignore_skip_ci: false,
+ save_incompleted: true)
end
let(:step) { described_class.new(pipeline, command) }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
index 0bbdd23f4d6..a973ccda8de 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
@@ -5,11 +5,12 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do
set(:user) { create(:user) }
let(:pipeline) do
- build_stubbed(:ci_pipeline, ref: ref, project: project)
+ build_stubbed(:ci_pipeline, project: project)
end
let(:command) do
- double('command', project: project, current_user: user)
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project, current_user: user, origin_ref: ref)
end
let(:step) { described_class.new(pipeline, command) }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
index 8357af38f92..5c12c6e6392 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
@@ -5,9 +5,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do
set(:user) { create(:user) }
let(:command) do
- double('command', project: project,
- current_user: user,
- save_incompleted: true)
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project,
+ current_user: user,
+ save_incompleted: true)
end
let!(:step) { described_class.new(pipeline, command) }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb
index bb356efe9ad..fb1b53fc55c 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb
@@ -3,10 +3,7 @@ require 'spec_helper'
describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do
set(:project) { create(:project, :repository) }
set(:user) { create(:user) }
-
- let(:command) do
- double('command', project: project, current_user: user)
- end
+ let(:pipeline) { build_stubbed(:ci_pipeline) }
let!(:step) { described_class.new(pipeline, command) }
@@ -14,9 +11,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do
step.perform!
end
- context 'when pipeline ref and sha exists' do
- let(:pipeline) do
- build_stubbed(:ci_pipeline, ref: 'master', sha: '123', project: project)
+ context 'when ref and sha exists' do
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project, current_user: user, origin_ref: 'master', checkout_sha: project.commit.id)
end
it 'does not break the chain' do
@@ -28,9 +26,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do
end
end
- context 'when pipeline ref does not exist' do
- let(:pipeline) do
- build_stubbed(:ci_pipeline, ref: 'something', project: project)
+ context 'when ref does not exist' do
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project, current_user: user, origin_ref: 'something')
end
it 'breaks the chain' do
@@ -43,9 +42,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do
end
end
- context 'when pipeline does not have SHA set' do
- let(:pipeline) do
- build_stubbed(:ci_pipeline, ref: 'master', sha: nil, project: project)
+ context 'when does not have existing SHA set' do
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project, current_user: user, origin_ref: 'master', checkout_sha: 'something')
end
it 'breaks the chain' do
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index fcddfad3f9f..b2f13fae73f 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -73,6 +73,28 @@ describe Gitlab::Database do
end
end
+ describe '.replication_slots_supported?' do
+ it 'returns false when using MySQL' do
+ allow(described_class).to receive(:postgresql?).and_return(false)
+
+ expect(described_class.replication_slots_supported?).to eq(false)
+ end
+
+ it 'returns false when using PostgreSQL 9.3' do
+ allow(described_class).to receive(:postgresql?).and_return(true)
+ allow(described_class).to receive(:version).and_return('9.3.1')
+
+ expect(described_class.replication_slots_supported?).to eq(false)
+ end
+
+ it 'returns true when using PostgreSQL 9.4.0 or newer' do
+ allow(described_class).to receive(:postgresql?).and_return(true)
+ allow(described_class).to receive(:version).and_return('9.4.0')
+
+ expect(described_class.replication_slots_supported?).to eq(true)
+ end
+ end
+
describe '.nulls_last_order' do
context 'when using PostgreSQL' do
before do
@@ -199,6 +221,22 @@ describe Gitlab::Database do
described_class.bulk_insert('test', rows)
end
+ it 'does not quote values of a column in the disable_quote option' do
+ [1, 2, 4, 5].each do |i|
+ expect(connection).to receive(:quote).with(i)
+ end
+
+ described_class.bulk_insert('test', rows, disable_quote: :c)
+ end
+
+ it 'does not quote values of columns in the disable_quote option' do
+ [2, 5].each do |i|
+ expect(connection).to receive(:quote).with(i)
+ end
+
+ described_class.bulk_insert('test', rows, disable_quote: [:a, :c])
+ end
+
it 'handles non-UTF-8 data' do
expect { described_class.bulk_insert('test', [{ a: "\255" }]) }.not_to raise_error
end
diff --git a/spec/lib/gitlab/diff/inline_diff_spec.rb b/spec/lib/gitlab/diff/inline_diff_spec.rb
index 15451c2cf99..0a41362f606 100644
--- a/spec/lib/gitlab/diff/inline_diff_spec.rb
+++ b/spec/lib/gitlab/diff/inline_diff_spec.rb
@@ -31,6 +31,10 @@ describe Gitlab::Diff::InlineDiff do
expect(subject[7]).to eq([17..17])
expect(subject[8]).to be_nil
end
+
+ it 'can handle unchanged empty lines' do
+ expect { described_class.for_lines(['- bar', '+ baz', '']) }.not_to raise_error
+ end
end
describe "#inline_diffs" do
diff --git a/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
index e361d1a7393..51ce3116880 100644
--- a/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
@@ -10,14 +10,14 @@ describe Gitlab::Email::Handler::CreateMergeRequestHandler do
stub_config_setting(host: 'localhost')
end
+ after do
+ TestEnv.clean_test_path
+ end
+
let(:email_raw) { fixture_file('emails/valid_new_merge_request.eml') }
let(:namespace) { create(:namespace, path: 'gitlabhq') }
- # project's git repository is not deleted when project is deleted
- # between tests. Then tests fail because re-creation of the project with
- # the same name fails on existing git repository -> skip_disk_validation
- # ignores repository existence on disk
- let!(:project) { create(:project, :public, :repository, skip_disk_validation: true, namespace: namespace, path: 'gitlabhq') }
+ let!(:project) { create(:project, :public, :repository, namespace: namespace, path: 'gitlabhq') }
let!(:user) do
create(
:user,
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 9f4e3c49adc..5ed639543e0 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -278,6 +278,35 @@ describe Gitlab::Git::Commit, seed_helper: true do
it { is_expected.not_to include(SeedRepo::FirstCommit::ID) }
end
+ shared_examples '.shas_with_signatures' do
+ let(:signed_shas) { %w[5937ac0a7beb003549fc5fd26fc247adbce4a52e 570e7b2abdd848b95f2f578043fc23bd6f6fd24d] }
+ let(:unsigned_shas) { %w[19e2e9b4ef76b422ce1154af39a91323ccc57434 c642fe9b8b9f28f9225d7ea953fe14e74748d53b] }
+ let(:first_signed_shas) { %w[5937ac0a7beb003549fc5fd26fc247adbce4a52e c642fe9b8b9f28f9225d7ea953fe14e74748d53b] }
+
+ it 'has 2 signed shas' do
+ ret = described_class.shas_with_signatures(repository, signed_shas)
+ expect(ret).to eq(signed_shas)
+ end
+
+ it 'has 0 signed shas' do
+ ret = described_class.shas_with_signatures(repository, unsigned_shas)
+ expect(ret).to eq([])
+ end
+
+ it 'has 1 signed sha' do
+ ret = described_class.shas_with_signatures(repository, first_signed_shas)
+ expect(ret).to contain_exactly(first_signed_shas.first)
+ end
+ end
+
+ describe '.shas_with_signatures with gitaly on' do
+ it_should_behave_like '.shas_with_signatures'
+ end
+
+ describe '.shas_with_signatures with gitaly disabled', :disable_gitaly do
+ it_should_behave_like '.shas_with_signatures'
+ end
+
describe '.find_all' do
shared_examples 'finding all commits' do
it 'should return a return a collection of commits' do
diff --git a/spec/lib/gitlab/git/remote_repository_spec.rb b/spec/lib/gitlab/git/remote_repository_spec.rb
index 0506210887c..eb148cc3804 100644
--- a/spec/lib/gitlab/git/remote_repository_spec.rb
+++ b/spec/lib/gitlab/git/remote_repository_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::Git::RemoteRepository, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
subject { described_class.new(repository) }
- describe '#empty_repo?' do
+ describe '#empty?' do
using RSpec::Parameterized::TableSyntax
where(:repository, :result) do
@@ -13,7 +13,7 @@ describe Gitlab::Git::RemoteRepository, seed_helper: true do
end
with_them do
- it { expect(subject.empty_repo?).to eq(result) }
+ it { expect(subject.empty?).to eq(result) }
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 08dd6ea80ff..f19b65a5f71 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -257,7 +257,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#empty?' do
- it { expect(repository.empty?).to be_falsey }
+ it { expect(repository).not_to be_empty }
end
describe '#ref_names' do
diff --git a/spec/lib/gitlab/git/storage/checker_spec.rb b/spec/lib/gitlab/git/storage/checker_spec.rb
new file mode 100644
index 00000000000..d74c3bcb04c
--- /dev/null
+++ b/spec/lib/gitlab/git/storage/checker_spec.rb
@@ -0,0 +1,132 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Storage::Checker, :clean_gitlab_redis_shared_state do
+ let(:storage_name) { 'default' }
+ let(:hostname) { Gitlab::Environment.hostname }
+ let(:cache_key) { "storage_accessible:#{storage_name}:#{hostname}" }
+
+ subject(:checker) { described_class.new(storage_name) }
+
+ def value_from_redis(name)
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.hmget(cache_key, name)
+ end.first
+ end
+
+ def set_in_redis(name, value)
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.hmset(cache_key, name, value)
+ end.first
+ end
+
+ describe '.check_all' do
+ it 'calls a check for each storage' do
+ fake_checker_default = double
+ fake_checker_broken = double
+ fake_logger = fake_logger
+
+ expect(described_class).to receive(:new).with('default', fake_logger) { fake_checker_default }
+ expect(described_class).to receive(:new).with('broken', fake_logger) { fake_checker_broken }
+ expect(fake_checker_default).to receive(:check_with_lease)
+ expect(fake_checker_broken).to receive(:check_with_lease)
+
+ described_class.check_all(fake_logger)
+ end
+
+ context 'with broken storage', :broken_storage do
+ it 'returns the results' do
+ expected_result = [
+ { storage: 'default', success: true },
+ { storage: 'broken', success: false }
+ ]
+
+ expect(described_class.check_all).to eq(expected_result)
+ end
+ end
+ end
+
+ describe '#initialize' do
+ it 'assigns the settings' do
+ expect(checker.hostname).to eq(hostname)
+ expect(checker.storage).to eq('default')
+ expect(checker.storage_path).to eq(TestEnv.repos_path)
+ end
+ end
+
+ describe '#check_with_lease' do
+ it 'only allows one check at a time' do
+ expect(checker).to receive(:check).once { sleep 1 }
+
+ thread = Thread.new { checker.check_with_lease }
+ checker.check_with_lease
+ thread.join
+ end
+
+ it 'returns a result hash' do
+ expect(checker.check_with_lease).to eq(storage: 'default', success: true)
+ end
+ end
+
+ describe '#check' do
+ it 'tracks that the storage was accessible' do
+ set_in_redis(:failure_count, 10)
+ set_in_redis(:last_failure, Time.now.to_f)
+
+ checker.check
+
+ expect(value_from_redis(:failure_count).to_i).to eq(0)
+ expect(value_from_redis(:last_failure)).to be_empty
+ expect(value_from_redis(:first_failure)).to be_empty
+ end
+
+ it 'calls the check with the correct arguments' do
+ stub_application_setting(circuitbreaker_storage_timeout: 30,
+ circuitbreaker_access_retries: 3)
+
+ expect(Gitlab::Git::Storage::ForkedStorageCheck)
+ .to receive(:storage_available?).with(TestEnv.repos_path, 30, 3)
+ .and_call_original
+
+ checker.check
+ end
+
+ it 'returns `true`' do
+ expect(checker.check).to eq(true)
+ end
+
+ it 'maintains known storage keys' do
+ Timecop.freeze do
+ # Insert an old key to expire
+ old_entry = Time.now.to_i - 3.days.to_i
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, old_entry, 'to_be_removed')
+ end
+
+ checker.check
+
+ known_keys = Gitlab::Git::Storage.redis.with do |redis|
+ redis.zrange(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, -1)
+ end
+
+ expect(known_keys).to contain_exactly(cache_key)
+ end
+ end
+
+ context 'the storage is not available', :broken_storage do
+ let(:storage_name) { 'broken' }
+
+ it 'tracks that the storage was inaccessible' do
+ Timecop.freeze do
+ expect { checker.check }.to change { value_from_redis(:failure_count).to_i }.by(1)
+
+ expect(value_from_redis(:last_failure)).not_to be_empty
+ expect(value_from_redis(:first_failure)).not_to be_empty
+ end
+ end
+
+ it 'returns `false`' do
+ expect(checker.check).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb
index f34c9f09057..210b90bfba9 100644
--- a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb
+++ b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb
@@ -1,11 +1,18 @@
require 'spec_helper'
-describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: true, broken_storage: true do
+describe Gitlab::Git::Storage::CircuitBreaker, :broken_storage do
let(:storage_name) { 'default' }
let(:circuit_breaker) { described_class.new(storage_name, hostname) }
let(:hostname) { Gitlab::Environment.hostname }
let(:cache_key) { "storage_accessible:#{storage_name}:#{hostname}" }
+ def set_in_redis(name, value)
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, cache_key)
+ redis.hmset(cache_key, name, value)
+ end.first
+ end
+
before do
# Override test-settings for the circuitbreaker with something more realistic
# for these specs.
@@ -19,36 +26,7 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
)
end
- def value_from_redis(name)
- Gitlab::Git::Storage.redis.with do |redis|
- redis.hmget(cache_key, name)
- end.first
- end
-
- def set_in_redis(name, value)
- Gitlab::Git::Storage.redis.with do |redis|
- redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, cache_key)
- redis.hmset(cache_key, name, value)
- end.first
- end
-
- describe '.reset_all!' do
- it 'clears all entries form redis' do
- set_in_redis(:failure_count, 10)
-
- described_class.reset_all!
-
- key_exists = Gitlab::Git::Storage.redis.with { |redis| redis.exists(cache_key) }
-
- expect(key_exists).to be_falsey
- end
-
- it 'does not break when there are no keys in redis' do
- expect { described_class.reset_all! }.not_to raise_error
- end
- end
-
- describe '.for_storage' do
+ describe '.for_storage', :request_store do
it 'only builds a single circuitbreaker per storage' do
expect(described_class).to receive(:new).once.and_call_original
@@ -71,7 +49,6 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
it 'assigns the settings' do
expect(circuit_breaker.hostname).to eq(hostname)
expect(circuit_breaker.storage).to eq('default')
- expect(circuit_breaker.storage_path).to eq(TestEnv.repos_path)
end
end
@@ -91,9 +68,9 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
end
end
- describe '#failure_wait_time' do
+ describe '#check_interval' do
it 'reads the value from settings' do
- expect(circuit_breaker.failure_wait_time).to eq(1)
+ expect(circuit_breaker.check_interval).to eq(1)
end
end
@@ -114,12 +91,6 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
expect(circuit_breaker.access_retries).to eq(4)
end
end
-
- describe '#backoff_threshold' do
- it 'reads the value from settings' do
- expect(circuit_breaker.backoff_threshold).to eq(5)
- end
- end
end
describe '#perform' do
@@ -134,19 +105,6 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
end
end
- it 'raises the correct exception when backing off' do
- Timecop.freeze do
- set_in_redis(:last_failure, 1.second.ago.to_f)
- set_in_redis(:failure_count, 90)
-
- expect { |b| circuit_breaker.perform(&b) }
- .to raise_error do |exception|
- expect(exception).to be_kind_of(Gitlab::Git::Storage::Failing)
- expect(exception.retry_after).to eq(30)
- end
- end
- end
-
it 'yields the block' do
expect { |b| circuit_breaker.perform(&b) }
.to yield_control
@@ -170,54 +128,6 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
.to raise_error(Rugged::OSError)
end
- it 'tracks that the storage was accessible' do
- set_in_redis(:failure_count, 10)
- set_in_redis(:last_failure, Time.now.to_f)
-
- circuit_breaker.perform { '' }
-
- expect(value_from_redis(:failure_count).to_i).to eq(0)
- expect(value_from_redis(:last_failure)).to be_empty
- expect(circuit_breaker.failure_count).to eq(0)
- expect(circuit_breaker.last_failure).to be_nil
- end
-
- it 'maintains known storage keys' do
- Timecop.freeze do
- # Insert an old key to expire
- old_entry = Time.now.to_i - 3.days.to_i
- Gitlab::Git::Storage.redis.with do |redis|
- redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, old_entry, 'to_be_removed')
- end
-
- circuit_breaker.perform { '' }
-
- known_keys = Gitlab::Git::Storage.redis.with do |redis|
- redis.zrange(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, -1)
- end
-
- expect(known_keys).to contain_exactly(cache_key)
- end
- end
-
- it 'only performs the accessibility check once' do
- expect(Gitlab::Git::Storage::ForkedStorageCheck)
- .to receive(:storage_available?).once.and_call_original
-
- 2.times { circuit_breaker.perform { '' } }
- end
-
- it 'calls the check with the correct arguments' do
- stub_application_setting(circuitbreaker_storage_timeout: 30,
- circuitbreaker_access_retries: 3)
-
- expect(Gitlab::Git::Storage::ForkedStorageCheck)
- .to receive(:storage_available?).with(TestEnv.repos_path, 30, 3)
- .and_call_original
-
- circuit_breaker.perform { '' }
- end
-
context 'with the feature disabled' do
before do
stub_feature_flags(git_storage_circuit_breaker: false)
@@ -240,31 +150,6 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
expect(result).to eq('hello')
end
end
-
- context 'the storage is not available' do
- let(:storage_name) { 'broken' }
-
- it 'raises the correct exception' do
- expect(circuit_breaker).to receive(:track_storage_inaccessible)
-
- expect { circuit_breaker.perform { '' } }
- .to raise_error do |exception|
- expect(exception).to be_kind_of(Gitlab::Git::Storage::Inaccessible)
- expect(exception.retry_after).to eq(30)
- end
- end
-
- it 'tracks that the storage was inaccessible' do
- Timecop.freeze do
- expect { circuit_breaker.perform { '' } }.to raise_error(Gitlab::Git::Storage::Inaccessible)
-
- expect(value_from_redis(:failure_count).to_i).to eq(1)
- expect(value_from_redis(:last_failure)).not_to be_empty
- expect(circuit_breaker.failure_count).to eq(1)
- expect(circuit_breaker.last_failure).to be_within(1.second).of(Time.now)
- end
- end
- end
end
describe '#circuit_broken?' do
@@ -283,32 +168,6 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state:
end
end
- describe '#backing_off?' do
- it 'is true when there was a recent failure' do
- Timecop.freeze do
- set_in_redis(:last_failure, 1.second.ago.to_f)
- set_in_redis(:failure_count, 90)
-
- expect(circuit_breaker.backing_off?).to be_truthy
- end
- end
-
- context 'the `failure_wait_time` is set to 0' do
- before do
- stub_application_setting(circuitbreaker_failure_wait_time: 0)
- end
-
- it 'is working even when there are failures' do
- Timecop.freeze do
- set_in_redis(:last_failure, 0.seconds.ago.to_f)
- set_in_redis(:failure_count, 90)
-
- expect(circuit_breaker.backing_off?).to be_falsey
- end
- end
- end
- end
-
describe '#last_failure' do
it 'returns the last failure time' do
time = Time.parse("2017-05-26 17:52:30")
diff --git a/spec/lib/gitlab/git/storage/failure_info_spec.rb b/spec/lib/gitlab/git/storage/failure_info_spec.rb
new file mode 100644
index 00000000000..bae88fdda86
--- /dev/null
+++ b/spec/lib/gitlab/git/storage/failure_info_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Storage::FailureInfo, :broken_storage do
+ let(:storage_name) { 'default' }
+ let(:hostname) { Gitlab::Environment.hostname }
+ let(:cache_key) { "storage_accessible:#{storage_name}:#{hostname}" }
+
+ def value_from_redis(name)
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.hmget(cache_key, name)
+ end.first
+ end
+
+ def set_in_redis(name, value)
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, cache_key)
+ redis.hmset(cache_key, name, value)
+ end.first
+ end
+
+ describe '.reset_all!' do
+ it 'clears all entries form redis' do
+ set_in_redis(:failure_count, 10)
+
+ described_class.reset_all!
+
+ key_exists = Gitlab::Git::Storage.redis.with { |redis| redis.exists(cache_key) }
+
+ expect(key_exists).to be_falsey
+ end
+
+ it 'does not break when there are no keys in redis' do
+ expect { described_class.reset_all! }.not_to raise_error
+ end
+ end
+
+ describe '.load' do
+ it 'loads failure information for a storage on a host' do
+ first_failure = Time.parse("2017-11-14 17:52:30")
+ last_failure = Time.parse("2017-11-14 18:54:37")
+ failure_count = 11
+
+ set_in_redis(:first_failure, first_failure.to_i)
+ set_in_redis(:last_failure, last_failure.to_i)
+ set_in_redis(:failure_count, failure_count.to_i)
+
+ info = described_class.load(cache_key)
+
+ expect(info.first_failure).to eq(first_failure)
+ expect(info.last_failure).to eq(last_failure)
+ expect(info.failure_count).to eq(failure_count)
+ end
+ end
+
+ describe '#no_failures?' do
+ it 'is true when there are no failures' do
+ info = described_class.new(nil, nil, 0)
+
+ expect(info.no_failures?).to be_truthy
+ end
+
+ it 'is false when there are failures' do
+ info = described_class.new(Time.parse("2017-11-14 17:52:30"),
+ Time.parse("2017-11-14 18:54:37"),
+ 20)
+
+ expect(info.no_failures?).to be_falsy
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/storage/health_spec.rb b/spec/lib/gitlab/git/storage/health_spec.rb
index d7a52a04fbb..bb670fc5d94 100644
--- a/spec/lib/gitlab/git/storage/health_spec.rb
+++ b/spec/lib/gitlab/git/storage/health_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Git::Storage::Health, clean_gitlab_redis_shared_state: true, broken_storage: true do
+describe Gitlab::Git::Storage::Health, broken_storage: true do
let(:host1_key) { 'storage_accessible:broken:web01' }
let(:host2_key) { 'storage_accessible:default:kiq01' }
diff --git a/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb
index 5db37f55e03..93ad20011de 100644
--- a/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb
+++ b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb
@@ -27,7 +27,7 @@ describe Gitlab::Git::Storage::NullCircuitBreaker do
end
describe '#failure_info' do
- it { Timecop.freeze { expect(breaker.failure_info).to eq(Gitlab::Git::Storage::CircuitBreaker::FailureInfo.new(Time.now, breaker.failure_count_threshold)) } }
+ it { expect(breaker.failure_info.no_failures?).to be_falsy }
end
end
@@ -49,7 +49,7 @@ describe Gitlab::Git::Storage::NullCircuitBreaker do
end
describe '#failure_info' do
- it { expect(breaker.failure_info).to eq(Gitlab::Git::Storage::CircuitBreaker::FailureInfo.new(nil, 0)) }
+ it { expect(breaker.failure_info.no_failures?).to be_truthy }
end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index c9643c5da47..2db560c2cec 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -193,7 +193,15 @@ describe Gitlab::GitAccess do
let(:actor) { build(:rsa_deploy_key_2048, user: user) }
end
- describe '#check_project_moved!' do
+ shared_examples 'check_project_moved' do
+ it 'enqueues a redirected message' do
+ push_access_check
+
+ expect(Gitlab::Checks::ProjectMoved.fetch_redirect_message(user.id, project.id)).not_to be_nil
+ end
+ end
+
+ describe '#check_project_moved!', :clean_gitlab_redis_shared_state do
before do
project.add_master(user)
end
@@ -207,7 +215,40 @@ describe Gitlab::GitAccess do
end
end
- context 'when a redirect was followed to find the project' do
+ context 'when a permanent redirect and ssh protocol' do
+ let(:redirected_path) { 'some/other-path' }
+
+ before do
+ allow_any_instance_of(Gitlab::Checks::ProjectMoved).to receive(:permanent_redirect?).and_return(true)
+ end
+
+ it 'allows push and pull access' do
+ aggregate_failures do
+ expect { push_access_check }.not_to raise_error
+ end
+ end
+
+ it_behaves_like 'check_project_moved'
+ end
+
+ context 'with a permanent redirect and http protocol' do
+ let(:redirected_path) { 'some/other-path' }
+ let(:protocol) { 'http' }
+
+ before do
+ allow_any_instance_of(Gitlab::Checks::ProjectMoved).to receive(:permanent_redirect?).and_return(true)
+ end
+
+ it 'allows_push and pull access' do
+ aggregate_failures do
+ expect { push_access_check }.not_to raise_error
+ end
+ end
+
+ it_behaves_like 'check_project_moved'
+ end
+
+ context 'with a temporal redirect and ssh protocol' do
let(:redirected_path) { 'some/other-path' }
it 'blocks push and pull access' do
@@ -219,16 +260,15 @@ describe Gitlab::GitAccess do
expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.ssh_url_to_repo}/)
end
end
+ end
- context 'http protocol' do
- let(:protocol) { 'http' }
+ context 'with a temporal redirect and http protocol' do
+ let(:redirected_path) { 'some/other-path' }
+ let(:protocol) { 'http' }
- it 'includes the path to the project using HTTP' do
- aggregate_failures do
- expect { push_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
- expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
- end
- end
+ it 'does not allow to push and pull access' do
+ expect { push_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
+ expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/)
end
end
end
diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb
index 494dfe0e595..ce15057dd7d 100644
--- a/spec/lib/gitlab/git_spec.rb
+++ b/spec/lib/gitlab/git_spec.rb
@@ -38,4 +38,29 @@ describe Gitlab::Git do
expect(described_class.ref_name(utf8_invalid_ref)).to eq("an_invalid_ref_Ã¥")
end
end
+
+ describe '.shas_eql?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:sha1, :sha2, :result) do
+ sha = RepoHelpers.sample_commit.id
+ short_sha = sha[0, Gitlab::Git::Commit::MIN_SHA_LENGTH]
+ too_short_sha = sha[0, Gitlab::Git::Commit::MIN_SHA_LENGTH - 1]
+
+ [
+ [sha, sha, true],
+ [sha, short_sha, true],
+ [sha, sha.reverse, false],
+ [sha, too_short_sha, false],
+ [sha, nil, false]
+ ]
+ end
+
+ with_them do
+ it { expect(described_class.shas_eql?(sha1, sha2)).to eq(result) }
+ it 'is commutative' do
+ expect(described_class.shas_eql?(sha2, sha1)).to eq(result)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/identifier_spec.rb b/spec/lib/gitlab/identifier_spec.rb
index cfaeb1f0d4f..0385dd762c2 100644
--- a/spec/lib/gitlab/identifier_spec.rb
+++ b/spec/lib/gitlab/identifier_spec.rb
@@ -70,6 +70,10 @@ describe Gitlab::Identifier do
expect(identifier.identify_using_commit(project, '123')).to eq(user)
end
end
+
+ it 'returns nil if the project & ref are not present' do
+ expect(identifier.identify_using_commit(nil, nil)).to be_nil
+ end
end
describe '#identify_using_user' do
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 476a3f1998d..8ec3f55e6de 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -115,6 +115,15 @@ describe Gitlab::ReferenceExtractor do
end
end
+ it 'does not include anchors from table of contents in issue references' do
+ issue1 = create(:issue, project: project)
+ issue2 = create(:issue, project: project)
+
+ subject.analyze("not real issue <h4>#{issue1.iid}</h4>, real issue #{issue2.to_reference}")
+
+ expect(subject.issues).to match_array([issue2])
+ end
+
it 'accesses valid issue objects' do
@i0 = create(:issue, project: project)
@i1 = create(:issue, project: project)
@@ -250,4 +259,34 @@ describe Gitlab::ReferenceExtractor do
subject { described_class.references_pattern }
it { is_expected.to be_kind_of Regexp }
end
+
+ describe 'referables prefixes' do
+ def prefixes
+ described_class::REFERABLES.each_with_object({}) do |referable, result|
+ klass = referable.to_s.camelize.constantize
+
+ next unless klass.respond_to?(:reference_prefix)
+
+ prefix = klass.reference_prefix
+ result[prefix] ||= []
+ result[prefix] << referable
+ end
+ end
+
+ it 'returns all supported prefixes' do
+ expect(prefixes.keys.uniq).to match_array(%w(@ # ~ % ! $ &))
+ end
+
+ it 'does not allow one prefix for multiple referables if not allowed specificly' do
+ # make sure you are not overriding existing prefix before changing this hash
+ multiple_allowed = {
+ '@' => 3
+ }
+
+ prefixes.each do |prefix, referables|
+ expected_count = multiple_allowed[prefix] || 1
+ expect(referables.count).to eq(expected_count)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/storage_check/cli_spec.rb b/spec/lib/gitlab/storage_check/cli_spec.rb
new file mode 100644
index 00000000000..6db0925899c
--- /dev/null
+++ b/spec/lib/gitlab/storage_check/cli_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Gitlab::StorageCheck::CLI do
+ let(:options) { Gitlab::StorageCheck::Options.new('unix://tmp/socket.sock', nil, 1, false) }
+ subject(:runner) { described_class.new(options) }
+
+ describe '#update_settings' do
+ it 'updates the interval when changed in a valid response and logs the change' do
+ fake_response = double
+ expect(fake_response).to receive(:valid?).and_return(true)
+ expect(fake_response).to receive(:check_interval).and_return(42)
+ expect(runner.logger).to receive(:info)
+
+ runner.update_settings(fake_response)
+
+ expect(options.interval).to eq(42)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/storage_check/gitlab_caller_spec.rb b/spec/lib/gitlab/storage_check/gitlab_caller_spec.rb
new file mode 100644
index 00000000000..d869022fd31
--- /dev/null
+++ b/spec/lib/gitlab/storage_check/gitlab_caller_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe Gitlab::StorageCheck::GitlabCaller do
+ let(:options) { Gitlab::StorageCheck::Options.new('unix://tmp/socket.sock', nil, nil, false) }
+ subject(:gitlab_caller) { described_class.new(options) }
+
+ describe '#call!' do
+ context 'when a socket is given' do
+ it 'calls a socket' do
+ fake_connection = double
+ expect(fake_connection).to receive(:post)
+ expect(Excon).to receive(:new).with('unix://tmp/socket.sock', socket: "tmp/socket.sock") { fake_connection }
+
+ gitlab_caller.call!
+ end
+ end
+
+ context 'when a host is given' do
+ let(:options) { Gitlab::StorageCheck::Options.new('http://localhost:8080', nil, nil, false) }
+
+ it 'it calls a http response' do
+ fake_connection = double
+ expect(Excon).to receive(:new).with('http://localhost:8080', socket: nil) { fake_connection }
+ expect(fake_connection).to receive(:post)
+
+ gitlab_caller.call!
+ end
+ end
+ end
+
+ describe '#headers' do
+ it 'Adds the JSON header' do
+ headers = gitlab_caller.headers
+
+ expect(headers['Content-Type']).to eq('application/json')
+ end
+
+ context 'when a token was provided' do
+ let(:options) { Gitlab::StorageCheck::Options.new('unix://tmp/socket.sock', 'atoken', nil, false) }
+
+ it 'adds it to the headers' do
+ expect(gitlab_caller.headers['TOKEN']).to eq('atoken')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/storage_check/option_parser_spec.rb b/spec/lib/gitlab/storage_check/option_parser_spec.rb
new file mode 100644
index 00000000000..cad4dfbefcf
--- /dev/null
+++ b/spec/lib/gitlab/storage_check/option_parser_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Gitlab::StorageCheck::OptionParser do
+ describe '.parse!' do
+ it 'assigns all options' do
+ args = %w(--target unix://tmp/hello/world.sock --token thetoken --interval 42)
+
+ options = described_class.parse!(args)
+
+ expect(options.token).to eq('thetoken')
+ expect(options.interval).to eq(42)
+ expect(options.target).to eq('unix://tmp/hello/world.sock')
+ end
+
+ it 'requires the interval to be a number' do
+ args = %w(--target unix://tmp/hello/world.sock --interval fortytwo)
+
+ expect { described_class.parse!(args) }.to raise_error(OptionParser::InvalidArgument)
+ end
+
+ it 'raises an error if the scheme is not included' do
+ args = %w(--target tmp/hello/world.sock)
+
+ expect { described_class.parse!(args) }.to raise_error(OptionParser::InvalidArgument)
+ end
+
+ it 'raises an error if both socket and host are missing' do
+ expect { described_class.parse!([]) }.to raise_error(OptionParser::InvalidArgument)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/storage_check/response_spec.rb b/spec/lib/gitlab/storage_check/response_spec.rb
new file mode 100644
index 00000000000..0ff2963e443
--- /dev/null
+++ b/spec/lib/gitlab/storage_check/response_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Gitlab::StorageCheck::Response do
+ let(:fake_json) do
+ {
+ check_interval: 42,
+ results: [
+ { storage: 'working', success: true },
+ { storage: 'skipped', success: nil },
+ { storage: 'failing', success: false }
+ ]
+ }.to_json
+ end
+
+ let(:fake_http_response) do
+ fake_response = instance_double("Excon::Response - Status check")
+ allow(fake_response).to receive(:status).and_return(200)
+ allow(fake_response).to receive(:body).and_return(fake_json)
+ allow(fake_response).to receive(:headers).and_return('Content-Type' => 'application/json')
+
+ fake_response
+ end
+ let(:response) { described_class.new(fake_http_response) }
+
+ describe '#valid?' do
+ it 'is valid for a success response with parseable JSON' do
+ expect(response).to be_valid
+ end
+ end
+
+ describe '#check_interval' do
+ it 'returns the result from the JSON' do
+ expect(response.check_interval).to eq(42)
+ end
+ end
+
+ describe '#responsive_shards' do
+ it 'contains the names of working shards' do
+ expect(response.responsive_shards).to contain_exactly('working')
+ end
+ end
+
+ describe '#skipped_shards' do
+ it 'contains the names of skipped shards' do
+ expect(response.skipped_shards).to contain_exactly('skipped')
+ end
+ end
+
+ describe '#failing_shards' do
+ it 'contains the name of failing shards' do
+ expect(response.failing_shards).to contain_exactly('failing')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index 3137a72fdc4..e872a5290c5 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Utils do
- delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, to: :described_class
+ delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, :which, to: :described_class
describe '.slugify' do
{
@@ -59,4 +59,12 @@ describe Gitlab::Utils do
expect(random_string).to be_kind_of(String)
end
end
+
+ describe '.which' do
+ it 'finds the full path to an executable binary' do
+ expect(File).to receive(:executable?).with('/bin/sh').and_return(true)
+
+ expect(which('sh', 'PATH' => '/bin')).to eq('/bin/sh')
+ end
+ end
end
diff --git a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
index 9f41534441b..05f281fffff 100644
--- a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
+++ b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
@@ -57,7 +57,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do
expect(cluster.platform_type).to eq('kubernetes')
expect(cluster.project).to eq(project)
- expect(project.cluster).to eq(cluster)
+ expect(project.clusters).to include(cluster)
expect(cluster.provider_gcp.cluster).to eq(cluster)
expect(cluster.provider_gcp.status).to eq(status)
@@ -134,7 +134,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do
expect(cluster.platform_type).to eq('kubernetes')
expect(cluster.project).to eq(project)
- expect(project.cluster).to eq(cluster)
+ expect(project.clusters).to include(cluster)
expect(cluster.provider_gcp.cluster).to eq(cluster)
expect(cluster.provider_gcp.status).to eq(status)
diff --git a/spec/migrations/track_untracked_uploads_spec.rb b/spec/migrations/track_untracked_uploads_spec.rb
new file mode 100644
index 00000000000..7fe7a140e2f
--- /dev/null
+++ b/spec/migrations/track_untracked_uploads_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20171103140253_track_untracked_uploads')
+
+describe TrackUntrackedUploads, :migration, :sidekiq do
+ include TrackUntrackedUploadsHelpers
+
+ matcher :be_scheduled_migration do
+ match do |migration|
+ BackgroundMigrationWorker.jobs.any? do |job|
+ job['args'] == [migration]
+ end
+ end
+
+ failure_message do |migration|
+ "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!"
+ end
+ end
+
+ it 'correctly schedules the follow-up background migration' do
+ Sidekiq::Testing.fake! do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_migration
+ expect(BackgroundMigrationWorker.jobs.size).to eq(1)
+ end
+ end
+end
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index 49f44525b29..56b5d616284 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -5,9 +5,6 @@ describe Appearance do
it { is_expected.to be_valid }
- it { is_expected.to validate_presence_of(:title) }
- it { is_expected.to validate_presence_of(:description) }
-
it { is_expected.to have_many(:uploads).dependent(:destroy) }
describe '.current', :use_clean_rails_memory_store_caching do
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 0b7e16cc33c..ef480e7a80a 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -115,9 +115,8 @@ describe ApplicationSetting do
end
context 'circuitbreaker settings' do
- [:circuitbreaker_backoff_threshold,
- :circuitbreaker_failure_count_threshold,
- :circuitbreaker_failure_wait_time,
+ [:circuitbreaker_failure_count_threshold,
+ :circuitbreaker_check_interval,
:circuitbreaker_failure_reset_time,
:circuitbreaker_storage_timeout].each do |field|
it "Validates #{field} as number" do
@@ -126,16 +125,6 @@ describe ApplicationSetting do
.is_greater_than_or_equal_to(0)
end
end
-
- it 'requires the `backoff_threshold` to be lower than the `failure_count_threshold`' do
- setting.circuitbreaker_failure_count_threshold = 10
- setting.circuitbreaker_backoff_threshold = 15
- failure_message = "The circuitbreaker backoff threshold should be lower "\
- "than the failure count threshold"
-
- expect(setting).not_to be_valid
- expect(setting.errors[:circuitbreaker_backoff_threshold]).to include(failure_message)
- end
end
context 'repository storages' do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 9070692abfe..a6258676767 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1868,6 +1868,94 @@ describe Ci::Build do
end
end
+ describe 'state transition: any => [:running]' do
+ shared_examples 'validation is active' do
+ context 'when depended job has not been completed yet' do
+ let!(:pre_stage_job) { create(:ci_build, :running, pipeline: pipeline, name: 'test', stage_idx: 0) }
+
+ it { expect { job.run! }.to raise_error(Ci::Build::MissingDependenciesError) }
+ end
+
+ context 'when artifacts of depended job has been expired' do
+ let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
+
+ it { expect { job.run! }.to raise_error(Ci::Build::MissingDependenciesError) }
+ end
+
+ context 'when artifacts of depended job has been erased' do
+ let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
+
+ before do
+ pre_stage_job.erase
+ end
+
+ it { expect { job.run! }.to raise_error(Ci::Build::MissingDependenciesError) }
+ end
+ end
+
+ shared_examples 'validation is not active' do
+ context 'when depended job has not been completed yet' do
+ let!(:pre_stage_job) { create(:ci_build, :running, pipeline: pipeline, name: 'test', stage_idx: 0) }
+
+ it { expect { job.run! }.not_to raise_error }
+ end
+
+ context 'when artifacts of depended job has been expired' do
+ let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
+
+ it { expect { job.run! }.not_to raise_error }
+ end
+
+ context 'when artifacts of depended job has been erased' do
+ let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
+
+ before do
+ pre_stage_job.erase
+ end
+
+ it { expect { job.run! }.not_to raise_error }
+ end
+ end
+
+ let!(:job) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 1, options: options) }
+
+ context 'when validates for dependencies is enabled' do
+ before do
+ stub_feature_flags(ci_disable_validates_dependencies: false)
+ end
+
+ let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) }
+
+ context 'when "dependencies" keyword is not defined' do
+ let(:options) { {} }
+
+ it { expect { job.run! }.not_to raise_error }
+ end
+
+ context 'when "dependencies" keyword is empty' do
+ let(:options) { { dependencies: [] } }
+
+ it { expect { job.run! }.not_to raise_error }
+ end
+
+ context 'when "dependencies" keyword is specified' do
+ let(:options) { { dependencies: ['test'] } }
+
+ it_behaves_like 'validation is active'
+ end
+ end
+
+ context 'when validates for dependencies is disabled' do
+ let(:options) { { dependencies: ['test'] } }
+
+ before do
+ stub_feature_flags(ci_disable_validates_dependencies: true)
+ end
+
+ it_behaves_like 'validation is not active'
+ end
+ end
+
describe 'state transition when build fails' do
let(:service) { MergeRequests::AddTodoWhenBuildFailsService.new(project, user) }
@@ -1921,4 +2009,77 @@ describe Ci::Build do
end
end
end
+
+ describe '.matches_tag_ids' do
+ set(:build) { create(:ci_build, project: project, user: user) }
+ let(:tag_ids) { ::ActsAsTaggableOn::Tag.named_any(tag_list).ids }
+
+ subject { described_class.where(id: build).matches_tag_ids(tag_ids) }
+
+ before do
+ build.update(tag_list: build_tag_list)
+ end
+
+ context 'when have different tags' do
+ let(:build_tag_list) { %w(A B) }
+ let(:tag_list) { %w(C D) }
+
+ it "does not match a build" do
+ is_expected.not_to contain_exactly(build)
+ end
+ end
+
+ context 'when have a subset of tags' do
+ let(:build_tag_list) { %w(A B) }
+ let(:tag_list) { %w(A B C D) }
+
+ it "does match a build" do
+ is_expected.to contain_exactly(build)
+ end
+ end
+
+ context 'when build does not have tags' do
+ let(:build_tag_list) { [] }
+ let(:tag_list) { %w(C D) }
+
+ it "does match a build" do
+ is_expected.to contain_exactly(build)
+ end
+ end
+
+ context 'when does not have a subset of tags' do
+ let(:build_tag_list) { %w(A B C) }
+ let(:tag_list) { %w(C D) }
+
+ it "does not match a build" do
+ is_expected.not_to contain_exactly(build)
+ end
+ end
+ end
+
+ describe '.matches_tags' do
+ set(:build) { create(:ci_build, project: project, user: user) }
+
+ subject { described_class.where(id: build).with_any_tags }
+
+ before do
+ build.update(tag_list: tag_list)
+ end
+
+ context 'when does have tags' do
+ let(:tag_list) { %w(A B) }
+
+ it "does match a build" do
+ is_expected.to contain_exactly(build)
+ end
+ end
+
+ context 'when does not have tags' do
+ let(:tag_list) { [] }
+
+ it "does not match a build" do
+ is_expected.not_to contain_exactly(build)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index d4b1e7c8dd4..bb89e093890 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1244,7 +1244,7 @@ describe Ci::Pipeline, :mailer do
describe '#execute_hooks' do
let!(:build_a) { create_build('a', 0) }
- let!(:build_b) { create_build('b', 1) }
+ let!(:build_b) { create_build('b', 0) }
let!(:hook) do
create(:project_hook, project: project, pipeline_events: enabled)
@@ -1300,6 +1300,8 @@ describe Ci::Pipeline, :mailer do
end
context 'when stage one failed' do
+ let!(:build_b) { create_build('b', 1) }
+
before do
build_a.drop
end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 7f43e747000..2683d21ddbe 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -198,4 +198,26 @@ describe Clusters::Cluster do
end
end
end
+
+ describe '#created?' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ subject { cluster.created? }
+
+ context 'when status_name is :created' do
+ before do
+ allow(cluster).to receive_message_chain(:provider, :status_name).and_return(:created)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when status_name is not :created' do
+ before do
+ allow(cluster).to receive_message_chain(:provider, :status_name).and_return(:creating)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index a53b59c4e08..9df26f06a11 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -171,7 +171,7 @@ describe Issuable do
it "returns false when record has been updated" do
allow(issue).to receive(:today?).and_return(true)
- issue.touch
+ issue.update_attribute(:updated_at, 1.hour.ago)
expect(issue.new?).to be_falsey
end
end
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index 8389d5c5430..4d0b3245a13 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -9,13 +9,14 @@ describe DiffNote do
let(:path) { "files/ruby/popen.rb" }
+ let(:diff_refs) { merge_request.diff_refs }
let!(:position) do
Gitlab::Diff::Position.new(
old_path: path,
new_path: path,
old_line: nil,
new_line: 14,
- diff_refs: merge_request.diff_refs
+ diff_refs: diff_refs
)
end
@@ -25,7 +26,7 @@ describe DiffNote do
new_path: path,
old_line: 16,
new_line: 22,
- diff_refs: merge_request.diff_refs
+ diff_refs: diff_refs
)
end
@@ -158,25 +159,21 @@ describe DiffNote do
describe "creation" do
describe "updating of position" do
context "when noteable is a commit" do
- let(:diff_note) { create(:diff_note_on_commit, project: project, position: position) }
+ let(:diff_refs) { commit.diff_refs }
- it "doesn't update the position" do
- diff_note
+ subject { create(:diff_note_on_commit, project: project, position: position, commit_id: commit.id) }
- expect(diff_note.original_position).to eq(position)
- expect(diff_note.position).to eq(position)
+ it "doesn't update the position" do
+ is_expected.to have_attributes(original_position: position,
+ position: position)
end
end
context "when noteable is a merge request" do
- let(:diff_note) { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) }
-
context "when the note is active" do
it "doesn't update the position" do
- diff_note
-
- expect(diff_note.original_position).to eq(position)
- expect(diff_note.position).to eq(position)
+ expect(subject.original_position).to eq(position)
+ expect(subject.position).to eq(position)
end
end
@@ -186,10 +183,8 @@ describe DiffNote do
end
it "updates the position" do
- diff_note
-
- expect(diff_note.original_position).to eq(position)
- expect(diff_note.position).not_to eq(position)
+ expect(subject.original_position).to eq(position)
+ expect(subject.position).not_to eq(position)
end
end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 5f901262598..0ea287d007a 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -765,4 +765,8 @@ describe Issue do
expect(described_class.public_only).to eq([public_issue])
end
end
+
+ it_behaves_like 'throttled touch' do
+ subject { create(:issue, updated_at: 1.hour.ago) }
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 728028746d8..30a5a3bbff7 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -827,20 +827,47 @@ describe MergeRequest do
end
end
- describe '#head_pipeline' do
- describe 'when the source project exists' do
- it 'returns the latest pipeline' do
- pipeline = create(:ci_empty_pipeline, project: subject.source_project, ref: 'master', status: 'running', sha: "123abc", head_pipeline_of: subject)
+ context 'head pipeline' do
+ before do
+ allow(subject).to receive(:diff_head_sha).and_return('lastsha')
+ end
+
+ describe '#head_pipeline' do
+ it 'returns nil for MR without head_pipeline_id' do
+ subject.update_attribute(:head_pipeline_id, nil)
- expect(subject.head_pipeline).to eq(pipeline)
+ expect(subject.head_pipeline).to be_nil
+ end
+
+ context 'when the source project does not exist' do
+ it 'returns nil' do
+ allow(subject).to receive(:source_project).and_return(nil)
+
+ expect(subject.head_pipeline).to be_nil
+ end
end
end
- describe 'when the source project does not exist' do
- it 'returns nil' do
+ describe '#actual_head_pipeline' do
+ it 'returns nil for MR with old pipeline' do
+ pipeline = create(:ci_empty_pipeline, sha: 'notlatestsha')
+ subject.update_attribute(:head_pipeline_id, pipeline.id)
+
+ expect(subject.actual_head_pipeline).to be_nil
+ end
+
+ it 'returns the pipeline for MR with recent pipeline' do
+ pipeline = create(:ci_empty_pipeline, sha: 'lastsha')
+ subject.update_attribute(:head_pipeline_id, pipeline.id)
+
+ expect(subject.actual_head_pipeline).to eq(subject.head_pipeline)
+ expect(subject.actual_head_pipeline).to eq(pipeline)
+ end
+
+ it 'returns nil when source project does not exist' do
allow(subject).to receive(:source_project).and_return(nil)
- expect(subject.head_pipeline).to be_nil
+ expect(subject.actual_head_pipeline).to be_nil
end
end
end
@@ -940,7 +967,7 @@ describe MergeRequest do
end
shared_examples 'returning all SHA' do
- it 'returns all SHA from all merge_request_diffs' do
+ it 'returns all SHAs from all merge_request_diffs' do
expect(subject.merge_request_diffs.size).to eq(2)
expect(subject.all_commit_shas).to match_array(all_commit_shas)
end
@@ -1179,7 +1206,7 @@ describe MergeRequest do
context 'when it is only allowed to merge when build is green' do
context 'and a failed pipeline is associated' do
before do
- pipeline.update(status: 'failed')
+ pipeline.update(status: 'failed', sha: subject.diff_head_sha)
allow(subject).to receive(:head_pipeline) { pipeline }
end
@@ -1188,7 +1215,7 @@ describe MergeRequest do
context 'and a successful pipeline is associated' do
before do
- pipeline.update(status: 'success')
+ pipeline.update(status: 'success', sha: subject.diff_head_sha)
allow(subject).to receive(:head_pipeline) { pipeline }
end
@@ -1197,7 +1224,7 @@ describe MergeRequest do
context 'and a skipped pipeline is associated' do
before do
- pipeline.update(status: 'skipped')
+ pipeline.update(status: 'skipped', sha: subject.diff_head_sha)
allow(subject).to receive(:head_pipeline) { pipeline }
end
@@ -1824,4 +1851,8 @@ describe MergeRequest do
.to change { project.open_merge_requests_count }.from(1).to(0)
end
end
+
+ it_behaves_like 'throttled touch' do
+ subject { create(:merge_request, updated_at: 1.hour.ago) }
+ end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 3817f20bfe7..b7c6286fd83 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -559,4 +559,34 @@ describe Namespace do
end
end
end
+
+ describe "#allowed_path_by_redirects" do
+ let(:namespace1) { create(:namespace, path: 'foo') }
+
+ context "when the path has been taken before" do
+ before do
+ namespace1.path = 'bar'
+ namespace1.save!
+ end
+
+ it 'should be invalid' do
+ namespace2 = build(:group, path: 'foo')
+ expect(namespace2).to be_invalid
+ end
+
+ it 'should return an error on path' do
+ namespace2 = build(:group, path: 'foo')
+ namespace2.valid?
+ expect(namespace2.errors.messages[:path].first).to eq('foo has been taken before. Please use another one')
+ end
+ end
+
+ context "when the path has not been taken before" do
+ it 'should be valid' do
+ expect(RedirectRoute.count).to eq(0)
+ namespace = build(:namespace)
+ expect(namespace).to be_valid
+ end
+ end
+ end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 6e7e8c4c570..e1a0c55b6a6 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -5,7 +5,7 @@ describe Note do
describe 'associations' do
it { is_expected.to belong_to(:project) }
- it { is_expected.to belong_to(:noteable).touch(true) }
+ it { is_expected.to belong_to(:noteable).touch(false) }
it { is_expected.to belong_to(:author).class_name('User') }
it { is_expected.to have_many(:todos).dependent(:destroy) }
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index 01440b15674..2bb1c49b740 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe PersonalAccessToken do
+ subject { described_class }
+
describe '.build' do
let(:personal_access_token) { build(:personal_access_token) }
let(:invalid_personal_access_token) { build(:personal_access_token, :invalid) }
@@ -45,6 +47,29 @@ describe PersonalAccessToken do
end
end
+ describe 'Redis storage' do
+ let(:user_id) { 123 }
+ let(:token) { 'abc000foo' }
+
+ before do
+ subject.redis_store!(user_id, token)
+ end
+
+ it 'returns stored data' do
+ expect(subject.redis_getdel(user_id)).to eq(token)
+ end
+
+ context 'after deletion' do
+ before do
+ expect(subject.redis_getdel(user_id)).to eq(token)
+ end
+
+ it 'token is removed' do
+ expect(subject.redis_getdel(user_id)).to be_nil
+ end
+ end
+ end
+
context "validations" do
let(:personal_access_token) { build(:personal_access_token) }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 6bda1eb15a8..f4699fd243d 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -78,7 +78,7 @@ describe Project do
it { is_expected.to have_many(:uploads).dependent(:destroy) }
it { is_expected.to have_many(:pipeline_schedules) }
it { is_expected.to have_many(:members_and_requesters) }
- it { is_expected.to have_one(:cluster) }
+ it { is_expected.to have_many(:clusters) }
it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') }
context 'after initialized' do
@@ -313,7 +313,6 @@ describe Project do
it { is_expected.to delegate_method(method).to(:team) }
end
- it { is_expected.to delegate_method(:empty_repo?).to(:repository) }
it { is_expected.to delegate_method(:members).to(:team).with_prefix(true) }
it { is_expected.to delegate_method(:name).to(:owner).with_prefix(true).with_arguments(allow_nil: true) }
end
@@ -656,6 +655,24 @@ describe Project do
end
end
+ describe '#empty_repo?' do
+ context 'when the repo does not exist' do
+ let(:project) { build_stubbed(:project) }
+
+ it 'returns true' do
+ expect(project.empty_repo?).to be(true)
+ end
+ end
+
+ context 'when the repo exists' do
+ let(:project) { create(:project, :repository) }
+ let(:empty_project) { create(:project, :empty_repo) }
+
+ it { expect(empty_project.empty_repo?).to be(true) }
+ it { expect(project.empty_repo?).to be(false) }
+ end
+ end
+
describe '#external_issue_tracker' do
let(:project) { create(:project) }
let(:ext_project) { create(:redmine_project) }
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index af0c86abe86..358bc3dfb94 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -29,7 +29,9 @@ describe Repository do
def expect_to_raise_storage_error
expect { yield }.to raise_error do |exception|
storage_exceptions = [Gitlab::Git::Storage::Inaccessible, Gitlab::Git::CommandError, GRPC::Unavailable]
- expect(exception.class).to be_in(storage_exceptions)
+ known_exception = storage_exceptions.select { |e| exception.is_a?(e) }
+
+ expect(known_exception).not_to be_nil
end
end
@@ -583,7 +585,7 @@ describe Repository do
end
it 'properly handles query when repo is empty' do
- repository = create(:project).repository
+ repository = create(:project, :empty_repo).repository
results = repository.search_files_by_content('test', 'master')
expect(results).to match_array([])
@@ -619,7 +621,7 @@ describe Repository do
end
it 'properly handles query when repo is empty' do
- repository = create(:project).repository
+ repository = create(:project, :empty_repo).repository
results = repository.search_files_by_name('test', 'master')
@@ -634,9 +636,7 @@ describe Repository do
end
describe '#fetch_ref' do
- # Setting the var here, sidesteps the stub that makes gitaly raise an error
- # before the actual test call
- set(:broken_repository) { create(:project, :broken_storage).repository }
+ let(:broken_repository) { create(:project, :broken_storage).repository }
describe 'when storage is broken', :broken_storage do
it 'should raise a storage error' do
@@ -1204,17 +1204,15 @@ describe Repository do
let(:empty_repository) { create(:project_empty_repo).repository }
it 'returns true for an empty repository' do
- expect(empty_repository.empty?).to eq(true)
+ expect(empty_repository).to be_empty
end
it 'returns false for a non-empty repository' do
- expect(repository.empty?).to eq(false)
+ expect(repository).not_to be_empty
end
it 'caches the output' do
- expect(repository.raw_repository).to receive(:empty?)
- .once
- .and_return(false)
+ expect(repository.raw_repository).to receive(:has_visible_content?).once
repository.empty?
repository.empty?
@@ -1372,38 +1370,48 @@ describe Repository do
end
describe '#revert' do
- let(:new_image_commit) { repository.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') }
- let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
- let(:message) { 'revert message' }
+ shared_examples 'reverting a commit' do
+ let(:new_image_commit) { repository.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') }
+ let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
+ let(:message) { 'revert message' }
- context 'when there is a conflict' do
- it 'raises an error' do
- expect { repository.revert(user, new_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
+ context 'when there is a conflict' do
+ it 'raises an error' do
+ expect { repository.revert(user, new_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
+ end
end
- end
- context 'when commit was already reverted' do
- it 'raises an error' do
- repository.revert(user, update_image_commit, 'master', message)
+ context 'when commit was already reverted' do
+ it 'raises an error' do
+ repository.revert(user, update_image_commit, 'master', message)
+
+ expect { repository.revert(user, update_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
+ end
+ end
- expect { repository.revert(user, update_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
+ context 'when commit can be reverted' do
+ it 'reverts the changes' do
+ expect(repository.revert(user, update_image_commit, 'master', message)).to be_truthy
+ end
end
- end
- context 'when commit can be reverted' do
- it 'reverts the changes' do
- expect(repository.revert(user, update_image_commit, 'master', message)).to be_truthy
+ context 'reverting a merge commit' do
+ it 'reverts the changes' do
+ merge_commit
+ expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).to be_present
+
+ repository.revert(user, merge_commit, 'master', message)
+ expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_to be_present
+ end
end
end
- context 'reverting a merge commit' do
- it 'reverts the changes' do
- merge_commit
- expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).to be_present
+ context 'when Gitaly revert feature is enabled' do
+ it_behaves_like 'reverting a commit'
+ end
- repository.revert(user, merge_commit, 'master', message)
- expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_to be_present
- end
+ context 'when Gitaly revert feature is disabled', :disable_gitaly do
+ it_behaves_like 'reverting a commit'
end
end
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index fece370c03f..ddad6862a63 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -87,6 +87,7 @@ describe Route do
end
context 'when conflicting redirects exist' do
+ let(:route) { create(:project).route }
let!(:conflicting_redirect1) { route.create_redirect('bar/test') }
let!(:conflicting_redirect2) { route.create_redirect('bar/test/foo') }
let!(:conflicting_redirect3) { route.create_redirect('gitlab-org') }
@@ -141,11 +142,50 @@ describe Route do
expect(redirect_route.source).to eq(route.source)
expect(redirect_route.path).to eq('foo')
end
+
+ context 'when the source is a Project' do
+ it 'creates a temporal RedirectRoute' do
+ project = create(:project)
+ route = project.route
+ redirect_route = route.create_redirect('foo')
+ expect(redirect_route.permanent?).to be_falsy
+ end
+ end
+
+ context 'when the source is not a project' do
+ it 'creates a permanent RedirectRoute' do
+ redirect_route = route.create_redirect('foo', permanent: true)
+ expect(redirect_route.permanent?).to be_truthy
+ end
+ end
end
describe '#delete_conflicting_redirects' do
+ context 'with permanent redirect' do
+ it 'does not delete the redirect' do
+ route.create_redirect("#{route.path}/foo", permanent: true)
+
+ expect do
+ route.delete_conflicting_redirects
+ end.not_to change { RedirectRoute.count }
+ end
+ end
+
+ context 'with temporal redirect' do
+ let(:route) { create(:project).route }
+
+ it 'deletes the redirect' do
+ route.create_redirect("#{route.path}/foo")
+
+ expect do
+ route.delete_conflicting_redirects
+ end.to change { RedirectRoute.count }.by(-1)
+ end
+ end
+
context 'when a redirect route with the same path exists' do
context 'when the redirect route has matching case' do
+ let(:route) { create(:project).route }
let!(:redirect1) { route.create_redirect(route.path) }
it 'deletes the redirect' do
@@ -169,6 +209,7 @@ describe Route do
end
context 'when the redirect route is differently cased' do
+ let(:route) { create(:project).route }
let!(:redirect1) { route.create_redirect(route.path.upcase) }
it 'deletes the redirect' do
@@ -185,7 +226,32 @@ describe Route do
expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation)
end
+ context 'with permanent redirects' do
+ it 'does not return anything' do
+ route.create_redirect("#{route.path}/foo", permanent: true)
+ route.create_redirect("#{route.path}/foo/bar", permanent: true)
+ route.create_redirect("#{route.path}/baz/quz", permanent: true)
+
+ expect(route.conflicting_redirects).to be_empty
+ end
+ end
+
+ context 'with temporal redirects' do
+ let(:route) { create(:project).route }
+
+ it 'returns the redirect routes' do
+ route = create(:project).route
+ redirect1 = route.create_redirect("#{route.path}/foo")
+ redirect2 = route.create_redirect("#{route.path}/foo/bar")
+ redirect3 = route.create_redirect("#{route.path}/baz/quz")
+
+ expect(route.conflicting_redirects).to match_array([redirect1, redirect2, redirect3])
+ end
+ end
+
context 'when a redirect route with the same path exists' do
+ let(:route) { create(:project).route }
+
context 'when the redirect route has matching case' do
let!(:redirect1) { route.create_redirect(route.path) }
@@ -214,4 +280,42 @@ describe Route do
end
end
end
+
+ describe "#conflicting_redirect_exists?" do
+ context 'when a conflicting redirect exists' do
+ let(:group1) { create(:group, path: 'foo') }
+ let(:group2) { create(:group, path: 'baz') }
+
+ it 'should not be saved' do
+ group1.path = 'bar'
+ group1.save
+
+ group2.path = 'foo'
+
+ expect(group2.save).to be_falsy
+ end
+
+ it 'should return an error on path' do
+ group1.path = 'bar'
+ group1.save
+
+ group2.path = 'foo'
+ group2.valid?
+ expect(group2.errors["route.path"].first).to eq('foo has been taken before. Please use another one')
+ end
+ end
+
+ context 'when a conflicting redirect does not exist' do
+ let(:project1) { create(:project, path: 'foo') }
+ let(:project2) { create(:project, path: 'baz') }
+
+ it 'should be saved' do
+ project1.path = 'bar'
+ project1.save
+
+ project2.path = 'foo'
+ expect(project2.save).to be_truthy
+ end
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index b27c1b2cd1a..cdabd35b6ba 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2433,4 +2433,187 @@ describe User do
expect(user).not_to be_blocked
end
end
+
+ describe '#max_member_access_for_project_ids' do
+ shared_examples 'max member access for projects' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:owner_project) { create(:project, group: group) }
+ let(:master_project) { create(:project) }
+ let(:reporter_project) { create(:project) }
+ let(:developer_project) { create(:project) }
+ let(:guest_project) { create(:project) }
+ let(:no_access_project) { create(:project) }
+
+ let(:projects) do
+ [owner_project, master_project, reporter_project, developer_project, guest_project, no_access_project].map(&:id)
+ end
+
+ let(:expected) do
+ {
+ owner_project.id => Gitlab::Access::OWNER,
+ master_project.id => Gitlab::Access::MASTER,
+ reporter_project.id => Gitlab::Access::REPORTER,
+ developer_project.id => Gitlab::Access::DEVELOPER,
+ guest_project.id => Gitlab::Access::GUEST,
+ no_access_project.id => Gitlab::Access::NO_ACCESS
+ }
+ end
+
+ before do
+ create(:group_member, user: user, group: group)
+ master_project.add_master(user)
+ reporter_project.add_reporter(user)
+ developer_project.add_developer(user)
+ guest_project.add_guest(user)
+ end
+
+ it 'returns correct roles for different projects' do
+ expect(user.max_member_access_for_project_ids(projects)).to eq(expected)
+ end
+ end
+
+ context 'with RequestStore enabled', :request_store do
+ include_examples 'max member access for projects'
+
+ def access_levels(projects)
+ user.max_member_access_for_project_ids(projects)
+ end
+
+ it 'does not perform extra queries when asked for projects who have already been found' do
+ access_levels(projects)
+
+ expect { access_levels(projects) }.not_to exceed_query_limit(0)
+
+ expect(access_levels(projects)).to eq(expected)
+ end
+
+ it 'only requests the extra projects when uncached projects are passed' do
+ second_master_project = create(:project)
+ second_developer_project = create(:project)
+ second_master_project.add_master(user)
+ second_developer_project.add_developer(user)
+
+ all_projects = projects + [second_master_project.id, second_developer_project.id]
+
+ expected_all = expected.merge(second_master_project.id => Gitlab::Access::MASTER,
+ second_developer_project.id => Gitlab::Access::DEVELOPER)
+
+ access_levels(projects)
+
+ queries = ActiveRecord::QueryRecorder.new { access_levels(all_projects) }
+
+ expect(queries.count).to eq(1)
+ expect(queries.log_message).to match(/\W(#{second_master_project.id}, #{second_developer_project.id})\W/)
+ expect(access_levels(all_projects)).to eq(expected_all)
+ end
+ end
+
+ context 'with RequestStore disabled' do
+ include_examples 'max member access for projects'
+ end
+ end
+
+ describe '#max_member_access_for_group_ids' do
+ shared_examples 'max member access for groups' do
+ let(:user) { create(:user) }
+ let(:owner_group) { create(:group) }
+ let(:master_group) { create(:group) }
+ let(:reporter_group) { create(:group) }
+ let(:developer_group) { create(:group) }
+ let(:guest_group) { create(:group) }
+ let(:no_access_group) { create(:group) }
+
+ let(:groups) do
+ [owner_group, master_group, reporter_group, developer_group, guest_group, no_access_group].map(&:id)
+ end
+
+ let(:expected) do
+ {
+ owner_group.id => Gitlab::Access::OWNER,
+ master_group.id => Gitlab::Access::MASTER,
+ reporter_group.id => Gitlab::Access::REPORTER,
+ developer_group.id => Gitlab::Access::DEVELOPER,
+ guest_group.id => Gitlab::Access::GUEST,
+ no_access_group.id => Gitlab::Access::NO_ACCESS
+ }
+ end
+
+ before do
+ owner_group.add_owner(user)
+ master_group.add_master(user)
+ reporter_group.add_reporter(user)
+ developer_group.add_developer(user)
+ guest_group.add_guest(user)
+ end
+
+ it 'returns correct roles for different groups' do
+ expect(user.max_member_access_for_group_ids(groups)).to eq(expected)
+ end
+ end
+
+ context 'with RequestStore enabled', :request_store do
+ include_examples 'max member access for groups'
+
+ def access_levels(groups)
+ user.max_member_access_for_group_ids(groups)
+ end
+
+ it 'does not perform extra queries when asked for groups who have already been found' do
+ access_levels(groups)
+
+ expect { access_levels(groups) }.not_to exceed_query_limit(0)
+
+ expect(access_levels(groups)).to eq(expected)
+ end
+
+ it 'only requests the extra groups when uncached groups are passed' do
+ second_master_group = create(:group)
+ second_developer_group = create(:group)
+ second_master_group.add_master(user)
+ second_developer_group.add_developer(user)
+
+ all_groups = groups + [second_master_group.id, second_developer_group.id]
+
+ expected_all = expected.merge(second_master_group.id => Gitlab::Access::MASTER,
+ second_developer_group.id => Gitlab::Access::DEVELOPER)
+
+ access_levels(groups)
+
+ queries = ActiveRecord::QueryRecorder.new { access_levels(all_groups) }
+
+ expect(queries.count).to eq(1)
+ expect(queries.log_message).to match(/\W(#{second_master_group.id}, #{second_developer_group.id})\W/)
+ expect(access_levels(all_groups)).to eq(expected_all)
+ end
+ end
+
+ context 'with RequestStore disabled' do
+ include_examples 'max member access for groups'
+ end
+ end
+
+ describe "#username_previously_taken?" do
+ let(:user1) { create(:user, username: 'foo') }
+
+ context 'when the username has been taken before' do
+ before do
+ user1.username = 'bar'
+ user1.save!
+ end
+
+ it 'should raise an ActiveRecord::RecordInvalid exception' do
+ user2 = build(:user, username: 'foo')
+ expect { user2.save! }.to raise_error(ActiveRecord::RecordInvalid, /Path foo has been taken before/)
+ end
+ end
+
+ context 'when the username has not been taken before' do
+ it 'should be valid' do
+ expect(RedirectRoute.count).to eq(0)
+ user2 = build(:user, username: 'baz')
+ expect(user2).to be_valid
+ end
+ end
+ end
end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 4f4e634829d..b4d25e06d9a 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -9,6 +9,8 @@ describe GroupPolicy do
let(:admin) { create(:admin) }
let(:group) { create(:group) }
+ let(:guest_permissions) { [:read_group, :upload_file, :read_namespace] }
+
let(:reporter_permissions) { [:admin_label] }
let(:developer_permissions) { [:admin_milestones] }
@@ -52,6 +54,7 @@ describe GroupPolicy do
it do
expect_allowed(:read_group)
+ expect_disallowed(:upload_file)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
@@ -64,7 +67,7 @@ describe GroupPolicy do
let(:current_user) { guest }
it do
- expect_allowed(:read_group, :read_namespace)
+ expect_allowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
@@ -76,7 +79,7 @@ describe GroupPolicy do
let(:current_user) { reporter }
it do
- expect_allowed(:read_group, :read_namespace)
+ expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
@@ -88,7 +91,7 @@ describe GroupPolicy do
let(:current_user) { developer }
it do
- expect_allowed(:read_group, :read_namespace)
+ expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
expect_disallowed(*master_permissions)
@@ -100,7 +103,7 @@ describe GroupPolicy do
let(:current_user) { master }
it do
- expect_allowed(:read_group, :read_namespace)
+ expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
@@ -114,7 +117,7 @@ describe GroupPolicy do
it do
allow(Group).to receive(:supports_nested_groups?).and_return(true)
- expect_allowed(:read_group, :read_namespace)
+ expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
@@ -128,7 +131,7 @@ describe GroupPolicy do
it do
allow(Group).to receive(:supports_nested_groups?).and_return(true)
- expect_allowed(:read_group, :read_namespace)
+ expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
@@ -187,7 +190,7 @@ describe GroupPolicy do
let(:current_user) { nil }
it do
- expect_disallowed(:read_group)
+ expect_disallowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
@@ -199,7 +202,7 @@ describe GroupPolicy do
let(:current_user) { guest }
it do
- expect_allowed(:read_group)
+ expect_allowed(*guest_permissions)
expect_disallowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
@@ -211,7 +214,7 @@ describe GroupPolicy do
let(:current_user) { reporter }
it do
- expect_allowed(:read_group)
+ expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
@@ -223,7 +226,7 @@ describe GroupPolicy do
let(:current_user) { developer }
it do
- expect_allowed(:read_group)
+ expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
expect_disallowed(*master_permissions)
@@ -235,7 +238,7 @@ describe GroupPolicy do
let(:current_user) { master }
it do
- expect_allowed(:read_group)
+ expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
@@ -249,7 +252,7 @@ describe GroupPolicy do
it do
allow(Group).to receive(:supports_nested_groups?).and_return(true)
- expect_allowed(:read_group)
+ expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb
index 48d4f3671c5..e96dbfb73c0 100644
--- a/spec/presenters/clusters/cluster_presenter_spec.rb
+++ b/spec/presenters/clusters/cluster_presenter_spec.rb
@@ -31,4 +31,44 @@ describe Clusters::ClusterPresenter do
it { is_expected.to include(cluster.provider.zone) }
it { is_expected.to include(cluster.name) }
end
+
+ describe '#can_toggle_cluster' do
+ let(:user) { create(:user) }
+
+ before do
+ allow(cluster).to receive(:current_user).and_return(user)
+ end
+
+ subject { described_class.new(cluster).can_toggle_cluster? }
+
+ context 'when user can update' do
+ before do
+ allow_any_instance_of(described_class).to receive(:can?).with(user, :update_cluster, cluster).and_return(true)
+ end
+
+ context 'when cluster is created' do
+ before do
+ allow(cluster).to receive(:created?).and_return(true)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when cluster is not created' do
+ before do
+ allow(cluster).to receive(:created?).and_return(false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'when user can not update' do
+ before do
+ allow_any_instance_of(described_class).to receive(:can?).with(user, :update_cluster, cluster).and_return(false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
end
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index 5e114434a67..f325d1776e4 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -31,7 +31,7 @@ describe MergeRequestPresenter do
let(:pipeline) { build_stubbed(:ci_pipeline) }
before do
- allow(resource).to receive(:head_pipeline).and_return(pipeline)
+ allow(resource).to receive(:actual_head_pipeline).and_return(pipeline)
end
context 'success with warnings' do
diff --git a/spec/requests/api/circuit_breakers_spec.rb b/spec/requests/api/circuit_breakers_spec.rb
index 3b858c40fd6..fe76f057115 100644
--- a/spec/requests/api/circuit_breakers_spec.rb
+++ b/spec/requests/api/circuit_breakers_spec.rb
@@ -47,7 +47,7 @@ describe API::CircuitBreakers do
describe 'DELETE circuit_breakers/repository_storage' do
it 'clears all circuit_breakers' do
- expect(Gitlab::Git::Storage::CircuitBreaker).to receive(:reset_all!)
+ expect(Gitlab::Git::Storage::FailureInfo).to receive(:reset_all!)
delete api('/circuit_breakers/repository_storage', admin)
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 67e1539cbc3..3c31980b273 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -537,16 +537,7 @@ describe API::Internal do
context 'the project path was changed' do
let!(:old_path_to_repo) { project.repository.path_to_repo }
- let!(:old_full_path) { project.full_path }
- let(:project_moved_message) do
- <<-MSG.strip_heredoc
- Project '#{old_full_path}' was moved to '#{project.full_path}'.
-
- Please update your Git remote and try again:
-
- git remote set-url origin #{project.ssh_url_to_repo}
- MSG
- end
+ let!(:repository) { project.repository }
before do
project.team << [user, :developer]
@@ -555,19 +546,17 @@ describe API::Internal do
end
it 'rejects the push' do
- push_with_path(key, old_path_to_repo)
+ push(key, project)
expect(response).to have_gitlab_http_status(200)
- expect(json_response['status']).to be_falsey
- expect(json_response['message']).to eq(project_moved_message)
+ expect(json_response['status']).to be_falsy
end
it 'rejects the SSH pull' do
- pull_with_path(key, old_path_to_repo)
+ pull(key, project)
expect(response).to have_gitlab_http_status(200)
- expect(json_response['status']).to be_falsey
- expect(json_response['message']).to eq(project_moved_message)
+ expect(json_response['status']).to be_falsy
end
end
end
@@ -695,7 +684,7 @@ describe API::Internal do
# end
# end
- describe 'POST /internal/post_receive' do
+ describe 'POST /internal/post_receive', :clean_gitlab_redis_shared_state do
let(:identifier) { 'key-123' }
let(:valid_params) do
@@ -713,6 +702,8 @@ describe API::Internal do
before do
project.team << [user, :developer]
+ allow(described_class).to receive(:identify).and_return(user)
+ allow_any_instance_of(Gitlab::Identifier).to receive(:identify).and_return(user)
end
it 'enqueues a PostReceive worker job' do
@@ -780,6 +771,19 @@ describe API::Internal do
expect(json_response['broadcast_message']).to eq(nil)
end
end
+
+ context 'with a redirected data' do
+ it 'returns redirected message on the response' do
+ project_moved = Gitlab::Checks::ProjectMoved.new(project, user, 'foo/baz', 'http')
+ project_moved.add_redirect_message
+
+ post api("/internal/post_receive"), valid_params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response["redirected_message"]).to be_present
+ expect(json_response["redirected_message"]).to eq(project_moved.redirect_message)
+ end
+ end
end
describe 'POST /internal/pre_receive' do
diff --git a/spec/requests/api/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb
index 07d7f96bd70..10e6a3c07c8 100644
--- a/spec/requests/api/protected_branches_spec.rb
+++ b/spec/requests/api/protected_branches_spec.rb
@@ -95,6 +95,12 @@ describe API::ProtectedBranches do
describe 'POST /projects/:id/protected_branches' do
let(:branch_name) { 'new_branch' }
+ let(:post_endpoint) { api("/projects/#{project.id}/protected_branches", user) }
+
+ def expect_protection_to_be_successful
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['name']).to eq(branch_name)
+ end
context 'when authenticated as a master' do
before do
@@ -102,7 +108,7 @@ describe API::ProtectedBranches do
end
it 'protects a single branch' do
- post api("/projects/#{project.id}/protected_branches", user), name: branch_name
+ post post_endpoint, name: branch_name
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(branch_name)
@@ -111,8 +117,7 @@ describe API::ProtectedBranches do
end
it 'protects a single branch and developers can push' do
- post api("/projects/#{project.id}/protected_branches", user),
- name: branch_name, push_access_level: 30
+ post post_endpoint, name: branch_name, push_access_level: 30
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(branch_name)
@@ -121,8 +126,7 @@ describe API::ProtectedBranches do
end
it 'protects a single branch and developers can merge' do
- post api("/projects/#{project.id}/protected_branches", user),
- name: branch_name, merge_access_level: 30
+ post post_endpoint, name: branch_name, merge_access_level: 30
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(branch_name)
@@ -131,8 +135,7 @@ describe API::ProtectedBranches do
end
it 'protects a single branch and developers can push and merge' do
- post api("/projects/#{project.id}/protected_branches", user),
- name: branch_name, push_access_level: 30, merge_access_level: 30
+ post post_endpoint, name: branch_name, push_access_level: 30, merge_access_level: 30
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(branch_name)
@@ -141,8 +144,7 @@ describe API::ProtectedBranches do
end
it 'protects a single branch and no one can push' do
- post api("/projects/#{project.id}/protected_branches", user),
- name: branch_name, push_access_level: 0
+ post post_endpoint, name: branch_name, push_access_level: 0
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(branch_name)
@@ -151,8 +153,7 @@ describe API::ProtectedBranches do
end
it 'protects a single branch and no one can merge' do
- post api("/projects/#{project.id}/protected_branches", user),
- name: branch_name, merge_access_level: 0
+ post post_endpoint, name: branch_name, merge_access_level: 0
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(branch_name)
@@ -161,8 +162,7 @@ describe API::ProtectedBranches do
end
it 'protects a single branch and no one can push or merge' do
- post api("/projects/#{project.id}/protected_branches", user),
- name: branch_name, push_access_level: 0, merge_access_level: 0
+ post post_endpoint, name: branch_name, push_access_level: 0, merge_access_level: 0
expect(response).to have_gitlab_http_status(201)
expect(json_response['name']).to eq(branch_name)
@@ -171,7 +171,8 @@ describe API::ProtectedBranches do
end
it 'returns a 409 error if the same branch is protected twice' do
- post api("/projects/#{project.id}/protected_branches", user), name: protected_name
+ post post_endpoint, name: protected_name
+
expect(response).to have_gitlab_http_status(409)
end
@@ -179,10 +180,9 @@ describe API::ProtectedBranches do
let(:branch_name) { 'feature/*' }
it "protects multiple branches with a wildcard in the name" do
- post api("/projects/#{project.id}/protected_branches", user), name: branch_name
+ post post_endpoint, name: branch_name
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['name']).to eq(branch_name)
+ expect_protection_to_be_successful
expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::MASTER)
expect(json_response['merge_access_levels'][0]['access_level']).to eq(Gitlab::Access::MASTER)
end
@@ -195,7 +195,7 @@ describe API::ProtectedBranches do
end
it "returns a 403 error if guest" do
- post api("/projects/#{project.id}/protected_branches/", user), name: branch_name
+ post post_endpoint, name: branch_name
expect(response).to have_gitlab_http_status(403)
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 63175c40a18..015d4b9a491 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -54,7 +54,7 @@ describe API::Settings, 'Settings' do
dsa_key_restriction: 2048,
ecdsa_key_restriction: 384,
ed25519_key_restriction: 256,
- circuitbreaker_failure_wait_time: 2
+ circuitbreaker_check_interval: 2
expect(response).to have_gitlab_http_status(200)
expect(json_response['default_projects_limit']).to eq(3)
@@ -75,7 +75,7 @@ describe API::Settings, 'Settings' do
expect(json_response['dsa_key_restriction']).to eq(2048)
expect(json_response['ecdsa_key_restriction']).to eq(384)
expect(json_response['ed25519_key_restriction']).to eq(256)
- expect(json_response['circuitbreaker_failure_wait_time']).to eq(2)
+ expect(json_response['circuitbreaker_check_interval']).to eq(2)
end
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index a16f98bec36..fa02fffc82a 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -324,9 +324,9 @@ describe 'Git HTTP requests' do
<<-MSG.strip_heredoc
Project '#{redirect.path}' was moved to '#{project.full_path}'.
- Please update your Git remote and try again:
+ Please update your Git remote:
- git remote set-url origin #{project.http_url_to_repo}
+ git remote set-url origin #{project.http_url_to_repo} and try again.
MSG
end
@@ -533,9 +533,9 @@ describe 'Git HTTP requests' do
<<-MSG.strip_heredoc
Project '#{redirect.path}' was moved to '#{project.full_path}'.
- Please update your Git remote and try again:
+ Please update your Git remote:
- git remote set-url origin #{project.http_url_to_repo}
+ git remote set-url origin #{project.http_url_to_repo} and try again.
MSG
end
diff --git a/spec/serializers/merge_request_entity_spec.rb b/spec/serializers/merge_request_entity_spec.rb
index f9285049c0d..1ad672fd355 100644
--- a/spec/serializers/merge_request_entity_spec.rb
+++ b/spec/serializers/merge_request_entity_spec.rb
@@ -5,22 +5,34 @@ describe MergeRequestEntity do
let(:resource) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
- let(:request) { double('request', current_user: user) }
+ let(:request) { double('request', current_user: user, project: project) }
subject do
described_class.new(resource, request: request).as_json
end
- it 'includes pipeline' do
- req = double('request', current_user: user)
- pipeline = build_stubbed(:ci_pipeline)
- allow(resource).to receive(:head_pipeline).and_return(pipeline)
+ describe 'pipeline' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: resource.source_branch, sha: resource.source_branch_sha, head_pipeline_of: resource) }
- pipeline_payload = PipelineDetailsEntity
- .represent(pipeline, request: req)
- .as_json
+ context 'when is up to date' do
+ let(:req) { double('request', current_user: user, project: project) }
- expect(subject[:pipeline]).to eq(pipeline_payload)
+ it 'returns pipeline' do
+ pipeline_payload = PipelineDetailsEntity
+ .represent(pipeline, request: req)
+ .as_json
+
+ expect(subject[:pipeline]).to eq(pipeline_payload)
+ end
+ end
+
+ context 'when is not up to date' do
+ it 'returns nil' do
+ pipeline.update(sha: "not up to date")
+
+ expect(subject[:pipeline]).to be_nil
+ end
+ end
end
it 'includes issues_links' do
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index befd0faf1b6..41ce81e0651 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -57,19 +57,39 @@ describe Ci::CreatePipelineService do
end
context 'when merge requests already exist for this source branch' do
- it 'updates head pipeline of each merge request' do
- merge_request_1 = create(:merge_request, source_branch: 'master',
- target_branch: "branch_1",
- source_project: project)
+ let(:merge_request_1) do
+ create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project)
+ end
+ let(:merge_request_2) do
+ create(:merge_request, source_branch: 'master', target_branch: "branch_2", source_project: project)
+ end
- merge_request_2 = create(:merge_request, source_branch: 'master',
- target_branch: "branch_2",
- source_project: project)
+ context 'when the head pipeline sha equals merge request sha' do
+ it 'updates head pipeline of each merge request' do
+ merge_request_1
+ merge_request_2
+
+ head_pipeline = execute_service
+
+ expect(merge_request_1.reload.head_pipeline).to eq(head_pipeline)
+ expect(merge_request_2.reload.head_pipeline).to eq(head_pipeline)
+ end
+ end
+
+ context 'when the head pipeline sha does not equal merge request sha' do
+ it 'raises the ArgumentError error from worker and does not update the head piepeline of MRs' do
+ merge_request_1
+ merge_request_2
+
+ allow_any_instance_of(Ci::Pipeline).to receive(:latest?).and_return(true)
- head_pipeline = execute_service
+ expect { execute_service(after: 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }.to raise_error(ArgumentError)
- expect(merge_request_1.reload.head_pipeline).to eq(head_pipeline)
- expect(merge_request_2.reload.head_pipeline).to eq(head_pipeline)
+ last_pipeline = Ci::Pipeline.last
+
+ expect(merge_request_1.reload.head_pipeline).not_to eq(last_pipeline)
+ expect(merge_request_2.reload.head_pipeline).not_to eq(last_pipeline)
+ end
end
context 'when there is no pipeline for source branch' do
@@ -106,8 +126,7 @@ describe Ci::CreatePipelineService do
target_branch: "branch_1",
source_project: project)
- allow_any_instance_of(Ci::Pipeline)
- .to receive(:latest?).and_return(false)
+ allow_any_instance_of(Ci::Pipeline).to receive(:latest?).and_return(false)
execute_service
@@ -499,5 +518,20 @@ describe Ci::CreatePipelineService do
end
end
end
+
+ context 'when pipeline is running for a tag' do
+ before do
+ config = YAML.dump(test: { script: 'test', only: ['branches'] },
+ deploy: { script: 'deploy', only: ['tags'] })
+
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'creates a tagged pipeline' do
+ pipeline = execute_service(ref: 'v1.0.0')
+
+ expect(pipeline.tag?).to be true
+ end
+ end
end
end
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 5ac30111ec9..3ee59014b5b 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -15,16 +15,14 @@ module Ci
describe '#execute' do
context 'runner follow tag list' do
it "picks build with the same tag" do
- pending_job.tag_list = ["linux"]
- pending_job.save
- specific_runner.tag_list = ["linux"]
+ pending_job.update(tag_list: ["linux"])
+ specific_runner.update(tag_list: ["linux"])
expect(execute(specific_runner)).to eq(pending_job)
end
it "does not pick build with different tag" do
- pending_job.tag_list = ["linux"]
- pending_job.save
- specific_runner.tag_list = ["win32"]
+ pending_job.update(tag_list: ["linux"])
+ specific_runner.update(tag_list: ["win32"])
expect(execute(specific_runner)).to be_falsey
end
@@ -33,13 +31,12 @@ module Ci
end
it "does not pick build with tag" do
- pending_job.tag_list = ["linux"]
- pending_job.save
+ pending_job.update(tag_list: ["linux"])
expect(execute(specific_runner)).to be_falsey
end
it "pick build without tag" do
- specific_runner.tag_list = ["win32"]
+ specific_runner.update(tag_list: ["win32"])
expect(execute(specific_runner)).to eq(pending_job)
end
end
@@ -172,7 +169,7 @@ module Ci
context 'when first build is stalled' do
before do
- pending_job.lock_version = 10
+ pending_job.update(lock_version: 0)
end
subject { described_class.new(specific_runner).execute }
@@ -182,7 +179,7 @@ module Ci
before do
allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner)
- .and_return([pending_job, other_build])
+ .and_return(Ci::Build.where(id: [pending_job, other_build]))
end
it "receives second build from the queue" do
@@ -194,7 +191,7 @@ module Ci
context 'when single build is in queue' do
before do
allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner)
- .and_return([pending_job])
+ .and_return(Ci::Build.where(id: pending_job))
end
it "does not receive any valid result" do
@@ -205,7 +202,7 @@ module Ci
context 'when there is no build in queue' do
before do
allow_any_instance_of(Ci::RegisterJobService).to receive(:builds_for_specific_runner)
- .and_return([])
+ .and_return(Ci::Build.none)
end
it "does not receive builds but result is valid" do
@@ -279,6 +276,89 @@ module Ci
end
end
+ context 'when "dependencies" keyword is specified' do
+ shared_examples 'not pick' do
+ it 'does not pick the build and drops the build' do
+ expect(subject).to be_nil
+ expect(pending_job.reload).to be_failed
+ expect(pending_job).to be_missing_dependency_failure
+ end
+ end
+
+ shared_examples 'validation is active' do
+ context 'when depended job has not been completed yet' do
+ let!(:pre_stage_job) { create(:ci_build, :running, pipeline: pipeline, name: 'test', stage_idx: 0) }
+
+ it_behaves_like 'not pick'
+ end
+
+ context 'when artifacts of depended job has been expired' do
+ let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
+
+ it_behaves_like 'not pick'
+ end
+
+ context 'when artifacts of depended job has been erased' do
+ let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
+
+ before do
+ pre_stage_job.erase
+ end
+
+ it_behaves_like 'not pick'
+ end
+ end
+
+ shared_examples 'validation is not active' do
+ context 'when depended job has not been completed yet' do
+ let!(:pre_stage_job) { create(:ci_build, :running, pipeline: pipeline, name: 'test', stage_idx: 0) }
+
+ it { expect(subject).to eq(pending_job) }
+ end
+
+ context 'when artifacts of depended job has been expired' do
+ let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) }
+
+ it { expect(subject).to eq(pending_job) }
+ end
+
+ context 'when artifacts of depended job has been erased' do
+ let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
+
+ before do
+ pre_stage_job.erase
+ end
+
+ it { expect(subject).to eq(pending_job) }
+ end
+ end
+
+ before do
+ stub_feature_flags(ci_disable_validates_dependencies: false)
+ end
+
+ let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) }
+ let!(:pending_job) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 1, options: { dependencies: ['test'] } ) }
+
+ subject { execute(specific_runner) }
+
+ context 'when validates for dependencies is enabled' do
+ before do
+ stub_feature_flags(ci_disable_validates_dependencies: false)
+ end
+
+ it_behaves_like 'validation is active'
+ end
+
+ context 'when validates for dependencies is disabled' do
+ before do
+ stub_feature_flags(ci_disable_validates_dependencies: true)
+ end
+
+ it_behaves_like 'validation is not active'
+ end
+ end
+
def execute(runner)
described_class.new(runner).execute.build
end
diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb
index 5b6edb73beb..e2e64659dfa 100644
--- a/spec/services/clusters/create_service_spec.rb
+++ b/spec/services/clusters/create_service_spec.rb
@@ -4,10 +4,11 @@ describe Clusters::CreateService do
let(:access_token) { 'xxx' }
let(:project) { create(:project) }
let(:user) { create(:user) }
- let(:result) { described_class.new(project, user, params).execute(access_token) }
+
+ subject { described_class.new(project, user, params).execute(access_token) }
context 'when provider is gcp' do
- context 'when correct params' do
+ shared_context 'valid params' do
let(:params) do
{
name: 'test-cluster',
@@ -20,27 +21,9 @@ describe Clusters::CreateService do
}
}
end
-
- it 'creates a cluster object and performs a worker' do
- expect(ClusterProvisionWorker).to receive(:perform_async)
-
- expect { result }
- .to change { Clusters::Cluster.count }.by(1)
- .and change { Clusters::Providers::Gcp.count }.by(1)
-
- expect(result.name).to eq('test-cluster')
- expect(result.user).to eq(user)
- expect(result.project).to eq(project)
- expect(result.provider.gcp_project_id).to eq('gcp-project')
- expect(result.provider.zone).to eq('us-central1-a')
- expect(result.provider.num_nodes).to eq(1)
- expect(result.provider.machine_type).to eq('machine_type-a')
- expect(result.provider.access_token).to eq(access_token)
- expect(result.platform).to be_nil
- end
end
- context 'when invalid params' do
+ shared_context 'invalid params' do
let(:params) do
{
name: 'test-cluster',
@@ -53,11 +36,57 @@ describe Clusters::CreateService do
}
}
end
+ end
+
+ shared_examples 'create cluster' do
+ it 'creates a cluster object and performs a worker' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+
+ expect { subject }
+ .to change { Clusters::Cluster.count }.by(1)
+ .and change { Clusters::Providers::Gcp.count }.by(1)
+ expect(subject.name).to eq('test-cluster')
+ expect(subject.user).to eq(user)
+ expect(subject.project).to eq(project)
+ expect(subject.provider.gcp_project_id).to eq('gcp-project')
+ expect(subject.provider.zone).to eq('us-central1-a')
+ expect(subject.provider.num_nodes).to eq(1)
+ expect(subject.provider.machine_type).to eq('machine_type-a')
+ expect(subject.provider.access_token).to eq(access_token)
+ expect(subject.platform).to be_nil
+ end
+ end
+
+ shared_examples 'error' do
it 'returns an error' do
expect(ClusterProvisionWorker).not_to receive(:perform_async)
- expect { result }.to change { Clusters::Cluster.count }.by(0)
- expect(result.errors[:"provider_gcp.gcp_project_id"]).to be_present
+ expect { subject }.to change { Clusters::Cluster.count }.by(0)
+ expect(subject.errors[:"provider_gcp.gcp_project_id"]).to be_present
+ end
+ end
+
+ context 'when project has no clusters' do
+ context 'when correct params' do
+ include_context 'valid params'
+
+ include_examples 'create cluster'
+ end
+
+ context 'when invalid params' do
+ include_context 'invalid params'
+
+ include_examples 'error'
+ end
+ end
+
+ context 'when project has a cluster' do
+ include_context 'valid params'
+ let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+
+ it 'does not create a cluster' do
+ expect(ClusterProvisionWorker).not_to receive(:perform_async)
+ expect { subject }.to raise_error(ArgumentError).and change { Clusters::Cluster.count }.by(0)
end
end
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index a2c05761f6b..61ec4709c59 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -74,6 +74,20 @@ describe MergeRequests::RefreshService do
end
end
+ context 'when pipeline exists for the source branch' do
+ let!(:pipeline) { create(:ci_empty_pipeline, ref: @merge_request.source_branch, project: @project, sha: @commits.first.sha)}
+
+ subject { service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master') }
+
+ it 'updates the head_pipeline_id for @merge_request' do
+ expect { subject }.to change { @merge_request.reload.head_pipeline_id }.from(nil).to(pipeline.id)
+ end
+
+ it 'does not update the head_pipeline_id for @fork_merge_request' do
+ expect { subject }.not_to change { @fork_merge_request.reload.head_pipeline_id }
+ end
+ end
+
context 'push to origin repo source branch when an MR was reopened' do
let(:refresh_service) { service.new(@project, @user) }
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 53862283a27..4057caca2ac 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -3,210 +3,253 @@ require 'spec_helper'
describe Projects::ForkService do
include ProjectForksHelper
let(:gitlab_shell) { Gitlab::Shell.new }
+ context 'when forking a new project' do
+ describe 'fork by user' do
+ before do
+ @from_user = create(:user)
+ @from_namespace = @from_user.namespace
+ avatar = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")
+ @from_project = create(:project,
+ :repository,
+ creator_id: @from_user.id,
+ namespace: @from_namespace,
+ star_count: 107,
+ avatar: avatar,
+ description: 'wow such project')
+ @to_user = create(:user)
+ @to_namespace = @to_user.namespace
+ @from_project.add_user(@to_user, :developer)
+ end
- describe 'fork by user' do
- before do
- @from_user = create(:user)
- @from_namespace = @from_user.namespace
- avatar = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")
- @from_project = create(:project,
- :repository,
- creator_id: @from_user.id,
- namespace: @from_namespace,
- star_count: 107,
- avatar: avatar,
- description: 'wow such project')
- @to_user = create(:user)
- @to_namespace = @to_user.namespace
- @from_project.add_user(@to_user, :developer)
- end
+ context 'fork project' do
+ context 'when forker is a guest' do
+ before do
+ @guest = create(:user)
+ @from_project.add_user(@guest, :guest)
+ end
+ subject { fork_project(@from_project, @guest) }
- context 'fork project' do
- context 'when forker is a guest' do
- before do
- @guest = create(:user)
- @from_project.add_user(@guest, :guest)
+ it { is_expected.not_to be_persisted }
+ it { expect(subject.errors[:forked_from_project_id]).to eq(['is forbidden']) }
end
- subject { fork_project(@from_project, @guest) }
- it { is_expected.not_to be_persisted }
- it { expect(subject.errors[:forked_from_project_id]).to eq(['is forbidden']) }
- end
+ describe "successfully creates project in the user namespace" do
+ let(:to_project) { fork_project(@from_project, @to_user, namespace: @to_user.namespace) }
- describe "successfully creates project in the user namespace" do
- let(:to_project) { fork_project(@from_project, @to_user, namespace: @to_user.namespace) }
-
- it { expect(to_project).to be_persisted }
- it { expect(to_project.errors).to be_empty }
- it { expect(to_project.owner).to eq(@to_user) }
- it { expect(to_project.namespace).to eq(@to_user.namespace) }
- it { expect(to_project.star_count).to be_zero }
- it { expect(to_project.description).to eq(@from_project.description) }
- it { expect(to_project.avatar.file).to be_exists }
-
- # This test is here because we had a bug where the from-project lost its
- # avatar after being forked.
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/26158
- it "after forking the from-project still has its avatar" do
- # If we do not fork the project first we cannot detect the bug.
- expect(to_project).to be_persisted
-
- expect(@from_project.avatar.file).to be_exists
- end
+ it { expect(to_project).to be_persisted }
+ it { expect(to_project.errors).to be_empty }
+ it { expect(to_project.owner).to eq(@to_user) }
+ it { expect(to_project.namespace).to eq(@to_user.namespace) }
+ it { expect(to_project.star_count).to be_zero }
+ it { expect(to_project.description).to eq(@from_project.description) }
+ it { expect(to_project.avatar.file).to be_exists }
- it 'flushes the forks count cache of the source project' do
- expect(@from_project.forks_count).to be_zero
+ # This test is here because we had a bug where the from-project lost its
+ # avatar after being forked.
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/26158
+ it "after forking the from-project still has its avatar" do
+ # If we do not fork the project first we cannot detect the bug.
+ expect(to_project).to be_persisted
- fork_project(@from_project, @to_user)
+ expect(@from_project.avatar.file).to be_exists
+ end
- expect(@from_project.forks_count).to eq(1)
- end
+ it 'flushes the forks count cache of the source project' do
+ expect(@from_project.forks_count).to be_zero
- it 'creates a fork network with the new project and the root project set' do
- to_project
- fork_network = @from_project.reload.fork_network
+ fork_project(@from_project, @to_user)
- expect(fork_network).not_to be_nil
- expect(fork_network.root_project).to eq(@from_project)
- expect(fork_network.projects).to contain_exactly(@from_project, to_project)
- end
- end
+ expect(@from_project.forks_count).to eq(1)
+ end
- context 'creating a fork of a fork' do
- let(:from_forked_project) { fork_project(@from_project, @to_user) }
- let(:other_namespace) do
- group = create(:group)
- group.add_owner(@to_user)
- group
- end
- let(:to_project) { fork_project(from_forked_project, @to_user, namespace: other_namespace) }
+ it 'creates a fork network with the new project and the root project set' do
+ to_project
+ fork_network = @from_project.reload.fork_network
- it 'sets the root of the network to the root project' do
- expect(to_project.fork_network.root_project).to eq(@from_project)
+ expect(fork_network).not_to be_nil
+ expect(fork_network.root_project).to eq(@from_project)
+ expect(fork_network.projects).to contain_exactly(@from_project, to_project)
+ end
end
- it 'sets the forked_from_project on the membership' do
- expect(to_project.fork_network_member.forked_from_project).to eq(from_forked_project)
+ context 'creating a fork of a fork' do
+ let(:from_forked_project) { fork_project(@from_project, @to_user) }
+ let(:other_namespace) do
+ group = create(:group)
+ group.add_owner(@to_user)
+ group
+ end
+ let(:to_project) { fork_project(from_forked_project, @to_user, namespace: other_namespace) }
+
+ it 'sets the root of the network to the root project' do
+ expect(to_project.fork_network.root_project).to eq(@from_project)
+ end
+
+ it 'sets the forked_from_project on the membership' do
+ expect(to_project.fork_network_member.forked_from_project).to eq(from_forked_project)
+ end
end
end
- end
- context 'project already exists' do
- it "fails due to validation, not transaction failure" do
- @existing_project = create(:project, :repository, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
- @to_project = fork_project(@from_project, @to_user, namespace: @to_namespace)
- expect(@existing_project).to be_persisted
+ context 'project already exists' do
+ it "fails due to validation, not transaction failure" do
+ @existing_project = create(:project, :repository, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
+ @to_project = fork_project(@from_project, @to_user, namespace: @to_namespace)
+ expect(@existing_project).to be_persisted
- expect(@to_project).not_to be_persisted
- expect(@to_project.errors[:name]).to eq(['has already been taken'])
- expect(@to_project.errors[:path]).to eq(['has already been taken'])
+ expect(@to_project).not_to be_persisted
+ expect(@to_project.errors[:name]).to eq(['has already been taken'])
+ expect(@to_project.errors[:path]).to eq(['has already been taken'])
+ end
end
- end
- context 'repository already exists' do
- let(:repository_storage) { 'default' }
- let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
+ context 'repository already exists' do
+ let(:repository_storage) { 'default' }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] }
- before do
- gitlab_shell.add_repository(repository_storage, "#{@to_user.namespace.full_path}/#{@from_project.path}")
- end
+ before do
+ gitlab_shell.add_repository(repository_storage, "#{@to_user.namespace.full_path}/#{@from_project.path}")
+ end
- after do
- gitlab_shell.remove_repository(repository_storage_path, "#{@to_user.namespace.full_path}/#{@from_project.path}")
- end
+ after do
+ gitlab_shell.remove_repository(repository_storage_path, "#{@to_user.namespace.full_path}/#{@from_project.path}")
+ end
- it 'does not allow creation' do
- to_project = fork_project(@from_project, @to_user, namespace: @to_user.namespace)
+ it 'does not allow creation' do
+ to_project = fork_project(@from_project, @to_user, namespace: @to_user.namespace)
- expect(to_project).not_to be_persisted
- expect(to_project.errors.messages).to have_key(:base)
- expect(to_project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ expect(to_project).not_to be_persisted
+ expect(to_project.errors.messages).to have_key(:base)
+ expect(to_project.errors.messages[:base].first).to match('There is already a repository with that name on disk')
+ end
end
- end
- context 'GitLab CI is enabled' do
- it "forks and enables CI for fork" do
- @from_project.enable_ci
- @to_project = fork_project(@from_project, @to_user)
- expect(@to_project.builds_enabled?).to be_truthy
+ context 'GitLab CI is enabled' do
+ it "forks and enables CI for fork" do
+ @from_project.enable_ci
+ @to_project = fork_project(@from_project, @to_user)
+ expect(@to_project.builds_enabled?).to be_truthy
+ end
end
- end
- context "when project has restricted visibility level" do
- context "and only one visibility level is restricted" do
- before do
- @from_project.update_attributes(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
+ context "when project has restricted visibility level" do
+ context "and only one visibility level is restricted" do
+ before do
+ @from_project.update_attributes(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
+ end
+
+ it "creates fork with highest allowed level" do
+ forked_project = fork_project(@from_project, @to_user)
+
+ expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
end
- it "creates fork with highest allowed level" do
- forked_project = fork_project(@from_project, @to_user)
+ context "and all visibility levels are restricted" do
+ before do
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PRIVATE])
+ end
+
+ it "creates fork with private visibility levels" do
+ forked_project = fork_project(@from_project, @to_user)
- expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
end
end
+ end
- context "and all visibility levels are restricted" do
- before do
- stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PRIVATE])
+ describe 'fork to namespace' do
+ before do
+ @group_owner = create(:user)
+ @developer = create(:user)
+ @project = create(:project, :repository,
+ creator_id: @group_owner.id,
+ star_count: 777,
+ description: 'Wow, such a cool project!')
+ @group = create(:group)
+ @group.add_user(@group_owner, GroupMember::OWNER)
+ @group.add_user(@developer, GroupMember::DEVELOPER)
+ @project.add_user(@developer, :developer)
+ @project.add_user(@group_owner, :developer)
+ @opts = { namespace: @group }
+ end
+
+ context 'fork project for group' do
+ it 'group owner successfully forks project into the group' do
+ to_project = fork_project(@project, @group_owner, @opts)
+
+ expect(to_project).to be_persisted
+ expect(to_project.errors).to be_empty
+ expect(to_project.owner).to eq(@group)
+ expect(to_project.namespace).to eq(@group)
+ expect(to_project.name).to eq(@project.name)
+ expect(to_project.path).to eq(@project.path)
+ expect(to_project.description).to eq(@project.description)
+ expect(to_project.star_count).to be_zero
end
+ end
- it "creates fork with private visibility levels" do
- forked_project = fork_project(@from_project, @to_user)
+ context 'fork project for group when user not owner' do
+ it 'group developer fails to fork project into the group' do
+ to_project = fork_project(@project, @developer, @opts)
+ expect(to_project.errors[:namespace]).to eq(['is not valid'])
+ end
+ end
- expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ context 'project already exists in group' do
+ it 'fails due to validation, not transaction failure' do
+ existing_project = create(:project, :repository,
+ name: @project.name,
+ namespace: @group)
+ to_project = fork_project(@project, @group_owner, @opts)
+ expect(existing_project.persisted?).to be_truthy
+ expect(to_project.errors[:name]).to eq(['has already been taken'])
+ expect(to_project.errors[:path]).to eq(['has already been taken'])
end
end
end
end
- describe 'fork to namespace' do
- before do
- @group_owner = create(:user)
- @developer = create(:user)
- @project = create(:project, :repository,
- creator_id: @group_owner.id,
- star_count: 777,
- description: 'Wow, such a cool project!')
- @group = create(:group)
- @group.add_user(@group_owner, GroupMember::OWNER)
- @group.add_user(@developer, GroupMember::DEVELOPER)
- @project.add_user(@developer, :developer)
- @project.add_user(@group_owner, :developer)
- @opts = { namespace: @group }
+ context 'when linking fork to an existing project' do
+ let(:fork_from_project) { create(:project, :public) }
+ let(:fork_to_project) { create(:project, :public) }
+ let(:user) { create(:user) }
+
+ subject { described_class.new(fork_from_project, user) }
+
+ def forked_from_project(project)
+ project.fork_network_member&.forked_from_project
end
- context 'fork project for group' do
- it 'group owner successfully forks project into the group' do
- to_project = fork_project(@project, @group_owner, @opts)
-
- expect(to_project).to be_persisted
- expect(to_project.errors).to be_empty
- expect(to_project.owner).to eq(@group)
- expect(to_project.namespace).to eq(@group)
- expect(to_project.name).to eq(@project.name)
- expect(to_project.path).to eq(@project.path)
- expect(to_project.description).to eq(@project.description)
- expect(to_project.star_count).to be_zero
+ context 'if project is already forked' do
+ it 'does not create fork relation' do
+ allow(fork_to_project).to receive(:forked?).and_return(true)
+ expect(forked_from_project(fork_to_project)).to be_nil
+ expect(subject.execute(fork_to_project)).to be_nil
+ expect(forked_from_project(fork_to_project)).to be_nil
end
end
- context 'fork project for group when user not owner' do
- it 'group developer fails to fork project into the group' do
- to_project = fork_project(@project, @developer, @opts)
- expect(to_project.errors[:namespace]).to eq(['is not valid'])
+ context 'if project is not forked' do
+ it 'creates fork relation' do
+ expect(fork_to_project.forked?).to be false
+ expect(forked_from_project(fork_to_project)).to be_nil
+
+ subject.execute(fork_to_project)
+
+ expect(fork_to_project.forked?).to be true
+ expect(forked_from_project(fork_to_project)).to eq fork_from_project
+ expect(fork_to_project.forked_from_project).to eq fork_from_project
end
- end
- context 'project already exists in group' do
- it 'fails due to validation, not transaction failure' do
- existing_project = create(:project, :repository,
- name: @project.name,
- namespace: @group)
- to_project = fork_project(@project, @group_owner, @opts)
- expect(existing_project.persisted?).to be_truthy
- expect(to_project.errors[:name]).to eq(['has already been taken'])
- expect(to_project.errors[:path]).to eq(['has already been taken'])
+ it 'flushes the forks count cache of the source project' do
+ expect(fork_from_project.forks_count).to be_zero
+
+ subject.execute(fork_to_project)
+
+ expect(fork_from_project.forks_count).to eq(1)
end
end
end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index fcd71857af3..d887f70efae 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -199,24 +199,53 @@ describe Projects::UpdateService do
end
describe '#run_auto_devops_pipeline?' do
- subject { described_class.new(project, user, params).run_auto_devops_pipeline? }
+ subject { described_class.new(project, user).run_auto_devops_pipeline? }
- context 'when neither pipeline setting is true' do
- let(:params) { {} }
+ context 'when master contains a .gitlab-ci.yml file' do
+ before do
+ allow(project.repository).to receive(:gitlab_ci_yml).and_return("script: ['test']")
+ end
it { is_expected.to eq(false) }
end
- context 'when run_auto_devops_pipeline_explicit is true' do
- let(:params) { { run_auto_devops_pipeline_explicit: 'true' } }
+ context 'when auto devops is explicitly enabled' do
+ before do
+ project.create_auto_devops!(enabled: true)
+ end
it { is_expected.to eq(true) }
end
- context 'when run_auto_devops_pipeline_implicit is true' do
- let(:params) { { run_auto_devops_pipeline_implicit: 'true' } }
+ context 'when auto devops is explicitly disabled' do
+ before do
+ project.create_auto_devops!(enabled: false)
+ end
- it { is_expected.to eq(true) }
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when auto devops is set to instance setting' do
+ before do
+ project.create_auto_devops!(enabled: nil)
+ allow(project.auto_devops).to receive(:previous_changes).and_return('enabled' => true)
+ end
+
+ context 'when auto devops is enabled system-wide' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when auto devops is disabled system-wide' do
+ before do
+ stub_application_setting(auto_devops_enabled: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index a918383ecd2..47412110b4b 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -692,9 +692,9 @@ describe SystemNoteService do
describe '.new_commit_summary' do
it 'escapes HTML titles' do
commit = double(title: '<pre>This is a test</pre>', short_id: '12345678')
- escaped = '* 12345678 - &lt;pre&gt;This is a test&lt;&#x2F;pre&gt;'
+ escaped = '&lt;pre&gt;This is a test&lt;&#x2F;pre&gt;'
- expect(described_class.new_commit_summary([commit])).to eq([escaped])
+ expect(described_class.new_commit_summary([commit])).to all(match(%r[- #{escaped}]))
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 242a2230b67..f94fb8733d5 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -121,18 +121,6 @@ RSpec.configure do |config|
reset_delivered_emails!
end
- # Stub the `ForkedStorageCheck.storage_available?` method unless
- # `:broken_storage` metadata is defined
- #
- # This check can be slow and is unnecessary in a test environment where we
- # know the storage is available, because we create it at runtime
- config.before(:example) do |example|
- unless example.metadata[:broken_storage]
- allow(Gitlab::Git::Storage::ForkedStorageCheck)
- .to receive(:storage_available?).and_return(true)
- end
- end
-
config.around(:each, :use_clean_rails_memory_store_caching) do |example|
caching_store = Rails.cache
Rails.cache = ActiveSupport::Cache::MemoryStore.new
diff --git a/spec/support/google_api/cloud_platform_helpers.rb b/spec/support/google_api/cloud_platform_helpers.rb
index dabf0db7666..8a073e58db8 100644
--- a/spec/support/google_api/cloud_platform_helpers.rb
+++ b/spec/support/google_api/cloud_platform_helpers.rb
@@ -63,7 +63,7 @@ module GoogleApi
##
# gcloud container clusters create
- # https://cloud.google.com/container-engine/reference/rest/v1/projects.zones.clusters/create
+ # https://cloud.google.com/kubernetes-engine/docs/reference/rest/v1/projects.zones.clusters/create
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
def cloud_platform_cluster_body(**options)
diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
new file mode 100644
index 00000000000..935c08221e0
--- /dev/null
+++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
@@ -0,0 +1,240 @@
+shared_examples 'handle uploads' do
+ let(:user) { create(:user) }
+ let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') }
+ let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
+
+ describe "POST #create" do
+ context 'when a user is not authorized to upload a file' do
+ it 'returns 404 status' do
+ post :create, params.merge(file: jpg, format: :json)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when a user can upload a file' do
+ before do
+ sign_in(user)
+ model.add_developer(user)
+ end
+
+ context "without params['file']" do
+ it "returns an error" do
+ post :create, params.merge(format: :json)
+
+ expect(response).to have_gitlab_http_status(422)
+ end
+ end
+
+ context 'with valid image' do
+ before do
+ post :create, params.merge(file: jpg, format: :json)
+ end
+
+ it 'returns a content with original filename, new link, and correct type.' do
+ expect(response.body).to match '\"alt\":\"rails_sample\"'
+ expect(response.body).to match "\"url\":\"/uploads"
+ end
+
+ # NOTE: This is as close as we're getting to an Integration test for this
+ # behavior. We're avoiding a proper Feature test because those should be
+ # testing things entirely user-facing, which the Upload model is very much
+ # not.
+ it 'creates a corresponding Upload record' do
+ upload = Upload.last
+
+ aggregate_failures do
+ expect(upload).to exist
+ expect(upload.model).to eq(model)
+ end
+ end
+ end
+
+ context 'with valid non-image file' do
+ before do
+ post :create, params.merge(file: txt, format: :json)
+ end
+
+ it 'returns a content with original filename, new link, and correct type.' do
+ expect(response.body).to match '\"alt\":\"doc_sample.txt\"'
+ expect(response.body).to match "\"url\":\"/uploads"
+ end
+ end
+ end
+ end
+
+ describe "GET #show" do
+ let(:show_upload) do
+ get :show, params.merge(secret: "123456", filename: "image.jpg")
+ end
+
+ context "when the model is public" do
+ before do
+ model.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ context "when not signed in" do
+ context "when the file exists" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
+ allow(jpg).to receive(:exists?).and_return(true)
+ end
+
+ it "responds with status 200" do
+ show_upload
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context "when the file doesn't exist" do
+ it "responds with status 404" do
+ show_upload
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ context "when signed in" do
+ before do
+ sign_in(user)
+ end
+
+ context "when the file exists" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
+ allow(jpg).to receive(:exists?).and_return(true)
+ end
+
+ it "responds with status 200" do
+ show_upload
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context "when the file doesn't exist" do
+ it "responds with status 404" do
+ show_upload
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+ end
+
+ context "when the model is private" do
+ before do
+ model.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ context "when not signed in" do
+ context "when the file exists" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
+ allow(jpg).to receive(:exists?).and_return(true)
+ end
+
+ context "when the file is an image" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:image?).and_return(true)
+ end
+
+ it "responds with status 200" do
+ show_upload
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context "when the file is not an image" do
+ it "redirects to the sign in page" do
+ show_upload
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ context "when the file doesn't exist" do
+ it "redirects to the sign in page" do
+ show_upload
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ context "when signed in" do
+ before do
+ sign_in(user)
+ end
+
+ context "when the user has access to the project" do
+ before do
+ model.add_developer(user)
+ end
+
+ context "when the file exists" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
+ allow(jpg).to receive(:exists?).and_return(true)
+ end
+
+ it "responds with status 200" do
+ show_upload
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context "when the file doesn't exist" do
+ it "responds with status 404" do
+ show_upload
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ context "when the user doesn't have access to the model" do
+ context "when the file exists" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
+ allow(jpg).to receive(:exists?).and_return(true)
+ end
+
+ context "when the file is an image" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:image?).and_return(true)
+ end
+
+ it "responds with status 200" do
+ show_upload
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context "when the file is not an image" do
+ it "responds with status 404" do
+ show_upload
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ context "when the file doesn't exist" do
+ it "responds with status 404" do
+ show_upload
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/throttled_touch.rb b/spec/support/shared_examples/throttled_touch.rb
new file mode 100644
index 00000000000..4a25bb9b750
--- /dev/null
+++ b/spec/support/shared_examples/throttled_touch.rb
@@ -0,0 +1,20 @@
+shared_examples_for 'throttled touch' do
+ describe '#touch' do
+ it 'updates the updated_at timestamp' do
+ Timecop.freeze do
+ subject.touch
+ expect(subject.updated_at).to eq(Time.zone.now)
+ end
+ end
+
+ it 'updates the object at most once per minute' do
+ first_updated_at = Time.zone.now - (ThrottledTouch::TOUCH_INTERVAL * 2)
+ second_updated_at = Time.zone.now - (ThrottledTouch::TOUCH_INTERVAL * 1.5)
+
+ Timecop.freeze(first_updated_at) { subject.touch }
+ Timecop.freeze(second_updated_at) { subject.touch }
+
+ expect(subject.updated_at).to eq(first_updated_at)
+ end
+ end
+end
diff --git a/spec/support/stored_repositories.rb b/spec/support/stored_repositories.rb
index f3deae0f455..f9121cce985 100644
--- a/spec/support/stored_repositories.rb
+++ b/spec/support/stored_repositories.rb
@@ -12,6 +12,25 @@ RSpec.configure do |config|
raise GRPC::Unavailable.new('Gitaly broken in this spec')
end
- Gitlab::Git::Storage::CircuitBreaker.reset_all!
+ # Track the maximum number of failures
+ first_failure = Time.parse("2017-11-14 17:52:30")
+ last_failure = Time.parse("2017-11-14 18:54:37")
+ failure_count = Gitlab::CurrentSettings
+ .current_application_settings
+ .circuitbreaker_failure_count_threshold + 1
+ cache_key = "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}broken:#{Gitlab::Environment.hostname}"
+
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.pipelined do
+ redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, cache_key)
+ redis.hset(cache_key, :first_failure, first_failure.to_i)
+ redis.hset(cache_key, :last_failure, last_failure.to_i)
+ redis.hset(cache_key, :failure_count, failure_count.to_i)
+ end
+ end
+ end
+
+ config.after(:each, :broken_storage) do
+ Gitlab::Git::Storage.redis.with(&:flushall)
end
end
diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb
index 4ead78529c3..b36cf3c544c 100644
--- a/spec/support/stub_configuration.rb
+++ b/spec/support/stub_configuration.rb
@@ -43,6 +43,8 @@ module StubConfiguration
end
def stub_storage_settings(messages)
+ messages.deep_stringify_keys!
+
# Default storage is always required
messages['default'] ||= Gitlab.config.repositories.storages.default
messages.each do |storage_name, storage_settings|
diff --git a/spec/support/track_untracked_uploads_helpers.rb b/spec/support/track_untracked_uploads_helpers.rb
new file mode 100644
index 00000000000..d05eda08201
--- /dev/null
+++ b/spec/support/track_untracked_uploads_helpers.rb
@@ -0,0 +1,20 @@
+module TrackUntrackedUploadsHelpers
+ def uploaded_file
+ fixture_path = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg')
+ fixture_file_upload(fixture_path)
+ end
+
+ def ensure_temporary_tracking_table_exists
+ Gitlab::BackgroundMigration::PrepareUntrackedUploads.new.send(:ensure_temporary_tracking_table_exists)
+ end
+
+ def drop_temp_table_if_exists
+ ActiveRecord::Base.connection.drop_table(:untracked_files_for_uploads) if ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads)
+ end
+
+ def create_or_update_appearance(attrs)
+ a = Appearance.first_or_initialize(title: 'foo', description: 'bar')
+ a.update!(attrs)
+ a
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index bf2e11bc360..b41c3b3958a 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -212,7 +212,7 @@ describe 'gitlab:app namespace rake task' do
# Avoid asking gitaly about the root ref (which will fail beacuse of the
# mocked storages)
- allow_any_instance_of(Repository).to receive(:empty_repo?).and_return(false)
+ allow_any_instance_of(Repository).to receive(:empty?).and_return(false)
end
after do
diff --git a/spec/uploaders/namespace_file_uploader_spec.rb b/spec/uploaders/namespace_file_uploader_spec.rb
new file mode 100644
index 00000000000..c6c4500c179
--- /dev/null
+++ b/spec/uploaders/namespace_file_uploader_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe NamespaceFileUploader do
+ let(:group) { build_stubbed(:group) }
+ let(:uploader) { described_class.new(group) }
+
+ describe "#store_dir" do
+ it "stores in the namespace id directory" do
+ expect(uploader.store_dir).to include(group.id.to_s)
+ end
+ end
+
+ describe ".absolute_path" do
+ it "stores in thecorrect directory" do
+ upload_record = create(:upload, :namespace_upload, model: group)
+
+ expect(described_class.absolute_path(upload_record))
+ .to include("-/system/namespace/#{group.id}")
+ end
+ end
+end
diff --git a/spec/views/dashboard/projects/_blank_state_admin_welcome.haml.rb b/spec/views/dashboard/projects/_blank_state_admin_welcome.haml.rb
new file mode 100644
index 00000000000..2f58eec86dc
--- /dev/null
+++ b/spec/views/dashboard/projects/_blank_state_admin_welcome.haml.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'dashboard/projects/_blank_state_admin_welcome.html.haml' do
+ let(:user) { create(:admin) }
+
+ before do
+ allow(view).to receive(:current_user).and_return(user)
+ end
+
+ it 'links to new group path' do
+ render
+
+ expect(rendered).to have_link('Create a group', href: new_group_path)
+ end
+end
diff --git a/spec/views/projects/commit/show.html.haml_spec.rb b/spec/views/projects/commit/show.html.haml_spec.rb
index 32c95c6bb0d..a9c32122600 100644
--- a/spec/views/projects/commit/show.html.haml_spec.rb
+++ b/spec/views/projects/commit/show.html.haml_spec.rb
@@ -2,14 +2,15 @@ require 'spec_helper'
describe 'projects/commit/show.html.haml' do
let(:project) { create(:project, :repository) }
+ let(:commit) { project.commit }
before do
assign(:project, project)
assign(:repository, project.repository)
- assign(:commit, project.commit)
- assign(:noteable, project.commit)
+ assign(:commit, commit)
+ assign(:noteable, commit)
assign(:notes, [])
- assign(:diffs, project.commit.diffs)
+ assign(:diffs, commit.diffs)
allow(view).to receive(:current_user).and_return(nil)
allow(view).to receive(:can?).and_return(false)
@@ -43,4 +44,19 @@ describe 'projects/commit/show.html.haml' do
expect(rendered).not_to have_selector('.limit-container-width')
end
end
+
+ context 'in the context of a merge request' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+
+ before do
+ assign(:merge_request, merge_request)
+ render
+ end
+
+ it 'shows that it is in the context of a merge request' do
+ merge_request_url = diffs_project_merge_request_url(project, merge_request, commit_id: commit.id)
+ expect(rendered).to have_content("This commit is part of merge request")
+ expect(rendered).to have_link(merge_request.to_reference, merge_request_url)
+ end
+ end
end
diff --git a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
index efed2e02a1b..3ca67114558 100644
--- a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
@@ -25,8 +25,8 @@ describe 'projects/merge_requests/_commits.html.haml' do
it 'shows commits from source project' do
render
- commit = source_project.commit(merge_request.source_branch)
- href = project_commit_path(source_project, commit)
+ commit = merge_request.commits.first # HEAD
+ href = diffs_project_merge_request_path(target_project, merge_request, commit_id: commit)
expect(rendered).to have_link(Commit.truncate_sha(commit.sha), href: href)
end
diff --git a/spec/views/projects/merge_requests/diffs/_diffs.html.haml_spec.rb b/spec/views/projects/merge_requests/diffs/_diffs.html.haml_spec.rb
new file mode 100644
index 00000000000..e7c40421f1f
--- /dev/null
+++ b/spec/views/projects/merge_requests/diffs/_diffs.html.haml_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe 'projects/merge_requests/diffs/_diffs.html.haml' do
+ include Devise::Test::ControllerHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+ let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project, author: user) }
+
+ before do
+ allow(view).to receive(:url_for).and_return(controller.request.fullpath)
+
+ assign(:merge_request, merge_request)
+ assign(:environment, merge_request.environments_for(user).last)
+ assign(:diffs, merge_request.diffs)
+ assign(:merge_request_diffs, merge_request.diffs)
+ assign(:diff_notes_disabled, true) # disable note creation
+ assign(:use_legacy_diff_notes, false)
+ assign(:grouped_diff_discussions, {})
+ assign(:notes, [])
+ end
+
+ context 'for a commit' do
+ let(:commit) { merge_request.commits.last }
+
+ before do
+ assign(:commit, commit)
+ end
+
+ it "shows the commit scope" do
+ render
+
+ expect(rendered).to have_content "Only comments from the following commit are shown below"
+ end
+ end
+end
diff --git a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
new file mode 100644
index 00000000000..522e1566271
--- /dev/null
+++ b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe UpdateHeadPipelineForMergeRequestWorker do
+ describe '#perform' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:latest_sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
+
+ context 'when pipeline exists for the source project and branch' do
+ before do
+ create(:ci_empty_pipeline, project: project, ref: merge_request.source_branch, sha: latest_sha)
+ end
+
+ it 'updates the head_pipeline_id of the merge_request' do
+ expect { subject.perform(merge_request.id) }.to change { merge_request.reload.head_pipeline_id }
+ end
+
+ context 'when merge request sha does not equal pipeline sha' do
+ before do
+ merge_request.merge_request_diff.update(head_commit_sha: 'different_sha')
+ end
+
+ it 'does not update head_pipeline_id' do
+ expect { subject.perform(merge_request.id) }.to raise_error(ArgumentError)
+
+ expect(merge_request.reload.head_pipeline_id).to eq(nil)
+ end
+ end
+ end
+
+ context 'when pipeline does not exist for the source project and branch' do
+ it 'does not update the head_pipeline_id of the merge_request' do
+ expect { subject.perform(merge_request.id) }.not_to change { merge_request.reload.head_pipeline_id }
+ end
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index 88897476a15..69e8f8a0647 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -55,8 +55,8 @@
to-fast-properties "^2.0.0"
"@gitlab-org/gitlab-svgs@^1.1.1":
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.1.1.tgz#6e07ea02c3b104fa8b5d860a5e2fa9dab4edab96"
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.1.3.tgz#2beead1bcdd83e7400de29b01014bf17bf76318e"
"@types/jquery@^2.0.40":
version "2.0.48"