summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke "Jared" Bennett <lbennett@gitlab.com>2017-04-05 14:19:43 +0100
committerLuke "Jared" Bennett <lbennett@gitlab.com>2017-04-05 14:19:43 +0100
commitfefd5a089fec3295542eabdfd29beecabc8d4037 (patch)
treee454aef58583801850172396f54b2b97c201af42
parentd197347d95ba69960978ea7103f2307ade64c05e (diff)
parenta40e357f27ddcefa9ef70be84c7e7ee0f3b15e02 (diff)
downloadgitlab-ce-fefd5a089fec3295542eabdfd29beecabc8d4037.tar.gz
Merge branch 'master' into update-droplab-to-webpack-version
-rw-r--r--.gitlab-ci.yml91
-rw-r--r--.rubocop.yml4
-rw-r--r--.rubocop_todo.yml5
-rw-r--r--CHANGELOG.md35
-rw-r--r--CONTRIBUTING.md16
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile25
-rw-r--r--Gemfile.lock81
-rw-r--r--VERSION2
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js8
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji/spread_string.js50
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js2
-rw-r--r--app/assets/javascripts/blob/file_template_mediator.js241
-rw-r--r--app/assets/javascripts/blob/file_template_selector.js60
-rw-r--r--app/assets/javascripts/blob/notebook/index.js85
-rw-r--r--app/assets/javascripts/blob/notebook_viewer.js3
-rw-r--r--app/assets/javascripts/blob/template_selector.js (renamed from app/assets/javascripts/blob/template_selectors/template_selector.js)0
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js9
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js23
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js9
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js23
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js9
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js23
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_license_selector.js13
-rw-r--r--app/assets/javascripts/blob/template_selectors/blob_license_selectors.js24
-rw-r--r--app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js32
-rw-r--r--app/assets/javascripts/blob/template_selectors/dockerfile_selector.js32
-rw-r--r--app/assets/javascripts/blob/template_selectors/gitignore_selector.js31
-rw-r--r--app/assets/javascripts/blob/template_selectors/license_selector.js38
-rw-r--r--app/assets/javascripts/blob/template_selectors/type_selector.js25
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js3
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js38
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js2
-rw-r--r--app/assets/javascripts/boards/components/board.js4
-rw-r--r--app/assets/javascripts/boards/components/board_card.js4
-rw-r--r--app/assets/javascripts/boards/components/board_list.js286
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.js4
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js6
-rw-r--r--app/assets/javascripts/boards/components/sidebar/remove_issue.js2
-rw-r--r--app/assets/javascripts/boards/models/list.js2
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js4
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js7
-rw-r--r--app/assets/javascripts/cycle_analytics/components/limit_warning_component.js17
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_code_component.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_issue_component.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_plan_component.js7
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_production_component.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_review_component.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_staging_component.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_test_component.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js2
-rw-r--r--app/assets/javascripts/dispatcher.js3
-rw-r--r--app/assets/javascripts/environments/components/environment.js29
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.js11
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.js14
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js49
-rw-r--r--app/assets/javascripts/environments/components/environment_monitoring.js31
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.js12
-rw-r--r--app/assets/javascripts/environments/components/environment_terminal_button.js16
-rw-r--r--app/assets/javascripts/environments/components/environments_table.js37
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js5
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js70
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_tokenizer.js20
-rw-r--r--app/assets/javascripts/group_name.js42
-rw-r--r--app/assets/javascripts/groups_select.js78
-rw-r--r--app/assets/javascripts/header.js2
-rw-r--r--app/assets/javascripts/layout_nav.js5
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js43
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js34
-rw-r--r--app/assets/javascripts/lib/utils/poll.js55
-rw-r--r--app/assets/javascripts/main.js5
-rw-r--r--app/assets/javascripts/merge_request_tabs.js33
-rw-r--r--app/assets/javascripts/monitoring/prometheus_graph.js323
-rw-r--r--app/assets/javascripts/profile/profile.js1
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_dropdown.js8
-rw-r--r--app/assets/javascripts/right_sidebar.js49
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js2
-rw-r--r--app/assets/javascripts/test_utils/index.js4
-rw-r--r--app/assets/javascripts/test_utils/simulate_drag.js262
-rw-r--r--app/assets/javascripts/user_callout.js43
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/async_button.js1
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js6
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js1
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js8
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.js4
-rw-r--r--app/assets/stylesheets/framework/buttons.scss10
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss78
-rw-r--r--app/assets/stylesheets/framework/header.scss105
-rw-r--r--app/assets/stylesheets/framework/mixins.scss12
-rw-r--r--app/assets/stylesheets/framework/nav.scss45
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss4
-rw-r--r--app/assets/stylesheets/pages/boards.scss11
-rw-r--r--app/assets/stylesheets/pages/commits.scss4
-rw-r--r--app/assets/stylesheets/pages/diff.scss15
-rw-r--r--app/assets/stylesheets/pages/editor.scss142
-rw-r--r--app/assets/stylesheets/pages/environments.scss21
-rw-r--r--app/assets/stylesheets/pages/groups.scss17
-rw-r--r--app/assets/stylesheets/pages/issuable.scss31
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss10
-rw-r--r--app/assets/stylesheets/pages/milestone.scss82
-rw-r--r--app/assets/stylesheets/pages/notes.scss30
-rw-r--r--app/assets/stylesheets/pages/projects.scss44
-rw-r--r--app/assets/stylesheets/pages/todos.scss19
-rw-r--r--app/controllers/admin/abuse_reports_controller.rb1
-rw-r--r--app/controllers/admin/application_settings_controller.rb10
-rw-r--r--app/controllers/admin/background_jobs_controller.rb4
-rw-r--r--app/controllers/admin/labels_controller.rb9
-rw-r--r--app/controllers/admin/users_controller.rb12
-rw-r--r--app/controllers/concerns/issuable_collections.rb3
-rw-r--r--app/controllers/groups/group_members_controller.rb1
-rw-r--r--app/controllers/groups/labels_controller.rb6
-rw-r--r--app/controllers/import/base_controller.rb28
-rw-r--r--app/controllers/profiles/accounts_controller.rb13
-rw-r--r--app/controllers/profiles/notifications_controller.rb2
-rw-r--r--app/controllers/projects/builds_controller.rb4
-rw-r--r--app/controllers/projects/git_http_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb9
-rw-r--r--app/controllers/projects/labels_controller.rb6
-rwxr-xr-xapp/controllers/projects/merge_requests_controller.rb53
-rw-r--r--app/controllers/projects/milestones_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_controller.rb6
-rw-r--r--app/controllers/projects/wikis_controller.rb2
-rw-r--r--app/controllers/registrations_controller.rb15
-rw-r--r--app/controllers/search_controller.rb40
-rw-r--r--app/controllers/sessions_controller.rb2
-rw-r--r--app/finders/group_finder.rb17
-rw-r--r--app/finders/issues_finder.rb2
-rw-r--r--app/finders/labels_finder.rb13
-rw-r--r--app/finders/merge_requests_finder.rb2
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/auth_helper.rb4
-rw-r--r--app/helpers/issuables_helper.rb17
-rw-r--r--app/helpers/milestones_helper.rb4
-rw-r--r--app/helpers/nav_helper.rb3
-rw-r--r--app/helpers/sidekiq_helper.rb6
-rw-r--r--app/mailers/base_mailer.rb4
-rw-r--r--app/models/application_setting.rb7
-rw-r--r--app/models/blob.rb6
-rw-r--r--app/models/board.rb4
-rw-r--r--app/models/ci/build.rb6
-rw-r--r--app/models/ci/pipeline.rb7
-rw-r--r--app/models/commit_status.rb4
-rw-r--r--app/models/concerns/importable.rb3
-rw-r--r--app/models/concerns/issuable.rb3
-rw-r--r--app/models/concerns/repository_mirroring.rb17
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/list.rb2
-rw-r--r--app/models/merge_request.rb1
-rw-r--r--app/models/merge_request_diff.rb18
-rw-r--r--app/models/milestone.rb2
-rw-r--r--app/models/namespace.rb18
-rw-r--r--app/models/note.rb5
-rw-r--r--app/models/notification_setting.rb17
-rw-r--r--app/models/project.rb6
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/project_services/kubernetes_service.rb43
-rw-r--r--app/models/project_services/mock_deployment_service.rb18
-rw-r--r--app/models/project_services/mock_monitoring_service.rb17
-rw-r--r--app/models/project_services/prometheus_service.rb6
-rw-r--r--app/models/project_team.rb3
-rw-r--r--app/models/repository.rb36
-rw-r--r--app/models/service.rb6
-rw-r--r--app/models/system_note_metadata.rb11
-rw-r--r--app/models/user.rb32
-rw-r--r--app/serializers/build_entity.rb7
-rw-r--r--app/serializers/build_serializer.rb8
-rw-r--r--app/serializers/environment_entity.rb7
-rw-r--r--app/serializers/pipeline_entity.rb11
-rw-r--r--app/serializers/pipeline_serializer.rb7
-rw-r--r--app/serializers/status_entity.rb2
-rw-r--r--app/services/base_service.rb4
-rw-r--r--app/services/boards/create_service.rb2
-rw-r--r--app/services/boards/issues/list_service.rb2
-rw-r--r--app/services/boards/issues/move_service.rb4
-rw-r--r--app/services/ci/process_pipeline_service.rb15
-rw-r--r--app/services/ci/retry_pipeline_service.rb4
-rw-r--r--app/services/groups/update_service.rb8
-rw-r--r--app/services/issues/update_service.rb2
-rw-r--r--app/services/labels/base_service.rb161
-rw-r--r--app/services/labels/create_service.rb25
-rw-r--r--app/services/labels/find_or_create_service.rb4
-rw-r--r--app/services/labels/update_service.rb15
-rw-r--r--app/services/merge_requests/update_service.rb2
-rw-r--r--app/services/note_summary.rb20
-rw-r--r--app/services/notes/update_service.rb4
-rw-r--r--app/services/notification_recipient_service.rb48
-rw-r--r--app/services/notification_service.rb7
-rw-r--r--app/services/projects/import_service.rb31
-rw-r--r--app/services/projects/update_pages_service.rb1
-rw-r--r--app/services/search/global_service.rb8
-rw-r--r--app/services/search/project_service.rb4
-rw-r--r--app/services/search/snippet_service.rb4
-rw-r--r--app/services/search_service.rb63
-rw-r--r--app/services/system_note_service.rb89
-rw-r--r--app/services/todo_service.rb40
-rw-r--r--app/services/users/create_service.rb112
-rw-r--r--app/services/users/destroy_service.rb4
-rw-r--r--app/views/admin/application_settings/_form.html.haml14
-rw-r--r--app/views/admin/projects/_projects.html.haml15
-rw-r--r--app/views/admin/users/_access_levels.html.haml2
-rw-r--r--app/views/dashboard/_projects_head.html.haml6
-rw-r--r--app/views/dashboard/milestones/show.html.haml2
-rw-r--r--app/views/dashboard/projects/index.html.haml4
-rw-r--r--app/views/discussions/_discussion.html.haml2
-rw-r--r--app/views/explore/projects/_filter.html.haml21
-rw-r--r--app/views/groups/milestones/show.html.haml6
-rw-r--r--app/views/layouts/header/_default.html.haml38
-rw-r--r--app/views/notify/project_was_exported_email.html.haml2
-rw-r--r--app/views/profiles/accounts/show.html.haml8
-rw-r--r--app/views/profiles/notifications/show.html.haml5
-rw-r--r--app/views/projects/_home_panel.html.haml2
-rw-r--r--app/views/projects/artifacts/browse.html.haml2
-rw-r--r--app/views/projects/blob/_editor.html.haml20
-rw-r--r--app/views/projects/blob/_notebook.html.haml5
-rw-r--r--app/views/projects/blob/_template_selectors.html.haml17
-rw-r--r--app/views/projects/blob/edit.html.haml7
-rw-r--r--app/views/projects/blob/new.html.haml8
-rw-r--r--app/views/projects/boards/_show.html.haml2
-rw-r--r--app/views/projects/boards/components/_board.html.haml4
-rw-r--r--app/views/projects/boards/components/_board_list.html.haml26
-rw-r--r--app/views/projects/builds/_sidebar.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/diffs/_stats.html.haml2
-rw-r--r--app/views/projects/edit.html.haml4
-rw-r--r--app/views/projects/environments/_external_url.html.haml1
-rw-r--r--app/views/projects/environments/_metrics_button.html.haml1
-rw-r--r--app/views/projects/environments/metrics.html.haml2
-rw-r--r--app/views/projects/environments/show.html.haml4
-rw-r--r--app/views/projects/labels/index.html.haml3
-rw-r--r--app/views/projects/merge_requests/_show.html.haml6
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml8
-rw-r--r--app/views/projects/new.html.haml26
-rw-r--r--app/views/projects/pipelines/_head.html.haml4
-rw-r--r--app/views/projects/protected_branches/_dropdown.html.haml2
-rw-r--r--app/views/projects/show.html.haml6
-rw-r--r--app/views/projects/wikis/_form.html.haml4
-rw-r--r--app/views/search/_category.html.haml141
-rw-r--r--app/views/shared/_group_form.html.haml6
-rw-r--r--app/views/shared/_user_callout.html.haml14
-rw-r--r--app/views/shared/icons/_activity.svg16
-rw-r--r--app/views/shared/icons/_commits.svg10
-rw-r--r--app/views/shared/icons/_contributionanalytics.svg17
-rw-r--r--app/views/shared/icons/_delta.svg3
-rw-r--r--app/views/shared/icons/_files.svg17
-rw-r--r--app/views/shared/icons/_icon_close.svg2
-rw-r--r--app/views/shared/icons/_icon_empty_groups.svg2
-rw-r--r--app/views/shared/icons/_icon_mr_issue.svg2
-rw-r--r--app/views/shared/icons/_icon_play.svg4
-rw-r--r--app/views/shared/icons/_icon_stopwatch.svg2
-rw-r--r--app/views/shared/icons/_icon_timer.svg2
-rw-r--r--app/views/shared/icons/_illustration_no_commits.svg2
-rw-r--r--app/views/shared/icons/_members.svg13
-rw-r--r--app/views/shared/icons/_milestones.svg15
-rw-r--r--app/views/shared/icons/_mr.svg13
-rw-r--r--app/views/shared/icons/_mr_bold.svg1
-rw-r--r--app/views/shared/icons/_pipelines.svg10
-rw-r--r--app/views/shared/icons/_wiki.svg10
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml13
-rw-r--r--app/views/shared/issuable/_sidebar_todo.html.haml15
-rw-r--r--app/views/shared/members/_member.html.haml4
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml131
-rw-r--r--app/views/shared/milestones/_summary.html.haml45
-rw-r--r--app/views/shared/milestones/_tabs.html.haml47
-rw-r--r--app/views/shared/milestones/_top.html.haml3
-rw-r--r--app/views/shared/notifications/_custom_notifications.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/views/users/calendar_activities.html.haml4
-rw-r--r--app/views/users/show.html.haml39
-rw-r--r--app/workers/post_receive.rb12
-rw-r--r--app/workers/process_commit_worker.rb2
-rw-r--r--app/workers/repository_import_worker.rb1
-rw-r--r--changelogs/unreleased/12818-expose-simple-cicd-status-endpoints-with-status-serializer-gitlab-ci-status-for-pipeline-job-and-merge-request.yml5
-rw-r--r--changelogs/unreleased/17325-rugged-gem-update.yml4
-rw-r--r--changelogs/unreleased/20914-project-home-width.yml4
-rw-r--r--changelogs/unreleased/22303-symbolic-in-tree.yml4
-rw-r--r--changelogs/unreleased/22850-404-when-requesting-build-trace.yml4
-rw-r--r--changelogs/unreleased/23363-use-strong-params-in-wikis-controller.yml4
-rw-r--r--changelogs/unreleased/23655-api-group-issues.yml4
-rw-r--r--changelogs/unreleased/23674-simplify-milestone-summary.yml4
-rw-r--r--changelogs/unreleased/24187-set-git-terminal-prompt-env-var-in-initializer.yml4
-rw-r--r--changelogs/unreleased/24784-system-notes-meta-data.yml4
-rw-r--r--changelogs/unreleased/24861-stringify-group-member-details.yml4
-rw-r--r--changelogs/unreleased/25332-make-file-templates-easy-to-use-and-discover.yml4
-rw-r--r--changelogs/unreleased/25556-prevent-users-from-disconnecting-gitlab-account-from-cas.yml4
-rw-r--r--changelogs/unreleased/27293-remove-repeated-labels.yml4
-rw-r--r--changelogs/unreleased/27878-new-service-for-creating-user.yml4
-rw-r--r--changelogs/unreleased/28424-labels-support-color-names-in-backend.yml4
-rw-r--r--changelogs/unreleased/28732-expandable-folders.yml4
-rw-r--r--changelogs/unreleased/28799-todo-creation.yml4
-rw-r--r--changelogs/unreleased/29034-fix-github-importer.yml4
-rw-r--r--changelogs/unreleased/29116-maxint-error.yml4
-rw-r--r--changelogs/unreleased/29341-add-metrics-button-env-overview.yml4
-rw-r--r--changelogs/unreleased/29414-fix-toggle-discussion-link-jump.yml4
-rw-r--r--changelogs/unreleased/29432-prevent-click-disabled-btn.yml4
-rw-r--r--changelogs/unreleased/29492-useless-queries.yml4
-rw-r--r--changelogs/unreleased/29669-redirect-referer-params.yml4
-rw-r--r--changelogs/unreleased/29670-jira-integration-documentation-improvment.yml4
-rw-r--r--changelogs/unreleased/29828-change-search-hint-in-new-filters.yml4
-rw-r--r--changelogs/unreleased/29830-build-scroll-indicator.yml4
-rw-r--r--changelogs/unreleased/29843-project-subgroup-transfer.yml4
-rw-r--r--changelogs/unreleased/29866-navbar-counters.yml4
-rw-r--r--changelogs/unreleased/29929-jira-doc.yml4
-rw-r--r--changelogs/unreleased/29950-vue-pagination-icons.yml4
-rw-r--r--changelogs/unreleased/30035-milestone-with-due-date-shows-escaped-html.yml4
-rw-r--r--changelogs/unreleased/30112-fix-pipelines-sub-nav-highlight.yml4
-rw-r--r--changelogs/unreleased/30400-fix-blob-highlighting-in-search.yml4
-rw-r--r--changelogs/unreleased/add-mock-deployment-and-monitoring-service-for-development.yaml4
-rw-r--r--changelogs/unreleased/better-priority-sorting-2.yml4
-rw-r--r--changelogs/unreleased/better-priority-sorting.yml4
-rw-r--r--changelogs/unreleased/bug-api_milestone_merge_requests_scope.yml4
-rw-r--r--changelogs/unreleased/calendar-tooltips.yml4
-rw-r--r--changelogs/unreleased/create-collapsed-todo-button.yml5
-rw-r--r--changelogs/unreleased/environment-performance-improvements.yml4
-rw-r--r--changelogs/unreleased/feature-use-gitaly-for-commit-is-ancestor.yml4
-rw-r--r--changelogs/unreleased/filter-bar-fix-ie.yml4
-rw-r--r--changelogs/unreleased/fix-admin-projects.yml4
-rw-r--r--changelogs/unreleased/fix-ci-api-regression-for-after-script.yml4
-rw-r--r--changelogs/unreleased/fix-gb-remove-deprecated-pipeline-processing-code.yml4
-rw-r--r--changelogs/unreleased/fix-github-importer-slowness.yml4
-rw-r--r--changelogs/unreleased/fix-import-fork.yml4
-rw-r--r--changelogs/unreleased/fix-import-namespace.yml4
-rw-r--r--changelogs/unreleased/fix_admin_monitoring_background.yml4
-rw-r--r--changelogs/unreleased/fix_rake_gitlab_check_sidekiq.yml4
-rw-r--r--changelogs/unreleased/fix_wiki_commit_message.yml4
-rw-r--r--changelogs/unreleased/introduce-polling-interval-multiplier.yml4
-rw-r--r--changelogs/unreleased/issue_91_ee_backport.yml4
-rw-r--r--changelogs/unreleased/jej-group-name-disclosure.yml4
-rw-r--r--changelogs/unreleased/make_user_mentions_case_insensitive.yml4
-rw-r--r--changelogs/unreleased/mr-diffs-speed-up.yml4
-rw-r--r--changelogs/unreleased/namespace-race-condition.yml4
-rw-r--r--changelogs/unreleased/option-to-be-notified-of-own-activity.yml4
-rw-r--r--changelogs/unreleased/pages-debug-log.yml4
-rw-r--r--changelogs/unreleased/quiet-pipelines.yml5
-rw-r--r--changelogs/unreleased/remove_index_for_users-current_sign_in_at.yml4
-rw-r--r--changelogs/unreleased/rename_done_to_closed.yml4
-rw-r--r--changelogs/unreleased/scrollable-secondary-tabs.yml4
-rw-r--r--changelogs/unreleased/sh-fix-ssh-keys-with-spaces.yml4
-rw-r--r--changelogs/unreleased/sh-relax-wiki-slug-constraint.yml4
-rw-r--r--changelogs/unreleased/sh-remove-tags-from-explore.yml4
-rw-r--r--changelogs/unreleased/slow-search-changelog.yml4
-rw-r--r--changelogs/unreleased/style-proc-cop.yml4
-rw-r--r--changelogs/unreleased/update-test-bundle-ignored-files.yml4
-rw-r--r--changelogs/unreleased/zj-kube-service-auto-fill.yml4
-rw-r--r--config/application.rb1
-rw-r--r--config/gitlab.yml.example18
-rw-r--r--config/initializers/0_inflections.rb2
-rw-r--r--config/initializers/1_settings.rb31
-rw-r--r--config/initializers/8_gitaly.rb20
-rw-r--r--config/initializers/bullet.rb13
-rw-r--r--config/initializers/rspec_profiling.rb6
-rw-r--r--config/routes/project.rb4
-rw-r--r--config/routes/wiki.rb4
-rw-r--r--config/webpack.config.js12
-rw-r--r--db/fixtures/development/18_abuse_reports.rb28
-rw-r--r--db/fixtures/development/19_environments.rb70
-rw-r--r--db/fixtures/development/20_nested_groups.rb (renamed from db/fixtures/development/19_nested_groups.rb)0
-rw-r--r--db/fixtures/production/001_admin.rb8
-rw-r--r--db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb2
-rw-r--r--db/migrate/20170314082049_create_system_note_metadata.rb23
-rw-r--r--db/migrate/20170316061730_readd_notified_of_own_activity_to_users.rb10
-rw-r--r--db/migrate/20170329124448_add_polling_interval_multiplier_to_application_settings.rb33
-rw-r--r--db/migrate/20170402231018_remove_index_for_users_current_sign_in_at.rb25
-rw-r--r--db/schema.rb14
-rw-r--r--doc/README.md15
-rw-r--r--doc/administration/high_availability/database.md4
-rw-r--r--doc/api/boards.md2
-rw-r--r--doc/api/labels.md4
-rw-r--r--doc/api/notes.md40
-rw-r--r--doc/api/settings.md7
-rw-r--r--doc/api/v3_to_v4.md1
-rw-r--r--doc/ci/docker/using_docker_build.md2
-rw-r--r--doc/ci/examples/deployment/README.md86
-rw-r--r--doc/ci/triggers/README.md24
-rw-r--r--doc/ci/triggers/img/triggers_page.pngbin5116 -> 110560 bytes
-rw-r--r--doc/ci/variables/README.md2
-rw-r--r--doc/development/README.md2
-rw-r--r--doc/development/doc_styleguide.md7
-rw-r--r--doc/development/fe_guide/performance.md3
-rw-r--r--doc/development/fe_guide/style_guide_js.md15
-rw-r--r--doc/development/fe_guide/testing.md4
-rw-r--r--doc/development/img/cache-hit.svg21
-rw-r--r--doc/development/img/cache-miss.svg24
-rw-r--r--doc/development/polling.md3
-rw-r--r--doc/development/ux_guide/components.md14
-rw-r--r--doc/development/writing_documentation.md72
-rw-r--r--doc/install/installation.md4
-rw-r--r--doc/install/requirements.md3
-rw-r--r--doc/topics/index.md16
-rw-r--r--doc/update/8.2-to-8.3.md8
-rw-r--r--doc/update/9.0-to-9.1.md366
-rw-r--r--doc/update/patch_versions.md2
-rw-r--r--doc/user/project/integrations/img/jira_project_settings.pngbin0 -> 32791 bytes
-rw-r--r--doc/user/project/integrations/jira.md5
-rw-r--r--doc/user/project/integrations/prometheus.md4
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md28
-rw-r--r--doc/user/project/pages/getting_started_part_four.md4
-rw-r--r--doc/user/project/pages/getting_started_part_one.md4
-rw-r--r--doc/user/project/pages/getting_started_part_three.md4
-rw-r--r--doc/user/project/pages/getting_started_part_two.md4
-rw-r--r--doc/workflow/notifications.md7
-rw-r--r--features/steps/dashboard/new_project.rb2
-rw-r--r--features/steps/dashboard/todos.rb9
-rw-r--r--features/steps/group/milestones.rb2
-rw-r--r--features/steps/project/hooks.rb4
-rw-r--r--features/steps/project/merge_requests.rb2
-rw-r--r--features/support/capybara.rb5
-rw-r--r--features/support/env.rb20
-rw-r--r--lib/api/api_guard.rb2
-rw-r--r--lib/api/entities.rb3
-rw-r--r--lib/api/helpers.rb5
-rw-r--r--lib/api/helpers/runner.rb6
-rw-r--r--lib/api/internal.rb2
-rw-r--r--lib/api/issues.rb4
-rw-r--r--lib/api/labels.rb9
-rw-r--r--lib/api/merge_requests.rb35
-rw-r--r--lib/api/milestones.rb2
-rw-r--r--lib/api/notes.rb14
-rw-r--r--lib/api/runner.rb15
-rw-r--r--lib/api/services.rb8
-rw-r--r--lib/api/settings.rb3
-rw-r--r--lib/api/users.rb29
-rw-r--r--lib/api/v3/issues.rb4
-rw-r--r--lib/api/v3/users.rb53
-rw-r--r--lib/backup/repository.rb4
-rw-r--r--lib/banzai/filter/merge_request_reference_filter.rb29
-rw-r--r--lib/banzai/filter/user_reference_filter.rb4
-rw-r--r--lib/ci/api/builds.rb15
-rw-r--r--lib/ci/api/helpers.rb6
-rw-r--r--lib/gitlab/bitbucket_import/importer.rb9
-rw-r--r--lib/gitlab/ci/status/canceled.rb4
-rw-r--r--lib/gitlab/ci/status/core.rb4
-rw-r--r--lib/gitlab/ci/status/created.rb4
-rw-r--r--lib/gitlab/ci/status/failed.rb4
-rw-r--r--lib/gitlab/ci/status/manual.rb4
-rw-r--r--lib/gitlab/ci/status/pending.rb4
-rw-r--r--lib/gitlab/ci/status/running.rb4
-rw-r--r--lib/gitlab/ci/status/skipped.rb4
-rw-r--r--lib/gitlab/ci/status/success.rb4
-rw-r--r--lib/gitlab/ee_compat_check.rb4
-rw-r--r--lib/gitlab/etag_caching/middleware.rb11
-rw-r--r--lib/gitlab/git/repository.rb35
-rw-r--r--lib/gitlab/git/tree.rb2
-rw-r--r--lib/gitlab/gitaly_client.rb36
-rw-r--r--lib/gitlab/gitaly_client/commit.rb20
-rw-r--r--lib/gitlab/gitaly_client/notifications.rb15
-rw-r--r--lib/gitlab/github_import/branch_formatter.rb8
-rw-r--r--lib/gitlab/github_import/importer.rb2
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb36
-rw-r--r--lib/gitlab/import_export/hash_util.rb25
-rw-r--r--lib/gitlab/import_export/import_export.yml2
-rw-r--r--lib/gitlab/import_export/importer.rb2
-rw-r--r--lib/gitlab/import_export/merge_request_parser.rb41
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb2
-rw-r--r--lib/gitlab/import_export/relation_factory.rb17
-rw-r--r--lib/gitlab/ldap/config.rb4
-rw-r--r--lib/gitlab/o_auth/user.rb6
-rw-r--r--lib/gitlab/polling_interval.rb22
-rw-r--r--lib/gitlab/project_search_results.rb2
-rw-r--r--lib/gitlab/repo_path.rb23
-rw-r--r--lib/gitlab/search_results.rb21
-rw-r--r--lib/gitlab/shell.rb27
-rw-r--r--lib/gitlab/uploads_transfer.rb2
-rw-r--r--lib/gitlab/workhorse.rb39
-rw-r--r--lib/tasks/gitlab/check.rake4
-rw-r--r--lib/tasks/karma.rake5
-rwxr-xr-xscripts/merge-reports1
-rwxr-xr-xscripts/sync-reports95
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb25
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb67
-rw-r--r--spec/controllers/import/gitlab_controller_spec.rb66
-rw-r--r--spec/controllers/profiles/accounts_controller_spec.rb52
-rw-r--r--spec/controllers/profiles/notifications_controller_spec.rb45
-rw-r--r--spec/controllers/profiles/personal_access_tokens_spec.rb2
-rw-r--r--spec/controllers/projects/builds_controller_spec.rb33
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb33
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb1
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb38
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb20
-rw-r--r--spec/controllers/registrations_controller_spec.rb25
-rw-r--r--spec/controllers/sessions_controller_spec.rb16
-rw-r--r--spec/factories/boards.rb2
-rw-r--r--spec/factories/chat_names.rb8
-rw-r--r--spec/factories/chat_teams.rb5
-rw-r--r--spec/factories/ci/pipelines.rb4
-rw-r--r--spec/factories/ci/runners.rb4
-rw-r--r--spec/factories/emails.rb2
-rw-r--r--spec/factories/issues.rb6
-rw-r--r--spec/factories/labels.rb11
-rw-r--r--spec/factories/lists.rb4
-rw-r--r--spec/factories/merge_requests.rb2
-rw-r--r--spec/factories/oauth_applications.rb4
-rw-r--r--spec/factories/personal_access_tokens.rb2
-rw-r--r--spec/factories/project_hooks.rb2
-rw-r--r--spec/factories/sequences.rb12
-rw-r--r--spec/factories/service_hooks.rb2
-rw-r--r--spec/factories/snippets.rb14
-rw-r--r--spec/factories/spam_logs.rb6
-rw-r--r--spec/factories/system_hooks.rb2
-rw-r--r--spec/factories/system_note_metadata.rb6
-rw-r--r--spec/factories/users.rb8
-rw-r--r--spec/features/admin/admin_browse_spam_logs_spec.rb2
-rw-r--r--spec/features/admin/admin_health_check_spec.rb6
-rw-r--r--spec/features/admin/admin_hooks_spec.rb2
-rw-r--r--spec/features/admin/admin_users_impersonation_tokens_spec.rb2
-rw-r--r--spec/features/auto_deploy_spec.rb4
-rw-r--r--spec/features/boards/boards_spec.rb22
-rw-r--r--spec/features/boards/new_issue_spec.rb2
-rw-r--r--spec/features/groups/group_name_toggle_spec.rb47
-rw-r--r--spec/features/groups_spec.rb12
-rw-r--r--spec/features/issuables/issuable_list_spec.rb9
-rw-r--r--spec/features/issues/filtered_search/dropdown_hint_spec.rb4
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb42
-rw-r--r--spec/features/issues/todo_spec.rb8
-rw-r--r--spec/features/issues_spec.rb17
-rw-r--r--spec/features/merge_requests/diff_notes_resolve_spec.rb6
-rw-r--r--spec/features/merge_requests/merge_commit_message_toggle_spec.rb2
-rw-r--r--spec/features/merge_requests/toggler_behavior_spec.rb2
-rw-r--r--spec/features/milestones/milestones_spec.rb6
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb4
-rw-r--r--spec/features/profiles/user_changes_notified_of_own_activity_spec.rb32
-rw-r--r--spec/features/projects/blobs/user_create_spec.rb2
-rw-r--r--spec/features/projects/environments/environment_spec.rb19
-rw-r--r--spec/features/projects/environments/environments_spec.rb40
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb4
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb4
-rw-r--r--spec/features/projects/files/template_type_dropdown_spec.rb135
-rw-r--r--spec/features/projects/files/undo_template_spec.rb67
-rw-r--r--spec/features/projects/group_links_spec.rb22
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb8
-rw-r--r--spec/features/projects/milestones/milestone_spec.rb18
-rw-r--r--spec/features/projects/services/mattermost_slash_command_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb20
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb6
-rw-r--r--spec/features/search_spec.rb4
-rw-r--r--spec/features/todos/todos_spec.rb6
-rw-r--r--spec/features/u2f_spec.rb17
-rw-r--r--spec/features/user_callout_spec.rb18
-rw-r--r--spec/finders/issues_finder_spec.rb2
-rw-r--r--spec/fixtures/api/schemas/list.json2
-rw-r--r--spec/fixtures/metrics.json1
-rw-r--r--spec/helpers/auth_helper_spec.rb14
-rw-r--r--spec/helpers/sidekiq_helper_spec.rb8
-rw-r--r--spec/initializers/trusted_proxies_spec.rb2
-rw-r--r--spec/javascripts/blob/notebook/index_spec.js159
-rw-r--r--spec/javascripts/boards/board_card_spec.js19
-rw-r--r--spec/javascripts/boards/board_list_spec.js201
-rw-r--r--spec/javascripts/boards/boards_store_spec.js18
-rw-r--r--spec/javascripts/boards/list_spec.js10
-rw-r--r--spec/javascripts/boards/mock_data.js8
-rw-r--r--spec/javascripts/collapsed_sidebar_todo_spec.js123
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js53
-rw-r--r--spec/javascripts/cycle_analytics/limit_warning_component_spec.js39
-rw-r--r--spec/javascripts/environments/environment_actions_spec.js7
-rw-r--r--spec/javascripts/environments/environment_monitoring_spec.js23
-rw-r--r--spec/javascripts/environments/environment_spec.js103
-rw-r--r--spec/javascripts/environments/environment_stop_spec.js2
-rw-r--r--spec/javascripts/environments/environment_terminal_button_spec.js2
-rw-r--r--spec/javascripts/environments/environments_store_spec.js110
-rw-r--r--spec/javascripts/environments/mock_data.js16
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js14
-rw-r--r--spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js8
-rw-r--r--spec/javascripts/fixtures/dashboard.rb31
-rw-r--r--spec/javascripts/fixtures/merge_requests.rb12
-rw-r--r--spec/javascripts/fixtures/notebook_viewer.html.haml1
-rw-r--r--spec/javascripts/fixtures/user_callout.html.haml2
-rw-r--r--spec/javascripts/header_spec.js14
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js80
-rw-r--r--spec/javascripts/lib/utils/number_utility_spec.js41
-rw-r--r--spec/javascripts/lib/utils/poll_spec.js40
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js40
-rw-r--r--spec/javascripts/monitoring/prometheus_graph_spec.js10
-rw-r--r--spec/javascripts/right_sidebar_spec.js8
-rw-r--r--spec/javascripts/test_bundle.js19
-rw-r--r--spec/javascripts/user_callout_spec.js44
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/table_pagination_spec.js8
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb13
-rw-r--r--spec/lib/banzai/filter/merge_request_reference_filter_spec.rb13
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb8
-rw-r--r--spec/lib/gitlab/backend/shell_spec.rb9
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb9
-rw-r--r--spec/lib/gitlab/ci/status/canceled_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/created_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/failed_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/manual_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/pending_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/running_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/skipped_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/success_spec.rb4
-rw-r--r--spec/lib/gitlab/etag_caching/middleware_spec.rb13
-rw-r--r--spec/lib/gitlab/git/blob_snippet_spec.rb2
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb8
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb28
-rw-r--r--spec/lib/gitlab/git/compare_spec.rb10
-rw-r--r--spec/lib/gitlab/git/diff_collection_spec.rb4
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb27
-rw-r--r--spec/lib/gitlab/git/tree_spec.rb11
-rw-r--r--spec/lib/gitlab/git/util_spec.rb2
-rw-r--r--spec/lib/gitlab/git_access_spec.rb8
-rw-r--r--spec/lib/gitlab/git_spec.rb17
-rw-r--r--spec/lib/gitlab/gitaly_client/notifications_spec.rb11
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb26
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb4
-rw-r--r--spec/lib/gitlab/github_import/label_formatter_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/pull_request_formatter_spec.rb56
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml3
-rw-r--r--spec/lib/gitlab/import_export/fork_spec.rb49
-rw-r--r--spec/lib/gitlab/import_export/hash_util_spec.rb28
-rw-r--r--spec/lib/gitlab/import_export/merge_request_parser_spec.rb31
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/relation_factory_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/repo_saver_spec.rb (renamed from spec/lib/gitlab/import_export/repo_bundler_spec.rb)0
-rw-r--r--spec/lib/gitlab/ldap/user_spec.rb2
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb9
-rw-r--r--spec/lib/gitlab/polling_interval_spec.rb34
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb8
-rw-r--r--spec/lib/gitlab/repo_path_spec.rb22
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb70
-rw-r--r--spec/mailers/notify_spec.rb19
-rw-r--r--spec/migrations/migrate_process_commit_worker_jobs_spec.rb2
-rw-r--r--spec/models/blob_spec.rb19
-rw-r--r--spec/models/ci/pipeline_spec.rb7
-rw-r--r--spec/models/commit_status_spec.rb36
-rw-r--r--spec/models/cycle_analytics/plan_spec.rb4
-rw-r--r--spec/models/cycle_analytics/production_spec.rb2
-rw-r--r--spec/models/cycle_analytics/staging_spec.rb2
-rw-r--r--spec/models/hooks/system_hook_spec.rb5
-rw-r--r--spec/models/issue_spec.rb8
-rw-r--r--spec/models/list_spec.rb22
-rw-r--r--spec/models/milestone_spec.rb2
-rw-r--r--spec/models/namespace_spec.rb79
-rw-r--r--spec/models/pages_domain_spec.rb14
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb89
-rw-r--r--spec/models/repository_spec.rb40
-rw-r--r--spec/models/system_note_metadata_spec.rb27
-rw-r--r--spec/models/user_spec.rb13
-rw-r--r--spec/policies/issue_policy_spec.rb2
-rw-r--r--spec/policies/project_policy_spec.rb2
-rw-r--r--spec/requests/api/files_spec.rb17
-rw-r--r--spec/requests/api/internal_spec.rb6
-rw-r--r--spec/requests/api/issues_spec.rb26
-rw-r--r--spec/requests/api/members_spec.rb2
-rw-r--r--spec/requests/api/merge_requests_spec.rb58
-rw-r--r--spec/requests/api/milestones_spec.rb2
-rw-r--r--spec/requests/api/notes_spec.rb60
-rw-r--r--spec/requests/api/projects_spec.rb4
-rw-r--r--spec/requests/api/runner_spec.rb23
-rw-r--r--spec/requests/api/users_spec.rb10
-rw-r--r--spec/requests/api/v3/files_spec.rb4
-rw-r--r--spec/requests/api/v3/issues_spec.rb26
-rw-r--r--spec/requests/api/v3/members_spec.rb2
-rw-r--r--spec/requests/api/v3/projects_spec.rb4
-rw-r--r--spec/requests/api/v3/users_spec.rb14
-rw-r--r--spec/routing/environments_spec.rb49
-rw-r--r--spec/serializers/analytics_issue_serializer_spec.rb2
-rw-r--r--spec/serializers/analytics_merge_request_serializer_spec.rb2
-rw-r--r--spec/serializers/build_entity_spec.rb13
-rw-r--r--spec/serializers/build_serializer_spec.rb45
-rw-r--r--spec/serializers/commit_entity_spec.rb2
-rw-r--r--spec/serializers/deployment_entity_spec.rb9
-rw-r--r--spec/serializers/environment_entity_spec.rb20
-rw-r--r--spec/serializers/environment_serializer_spec.rb15
-rw-r--r--spec/serializers/pipeline_entity_spec.rb2
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb16
-rw-r--r--spec/serializers/status_entity_spec.rb2
-rw-r--r--spec/services/after_branch_delete_service_spec.rb2
-rw-r--r--spec/services/boards/create_service_spec.rb2
-rw-r--r--spec/services/boards/issues/list_service_spec.rb6
-rw-r--r--spec/services/boards/issues/move_service_spec.rb10
-rw-r--r--spec/services/boards/lists/destroy_service_spec.rb8
-rw-r--r--spec/services/boards/lists/list_service_spec.rb2
-rw-r--r--spec/services/boards/lists/move_service_spec.rb6
-rw-r--r--spec/services/chat_names/find_user_service_spec.rb9
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb2
-rw-r--r--spec/services/ci/create_trigger_request_service_spec.rb2
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb59
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb13
-rw-r--r--spec/services/ci/stop_environments_service_spec.rb2
-rw-r--r--spec/services/ci/update_build_queue_service_spec.rb2
-rw-r--r--spec/services/compare_service_spec.rb2
-rw-r--r--spec/services/create_release_service_spec.rb2
-rw-r--r--spec/services/delete_branch_service_spec.rb2
-rw-r--r--spec/services/delete_merged_branches_service_spec.rb2
-rw-r--r--spec/services/files/update_service_spec.rb2
-rw-r--r--spec/services/git_hooks_service_spec.rb4
-rw-r--r--spec/services/git_push_service_spec.rb4
-rw-r--r--spec/services/git_tag_push_service_spec.rb4
-rw-r--r--spec/services/groups/create_service_spec.rb2
-rw-r--r--spec/services/groups/destroy_service_spec.rb2
-rw-r--r--spec/services/groups/update_service_spec.rb20
-rw-r--r--spec/services/issues/build_service_spec.rb2
-rw-r--r--spec/services/issues/move_service_spec.rb4
-rw-r--r--spec/services/issues/resolve_discussions_spec.rb2
-rw-r--r--spec/services/issues/update_service_spec.rb11
-rw-r--r--spec/services/labels/create_service_spec.rb186
-rw-r--r--spec/services/labels/find_or_create_service_spec.rb2
-rw-r--r--spec/services/labels/transfer_service_spec.rb4
-rw-r--r--spec/services/labels/update_service_spec.rb80
-rw-r--r--spec/services/members/destroy_service_spec.rb2
-rw-r--r--spec/services/members/request_access_service_spec.rb6
-rw-r--r--spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb2
-rw-r--r--spec/services/merge_requests/assign_issues_service_spec.rb2
-rw-r--r--spec/services/merge_requests/build_service_spec.rb2
-rw-r--r--spec/services/merge_requests/create_service_spec.rb2
-rw-r--r--spec/services/merge_requests/get_urls_service_spec.rb2
-rw-r--r--spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb2
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb6
-rw-r--r--spec/services/merge_requests/resolve_service_spec.rb2
-rw-r--r--spec/services/merge_requests/update_service_spec.rb13
-rw-r--r--spec/services/milestones/close_service_spec.rb4
-rw-r--r--spec/services/note_summary_spec.rb44
-rw-r--r--spec/services/notes/diff_position_update_service_spec.rb2
-rw-r--r--spec/services/notes/update_service_spec.rb16
-rw-r--r--spec/services/notification_service_spec.rb261
-rw-r--r--spec/services/projects/destroy_service_spec.rb2
-rw-r--r--spec/services/projects/download_service_spec.rb4
-rw-r--r--spec/services/projects/fork_service_spec.rb19
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb2
-rw-r--r--spec/services/projects/import_service_spec.rb83
-rw-r--r--spec/services/projects/transfer_service_spec.rb6
-rw-r--r--spec/services/projects/update_pages_service_spec.rb6
-rw-r--r--spec/services/projects/update_service_spec.rb4
-rw-r--r--spec/services/projects/upload_service_spec.rb4
-rw-r--r--spec/services/search/global_service_spec.rb66
-rw-r--r--spec/services/search_service_spec.rb299
-rw-r--r--spec/services/slash_commands/interpret_service_spec.rb7
-rw-r--r--spec/services/spam_service_spec.rb2
-rw-r--r--spec/services/system_hooks_service_spec.rb14
-rw-r--r--spec/services/system_note_service_spec.rb231
-rw-r--r--spec/services/tags/create_service_spec.rb2
-rw-r--r--spec/services/tags/destroy_service_spec.rb2
-rw-r--r--spec/services/test_hook_service_spec.rb6
-rw-r--r--spec/services/todo_service_spec.rb177
-rw-r--r--spec/services/update_release_service_spec.rb2
-rw-r--r--spec/services/users/create_service_spec.rb225
-rw-r--r--spec/services/users/destroy_spec.rb23
-rw-r--r--spec/services/users/refresh_authorized_projects_service_spec.rb4
-rw-r--r--spec/spec_helper.rb3
-rw-r--r--spec/support/capybara.rb10
-rw-r--r--spec/support/controllers/githubish_import_controller_shared_examples.rb71
-rw-r--r--spec/support/cycle_analytics_helpers.rb10
-rw-r--r--spec/support/drag_to_helper.rb4
-rw-r--r--spec/support/filter_spec_helper.rb4
-rw-r--r--spec/support/git_helpers.rb9
-rw-r--r--spec/support/issuables_list_metadata_shared_examples.rb19
-rw-r--r--spec/support/matchers/query_matcher.rb33
-rw-r--r--spec/support/notify_shared_examples.rb2
-rw-r--r--spec/support/prometheus_helpers.rb4
-rw-r--r--spec/support/select2_helper.rb8
-rw-r--r--spec/support/services/issuable_create_service_slash_commands_shared_examples.rb4
-rw-r--r--spec/support/stored_repositories.rb5
-rw-r--r--spec/support/stub_configuration.rb4
-rw-r--r--spec/support/test_env.rb32
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb20
-rw-r--r--spec/views/projects/builds/_build.html.haml_spec.rb2
-rw-r--r--spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb2
-rw-r--r--spec/views/projects/builds/show.html.haml_spec.rb2
-rw-r--r--spec/views/projects/commit/_commit_box.html.haml_spec.rb2
-rw-r--r--spec/views/projects/issues/_related_branches.html.haml_spec.rb2
-rw-r--r--spec/views/projects/merge_requests/_commits.html.haml_spec.rb7
-rw-r--r--spec/views/projects/merge_requests/edit.html.haml_spec.rb4
-rw-r--r--spec/views/projects/merge_requests/show.html.haml_spec.rb4
-rw-r--r--spec/views/projects/pipelines/_stage.html.haml_spec.rb2
-rw-r--r--spec/views/projects/pipelines/show.html.haml_spec.rb2
-rw-r--r--spec/views/projects/tree/show.html.haml_spec.rb2
-rw-r--r--spec/workers/delete_merged_branches_worker_spec.rb2
-rw-r--r--spec/workers/emails_on_push_worker_spec.rb2
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb2
-rw-r--r--spec/workers/group_destroy_worker_spec.rb2
-rw-r--r--spec/workers/pipeline_metrics_worker_spec.rb2
-rw-r--r--spec/workers/pipeline_notification_worker_spec.rb126
-rw-r--r--spec/workers/post_receive_spec.rb2
-rw-r--r--spec/workers/process_commit_worker_spec.rb9
-rw-r--r--spec/workers/project_cache_worker_spec.rb2
-rw-r--r--spec/workers/project_destroy_worker_spec.rb2
-rw-r--r--spec/workers/repository_check/batch_worker_spec.rb8
-rw-r--r--spec/workers/repository_fork_worker_spec.rb4
-rw-r--r--spec/workers/repository_import_worker_spec.rb2
-rw-r--r--spec/workers/update_merge_requests_worker_spec.rb2
-rw-r--r--vendor/assets/javascripts/notebooklab.js5887
785 files changed, 16606 insertions, 3518 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f271ab4c4c8..66f8b6e6f9a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -8,19 +8,22 @@ cache:
variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
RAILS_ENV: "test"
+ NODE_ENV: "test"
SIMPLECOV: "true"
SETUP_DB: "true"
USE_BUNDLE_INSTALL: "true"
GIT_DEPTH: "20"
PHANTOMJS_VERSION: "2.1.1"
GET_SOURCES_ATTEMPTS: "3"
+ KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/rspec_report-master.json
+ KNAPSACK_SPINACH_SUITE_REPORT_PATH: knapsack/${CI_PROJECT_NAME}/spinach_report-master.json
before_script:
- source ./scripts/prepare_build.sh
- cp config/gitlab.yml.example config/gitlab.yml
- bundle --version
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) --clean $FLAGS'
- - retry gem install knapsack
+ - retry gem install knapsack fog-aws mime-types
- '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate add_limits_mysql'
stages:
@@ -39,14 +42,15 @@ stages:
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
+ KNAPSACK_S3_BUCKET: "gitlab-ce-cache"
cache:
key: "knapsack"
paths:
- - knapsack/
+ - knapsack/
artifacts:
expire_in: 31d
paths:
- - knapsack/
+ - knapsack/
.use-db: &use-db
services:
@@ -61,17 +65,17 @@ stages:
- JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]}
- - export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
+ - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- - cp knapsack/rspec_report.json ${KNAPSACK_REPORT_PATH}
+ - cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- knapsack rspec "--color --format documentation"
artifacts:
expire_in: 31d
when: always
paths:
- - coverage/
- - knapsack/
- - tmp/capybara/
+ - coverage/
+ - knapsack/
+ - tmp/capybara/
.spinach-knapsack: &spinach-knapsack
stage: test
@@ -81,28 +85,44 @@ stages:
- JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]}
- - export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
+ - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- - cp knapsack/spinach_report.json ${KNAPSACK_REPORT_PATH}
+ - cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts:
expire_in: 31d
when: always
paths:
- - coverage/
- - knapsack/
- - tmp/capybara/
+ - coverage/
+ - knapsack/
+ - tmp/capybara/
# Prepare and merge knapsack tests
-
knapsack:
<<: *knapsack-state
<<: *dedicated-runner
stage: prepare
script:
- - mkdir -p knapsack/
- - '[[ -f knapsack/rspec_report.json ]] || echo "{}" > knapsack/rspec_report.json'
- - '[[ -f knapsack/spinach_report.json ]] || echo "{}" > knapsack/spinach_report.json'
+ - mkdir -p knapsack/${CI_PROJECT_NAME}/
+ - wget -O $KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${KNAPSACK_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $KNAPSACK_RSPEC_SUITE_REPORT_PATH
+ - wget -O $KNAPSACK_SPINACH_SUITE_REPORT_PATH http://${KNAPSACK_S3_BUCKET}.s3.amazonaws.com/$KNAPSACK_SPINACH_SUITE_REPORT_PATH || rm $KNAPSACK_SPINACH_SUITE_REPORT_PATH
+ - '[[ -f $KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_RSPEC_SUITE_REPORT_PATH}'
+ - '[[ -f $KNAPSACK_SPINACH_SUITE_REPORT_PATH ]] || echo "{}" > ${KNAPSACK_SPINACH_SUITE_REPORT_PATH}'
+
+update-knapsack:
+ <<: *knapsack-state
+ <<: *dedicated-runner
+ stage: post-test
+ script:
+ - scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec_node_*.json
+ - scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach_node_*.json
+ - '[[ -z ${KNAPSACK_S3_BUCKET} ]] || scripts/sync-reports put $KNAPSACK_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
+ - rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json
+ only:
+ - master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
+ - master@gitlab/gitlabhq
+ - master@gitlab/gitlab-ee
setup-test-env:
<<: *use-db
@@ -110,9 +130,7 @@ setup-test-env:
stage: prepare
script:
- node --version
- - yarn --version
- yarn install --pure-lockfile
- - yarn check # ensure that yarn.lock matches package.json
- bundle exec rake gitlab:assets:compile
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
artifacts:
@@ -122,20 +140,6 @@ setup-test-env:
- public/assets
- tmp/tests
-update-knapsack:
- <<: *knapsack-state
- <<: *dedicated-runner
- stage: post-test
- script:
- - scripts/merge-reports knapsack/rspec_report.json knapsack/rspec_node_*.json
- - scripts/merge-reports knapsack/spinach_report.json knapsack/spinach_node_*.json
- - rm -f knapsack/*_node_*.json
- only:
- - master@gitlab-org/gitlab-ce
- - master@gitlab-org/gitlab-ee
- - master@gitlab/gitlabhq
- - master@gitlab/gitlab-ee
-
rspec 0 20: *rspec-knapsack
rspec 1 20: *rspec-knapsack
rspec 2 20: *rspec-knapsack
@@ -287,14 +291,31 @@ rake karma:
paths:
- coverage-javascript/
-lint-doc:
+docs:check:apilint:
+ image: "phusion/baseimage"
stage: test
<<: *dedicated-runner
- image: "phusion/baseimage:latest"
+ cache: {}
+ dependencies: []
before_script: []
script:
- scripts/lint-doc.sh
+docs:check:links:
+ image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
+ stage: test
+ <<: *dedicated-runner
+ cache: {}
+ dependencies: []
+ before_script: []
+ script:
+ - mv doc/ /nanoc/content/
+ - cd /nanoc
+ # Build HTML from Markdown
+ - bundle exec nanoc
+ # Check the internal links
+ - bundle exec nanoc check internal_links
+
bundler:check:
stage: test
<<: *dedicated-runner
diff --git a/.rubocop.yml b/.rubocop.yml
index fa1370ea1f3..ac6b141cea3 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -533,6 +533,10 @@ Style/WhileUntilModifier:
Style/WordArray:
Enabled: true
+# Use `proc` instead of `Proc.new`.
+Style/Proc:
+ Enabled: true
+
# Metrics #####################################################################
# A calculated magnitude based on number of assignments,
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index c24142c0a11..8588988dc87 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -226,11 +226,6 @@ Style/PredicateName:
Style/PreferredHashMethods:
Enabled: false
-# Offense count: 8
-# Cop supports --auto-correct.
-Style/Proc:
- Enabled: false
-
# Offense count: 62
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4291eca8dc7..3e5475a2296 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,41 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 9.0.3 (2017-04-05)
+
+- Fix name colision when importing GitHub pull requests from forked repositories. !9719
+- Fix GitHub Importer for PRs of deleted forked repositories. !9992
+- Fix environment folder route when special chars present in environment name. !10250
+- Improve Markdown rendering when a lot of merge requests are referenced. !10252
+- Allow users to import GitHub projects to subgroups.
+- Backport API changes needed to fix sticking in EE.
+- Remove unnecessary ORDER BY clause from `forked_to_project_id` subquery. (mhasbini)
+- Make CI build to use optimistic locking only on status change.
+- Fix race condition where a namespace would be deleted before a project was deleted.
+- Fix linking to new issue with selected template via url parameter.
+- Remove unnecessary ORDER BY clause when updating todos. (mhasbini)
+- API: Make the /notes endpoint work with noteable iid instead of id.
+- Fixes method not replacing URL parameters correctly and breaking pipelines pagination.
+- Move issue, mr, todos next to profile dropdown in top nav.
+
+## 9.0.2 (2017-03-29)
+
+- Correctly update paths when changing a child group.
+- Fixed private group name disclosure via new/update forms.
+
+## 9.0.1 (2017-03-28)
+
+- Resolve "404 when requesting build trace". !9759 (dosuken123)
+- Simplify search queries for projects and merge requests. !10053 (mhasbini)
+- Fix after_script processing for Runners APIv4. !10185
+- Fix escaped html appearing in milestone page. !10224
+- Fix bug that caused jobs that already had been retried to be retried again when retrying a pipeline. !10249
+- Allow filtering by all started milestones.
+- Allow sorting by due date and priority.
+- Fixed branches pagination not displaying.
+- Fixed filtered search not working in IE.
+- Optimize labels finder query when searching for a project with a group. (mhasbini)
+
## 9.0.0 (2017-03-22)
- Fix inconsistent naming for services that delete things. !5803 (dixpac)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 275c0cd1777..73c8a77364b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -314,9 +314,12 @@ request is as follows:
organized commits by [squashing them][git-squash]
1. Push the commit(s) to your fork
1. Submit a merge request (MR) to the `master` branch
-1. Leave the approvals settings as they are:
- 1. Your merge request needs at least 1 approval
- 1. You don't have to select any approvers
+ 1. Your merge request needs at least 1 approval but feel free to require more.
+ For instance if you're touching backend and frontend code, it's a good idea
+ to require 2 approvals: 1 from a backend maintainer and 1 from a frontend
+ maintainer
+ 1. You don't have to select any approvers, but you can if you really want
+ specific people to approve your merge request
1. The MR title should describe the change you want to make
1. The MR description should give a motive for your change and the method you
used to achieve it.
@@ -376,7 +379,7 @@ There are a few rules to get your merge request accepted:
1. If your merge request includes only frontend changes [^1], it must be
**approved by a [frontend maintainer][team]**.
1. If your merge request includes frontend and backend changes [^1], it must
- be approved by a frontend **and** a backend maintainer.
+ be **approved by a [frontend and a backend maintainer][team]**.
1. To lower the amount of merge requests maintainers need to review, you can
ask or assign any [reviewers][team] for a first review.
1. If you need some guidance (e.g. it's your first merge request), feel free
@@ -556,6 +559,5 @@ 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
-[^1]: Specs other than JavaScript specs are considered backend code. Haml
- changes are considered backend code if they include Ruby code other than just
- pure HTML.
+[^1]: Please note that specs other than JavaScript specs are considered backend
+ code.
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 1d0ba9ea182..8f0916f768f 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.4.0
+0.5.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 0062ac97180..a1ef0cae183 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-5.0.0
+5.0.2
diff --git a/Gemfile b/Gemfile
index 38158387642..6a45b3d7339 100644
--- a/Gemfile
+++ b/Gemfile
@@ -15,7 +15,7 @@ gem 'default_value_for', '~> 3.0.0'
gem 'mysql2', '~> 0.3.16', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres
-gem 'rugged', '~> 0.24.0'
+gem 'rugged', '~> 0.25.1.1'
# Authentication libraries
gem 'devise', '~> 4.2'
@@ -63,7 +63,7 @@ gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: 'omniauth-ldap'
# Git Wiki
# Required manually in config/initializers/gollum.rb to control load order
gem 'gollum-lib', '~> 4.2', require: false
-gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
+gem 'gollum-rugged_adapter', '~> 0.4.4', require: false
# Language detection
gem 'github-linguist', '~> 4.7.0', require: 'linguist'
@@ -223,7 +223,7 @@ gem 'oj', '~> 2.17.4'
gem 'chronic', '~> 0.10.2'
gem 'chronic_duration', '~> 0.10.6'
-gem 'webpack-rails', '~> 0.9.9'
+gem 'webpack-rails', '~> 0.9.10'
gem 'rack-proxy', '~> 0.6.0'
gem 'sass-rails', '~> 5.0.6'
@@ -244,7 +244,7 @@ gem 'net-ssh', '~> 3.0.1'
gem 'base32', '~> 0.3.0'
# Sentry integration
-gem 'sentry-raven', '~> 2.0.0'
+gem 'sentry-raven', '~> 2.4.0'
gem 'premailer-rails', '~> 1.9.0'
@@ -257,15 +257,13 @@ end
group :development do
gem 'foreman', '~> 0.78.0'
- gem 'brakeman', '~> 3.4.0', require: false
+ gem 'brakeman', '~> 3.6.0', require: false
gem 'letter_opener_web', '~> 1.3.0'
- gem 'bullet', '~> 5.2.0', require: false
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
- gem 'web-console', '~> 2.0'
# Better errors handler
- gem 'better_errors', '~> 1.0.1'
+ gem 'better_errors', '~> 2.1.0'
gem 'binding_of_caller', '~> 0.7.2'
# thin instead webrick
@@ -273,6 +271,7 @@ group :development do
end
group :development, :test do
+ gem 'bullet', '~> 5.5.0', require: !!ENV['ENABLE_BULLET']
gem 'pry-byebug', '~> 3.4.1', platform: :mri
gem 'pry-rails', '~> 0.3.4'
@@ -297,7 +296,7 @@ group :development, :test do
gem 'capybara-screenshot', '~> 1.0.0'
gem 'poltergeist', '~> 1.9.0'
- gem 'spring', '~> 1.7.0'
+ gem 'spring', '~> 2.0.0'
gem 'spring-commands-rspec', '~> 1.0.4'
gem 'spring-commands-spinach', '~> 1.1.0'
@@ -305,8 +304,8 @@ group :development, :test do
gem 'rubocop-rspec', '~> 1.12.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false
gem 'haml_lint', '~> 0.21.0', require: false
- gem 'simplecov', '0.12.0', require: false
- gem 'flay', '~> 2.6.1', require: false
+ gem 'simplecov', '~> 0.14.0', require: false
+ gem 'flay', '~> 2.8.0', require: false
gem 'bundler-audit', '~> 0.5.0', require: false
gem 'benchmark-ips', '~> 2.3.0', require: false
@@ -323,7 +322,7 @@ group :test do
gem 'shoulda-matchers', '~> 2.8.0', require: false
gem 'email_spec', '~> 1.6.0'
gem 'json-schema', '~> 2.6.2'
- gem 'webmock', '~> 1.21.0'
+ gem 'webmock', '~> 1.24.0'
gem 'test_after_commit', '~> 1.1'
gem 'sham_rack', '~> 1.3.6'
gem 'timecop', '~> 0.8.0'
@@ -353,4 +352,4 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client
-gem 'gitaly', '~> 0.3.0'
+gem 'gitaly', '~> 0.5.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 07be5d7aded..50ca9af7a7a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -75,19 +75,20 @@ GEM
base32 (0.3.2)
bcrypt (3.1.11)
benchmark-ips (2.3.0)
- better_errors (1.0.1)
+ better_errors (2.1.1)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
+ rack (>= 0.9.0)
bindata (2.3.5)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
- brakeman (3.4.1)
+ brakeman (3.6.1)
browser (2.2.0)
builder (3.2.3)
- bullet (5.2.0)
+ bullet (5.5.1)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.10.0)
bundler-audit (0.5.0)
@@ -101,7 +102,7 @@ GEM
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
- capybara-screenshot (1.0.11)
+ capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
carrierwave (0.11.2)
@@ -117,7 +118,7 @@ GEM
numerizer (~> 0.1.1)
chunky_png (1.3.5)
cliver (0.3.2)
- coderay (1.1.0)
+ coderay (1.1.1)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
coffee-rails (4.1.1)
@@ -200,7 +201,9 @@ GEM
multi_json
ffaker (2.4.0)
ffi (1.9.10)
- flay (2.6.1)
+ flay (2.8.1)
+ erubis (~> 2.7.0)
+ path_expander (~> 1.0)
ruby_parser (~> 3.0)
sexp_processor (~> 4.0)
flowdock (0.7.1)
@@ -250,7 +253,7 @@ GEM
json
get_process_mem (0.2.0)
gherkin-ruby (0.3.2)
- gitaly (0.3.0)
+ gitaly (0.5.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@@ -285,9 +288,9 @@ GEM
rouge (~> 2.0)
sanitize (~> 2.1.0)
stringex (~> 2.5.1)
- gollum-rugged_adapter (0.4.2)
+ gollum-rugged_adapter (0.4.4)
mime-types (>= 1.15)
- rugged (~> 0.24.0, >= 0.21.3)
+ rugged (~> 0.25)
gon (6.1.0)
actionpack (>= 3.0)
json
@@ -340,6 +343,7 @@ GEM
temple (~> 0.7.6)
thor
tilt
+ hashdiff (0.3.2)
hashie (3.5.5)
health_check (2.6.0)
rails (>= 4.0)
@@ -518,6 +522,7 @@ GEM
activerecord (>= 4.0, < 5.1)
parser (2.4.0.0)
ast (~> 2.2)
+ path_expander (1.0.1)
pg (0.18.4)
poltergeist (1.9.0)
capybara (~> 2.1)
@@ -532,14 +537,14 @@ GEM
premailer-rails (1.9.2)
actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9)
- pry (0.10.3)
+ pry (0.10.4)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
- pry-byebug (3.4.1)
+ pry-byebug (3.4.2)
byebug (~> 9.0)
pry (~> 0.10)
- pry-rails (0.3.4)
+ pry-rails (0.3.5)
pry (>= 0.9.10)
pyu-ruby-sasl (0.0.3.3)
rack (1.6.5)
@@ -671,13 +676,13 @@ GEM
ruby-progressbar (1.8.1)
ruby-saml (1.4.1)
nokogiri (>= 1.5.10)
- ruby_parser (3.8.2)
+ ruby_parser (3.8.4)
sexp_processor (~> 4.1)
rubyntlm (0.5.2)
rubypants (0.2.0)
rubyzip (1.2.1)
rufus-scheduler (3.1.10)
- rugged (0.24.0)
+ rugged (0.25.1.1)
safe_yaml (1.0.4)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
@@ -700,10 +705,10 @@ GEM
activesupport (>= 3.1)
select2-rails (3.5.9.3)
thor (~> 0.14)
- sentry-raven (2.0.2)
- faraday (>= 0.7.6, < 0.10.x)
+ sentry-raven (2.4.0)
+ faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
- sexp_processor (4.7.0)
+ sexp_processor (4.8.0)
sham_rack (1.3.6)
rack
shoulda-matchers (2.8.0)
@@ -724,7 +729,7 @@ GEM
faraday (~> 0.9)
jwt (~> 1.5)
multi_json (~> 1.10)
- simplecov (0.12.0)
+ simplecov (0.14.1)
docile (~> 1.1.0)
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
@@ -741,7 +746,8 @@ GEM
spinach (>= 0.4)
spinach-rerun-reporter (0.0.2)
spinach (~> 0.8)
- spring (1.7.2)
+ spring (2.0.1)
+ activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
spring-commands-spinach (1.1.0)
@@ -813,16 +819,12 @@ GEM
vmstat (2.3.0)
warden (1.2.6)
rack (>= 1.0)
- web-console (2.3.0)
- activemodel (>= 4.0)
- binding_of_caller (>= 0.7.2)
- railties (>= 4.0)
- sprockets-rails (>= 2.0, < 4.0)
- webmock (1.21.0)
+ webmock (1.24.6)
addressable (>= 2.3.6)
crack (>= 0.3.2)
- webpack-rails (0.9.9)
- rails (>= 3.2.0)
+ hashdiff
+ webpack-rails (0.9.10)
+ railties (>= 3.2.0)
websocket-driver (0.6.3)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
@@ -854,12 +856,12 @@ DEPENDENCIES
babosa (~> 1.0.2)
base32 (~> 0.3.0)
benchmark-ips (~> 2.3.0)
- better_errors (~> 1.0.1)
+ better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0)
- brakeman (~> 3.4.0)
+ brakeman (~> 3.6.0)
browser (~> 2.2)
- bullet (~> 5.2.0)
+ bullet (~> 5.5.0)
bundler-audit (~> 0.5.0)
capybara (~> 2.6.2)
capybara-screenshot (~> 1.0.0)
@@ -885,7 +887,7 @@ DEPENDENCIES
email_spec (~> 1.6.0)
factory_girl_rails (~> 4.7.0)
ffaker (~> 2.4)
- flay (~> 2.6.1)
+ flay (~> 2.8.0)
fog-aws (~> 0.9)
fog-core (~> 1.40)
fog-google (~> 0.5)
@@ -897,13 +899,13 @@ DEPENDENCIES
fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.0)
- gitaly (~> 0.3.0)
+ gitaly (~> 0.5.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
- gollum-rugged_adapter (~> 0.4.2)
+ gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
google-api-client (~> 0.8.6)
grape (~> 0.19.0)
@@ -985,24 +987,24 @@ DEPENDENCIES
rubocop-rspec (~> 1.12.0)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2)
- rugged (~> 0.24.0)
+ rugged (~> 0.25.1.1)
sanitize (~> 2.0)
sass-rails (~> 5.0.6)
scss_lint (~> 0.47.0)
seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
- sentry-raven (~> 2.0.0)
+ sentry-raven (~> 2.4.0)
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0)
sidekiq (~> 4.2.7)
sidekiq-cron (~> 0.4.4)
sidekiq-limit_fetch (~> 3.4)
- simplecov (= 0.12.0)
+ simplecov (~> 0.14.0)
slack-notifier (~> 1.5.1)
spinach-rails (~> 0.2.1)
spinach-rerun-reporter (~> 0.0.2)
- spring (~> 1.7.0)
+ spring (~> 2.0.0)
spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.1.0)
sprockets (~> 3.7.0)
@@ -1023,9 +1025,8 @@ DEPENDENCIES
version_sorter (~> 2.1.0)
virtus (~> 1.0.1)
vmstat (~> 2.3.0)
- web-console (~> 2.0)
- webmock (~> 1.21.0)
- webpack-rails (~> 0.9.9)
+ webmock (~> 1.24.0)
+ webpack-rails (~> 0.9.10)
wikicloth (= 0.8.1)
BUNDLED WITH
diff --git a/VERSION b/VERSION
index 64de8316674..c3996a4a61f 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.18.0-pre
+9.1.0-pre
diff --git a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js
index 5e3c45f7e92..20ab2d7e827 100644
--- a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js
+++ b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_unicode_supported.js
@@ -1,5 +1,3 @@
-import spreadString from './spread_string';
-
// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/
const flagACodePoint = 127462; // parseInt('1F1E6', 16)
const flagZCodePoint = 127487; // parseInt('1F1FF', 16)
@@ -20,7 +18,7 @@ function isKeycapEmoji(emojiUnicode) {
const tone1 = 127995;// parseInt('1F3FB', 16)
const tone5 = 127999;// parseInt('1F3FF', 16)
function isSkinToneComboEmoji(emojiUnicode) {
- return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => {
+ return emojiUnicode.length > 2 && Array.from(emojiUnicode).some((char) => {
const cp = char.codePointAt(0);
return cp >= tone1 && cp <= tone5;
});
@@ -30,7 +28,7 @@ function isSkinToneComboEmoji(emojiUnicode) {
// doesn't support the skin tone versions of horse racing
const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
- return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
+ return Array.from(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
isSkinToneComboEmoji(emojiUnicode);
}
@@ -42,7 +40,7 @@ const personEndCodePoint = 128105; // parseInt('1F469', 16)
function isPersonZwjEmoji(emojiUnicode) {
let hasPersonEmoji = false;
let hasZwj = false;
- spreadString(emojiUnicode).forEach((character) => {
+ Array.from(emojiUnicode).forEach((character) => {
const cp = character.codePointAt(0);
if (cp === zwj) {
hasZwj = true;
diff --git a/app/assets/javascripts/behaviors/gl_emoji/spread_string.js b/app/assets/javascripts/behaviors/gl_emoji/spread_string.js
deleted file mode 100644
index 327764ec6e9..00000000000
--- a/app/assets/javascripts/behaviors/gl_emoji/spread_string.js
+++ /dev/null
@@ -1,50 +0,0 @@
-// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt#Fixing_charCodeAt()_to_handle_non-Basic-Multilingual-Plane_characters_if_their_presence_earlier_in_the_string_is_known
-function knownCharCodeAt(givenString, index) {
- const str = `${givenString}`;
- const end = str.length;
-
- const surrogatePairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
- let idx = index;
- while ((surrogatePairs.exec(str)) != null) {
- const li = surrogatePairs.lastIndex;
- if (li - 2 < idx) {
- idx += 1;
- } else {
- break;
- }
- }
-
- if (idx >= end || idx < 0) {
- return NaN;
- }
-
- const code = str.charCodeAt(idx);
-
- let high;
- let low;
- if (code >= 0xD800 && code <= 0xDBFF) {
- high = code;
- low = str.charCodeAt(idx + 1);
- // Go one further, since one of the "characters" is part of a surrogate pair
- return ((high - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
- }
- return code;
-}
-
-// See http://stackoverflow.com/a/38901550/796832
-// ES5/PhantomJS compatible version of spreading a string
-//
-// [...'foo'] -> ['f', 'o', 'o']
-// [...'🖐🏿'] -> ['🖐', '🏿']
-function spreadString(str) {
- const arr = [];
- let i = 0;
- while (!isNaN(knownCharCodeAt(str, i))) {
- const codePoint = knownCharCodeAt(str, i);
- arr.push(String.fromCodePoint(codePoint));
- i += 1;
- }
- return arr;
-}
-
-export default spreadString;
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 86927314dd4..576b8a0425f 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -18,7 +18,7 @@
// Button does not change visibility. If button has icon - it changes chevron style.
//
// %div.js-toggle-container
- // %a.js-toggle-button
+ // %button.js-toggle-button
// %div.js-toggle-content
//
$('body').on('click', '.js-toggle-button', function(e) {
diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js
new file mode 100644
index 00000000000..3062cd51ee3
--- /dev/null
+++ b/app/assets/javascripts/blob/file_template_mediator.js
@@ -0,0 +1,241 @@
+/* eslint-disable class-methods-use-this */
+/* global Flash */
+
+import FileTemplateTypeSelector from './template_selectors/type_selector';
+import BlobCiYamlSelector from './template_selectors/ci_yaml_selector';
+import DockerfileSelector from './template_selectors/dockerfile_selector';
+import GitignoreSelector from './template_selectors/gitignore_selector';
+import LicenseSelector from './template_selectors/license_selector';
+
+export default class FileTemplateMediator {
+ constructor({ editor, currentAction }) {
+ this.editor = editor;
+ this.currentAction = currentAction;
+
+ this.initTemplateSelectors();
+ this.initTemplateTypeSelector();
+ this.initDomElements();
+ this.initDropdowns();
+ this.initPageEvents();
+ }
+
+ initTemplateSelectors() {
+ // Order dictates template type dropdown item order
+ this.templateSelectors = [
+ GitignoreSelector,
+ BlobCiYamlSelector,
+ DockerfileSelector,
+ LicenseSelector,
+ ].map(TemplateSelectorClass => new TemplateSelectorClass({ mediator: this }));
+ }
+
+ initTemplateTypeSelector() {
+ this.typeSelector = new FileTemplateTypeSelector({
+ mediator: this,
+ dropdownData: this.templateSelectors
+ .map((templateSelector) => {
+ const cfg = templateSelector.config;
+
+ return {
+ name: cfg.name,
+ key: cfg.key,
+ };
+ }),
+ });
+ }
+
+ initDomElements() {
+ const $templatesMenu = $('.template-selectors-menu');
+ const $undoMenu = $templatesMenu.find('.template-selectors-undo-menu');
+ const $fileEditor = $('.file-editor');
+
+ this.$templatesMenu = $templatesMenu;
+ this.$undoMenu = $undoMenu;
+ this.$undoBtn = $undoMenu.find('button');
+ this.$templateSelectors = $templatesMenu.find('.template-selector-dropdowns-wrap');
+ this.$filenameInput = $fileEditor.find('.js-file-path-name-input');
+ this.$fileContent = $fileEditor.find('#file-content');
+ this.$commitForm = $fileEditor.find('form');
+ this.$navLinks = $fileEditor.find('.nav-links');
+ }
+
+ initDropdowns() {
+ if (this.currentAction === 'create') {
+ this.typeSelector.show();
+ } else {
+ this.hideTemplateSelectorMenu();
+ }
+
+ this.displayMatchedTemplateSelector();
+ }
+
+ initPageEvents() {
+ this.listenForFilenameInput();
+ this.prepFileContentForSubmit();
+ this.listenForPreviewMode();
+ }
+
+ listenForFilenameInput() {
+ this.$filenameInput.on('keyup blur', () => {
+ this.displayMatchedTemplateSelector();
+ });
+ }
+
+ prepFileContentForSubmit() {
+ this.$commitForm.submit(() => {
+ this.$fileContent.val(this.editor.getValue());
+ });
+ }
+
+ listenForPreviewMode() {
+ this.$navLinks.on('click', 'a', (e) => {
+ const urlPieces = e.target.href.split('#');
+ const hash = urlPieces[1];
+ if (hash === 'preview') {
+ this.hideTemplateSelectorMenu();
+ } else if (hash === 'editor') {
+ this.showTemplateSelectorMenu();
+ }
+ });
+ }
+
+ selectTemplateType(item, el, e) {
+ if (e) {
+ e.preventDefault();
+ }
+
+ this.templateSelectors.forEach((selector) => {
+ if (selector.config.key === item.key) {
+ selector.show();
+ } else {
+ selector.hide();
+ }
+ });
+
+ this.typeSelector.setToggleText(item.name);
+
+ this.cacheToggleText();
+ }
+
+ selectTemplateFile(selector, query, data) {
+ selector.renderLoading();
+ // in case undo menu is already already there
+ this.destroyUndoMenu();
+ this.fetchFileTemplate(selector.config.endpoint, query, data)
+ .then((file) => {
+ this.showUndoMenu();
+ this.setEditorContent(file);
+ this.setFilename(selector.config.name);
+ selector.renderLoaded();
+ })
+ .catch(err => new Flash(`An error occurred while fetching the template: ${err}`));
+ }
+
+ displayMatchedTemplateSelector() {
+ const currentInput = this.getFilename();
+ this.templateSelectors.forEach((selector) => {
+ const match = selector.config.pattern.test(currentInput);
+
+ if (match) {
+ this.typeSelector.show();
+ this.selectTemplateType(selector.config);
+ this.showTemplateSelectorMenu();
+ }
+ });
+ }
+
+ fetchFileTemplate(apiCall, query, data) {
+ return new Promise((resolve) => {
+ const resolveFile = file => resolve(file);
+
+ if (!data) {
+ apiCall(query, resolveFile);
+ } else {
+ apiCall(query, data, resolveFile);
+ }
+ });
+ }
+
+ setEditorContent(file) {
+ if (!file && file !== '') return;
+
+ const newValue = file.content || file;
+
+ this.editor.setValue(newValue, 1);
+
+ this.editor.focus();
+
+ this.editor.navigateFileStart();
+ }
+
+ findTemplateSelectorByKey(key) {
+ return this.templateSelectors.find(selector => selector.config.key === key);
+ }
+
+ showUndoMenu() {
+ this.$undoMenu.removeClass('hidden');
+
+ this.$undoBtn.on('click', () => {
+ this.restoreFromCache();
+ this.destroyUndoMenu();
+ });
+ }
+
+ destroyUndoMenu() {
+ this.cacheFileContents();
+ this.cacheToggleText();
+ this.$undoMenu.addClass('hidden');
+ this.$undoBtn.off('click');
+ }
+
+ hideTemplateSelectorMenu() {
+ this.$templatesMenu.hide();
+ }
+
+ showTemplateSelectorMenu() {
+ this.$templatesMenu.show();
+ }
+
+ cacheToggleText() {
+ this.cachedToggleText = this.getTemplateSelectorToggleText();
+ }
+
+ cacheFileContents() {
+ this.cachedContent = this.editor.getValue();
+ this.cachedFilename = this.getFilename();
+ }
+
+ restoreFromCache() {
+ this.setEditorContent(this.cachedContent);
+ this.setFilename(this.cachedFilename);
+ this.setTemplateSelectorToggleText();
+ }
+
+ getTemplateSelectorToggleText() {
+ return this.$templateSelectors
+ .find('.js-template-selector-wrap:visible .dropdown-toggle-text')
+ .text();
+ }
+
+ setTemplateSelectorToggleText() {
+ return this.$templateSelectors
+ .find('.js-template-selector-wrap:visible .dropdown-toggle-text')
+ .text(this.cachedToggleText);
+ }
+
+ getTypeSelectorToggleText() {
+ return this.typeSelector.getToggleText();
+ }
+
+ getFilename() {
+ return this.$filenameInput.val();
+ }
+
+ setFilename(name) {
+ this.$filenameInput.val(name);
+ }
+
+ getSelected() {
+ return this.templateSelectors.find(selector => selector.selected);
+ }
+}
diff --git a/app/assets/javascripts/blob/file_template_selector.js b/app/assets/javascripts/blob/file_template_selector.js
new file mode 100644
index 00000000000..31dd45fac89
--- /dev/null
+++ b/app/assets/javascripts/blob/file_template_selector.js
@@ -0,0 +1,60 @@
+/* global Api */
+
+export default class FileTemplateSelector {
+ constructor(mediator) {
+ this.mediator = mediator;
+ this.$dropdown = null;
+ this.$wrapper = null;
+ }
+
+ init() {
+ const cfg = this.config;
+
+ this.$dropdown = $(cfg.dropdown);
+ this.$wrapper = $(cfg.wrapper);
+ this.$loadingIcon = this.$wrapper.find('.fa-chevron-down');
+ this.$dropdownToggleText = this.$wrapper.find('.dropdown-toggle-text');
+
+ this.initDropdown();
+ }
+
+ show() {
+ if (this.$dropdown === null) {
+ this.init();
+ }
+
+ this.$wrapper.removeClass('hidden');
+ }
+
+ hide() {
+ if (this.$dropdown !== null) {
+ this.$wrapper.addClass('hidden');
+ }
+ }
+
+ getToggleText() {
+ return this.$dropdownToggleText.text();
+ }
+
+ setToggleText(text) {
+ this.$dropdownToggleText.text(text);
+ }
+
+ renderLoading() {
+ this.$loadingIcon
+ .addClass('fa-spinner fa-spin')
+ .removeClass('fa-chevron-down');
+ }
+
+ renderLoaded() {
+ this.$loadingIcon
+ .addClass('fa-chevron-down')
+ .removeClass('fa-spinner fa-spin');
+ }
+
+ reportSelection(query, el, e, data) {
+ e.preventDefault();
+ return this.mediator.selectTemplateFile(this, query, data);
+ }
+}
+
diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js
new file mode 100644
index 00000000000..9b8bfbfc8c0
--- /dev/null
+++ b/app/assets/javascripts/blob/notebook/index.js
@@ -0,0 +1,85 @@
+/* eslint-disable no-new */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+import NotebookLab from 'vendor/notebooklab';
+
+Vue.use(VueResource);
+Vue.use(NotebookLab);
+
+export default () => {
+ const el = document.getElementById('js-notebook-viewer');
+
+ new Vue({
+ el,
+ data() {
+ return {
+ error: false,
+ loadError: false,
+ loading: true,
+ json: {},
+ };
+ },
+ template: `
+ <div class="container-fluid md prepend-top-default append-bottom-default">
+ <div
+ class="text-center loading"
+ v-if="loading && !error">
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true"
+ aria-label="iPython notebook loading">
+ </i>
+ </div>
+ <notebook-lab
+ v-if="!loading && !error"
+ :notebook="json"
+ code-css-class="code white" />
+ <p
+ class="text-center"
+ v-if="error">
+ <span v-if="loadError">
+ An error occured whilst loading the file. Please try again later.
+ </span>
+ <span v-else>
+ An error occured whilst parsing the file.
+ </span>
+ </p>
+ </div>
+ `,
+ methods: {
+ loadFile() {
+ this.$http.get(el.dataset.endpoint)
+ .then((res) => {
+ this.json = res.json();
+ this.loading = false;
+ })
+ .catch((e) => {
+ if (e.status) {
+ this.loadError = true;
+ }
+
+ this.error = true;
+ });
+ },
+ },
+ mounted() {
+ if (gon.katex_css_url) {
+ const katexStyles = document.createElement('link');
+ katexStyles.setAttribute('rel', 'stylesheet');
+ katexStyles.setAttribute('href', gon.katex_css_url);
+ document.head.appendChild(katexStyles);
+ }
+
+ if (gon.katex_js_url) {
+ const katexScript = document.createElement('script');
+ katexScript.addEventListener('load', () => {
+ this.loadFile();
+ });
+ katexScript.setAttribute('src', gon.katex_js_url);
+ document.head.appendChild(katexScript);
+ } else {
+ this.loadFile();
+ }
+ },
+ });
+};
diff --git a/app/assets/javascripts/blob/notebook_viewer.js b/app/assets/javascripts/blob/notebook_viewer.js
new file mode 100644
index 00000000000..b7a0a195a92
--- /dev/null
+++ b/app/assets/javascripts/blob/notebook_viewer.js
@@ -0,0 +1,3 @@
+import renderNotebook from './notebook';
+
+document.addEventListener('DOMContentLoaded', renderNotebook);
diff --git a/app/assets/javascripts/blob/template_selectors/template_selector.js b/app/assets/javascripts/blob/template_selector.js
index d7c1c32efbd..d7c1c32efbd 100644
--- a/app/assets/javascripts/blob/template_selectors/template_selector.js
+++ b/app/assets/javascripts/blob/template_selector.js
diff --git a/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js
deleted file mode 100644
index 5a5954e7751..00000000000
--- a/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selector.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/* global Api */
-
-import TemplateSelector from './template_selector';
-
-export default class BlobCiYamlSelector extends TemplateSelector {
- requestFile(query) {
- return Api.gitlabCiYml(query.name, (file, config) => this.setEditorContent(file, config));
- }
-}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js
deleted file mode 100644
index 7a4d6a42a03..00000000000
--- a/app/assets/javascripts/blob/template_selectors/blob_ci_yaml_selectors.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* global Api */
-
-import BlobCiYamlSelector from './blob_ci_yaml_selector';
-
-export default class BlobCiYamlSelectors {
- constructor({ editor, $dropdowns }) {
- this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector');
- this.initSelectors(editor);
- }
-
- initSelectors(editor) {
- this.$dropdowns.each((i, dropdown) => {
- const $dropdown = $(dropdown);
- return new BlobCiYamlSelector({
- editor,
- pattern: /(.gitlab-ci.yml)/,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
- dropdown: $dropdown,
- });
- });
- }
-}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js
deleted file mode 100644
index 19f8820a0cb..00000000000
--- a/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selector.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/* global Api */
-
-import TemplateSelector from './template_selector';
-
-export default class BlobDockerfileSelector extends TemplateSelector {
- requestFile(query) {
- return Api.dockerfileYml(query.name, (file, config) => this.setEditorContent(file, config));
- }
-}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js
deleted file mode 100644
index da067035b43..00000000000
--- a/app/assets/javascripts/blob/template_selectors/blob_dockerfile_selectors.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import BlobDockerfileSelector from './blob_dockerfile_selector';
-
-export default class BlobDockerfileSelectors {
- constructor({ editor, $dropdowns }) {
- this.editor = editor;
- this.$dropdowns = $dropdowns || $('.js-dockerfile-selector');
- this.initSelectors();
- }
-
- initSelectors() {
- const editor = this.editor;
- this.$dropdowns.each((i, dropdown) => {
- const $dropdown = $(dropdown);
- return new BlobDockerfileSelector({
- editor,
- pattern: /(Dockerfile)/,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'),
- dropdown: $dropdown,
- });
- });
- }
-}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js
deleted file mode 100644
index 0b6b02fc2b3..00000000000
--- a/app/assets/javascripts/blob/template_selectors/blob_gitignore_selector.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/* global Api */
-
-import TemplateSelector from './template_selector';
-
-export default class BlobGitignoreSelector extends TemplateSelector {
- requestFile(query) {
- return Api.gitignoreText(query.name, (file, config) => this.setEditorContent(file, config));
- }
-}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js
deleted file mode 100644
index dc485d97677..00000000000
--- a/app/assets/javascripts/blob/template_selectors/blob_gitignore_selectors.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import BlobGitignoreSelector from './blob_gitignore_selector';
-
-export default class BlobGitignoreSelectors {
- constructor({ editor, $dropdowns }) {
- this.$dropdowns = $dropdowns || $('.js-gitignore-selector');
- this.editor = editor;
- this.initSelectors();
- }
-
- initSelectors() {
- this.$dropdowns.each((i, dropdown) => {
- const $dropdown = $(dropdown);
-
- return new BlobGitignoreSelector({
- pattern: /(.gitignore)/,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-gitignore-selector-wrap'),
- dropdown: $dropdown,
- editor: this.editor,
- });
- });
- }
-}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_license_selector.js b/app/assets/javascripts/blob/template_selectors/blob_license_selector.js
deleted file mode 100644
index e9cb31cc2dc..00000000000
--- a/app/assets/javascripts/blob/template_selectors/blob_license_selector.js
+++ /dev/null
@@ -1,13 +0,0 @@
-/* global Api */
-
-import TemplateSelector from './template_selector';
-
-export default class BlobLicenseSelector extends TemplateSelector {
- requestFile(query) {
- const data = {
- project: this.dropdown.data('project'),
- fullname: this.dropdown.data('fullname'),
- };
- return Api.licenseText(query.id, data, (file, config) => this.setEditorContent(file, config));
- }
-}
diff --git a/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js b/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js
deleted file mode 100644
index a44f4f78b2d..00000000000
--- a/app/assets/javascripts/blob/template_selectors/blob_license_selectors.js
+++ /dev/null
@@ -1,24 +0,0 @@
-/* eslint-disable no-unused-vars, no-param-reassign */
-
-import BlobLicenseSelector from './blob_license_selector';
-
-export default class BlobLicenseSelectors {
- constructor({ $dropdowns, editor }) {
- this.$dropdowns = $dropdowns || $('.js-license-selector');
- this.initSelectors(editor);
- }
-
- initSelectors(editor) {
- this.$dropdowns.each((i, dropdown) => {
- const $dropdown = $(dropdown);
-
- return new BlobLicenseSelector({
- editor,
- pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-license-selector-wrap'),
- dropdown: $dropdown,
- });
- });
- }
-}
diff --git a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js
new file mode 100644
index 00000000000..935df07677c
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js
@@ -0,0 +1,32 @@
+/* global Api */
+
+import FileTemplateSelector from '../file_template_selector';
+
+export default class BlobCiYamlSelector extends FileTemplateSelector {
+ constructor({ mediator }) {
+ super(mediator);
+ this.config = {
+ key: 'gitlab-ci-yaml',
+ name: '.gitlab-ci.yml',
+ pattern: /(.gitlab-ci.yml)/,
+ endpoint: Api.gitlabCiYml,
+ dropdown: '.js-gitlab-ci-yml-selector',
+ wrapper: '.js-gitlab-ci-yml-selector-wrap',
+ };
+ }
+
+ initDropdown() {
+ // maybe move to super class as well
+ this.$dropdown.glDropdown({
+ data: this.$dropdown.data('data'),
+ filterable: true,
+ selectable: true,
+ toggleLabel: item => item.name,
+ search: {
+ fields: ['name'],
+ },
+ clicked: (query, el, e) => this.reportSelection(query.name, el, e),
+ text: item => item.name,
+ });
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
new file mode 100644
index 00000000000..b4b4d09c315
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js
@@ -0,0 +1,32 @@
+/* global Api */
+
+import FileTemplateSelector from '../file_template_selector';
+
+export default class DockerfileSelector extends FileTemplateSelector {
+ constructor({ mediator }) {
+ super(mediator);
+ this.config = {
+ key: 'dockerfile',
+ name: 'Dockerfile',
+ pattern: /(Dockerfile)/,
+ endpoint: Api.dockerfileYml,
+ dropdown: '.js-dockerfile-selector',
+ wrapper: '.js-dockerfile-selector-wrap',
+ };
+ }
+
+ initDropdown() {
+ // maybe move to super class as well
+ this.$dropdown.glDropdown({
+ data: this.$dropdown.data('data'),
+ filterable: true,
+ selectable: true,
+ toggleLabel: item => item.name,
+ search: {
+ fields: ['name'],
+ },
+ clicked: (query, el, e) => this.reportSelection(query.name, el, e),
+ text: item => item.name,
+ });
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js
new file mode 100644
index 00000000000..aefae54ae71
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js
@@ -0,0 +1,31 @@
+/* global Api */
+
+import FileTemplateSelector from '../file_template_selector';
+
+export default class BlobGitignoreSelector extends FileTemplateSelector {
+ constructor({ mediator }) {
+ super(mediator);
+ this.config = {
+ key: 'gitignore',
+ name: '.gitignore',
+ pattern: /(.gitignore)/,
+ endpoint: Api.gitignoreText,
+ dropdown: '.js-gitignore-selector',
+ wrapper: '.js-gitignore-selector-wrap',
+ };
+ }
+
+ initDropdown() {
+ this.$dropdown.glDropdown({
+ data: this.$dropdown.data('data'),
+ filterable: true,
+ selectable: true,
+ toggleLabel: item => item.name,
+ search: {
+ fields: ['name'],
+ },
+ clicked: (query, el, e) => this.reportSelection(query.name, el, e),
+ text: item => item.name,
+ });
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/license_selector.js b/app/assets/javascripts/blob/template_selectors/license_selector.js
new file mode 100644
index 00000000000..c8abd689ab4
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/license_selector.js
@@ -0,0 +1,38 @@
+/* global Api */
+
+import FileTemplateSelector from '../file_template_selector';
+
+export default class BlobLicenseSelector extends FileTemplateSelector {
+ constructor({ mediator }) {
+ super(mediator);
+ this.config = {
+ key: 'license',
+ name: 'LICENSE',
+ pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
+ endpoint: Api.licenseText,
+ dropdown: '.js-license-selector',
+ wrapper: '.js-license-selector-wrap',
+ };
+ }
+
+ initDropdown() {
+ this.$dropdown.glDropdown({
+ data: this.$dropdown.data('data'),
+ filterable: true,
+ selectable: true,
+ toggleLabel: item => item.name,
+ search: {
+ fields: ['name'],
+ },
+ clicked: (query, el, e) => {
+ const data = {
+ project: this.$dropdown.data('project'),
+ fullname: this.$dropdown.data('fullname'),
+ };
+
+ this.reportSelection(query.id, el, e, data);
+ },
+ text: item => item.name,
+ });
+ }
+}
diff --git a/app/assets/javascripts/blob/template_selectors/type_selector.js b/app/assets/javascripts/blob/template_selectors/type_selector.js
new file mode 100644
index 00000000000..56f23ef0568
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selectors/type_selector.js
@@ -0,0 +1,25 @@
+import FileTemplateSelector from '../file_template_selector';
+
+export default class FileTemplateTypeSelector extends FileTemplateSelector {
+ constructor({ mediator, dropdownData }) {
+ super(mediator);
+ this.mediator = mediator;
+ this.config = {
+ dropdown: '.js-template-type-selector',
+ wrapper: '.js-template-type-selector-wrap',
+ dropdownData,
+ };
+ }
+
+ initDropdown() {
+ this.$dropdown.glDropdown({
+ data: this.config.dropdownData,
+ filterable: false,
+ selectable: true,
+ toggleLabel: item => item.name,
+ clicked: (item, el, e) => this.mediator.selectTemplateType(item, el, e),
+ text: item => item.name,
+ });
+ }
+
+}
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index c5deccf631e..1c64ccf536f 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -13,8 +13,9 @@ $(() => {
const urlRoot = editBlobForm.data('relative-url-root');
const assetsPath = editBlobForm.data('assets-prefix');
const blobLanguage = editBlobForm.data('blob-language');
+ const currentAction = $('.js-file-title').data('current-action');
- new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage);
+ new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage, currentAction);
new NewCommitForm(editBlobForm);
}
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index d3560d5df3b..b37988a674d 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,17 +1,13 @@
/* global ace */
-import BlobLicenseSelectors from '../blob/template_selectors/blob_license_selectors';
-import BlobGitignoreSelectors from '../blob/template_selectors/blob_gitignore_selectors';
-import BlobCiYamlSelectors from '../blob/template_selectors/blob_ci_yaml_selectors';
-import BlobDockerfileSelectors from '../blob/template_selectors/blob_dockerfile_selectors';
+import TemplateSelectorMediator from '../blob/file_template_mediator';
export default class EditBlob {
- constructor(assetsPath, aceMode) {
+ constructor(assetsPath, aceMode, currentAction) {
this.configureAceEditor(aceMode, assetsPath);
- this.prepFileContentForSubmit();
this.initModePanesAndLinks();
this.initSoftWrap();
- this.initFileSelectors();
+ this.initFileSelectors(currentAction);
}
configureAceEditor(aceMode, assetsPath) {
@@ -19,6 +15,10 @@ export default class EditBlob {
ace.config.loadModule('ace/ext/searchbox');
this.editor = ace.edit('editor');
+
+ // This prevents warnings re: automatic scrolling being logged
+ this.editor.$blockScrolling = Infinity;
+
this.editor.focus();
if (aceMode) {
@@ -26,29 +26,13 @@ export default class EditBlob {
}
}
- prepFileContentForSubmit() {
- $('form').submit(() => {
- $('#file-content').val(this.editor.getValue());
+ initFileSelectors(currentAction) {
+ this.fileTemplateMediator = new TemplateSelectorMediator({
+ currentAction,
+ editor: this.editor,
});
}
- initFileSelectors() {
- this.blobTemplateSelectors = [
- new BlobLicenseSelectors({
- editor: this.editor,
- }),
- new BlobGitignoreSelectors({
- editor: this.editor,
- }),
- new BlobCiYamlSelectors({
- editor: this.editor,
- }),
- new BlobDockerfileSelectors({
- editor: this.editor,
- }),
- ];
- }
-
initModePanesAndLinks() {
this.$editModePanes = $('.js-edit-mode-pane');
this.$editModeLinks = $('.js-edit-mode a');
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
index 149bfbc8e8b..e057ac8df02 100644
--- a/app/assets/javascripts/boards/boards_bundle.js
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -79,7 +79,7 @@ $(() => {
resp.json().forEach((board) => {
const list = Store.addList(board);
- if (list.type === 'done') {
+ if (list.type === 'closed') {
list.position = Infinity;
}
});
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index 35b3205cca7..93b8960da2e 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -1,7 +1,7 @@
/* eslint-disable comma-dangle, space-before-function-paren, one-var */
/* global Sortable */
-
import Vue from 'vue';
+import boardList from './board_list';
import boardBlankState from './board_blank_state';
require('./board_delete');
@@ -16,7 +16,7 @@ require('./board_list');
gl.issueBoards.Board = Vue.extend({
template: '#js-board-template',
components: {
- 'board-list': gl.issueBoards.BoardList,
+ boardList,
'board-delete': gl.issueBoards.BoardDelete,
boardBlankState,
},
diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.js
index 9320848bcca..f591134c548 100644
--- a/app/assets/javascripts/boards/components/board_card.js
+++ b/app/assets/javascripts/boards/components/board_card.js
@@ -50,9 +50,7 @@ export default {
this.showDetail = false;
},
showIssue(e) {
- const targetTagName = e.target.tagName.toLowerCase();
-
- if (targetTagName === 'a' || targetTagName === 'button') return;
+ if (e.target.classList.contains('js-no-trigger')) return;
if (this.showDetail) {
this.showDetail = false;
diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js
index 86e6c26e570..adbd82cb687 100644
--- a/app/assets/javascripts/boards/components/board_list.js
+++ b/app/assets/javascripts/boards/components/board_list.js
@@ -1,131 +1,197 @@
-/* eslint-disable comma-dangle, space-before-function-paren, max-len */
/* global Sortable */
-
-import Vue from 'vue';
import boardNewIssue from './board_new_issue';
import boardCard from './board_card';
+import eventHub from '../eventhub';
-(() => {
- const Store = gl.issueBoards.BoardsStore;
-
- window.gl = window.gl || {};
- window.gl.issueBoards = window.gl.issueBoards || {};
+const Store = gl.issueBoards.BoardsStore;
- gl.issueBoards.BoardList = Vue.extend({
- template: '#js-board-list-template',
- components: {
- boardCard,
- boardNewIssue,
+export default {
+ name: 'BoardList',
+ props: {
+ disabled: {
+ type: Boolean,
+ required: true,
},
- props: {
- disabled: Boolean,
- list: Object,
- issues: Array,
- loading: Boolean,
- issueLinkBase: String,
- rootPath: String,
+ list: {
+ type: Object,
+ required: true,
},
- data () {
- return {
- scrollOffset: 250,
- filters: Store.state.filters,
- showCount: false,
- showIssueForm: false
- };
+ issues: {
+ type: Array,
+ required: true,
},
- watch: {
- filters: {
- handler () {
- this.list.loadingMore = false;
- this.$refs.list.scrollTop = 0;
- },
- deep: true
- },
- issues () {
- this.$nextTick(() => {
- if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) {
- this.list.page += 1;
- this.list.getIssues(false);
- }
+ loading: {
+ type: Boolean,
+ required: true,
+ },
+ issueLinkBase: {
+ type: String,
+ required: true,
+ },
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ scrollOffset: 250,
+ filters: Store.state.filters,
+ showCount: false,
+ showIssueForm: false,
+ };
+ },
+ components: {
+ boardCard,
+ boardNewIssue,
+ },
+ methods: {
+ listHeight() {
+ return this.$refs.list.getBoundingClientRect().height;
+ },
+ scrollHeight() {
+ return this.$refs.list.scrollHeight;
+ },
+ scrollTop() {
+ return this.$refs.list.scrollTop + this.listHeight();
+ },
+ loadNextPage() {
+ const getIssues = this.list.nextPage();
- if (this.scrollHeight() > Math.ceil(this.listHeight())) {
- this.showCount = true;
- } else {
- this.showCount = false;
- }
+ if (getIssues) {
+ this.list.loadingMore = true;
+ getIssues.then(() => {
+ this.list.loadingMore = false;
});
}
},
- methods: {
- listHeight () {
- return this.$refs.list.getBoundingClientRect().height;
- },
- scrollHeight () {
- return this.$refs.list.scrollHeight;
- },
- scrollTop () {
- return this.$refs.list.scrollTop + this.listHeight();
+ toggleForm() {
+ this.showIssueForm = !this.showIssueForm;
+ },
+ onScroll() {
+ if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
+ this.loadNextPage();
+ }
+ },
+ },
+ watch: {
+ filters: {
+ handler() {
+ this.list.loadingMore = false;
+ this.$refs.list.scrollTop = 0;
},
- loadNextPage () {
- const getIssues = this.list.nextPage();
+ deep: true,
+ },
+ issues() {
+ this.$nextTick(() => {
+ if (this.scrollHeight() <= this.listHeight() &&
+ this.list.issuesSize > this.list.issues.length) {
+ this.list.page += 1;
+ this.list.getIssues(false);
+ }
- if (getIssues) {
- this.list.loadingMore = true;
- getIssues.then(() => {
- this.list.loadingMore = false;
- });
+ if (this.scrollHeight() > Math.ceil(this.listHeight())) {
+ this.showCount = true;
+ } else {
+ this.showCount = false;
}
- },
- toggleForm() {
- this.showIssueForm = !this.showIssueForm;
- },
- },
- created() {
- gl.IssueBoardsApp.$on(`hide-issue-form-${this.list.id}`, this.toggleForm);
+ });
},
- mounted () {
- const options = gl.issueBoards.getBoardSortableDefaultOptions({
- scroll: document.querySelectorAll('.boards-list')[0],
- group: 'issues',
- disabled: this.disabled,
- filter: '.board-list-count, .is-disabled',
- dataIdAttr: 'data-issue-id',
- onStart: (e) => {
- const card = this.$refs.issue[e.oldIndex];
+ },
+ created() {
+ eventHub.$on(`hide-issue-form-${this.list.id}`, this.toggleForm);
+ },
+ mounted() {
+ const options = gl.issueBoards.getBoardSortableDefaultOptions({
+ scroll: document.querySelectorAll('.boards-list')[0],
+ group: 'issues',
+ disabled: this.disabled,
+ filter: '.board-list-count, .is-disabled',
+ dataIdAttr: 'data-issue-id',
+ onStart: (e) => {
+ const card = this.$refs.issue[e.oldIndex];
- card.showDetail = false;
- Store.moving.list = card.list;
- Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId);
+ card.showDetail = false;
+ Store.moving.list = card.list;
+ Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId);
- gl.issueBoards.onStart();
- },
- onAdd: (e) => {
- gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex);
+ gl.issueBoards.onStart();
+ },
+ onAdd: (e) => {
+ gl.issueBoards.BoardsStore
+ .moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex);
- this.$nextTick(() => {
- e.item.remove();
- });
- },
- onUpdate: (e) => {
- const sortedArray = this.sortable.toArray().filter(id => id !== '-1');
- gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray);
- },
- onMove(e) {
- return !e.related.classList.contains('board-list-count');
- }
- });
+ this.$nextTick(() => {
+ e.item.remove();
+ });
+ },
+ onUpdate: (e) => {
+ const sortedArray = this.sortable.toArray().filter(id => id !== '-1');
+ gl.issueBoards.BoardsStore
+ .moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray);
+ },
+ onMove(e) {
+ return !e.related.classList.contains('board-list-count');
+ },
+ });
- this.sortable = Sortable.create(this.$refs.list, options);
+ this.sortable = Sortable.create(this.$refs.list, options);
- // Scroll event on list to load more
- this.$refs.list.onscroll = () => {
- if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
- this.loadNextPage();
- }
- };
- },
- beforeDestroy() {
- gl.IssueBoardsApp.$off(`hide-issue-form-${this.list.id}`, this.toggleForm);
- },
- });
-})();
+ // Scroll event on list to load more
+ this.$refs.list.addEventListener('scroll', this.onScroll);
+ },
+ beforeDestroy() {
+ eventHub.$off(`hide-issue-form-${this.list.id}`, this.toggleForm);
+ this.$refs.list.removeEventListener('scroll', this.onScroll);
+ },
+ template: `
+ <div class="board-list-component">
+ <div
+ class="board-list-loading text-center"
+ aria-label="Loading issues"
+ v-if="loading">
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true">
+ </i>
+ </div>
+ <board-new-issue
+ :list="list"
+ v-if="list.type !== 'closed' && showIssueForm"/>
+ <ul
+ class="board-list"
+ v-show="!loading"
+ ref="list"
+ :data-board="list.id"
+ :class="{ 'is-smaller': showIssueForm }">
+ <board-card
+ v-for="(issue, index) in issues"
+ ref="issue"
+ :index="index"
+ :list="list"
+ :issue="issue"
+ :issue-link-base="issueLinkBase"
+ :root-path="rootPath"
+ :disabled="disabled"
+ :key="issue.id" />
+ <li
+ class="board-list-count text-center"
+ v-if="showCount"
+ data-id="-1">
+ <i
+ class="fa fa-spinner fa-spin"
+ aria-label="Loading more issues"
+ aria-hidden="true"
+ v-show="list.loadingMore">
+ </i>
+ <span v-if="list.issues.length === list.issuesSize">
+ Showing all issues
+ </span>
+ <span v-else>
+ Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
+ </span>
+ </li>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.js
index b88f59dd6d4..0fa85b6fe14 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.js
+++ b/app/assets/javascripts/boards/components/board_new_issue.js
@@ -1,4 +1,6 @@
/* global ListIssue */
+import eventHub from '../eventhub';
+
const Store = gl.issueBoards.BoardsStore;
export default {
@@ -49,7 +51,7 @@ export default {
},
cancel() {
this.title = '';
- gl.IssueBoardsApp.$emit(`hide-issue-form-${this.list.id}`);
+ eventHub.$emit(`hide-issue-form-${this.list.id}`);
},
},
mounted() {
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
index ba44dc5ed94..a4629b092bf 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -84,20 +84,20 @@ import eventHub from '../eventhub';
#{{ issue.id }}
</span>
<a
- class="card-assignee has-tooltip"
+ class="card-assignee has-tooltip js-no-trigger"
:href="rootPath + issue.assignee.username"
:title="'Assigned to ' + issue.assignee.name"
v-if="issue.assignee"
data-container="body">
<img
- class="avatar avatar-inline s20"
+ class="avatar avatar-inline s20 js-no-trigger"
:src="issue.assignee.avatar"
width="20"
height="20"
:alt="'Avatar for ' + issue.assignee.name" />
</a>
<button
- class="label color-label has-tooltip"
+ class="label color-label has-tooltip js-no-trigger"
v-for="label in issue.labels"
type="button"
v-if="showLabel(label)"
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
index d8322b34d44..772ea4c5565 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
@@ -48,7 +48,7 @@ import Vue from 'vue';
template: `
<div
class="block list"
- v-if="list.type !== 'done'">
+ v-if="list.type !== 'closed'">
<button
class="btn btn-default btn-block"
type="button"
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index f18ad2a0fac..91e5fb2a666 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -10,7 +10,7 @@ class List {
this.position = obj.position;
this.title = obj.title;
this.type = obj.list_type;
- this.preset = ['done', 'blank'].indexOf(this.type) > -1;
+ this.preset = ['closed', 'blank'].indexOf(this.type) > -1;
this.page = 1;
this.loading = true;
this.loadingMore = false;
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 8912f234aa6..bcda70d0638 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -45,7 +45,7 @@ import Cookies from 'js-cookie';
},
shouldAddBlankState () {
// Decide whether to add the blank state
- return !(this.state.lists.filter(list => list.type !== 'done')[0]);
+ return !(this.state.lists.filter(list => list.type !== 'closed')[0]);
},
addBlankState () {
if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
@@ -98,7 +98,7 @@ import Cookies from 'js-cookie';
issueTo.removeLabel(listFrom.label);
}
- if (listTo.type === 'done') {
+ if (listTo.type === 'closed') {
issueLists.forEach((list) => {
list.removeIssue(issue);
});
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js
index a20e5bc3b1b..4d5a857d705 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js
@@ -33,12 +33,11 @@ export default Vue.component('pipelines-table', {
* @return {Object}
*/
data() {
- const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
const store = new PipelineStore();
return {
- endpoint: pipelinesTableData.endpoint,
- helpPagePath: pipelinesTableData.helpPagePath,
+ endpoint: null,
+ helpPagePath: null,
store,
state: store.state,
isLoading: false,
@@ -65,6 +64,8 @@ export default Vue.component('pipelines-table', {
*
*/
beforeMount() {
+ this.endpoint = this.$el.dataset.endpoint;
+ this.helpPagePath = this.$el.dataset.helpPagePath;
this.service = new PipelinesService(this.endpoint);
this.fetchPipelines();
diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js
new file mode 100644
index 00000000000..abe48572347
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.js
@@ -0,0 +1,17 @@
+export default {
+ props: {
+ count: {
+ type: Number,
+ required: true,
+ },
+ },
+ template: `
+ <span v-if="count === 50" class="events-info pull-right">
+ <i class="fa fa-warning has-tooltip"
+ aria-hidden="true"
+ title="Limited to showing 50 events at most"
+ data-placement="top"></i>
+ Showing 50 events
+ </span>
+ `,
+};
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
index 9947f355aca..3f419a96ff9 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
@@ -14,6 +14,7 @@ import Vue from 'vue';
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
index 6ad4805e8c5..7ffa38edd9e 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
@@ -14,6 +14,7 @@ import Vue from 'vue';
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
index 42e1bbce744..d736c8b0c28 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
@@ -19,12 +19,7 @@ import iconCommit from '../svg/icon_commit.svg';
<div>
<div class="events-description">
{{ stage.description }}
- <span v-if="items.length === 50" class="events-info pull-right">
- <i class="fa fa-warning has-tooltip"
- title="Limited to showing 50 events at most"
- data-placement="top"></i>
- Showing 50 events
- </span>
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
index da80450a32c..698a79ca68c 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
@@ -14,6 +14,7 @@ import Vue from 'vue';
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
index 2200f43914f..e63c41f2a57 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
@@ -14,6 +14,7 @@ import Vue from 'vue';
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
index 8fa63734cf1..d51f7134e25 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
@@ -17,6 +17,7 @@ import iconBranch from '../svg/icon_branch.svg';
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
index 0015249cfaa..17ae3a9ddc1 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
@@ -18,6 +18,7 @@ import iconBranch from '../svg/icon_branch.svg';
<div>
<div class="events-description">
{{ stage.description }}
+ <limit-warning :count="items.length" />
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index ae17d05e679..b099b39e58f 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -2,6 +2,7 @@
import Vue from 'vue';
import Cookies from 'js-cookie';
+import LimitWarningComponent from './components/limit_warning_component';
require('./components/stage_code_component');
require('./components/stage_issue_component');
@@ -130,5 +131,6 @@ $(() => {
});
// Register global components
+ Vue.component('limit-warning', LimitWarningComponent);
Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent);
});
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index d1a662459e1..80490052389 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -33,6 +33,8 @@
/* global ProjectShow */
/* global Labels */
/* global Shortcuts */
+/* global Sidebar */
+
import Issue from './issue';
import BindInOut from './behaviors/bind_in_out';
@@ -118,6 +120,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'groups:milestones:show':
case 'dashboard:milestones:show':
new Milestone();
+ new Sidebar();
break;
case 'dashboard:todos:index':
new gl.Todos();
diff --git a/app/assets/javascripts/environments/components/environment.js b/app/assets/javascripts/environments/components/environment.js
index 51aab8460f6..0518422e475 100644
--- a/app/assets/javascripts/environments/components/environment.js
+++ b/app/assets/javascripts/environments/components/environment.js
@@ -24,6 +24,7 @@ export default Vue.component('environment-component', {
state: store.state,
visibility: 'available',
isLoading: false,
+ isLoadingFolderContent: false,
cssContainerClass: environmentsData.cssClass,
endpoint: environmentsData.environmentsDataEndpoint,
canCreateDeployment: environmentsData.canCreateDeployment,
@@ -68,15 +69,21 @@ export default Vue.component('environment-component', {
this.fetchEnvironments();
eventHub.$on('refreshEnvironments', this.fetchEnvironments);
+ eventHub.$on('toggleFolder', this.toggleFolder);
},
beforeDestroyed() {
eventHub.$off('refreshEnvironments');
+ eventHub.$off('toggleFolder');
},
methods: {
- toggleRow(model) {
- return this.store.toggleFolder(model.name);
+ toggleFolder(folder, folderUrl) {
+ this.store.toggleFolder(folder);
+
+ if (!folder.isOpen) {
+ this.fetchChildEnvironments(folder, folderUrl);
+ }
},
/**
@@ -117,6 +124,21 @@ export default Vue.component('environment-component', {
new Flash('An error occurred while fetching the environments.');
});
},
+
+ fetchChildEnvironments(folder, folderUrl) {
+ this.isLoadingFolderContent = true;
+
+ this.service.getFolderContent(folderUrl)
+ .then(resp => resp.json())
+ .then((response) => {
+ this.store.setfolderContent(folder, response.environments);
+ this.isLoadingFolderContent = false;
+ })
+ .catch(() => {
+ this.isLoadingFolderContent = false;
+ new Flash('An error occurred while fetching the environments.');
+ });
+ },
},
template: `
@@ -179,7 +201,8 @@ export default Vue.component('environment-component', {
:environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
- :service="service"/>
+ :service="service"
+ :is-loading-folder-content="isLoadingFolderContent" />
</div>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
diff --git a/app/assets/javascripts/environments/components/environment_actions.js b/app/assets/javascripts/environments/components/environment_actions.js
index 455a8819549..385085c03e2 100644
--- a/app/assets/javascripts/environments/components/environment_actions.js
+++ b/app/assets/javascripts/environments/components/environment_actions.js
@@ -25,6 +25,12 @@ export default {
};
},
+ computed: {
+ title() {
+ return 'Deploy to...';
+ },
+ },
+
methods: {
onClickAction(endpoint) {
this.isLoading = true;
@@ -44,8 +50,11 @@ export default {
template: `
<div class="btn-group" role="group">
<button
- class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
+ class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
+ data-container="body"
data-toggle="dropdown"
+ :title="title"
+ :aria-label="title"
:disabled="isLoading">
<span>
<span v-html="playIconSvg"></span>
diff --git a/app/assets/javascripts/environments/components/environment_external_url.js b/app/assets/javascripts/environments/components/environment_external_url.js
index b4f9eb357fd..d79b916c360 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.js
+++ b/app/assets/javascripts/environments/components/environment_external_url.js
@@ -9,13 +9,21 @@ export default {
},
},
+ computed: {
+ title() {
+ return 'Open';
+ },
+ },
+
template: `
<a
- class="btn external_url"
+ class="btn external-url has-tooltip"
+ data-container="body"
:href="externalUrl"
target="_blank"
- rel="noopener noreferrer"
- title="Environment external URL">
+ rel="noopener noreferrer nofollow"
+ :title="title"
+ :aria-label="title">
<i class="fa fa-external-link" aria-hidden="true"></i>
</a>
`,
diff --git a/app/assets/javascripts/environments/components/environment_item.js b/app/assets/javascripts/environments/components/environment_item.js
index 66ed10e19d1..e44d93a30c7 100644
--- a/app/assets/javascripts/environments/components/environment_item.js
+++ b/app/assets/javascripts/environments/components/environment_item.js
@@ -5,7 +5,9 @@ import ExternalUrlComponent from './environment_external_url';
import StopComponent from './environment_stop';
import RollbackComponent from './environment_rollback';
import TerminalButtonComponent from './environment_terminal_button';
+import MonitoringButtonComponent from './environment_monitoring';
import CommitComponent from '../../vue_shared/components/commit';
+import eventHub from '../event_hub';
/**
* Envrionment Item Component
@@ -22,6 +24,7 @@ export default {
'stop-component': StopComponent,
'rollback-component': RollbackComponent,
'terminal-button-component': TerminalButtonComponent,
+ 'monitoring-button-component': MonitoringButtonComponent,
},
props: {
@@ -392,6 +395,14 @@ export default {
return '';
},
+ monitoringUrl() {
+ if (this.model && this.model.metrics_path) {
+ return this.model.metrics_path;
+ }
+
+ return '';
+ },
+
/**
* Constructs folder URL based on the current location and the folder id.
*
@@ -400,7 +411,6 @@ export default {
folderUrl() {
return `${window.location.pathname}/folders/${this.model.folderName}`;
},
-
},
/**
@@ -418,15 +428,37 @@ export default {
return true;
},
+ methods: {
+ onClickFolder() {
+ eventHub.$emit('toggleFolder', this.model, this.folderUrl);
+ },
+ },
+
template: `
- <tr>
+ <tr :class="{ 'js-child-row': model.isChildren }">
<td>
<a v-if="!model.isFolder"
class="environment-name"
+ :class="{ 'prepend-left-default': model.isChildren }"
:href="environmentPath">
{{model.name}}
</a>
- <a v-else class="folder-name" :href="folderUrl">
+ <span v-else
+ class="folder-name"
+ @click="onClickFolder"
+ role="button">
+
+ <span class="folder-icon">
+ <i
+ v-show="model.isOpen"
+ class="fa fa-caret-down"
+ aria-hidden="true" />
+ <i
+ v-show="!model.isOpen"
+ class="fa fa-caret-right"
+ aria-hidden="true"/>
+ </span>
+
<span class="folder-icon">
<i class="fa fa-folder" aria-hidden="true"></i>
</span>
@@ -438,7 +470,7 @@ export default {
<span class="badge">
{{model.size}}
</span>
- </a>
+ </span>
</td>
<td class="deployment-column">
@@ -496,13 +528,16 @@ export default {
<external-url-component v-if="externalURL && canReadEnvironment"
:external-url="externalURL"/>
- <stop-component v-if="hasStopAction && canCreateDeployment"
- :stop-url="model.stop_path"
- :service="service"/>
+ <monitoring-button-component v-if="monitoringUrl && canReadEnvironment"
+ :monitoring-url="monitoringUrl"/>
<terminal-button-component v-if="model && model.terminal_path"
:terminal-path="model.terminal_path"/>
+ <stop-component v-if="hasStopAction && canCreateDeployment"
+ :stop-url="model.stop_path"
+ :service="service"/>
+
<rollback-component v-if="canRetry && canCreateDeployment"
:is-last-deployment="isLastDeployment"
:retry-url="retryUrl"
diff --git a/app/assets/javascripts/environments/components/environment_monitoring.js b/app/assets/javascripts/environments/components/environment_monitoring.js
new file mode 100644
index 00000000000..064e2fc7434
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_monitoring.js
@@ -0,0 +1,31 @@
+/**
+ * Renders the Monitoring (Metrics) link in environments table.
+ */
+export default {
+ props: {
+ monitoringUrl: {
+ type: String,
+ default: '',
+ required: true,
+ },
+ },
+
+ computed: {
+ title() {
+ return 'Monitoring';
+ },
+ },
+
+ template: `
+ <a
+ class="btn monitoring-url has-tooltip"
+ data-container="body"
+ :href="monitoringUrl"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ :title="title"
+ :aria-label="title">
+ <i class="fa fa-area-chart" aria-hidden="true"></i>
+ </a>
+ `,
+};
diff --git a/app/assets/javascripts/environments/components/environment_stop.js b/app/assets/javascripts/environments/components/environment_stop.js
index 5404d647745..47102692024 100644
--- a/app/assets/javascripts/environments/components/environment_stop.js
+++ b/app/assets/javascripts/environments/components/environment_stop.js
@@ -25,6 +25,12 @@ export default {
};
},
+ computed: {
+ title() {
+ return 'Stop';
+ },
+ },
+
methods: {
onClick() {
if (confirm('Are you sure you want to stop this environment?')) {
@@ -45,10 +51,12 @@ export default {
template: `
<button type="button"
- class="btn stop-env-link"
+ class="btn stop-env-link has-tooltip"
+ data-container="body"
@click="onClick"
:disabled="isLoading"
- title="Stop Environment">
+ :title="title"
+ :aria-label="title">
<i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</button>
diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js b/app/assets/javascripts/environments/components/environment_terminal_button.js
index 66a71faa02f..092a50a0d6f 100644
--- a/app/assets/javascripts/environments/components/environment_terminal_button.js
+++ b/app/assets/javascripts/environments/components/environment_terminal_button.js
@@ -14,12 +14,22 @@ export default {
},
data() {
- return { terminalIconSvg };
+ return {
+ terminalIconSvg,
+ };
+ },
+
+ computed: {
+ title() {
+ return 'Terminal';
+ },
},
template: `
- <a class="btn terminal-button"
- title="Open web terminal"
+ <a class="btn terminal-button has-tooltip"
+ data-container="body"
+ :title="title"
+ :aria-label="title"
:href="terminalPath">
${terminalIconSvg}
</a>
diff --git a/app/assets/javascripts/environments/components/environments_table.js b/app/assets/javascripts/environments/components/environments_table.js
index 338dff40bc9..5e6af3a1d45 100644
--- a/app/assets/javascripts/environments/components/environments_table.js
+++ b/app/assets/javascripts/environments/components/environments_table.js
@@ -31,6 +31,18 @@ export default {
type: Object,
required: true,
},
+
+ isLoadingFolderContent: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+
+ methods: {
+ folderUrl(model) {
+ return `${window.location.pathname}/folders/${model.folderName}`;
+ },
},
template: `
@@ -53,6 +65,31 @@ export default {
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:service="service"></tr>
+
+ <template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
+ <tr v-if="isLoadingFolderContent">
+ <td colspan="6" class="text-center">
+ <i class="fa fa-spin fa-spinner fa-2x" aria-hidden="true"/>
+ </td>
+ </tr>
+
+ <template v-else>
+ <tr is="environment-item"
+ v-for="children in model.children"
+ :model="children"
+ :can-create-deployment="canCreateDeployment"
+ :can-read-environment="canReadEnvironment"
+ :service="service"></tr>
+
+ <tr>
+ <td colspan="6" class="text-center">
+ <a :href="folderUrl(model)" class="btn btn-default">
+ Show all
+ </a>
+ </td>
+ </tr>
+ </template>
+ </template>
</template>
</tbody>
</table>
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
index 07040bf0d73..8adb53ea86d 100644
--- a/app/assets/javascripts/environments/services/environments_service.js
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -7,6 +7,7 @@ Vue.use(VueResource);
export default class EnvironmentsService {
constructor(endpoint) {
this.environments = Vue.resource(endpoint);
+ this.folderResults = 3;
}
get(scope, page) {
@@ -16,4 +17,8 @@ export default class EnvironmentsService {
postAction(endpoint) {
return Vue.http.post(endpoint, {}, { emulateJSON: true });
}
+
+ getFolderContent(folderUrl) {
+ return Vue.http.get(`${folderUrl}.json?per_page=${this.folderResults}`);
+ }
}
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 3c3084f3b78..158e7922e3c 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -38,7 +38,12 @@ export default class EnvironmentsStore {
let filtered = {};
if (env.size > 1) {
- filtered = Object.assign({}, env, { isFolder: true, folderName: env.name });
+ filtered = Object.assign({}, env, {
+ isFolder: true,
+ folderName: env.name,
+ isOpen: false,
+ children: [],
+ });
}
if (env.latest) {
@@ -85,4 +90,67 @@ export default class EnvironmentsStore {
this.state.stoppedCounter = count;
return count;
}
+
+ /**
+ * Toggles folder open property for the given folder.
+ *
+ * @param {Object} folder
+ * @return {Array}
+ */
+ toggleFolder(folder) {
+ return this.updateFolder(folder, 'isOpen', !folder.isOpen);
+ }
+
+ /**
+ * Updates the folder with the received environments.
+ *
+ *
+ * @param {Object} folder Folder to update
+ * @param {Array} environments Received environments
+ * @return {Object}
+ */
+ setfolderContent(folder, environments) {
+ const updatedEnvironments = environments.map((env) => {
+ let updated = env;
+
+ if (env.latest) {
+ updated = Object.assign({}, env, env.latest);
+ delete updated.latest;
+ } else {
+ updated = env;
+ }
+
+ updated.isChildren = true;
+
+ return updated;
+ });
+
+ return this.updateFolder(folder, 'children', updatedEnvironments);
+ }
+
+ /**
+ * Given a folder a prop and a new value updates the correct folder.
+ *
+ * @param {Object} folder
+ * @param {String} prop
+ * @param {String|Boolean|Object|Array} newValue
+ * @return {Array}
+ */
+ updateFolder(folder, prop, newValue) {
+ const environments = this.state.environments;
+
+ const updatedEnvironments = environments.map((env) => {
+ const updateEnv = Object.assign({}, env);
+ if (env.isFolder && env.id === folder.id) {
+ updateEnv[prop] = newValue;
+ }
+
+ return updateEnv;
+ });
+
+ this.state.environments = updatedEnvironments;
+
+ return updatedEnvironments;
+ }
+
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
index 9bf1b1ced88..a2729dc0e95 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js
@@ -8,21 +8,31 @@ require('./filtered_search_token_keys');
// Values that start with a double quote must end in a double quote (same for single)
const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g');
const tokens = [];
+ const tokenIndexes = []; // stores key+value for simple search
let lastToken = null;
const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
let tokenValue = v1 || v2 || v3;
let tokenSymbol = symbol;
+ let tokenIndex = '';
if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
tokenSymbol = tokenValue;
tokenValue = '';
}
- tokens.push({
- key,
- value: tokenValue || '',
- symbol: tokenSymbol || '',
- });
+ tokenIndex = `${key}:${tokenValue}`;
+
+ // Prevent adding duplicates
+ if (tokenIndexes.indexOf(tokenIndex) === -1) {
+ tokenIndexes.push(tokenIndex);
+
+ tokens.push({
+ key,
+ value: tokenValue || '',
+ symbol: tokenSymbol || '',
+ });
+ }
+
return '';
}).replace(/\s{2,}/g, ' ').trim() || '';
diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js
index 6a028f299b1..62675d7e67e 100644
--- a/app/assets/javascripts/group_name.js
+++ b/app/assets/javascripts/group_name.js
@@ -1,40 +1,64 @@
-const GROUP_LIMIT = 2;
+
+import _ from 'underscore';
export default class GroupName {
constructor() {
- this.titleContainer = document.querySelector('.title');
- this.groups = document.querySelectorAll('.group-path');
+ this.titleContainer = document.querySelector('.title-container');
+ this.title = document.querySelector('.title');
+ this.titleWidth = this.title.offsetWidth;
this.groupTitle = document.querySelector('.group-title');
+ this.groups = document.querySelectorAll('.group-path');
this.toggle = null;
this.isHidden = false;
this.init();
}
init() {
- if (this.groups.length > GROUP_LIMIT) {
+ if (this.groups.length > 0) {
this.groups[this.groups.length - 1].classList.remove('hidable');
- this.addToggle();
+ this.toggleHandler();
+ window.addEventListener('resize', _.debounce(this.toggleHandler.bind(this), 100));
}
this.render();
}
- addToggle() {
- const header = document.querySelector('.header-content');
+ toggleHandler() {
+ if (this.titleWidth > this.titleContainer.offsetWidth) {
+ if (!this.toggle) this.createToggle();
+ this.showToggle();
+ } else if (this.toggle) {
+ this.hideToggle();
+ }
+ }
+
+ createToggle() {
this.toggle = document.createElement('button');
this.toggle.className = 'text-expander group-name-toggle';
this.toggle.setAttribute('aria-label', 'Toggle full path');
this.toggle.innerHTML = '...';
this.toggle.addEventListener('click', this.toggleGroups.bind(this));
- header.insertBefore(this.toggle, this.titleContainer);
+ this.titleContainer.insertBefore(this.toggle, this.title);
this.toggleGroups();
}
+ showToggle() {
+ this.title.classList.add('wrap');
+ this.toggle.classList.remove('hidden');
+ if (this.isHidden) this.groupTitle.classList.add('is-hidden');
+ }
+
+ hideToggle() {
+ this.title.classList.remove('wrap');
+ this.toggle.classList.add('hidden');
+ if (this.isHidden) this.groupTitle.classList.remove('is-hidden');
+ }
+
toggleGroups() {
this.isHidden = !this.isHidden;
this.groupTitle.classList.toggle('is-hidden');
}
render() {
- this.titleContainer.classList.remove('initializing');
+ this.title.classList.remove('initializing');
}
}
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index e5dfa30edab..602a3b78189 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -6,23 +6,60 @@ var slice = [].slice;
window.GroupsSelect = (function() {
function GroupsSelect() {
$('.ajax-groups-select').each((function(_this) {
+ const self = _this;
+
return function(i, select) {
var all_available, skip_groups;
- all_available = $(select).data('all-available');
- skip_groups = $(select).data('skip-groups') || [];
- return $(select).select2({
+ const $select = $(select);
+ all_available = $select.data('all-available');
+ skip_groups = $select.data('skip-groups') || [];
+
+ $select.select2({
placeholder: "Search for a group",
- multiple: $(select).hasClass('multiselect'),
+ multiple: $select.hasClass('multiselect'),
minimumInputLength: 0,
- query: function(query) {
- var options = { all_available: all_available, skip_groups: skip_groups };
- return Api.groups(query.term, options, function(groups) {
- var data;
- data = {
- results: groups
+ ajax: {
+ url: Api.buildUrl(Api.groupsPath),
+ dataType: 'json',
+ quietMillis: 250,
+ transport: function (params) {
+ $.ajax(params).then((data, status, xhr) => {
+ const results = data || [];
+
+ const headers = gl.utils.normalizeCRLFHeaders(xhr.getAllResponseHeaders());
+ const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
+ const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0;
+ const more = currentPage < totalPages;
+
+ return {
+ results,
+ pagination: {
+ more,
+ },
+ };
+ }).then(params.success).fail(params.error);
+ },
+ data: function (search, page) {
+ return {
+ search,
+ page,
+ per_page: GroupsSelect.PER_PAGE,
+ all_available,
+ skip_groups,
+ };
+ },
+ results: function (data, page) {
+ if (data.length) return { results: [] };
+
+ const results = data.length ? data : data.results || [];
+ const more = data.pagination ? data.pagination.more : false;
+
+ return {
+ results,
+ page,
+ more,
};
- return query.callback(data);
- });
+ },
},
initSelection: function(element, callback) {
var id;
@@ -34,19 +71,23 @@ window.GroupsSelect = (function() {
formatResult: function() {
var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
- return _this.formatResult.apply(_this, args);
+ return self.formatResult.apply(self, args);
},
formatSelection: function() {
var args;
args = 1 <= arguments.length ? slice.call(arguments, 0) : [];
- return _this.formatSelection.apply(_this, args);
+ return self.formatSelection.apply(self, args);
},
- dropdownCssClass: "ajax-groups-dropdown",
+ dropdownCssClass: "ajax-groups-dropdown select2-infinite",
// we do not want to escape markup since we are displaying html in results
escapeMarkup: function(m) {
return m;
}
});
+
+ self.dropdown = document.querySelector('.select2-infinite .select2-results');
+
+ $select.on('select2-loaded', self.forceOverflow.bind(self));
};
})(this));
}
@@ -65,5 +106,12 @@ window.GroupsSelect = (function() {
return group.full_name;
};
+ GroupsSelect.prototype.forceOverflow = function (e) {
+ const itemHeight = this.dropdown.querySelector('.select2-result:first-child').clientHeight;
+ this.dropdown.style.height = `${Math.floor(this.dropdown.scrollHeight - (itemHeight * 0.9))}px`;
+ };
+
+ GroupsSelect.PER_PAGE = 20;
+
return GroupsSelect;
})();
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 34f44dad7a5..dc170c60456 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,7 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var */
$(document).on('todo:toggle', function(e, count) {
- var $todoPendingCount = $('.todos-pending-count');
+ var $todoPendingCount = $('.todos-count');
$todoPendingCount.text(gl.text.highCountTrim(count));
$todoPendingCount.toggleClass('hidden', count === 0);
});
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 08ca9e4fa4d..a5f99bcdd8f 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -11,8 +11,9 @@
});
};
- $(function() {
- var $scrollingTabs = $('.scrolling-tabs');
+ $(document).on('init.scrolling-tabs', () => {
+ const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized');
+ $scrollingTabs.addClass('is-initialized');
hideEndFade($scrollingTabs);
$(window).off('resize.nav').on('resize.nav', function() {
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index a1423b6fda5..46b80c04e20 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -232,6 +232,22 @@
};
/**
+ this will take in the getAllResponseHeaders result and normalize them
+ this way we don't run into production issues when nginx gives us lowercased header keys
+ */
+ w.gl.utils.normalizeCRLFHeaders = (headers) => {
+ const headersObject = {};
+ const headersArray = headers.split('\n');
+
+ headersArray.forEach((header) => {
+ const keyValue = header.split(': ');
+ headersObject[keyValue[0]] = keyValue[1];
+ });
+
+ return w.gl.utils.normalizeHeaders(headersObject);
+ };
+
+ /**
* Parses pagination object string values into numbers.
*
* @param {Object} paginationInformation
@@ -247,7 +263,7 @@
});
/**
- * Updates the search parameter of a URL given the parameter and values provided.
+ * Updates the search parameter of a URL given the parameter and value provided.
*
* If no search params are present we'll add it.
* If param for page is already present, we'll update it
@@ -262,17 +278,24 @@
let search;
const locationSearch = window.location.search;
- if (locationSearch.length === 0) {
- search = `?${param}=${value}`;
- }
+ if (locationSearch.length) {
+ const parameters = locationSearch.substring(1, locationSearch.length)
+ .split('&')
+ .reduce((acc, element) => {
+ const val = element.split('=');
+ acc[val[0]] = decodeURIComponent(val[1]);
+ return acc;
+ }, {});
- if (locationSearch.indexOf(param) !== -1) {
- const regex = new RegExp(param + '=\\d');
- search = locationSearch.replace(regex, `${param}=${value}`);
- }
+ parameters[param] = value;
- if (locationSearch.length && locationSearch.indexOf(param) === -1) {
- search = `${locationSearch}&${param}=${value}`;
+ const toString = Object.keys(parameters)
+ .map(val => `${val}=${encodeURIComponent(parameters[val])}`)
+ .join('&');
+
+ search = `?${toString}`;
+ } else {
+ search = `?${param}=${value}`;
}
return search;
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
new file mode 100644
index 00000000000..e2bf69ee52e
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -0,0 +1,34 @@
+/* eslint-disable import/prefer-default-export */
+
+/**
+ * Function that allows a number with an X amount of decimals
+ * to be formatted in the following fashion:
+ * * For 1 digit to the left of the decimal point and X digits to the right of it
+ * * * Show 3 digits to the right
+ * * For 2 digits to the left of the decimal point and X digits to the right of it
+ * * * Show 2 digits to the right
+*/
+export function formatRelevantDigits(number) {
+ let digitsLeft = '';
+ let relevantDigits = 0;
+ let formattedNumber = '';
+ if (!isNaN(Number(number))) {
+ digitsLeft = number.split('.')[0];
+ switch (digitsLeft.length) {
+ case 1:
+ relevantDigits = 3;
+ break;
+ case 2:
+ relevantDigits = 2;
+ break;
+ case 3:
+ relevantDigits = 1;
+ break;
+ default:
+ relevantDigits = 4;
+ break;
+ }
+ formattedNumber = Number(number).toFixed(relevantDigits);
+ }
+ return formattedNumber;
+}
diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js
index c30a1fcb5da..5c22aea51cd 100644
--- a/app/assets/javascripts/lib/utils/poll.js
+++ b/app/assets/javascripts/lib/utils/poll.js
@@ -5,23 +5,37 @@ import httpStatusCodes from './http_status';
* Service for vue resouce and method need to be provided as props
*
* @example
- * new poll({
+ * new Poll({
* resource: resource,
* method: 'name',
- * data: {page: 1, scope: 'all'},
+ * data: {page: 1, scope: 'all'}, // optional
* successCallback: () => {},
* errorCallback: () => {},
+ * notificationCallback: () => {}, // optional
* }).makeRequest();
*
- * this.service = new BoardsService(endpoint);
- * new poll({
- * resource: this.service,
- * method: 'get',
- * data: {page: 1, scope: 'all'},
- * successCallback: () => {},
- * errorCallback: () => {},
- * }).makeRequest();
+ * Usage in pipelines table with visibility lib:
*
+ * const poll = new Poll({
+ * resource: this.service,
+ * method: 'getPipelines',
+ * data: { page: pageNumber, scope },
+ * successCallback: this.successCallback,
+ * errorCallback: this.errorCallback,
+ * notificationCallback: this.updateLoading,
+ * });
+ *
+ * if (!Visibility.hidden()) {
+ * poll.makeRequest();
+ * }
+ *
+ * Visibility.change(() => {
+ * if (!Visibility.hidden()) {
+ * poll.restart();
+ * } else {
+ * poll.stop();
+ * }
+* });
*
* 1. Checks for response and headers before start polling
* 2. Interval is provided by `Poll-Interval` header.
@@ -34,6 +48,8 @@ export default class Poll {
constructor(options = {}) {
this.options = options;
this.options.data = options.data || {};
+ this.options.notificationCallback = options.notificationCallback ||
+ function notificationCallback() {};
this.intervalHeader = 'POLL-INTERVAL';
this.timeoutID = null;
@@ -42,7 +58,7 @@ export default class Poll {
checkConditions(response) {
const headers = gl.utils.normalizeHeaders(response.headers);
- const pollInterval = headers[this.intervalHeader];
+ const pollInterval = parseInt(headers[this.intervalHeader], 10);
if (pollInterval > 0 && response.status === httpStatusCodes.OK && this.canPoll) {
this.timeoutID = setTimeout(() => {
@@ -54,11 +70,14 @@ export default class Poll {
}
makeRequest() {
- const { resource, method, data, errorCallback } = this.options;
+ const { resource, method, data, errorCallback, notificationCallback } = this.options;
+
+ // It's called everytime a new request is made. Useful to update the status.
+ notificationCallback(true);
return resource[method](data)
- .then(response => this.checkConditions(response))
- .catch(error => errorCallback(error));
+ .then(response => this.checkConditions(response))
+ .catch(error => errorCallback(error));
}
/**
@@ -70,4 +89,12 @@ export default class Poll {
this.canPoll = false;
clearTimeout(this.timeoutID);
}
+
+ /**
+ * Restarts polling after it has been stoped
+ */
+ restart() {
+ this.canPoll = true;
+ this.makeRequest();
+ }
}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index e2f015c8e82..177cf66b37d 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -181,6 +181,9 @@ import './visibility_select';
import './wikis';
import './zen_mode';
+// eslint-disable-next-line global-require
+if (process.env.NODE_ENV !== 'production') require('./test_utils/');
+
document.addEventListener('beforeunload', function () {
// Unbind scroll events
$(document).off('scroll');
@@ -364,4 +367,6 @@ $(function () {
new Aside();
gl.utils.initTimeagoTimeout();
+
+ $(document).trigger('init.scrolling-tabs');
});
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 811f90c5a87..3c4e6102469 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -4,8 +4,10 @@
import Cookies from 'js-cookie';
-require('./breakpoints');
-require('./flash');
+import CommitPipelinesTable from './commit/pipelines/pipelines_table';
+
+import './breakpoints';
+import './flash';
/* eslint-disable max-len */
// MergeRequestTabs
@@ -97,6 +99,13 @@ require('./flash');
.off('click', this.clickTab);
}
+ destroy() {
+ this.unbindEvents();
+ if (this.commitPipelinesTable) {
+ this.commitPipelinesTable.$destroy();
+ }
+ }
+
showTab(e) {
e.preventDefault();
this.activateTab($(e.target).data('action'));
@@ -128,12 +137,8 @@ require('./flash');
this.expandViewContainer();
}
} else if (action === 'pipelines') {
- if (this.pipelinesLoaded) {
- return;
- }
- const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
- gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
- this.pipelinesLoaded = true;
+ this.resetViewContainer();
+ this.loadPipelines();
} else {
this.expandView();
this.resetViewContainer();
@@ -222,6 +227,18 @@ require('./flash');
});
}
+ loadPipelines() {
+ if (this.pipelinesLoaded) {
+ return;
+ }
+ const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
+ // Could already be mounted from the `pipelines_bundle`
+ if (pipelineTableViewEl) {
+ this.commitPipelinesTable = new CommitPipelinesTable().$mount(pipelineTableViewEl);
+ }
+ this.pipelinesLoaded = true;
+ }
+
loadDiff(source) {
if (this.diffsLoaded) {
return;
diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js
index 844a0785bc9..a6ffa0f59de 100644
--- a/app/assets/javascripts/monitoring/prometheus_graph.js
+++ b/app/assets/javascripts/monitoring/prometheus_graph.js
@@ -1,9 +1,9 @@
-/* eslint-disable no-new*/
+/* eslint-disable no-new */
/* global Flash */
import d3 from 'd3';
import statusCodes from '~/lib/utils/http_status';
-import '../lib/utils/common_utils';
+import { formatRelevantDigits } from '~/lib/utils/number_utils';
import '../flash';
const prometheusGraphsContainer = '.prometheus-graph';
@@ -21,19 +21,19 @@ class PrometheusGraph {
const parentContainerWidth = $(prometheusGraphsContainer).parent().width() +
extraAddedWidthParent;
this.originalWidth = parentContainerWidth;
- this.originalHeight = 400;
+ this.originalHeight = 330;
this.width = parentContainerWidth - this.margin.left - this.margin.right;
- this.height = 400 - this.margin.top - this.margin.bottom;
+ this.height = this.originalHeight - this.margin.top - this.margin.bottom;
this.backOffRequestCounter = 0;
this.configureGraph();
this.init();
}
createGraph() {
- Object.keys(this.data).forEach((key) => {
- const value = this.data[key];
- if (value.length > 0) {
- this.plotValues(value, key);
+ Object.keys(this.graphSpecificProperties).forEach((key) => {
+ const value = this.graphSpecificProperties[key];
+ if (value.data.length > 0) {
+ this.plotValues(key);
}
});
}
@@ -49,53 +49,56 @@ class PrometheusGraph {
});
}
- plotValues(valuesToPlot, key) {
+ plotValues(key) {
+ const graphSpecifics = this.graphSpecificProperties[key];
+
const x = d3.time.scale()
.range([0, this.width]);
const y = d3.scale.linear()
.range([this.height, 0]);
- const prometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
+ graphSpecifics.xScale = x;
+ graphSpecifics.yScale = y;
- const graphSpecifics = this.graphSpecificProperties[key];
+ const prometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
const chart = d3.select(prometheusGraphContainer)
- .attr('width', this.width + this.margin.left + this.margin.right)
- .attr('height', this.height + this.margin.bottom + this.margin.top)
- .append('g')
- .attr('transform', `translate(${this.margin.left},${this.margin.top})`);
+ .attr('width', this.width + this.margin.left + this.margin.right)
+ .attr('height', this.height + this.margin.bottom + this.margin.top)
+ .append('g')
+ .attr('transform', `translate(${this.margin.left},${this.margin.top})`);
const axisLabelContainer = d3.select(prometheusGraphContainer)
- .attr('width', this.originalWidth + this.marginLabelContainer.left + this.marginLabelContainer.right)
- .attr('height', this.originalHeight + this.marginLabelContainer.bottom + this.marginLabelContainer.top)
+ .attr('width', this.originalWidth)
+ .attr('height', this.originalHeight)
.append('g')
.attr('transform', `translate(${this.marginLabelContainer.left},${this.marginLabelContainer.top})`);
- x.domain(d3.extent(valuesToPlot, d => d.time));
- y.domain([0, d3.max(valuesToPlot.map(metricValue => metricValue.value))]);
+ x.domain(d3.extent(graphSpecifics.data, d => d.time));
+ y.domain([0, d3.max(graphSpecifics.data.map(metricValue => metricValue.value))]);
const xAxis = d3.svg.axis()
- .scale(x)
- .ticks(this.commonGraphProperties.axis_no_ticks)
- .orient('bottom');
+ .scale(x)
+ .ticks(this.commonGraphProperties.axis_no_ticks)
+ .orient('bottom');
const yAxis = d3.svg.axis()
- .scale(y)
- .ticks(this.commonGraphProperties.axis_no_ticks)
- .tickSize(-this.width)
- .orient('left');
+ .scale(y)
+ .ticks(this.commonGraphProperties.axis_no_ticks)
+ .tickSize(-this.width)
+ .orient('left');
this.createAxisLabelContainers(axisLabelContainer, key);
chart.append('g')
- .attr('class', 'x-axis')
- .attr('transform', `translate(0,${this.height})`)
- .call(xAxis);
+ .attr('class', 'x-axis')
+ .attr('transform', `translate(0,${this.height})`)
+ .call(xAxis);
chart.append('g')
- .attr('class', 'y-axis')
- .call(yAxis);
+ .attr('class', 'y-axis')
+ .call(yAxis);
const area = d3.svg.area()
.x(d => x(d.time))
@@ -108,13 +111,13 @@ class PrometheusGraph {
.y(d => y(d.value));
chart.append('path')
- .datum(valuesToPlot)
- .attr('d', area)
- .attr('class', 'metric-area')
- .attr('fill', graphSpecifics.area_fill_color);
+ .datum(graphSpecifics.data)
+ .attr('d', area)
+ .attr('class', 'metric-area')
+ .attr('fill', graphSpecifics.area_fill_color);
chart.append('path')
- .datum(valuesToPlot)
+ .datum(graphSpecifics.data)
.attr('class', 'metric-line')
.attr('stroke', graphSpecifics.line_color)
.attr('fill', 'none')
@@ -126,7 +129,7 @@ class PrometheusGraph {
.attr('class', 'prometheus-graph-overlay')
.attr('width', this.width)
.attr('height', this.height)
- .on('mousemove', this.handleMouseOverGraph.bind(this, x, y, valuesToPlot, chart, prometheusGraphContainer, key));
+ .on('mousemove', this.handleMouseOverGraph.bind(this, prometheusGraphContainer));
}
// The legends from the metric
@@ -134,128 +137,150 @@ class PrometheusGraph {
const graphSpecifics = this.graphSpecificProperties[key];
axisLabelContainer.append('line')
- .attr('class', 'label-x-axis-line')
- .attr('stroke', '#000000')
- .attr('stroke-width', '1')
- .attr({
- x1: 0,
- y1: this.originalHeight - this.marginLabelContainer.top,
- x2: this.originalWidth - this.margin.right,
- y2: this.originalHeight - this.marginLabelContainer.top,
- });
+ .attr('class', 'label-x-axis-line')
+ .attr('stroke', '#000000')
+ .attr('stroke-width', '1')
+ .attr({
+ x1: 10,
+ y1: this.originalHeight - this.margin.top,
+ x2: (this.originalWidth - this.margin.right) + 10,
+ y2: this.originalHeight - this.margin.top,
+ });
axisLabelContainer.append('line')
- .attr('class', 'label-y-axis-line')
- .attr('stroke', '#000000')
- .attr('stroke-width', '1')
- .attr({
- x1: 0,
- y1: 0,
- x2: 0,
- y2: this.originalHeight - this.marginLabelContainer.top,
- });
+ .attr('class', 'label-y-axis-line')
+ .attr('stroke', '#000000')
+ .attr('stroke-width', '1')
+ .attr({
+ x1: 10,
+ y1: 0,
+ x2: 10,
+ y2: this.originalHeight - this.margin.top,
+ });
+
+ axisLabelContainer.append('rect')
+ .attr('class', 'rect-axis-text')
+ .attr('x', 0)
+ .attr('y', 50)
+ .attr('width', 30)
+ .attr('height', 150);
axisLabelContainer.append('text')
- .attr('class', 'label-axis-text')
- .attr('text-anchor', 'middle')
- .attr('transform', `translate(15, ${(this.originalHeight - this.marginLabelContainer.top) / 2}) rotate(-90)`)
- .text(graphSpecifics.graph_legend_title);
+ .attr('class', 'label-axis-text')
+ .attr('text-anchor', 'middle')
+ .attr('transform', `translate(15, ${(this.originalHeight - this.margin.top) / 2}) rotate(-90)`)
+ .text(graphSpecifics.graph_legend_title);
axisLabelContainer.append('rect')
- .attr('class', 'rect-axis-text')
- .attr('x', (this.originalWidth / 2) - this.margin.right)
- .attr('y', this.originalHeight - this.marginLabelContainer.top - 20)
- .attr('width', 30)
- .attr('height', 80);
+ .attr('class', 'rect-axis-text')
+ .attr('x', (this.originalWidth / 2) - this.margin.right)
+ .attr('y', this.originalHeight - 100)
+ .attr('width', 30)
+ .attr('height', 80);
axisLabelContainer.append('text')
- .attr('class', 'label-axis-text')
- .attr('x', (this.originalWidth / 2) - this.margin.right)
- .attr('y', this.originalHeight - this.marginLabelContainer.top)
- .attr('dy', '.35em')
- .text('Time');
+ .attr('class', 'label-axis-text')
+ .attr('x', (this.originalWidth / 2) - this.margin.right)
+ .attr('y', this.originalHeight - this.margin.top)
+ .attr('dy', '.35em')
+ .text('Time');
// Legends
// Metric Usage
axisLabelContainer.append('rect')
- .attr('x', this.originalWidth - 170)
- .attr('y', (this.originalHeight / 2) - 60)
- .style('fill', graphSpecifics.area_fill_color)
- .attr('width', 20)
- .attr('height', 35);
+ .attr('x', this.originalWidth - 170)
+ .attr('y', (this.originalHeight / 2) - 60)
+ .style('fill', graphSpecifics.area_fill_color)
+ .attr('width', 20)
+ .attr('height', 35);
axisLabelContainer.append('text')
- .attr('class', 'label-axis-text')
- .attr('x', this.originalWidth - 140)
- .attr('y', (this.originalHeight / 2) - 50)
- .text('Average');
+ .attr('class', 'text-metric-title')
+ .attr('x', this.originalWidth - 140)
+ .attr('y', (this.originalHeight / 2) - 50)
+ .text('Average');
axisLabelContainer.append('text')
- .attr('class', 'text-metric-usage')
- .attr('x', this.originalWidth - 140)
- .attr('y', (this.originalHeight / 2) - 25);
+ .attr('class', 'text-metric-usage')
+ .attr('x', this.originalWidth - 140)
+ .attr('y', (this.originalHeight / 2) - 25);
}
- handleMouseOverGraph(x, y, valuesToPlot, chart, prometheusGraphContainer, key) {
+ handleMouseOverGraph(prometheusGraphContainer) {
const rectOverlay = document.querySelector(`${prometheusGraphContainer} .prometheus-graph-overlay`);
- const timeValueFromOverlay = x.invert(d3.mouse(rectOverlay)[0]);
- const timeValueIndex = bisectDate(valuesToPlot, timeValueFromOverlay, 1);
- const d0 = valuesToPlot[timeValueIndex - 1];
- const d1 = valuesToPlot[timeValueIndex];
- const currentData = timeValueFromOverlay - d0.time > d1.time - timeValueFromOverlay ? d1 : d0;
- const maxValueMetric = y(d3.max(valuesToPlot.map(metricValue => metricValue.value)));
- const currentTimeCoordinate = x(currentData.time);
- const graphSpecifics = this.graphSpecificProperties[key];
- // Remove the current selectors
- d3.selectAll(`${prometheusGraphContainer} .selected-metric-line`).remove();
- d3.selectAll(`${prometheusGraphContainer} .circle-metric`).remove();
- d3.selectAll(`${prometheusGraphContainer} .rect-text-metric`).remove();
- d3.selectAll(`${prometheusGraphContainer} .text-metric`).remove();
-
- chart.append('line')
- .attr('class', 'selected-metric-line')
- .attr({
- x1: currentTimeCoordinate,
- y1: y(0),
- x2: currentTimeCoordinate,
- y2: maxValueMetric,
- });
+ const currentXCoordinate = d3.mouse(rectOverlay)[0];
+
+ Object.keys(this.graphSpecificProperties).forEach((key) => {
+ const currentGraphProps = this.graphSpecificProperties[key];
+ const timeValueOverlay = currentGraphProps.xScale.invert(currentXCoordinate);
+ const overlayIndex = bisectDate(currentGraphProps.data, timeValueOverlay, 1);
+ const d0 = currentGraphProps.data[overlayIndex - 1];
+ const d1 = currentGraphProps.data[overlayIndex];
+ const evalTime = timeValueOverlay - d0.time > d1.time - timeValueOverlay;
+ const currentData = evalTime ? d1 : d0;
+ const currentTimeCoordinate = currentGraphProps.xScale(currentData.time);
+ const currentPrometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
+ const maxValueFromData = d3.max(currentGraphProps.data.map(metricValue => metricValue.value));
+ const maxMetricValue = currentGraphProps.yScale(maxValueFromData);
+
+ // Clear up all the pieces of the flag
+ d3.selectAll(`${currentPrometheusGraphContainer} .selected-metric-line`).remove();
+ d3.selectAll(`${currentPrometheusGraphContainer} .circle-metric`).remove();
+ d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric`).remove();
+ d3.selectAll(`${currentPrometheusGraphContainer} .text-metric`).remove();
+
+ const currentChart = d3.select(currentPrometheusGraphContainer).select('g');
+ currentChart.append('line')
+ .attr('class', 'selected-metric-line')
+ .attr({
+ x1: currentTimeCoordinate,
+ y1: currentGraphProps.yScale(0),
+ x2: currentTimeCoordinate,
+ y2: maxMetricValue,
+ });
+
+ currentChart.append('circle')
+ .attr('class', 'circle-metric')
+ .attr('fill', currentGraphProps.line_color)
+ .attr('cx', currentTimeCoordinate)
+ .attr('cy', currentGraphProps.yScale(currentData.value))
+ .attr('r', this.commonGraphProperties.circle_radius_metric);
+
+ // The little box with text
+ const rectTextMetric = currentChart.append('g')
+ .attr('class', 'rect-text-metric')
+ .attr('translate', `(${currentTimeCoordinate}, ${currentGraphProps.yScale(currentData.value)})`);
+
+ rectTextMetric.append('rect')
+ .attr('class', 'rect-metric')
+ .attr('x', currentTimeCoordinate + 10)
+ .attr('y', maxMetricValue)
+ .attr('width', this.commonGraphProperties.rect_text_width)
+ .attr('height', this.commonGraphProperties.rect_text_height);
+
+ rectTextMetric.append('text')
+ .attr('class', 'text-metric')
+ .attr('x', currentTimeCoordinate + 35)
+ .attr('y', maxMetricValue + 35)
+ .text(timeFormat(currentData.time));
+
+ rectTextMetric.append('text')
+ .attr('class', 'text-metric-date')
+ .attr('x', currentTimeCoordinate + 15)
+ .attr('y', maxMetricValue + 15)
+ .text(dayFormat(currentData.time));
+
+ let currentMetricValue = formatRelevantDigits(currentData.value);
+ if (key === 'cpu_values') {
+ currentMetricValue = `${currentMetricValue}%`;
+ } else {
+ currentMetricValue = `${currentMetricValue} MB`;
+ }
- chart.append('circle')
- .attr('class', 'circle-metric')
- .attr('fill', graphSpecifics.line_color)
- .attr('cx', currentTimeCoordinate)
- .attr('cy', y(currentData.value))
- .attr('r', this.commonGraphProperties.circle_radius_metric);
-
- // The little box with text
- const rectTextMetric = chart.append('g')
- .attr('class', 'rect-text-metric')
- .attr('translate', `(${currentTimeCoordinate}, ${y(currentData.value)})`);
-
- rectTextMetric.append('rect')
- .attr('class', 'rect-metric')
- .attr('x', currentTimeCoordinate + 10)
- .attr('y', maxValueMetric)
- .attr('width', this.commonGraphProperties.rect_text_width)
- .attr('height', this.commonGraphProperties.rect_text_height);
-
- rectTextMetric.append('text')
- .attr('class', 'text-metric')
- .attr('x', currentTimeCoordinate + 35)
- .attr('y', maxValueMetric + 35)
- .text(timeFormat(currentData.time));
-
- rectTextMetric.append('text')
- .attr('class', 'text-metric-date')
- .attr('x', currentTimeCoordinate + 15)
- .attr('y', maxValueMetric + 15)
- .text(dayFormat(currentData.time));
-
- // Update the text
- d3.select(`${prometheusGraphContainer} .text-metric-usage`)
- .text(currentData.value.substring(0, 8));
+ d3.select(`${currentPrometheusGraphContainer} .text-metric-usage`)
+ .text(currentMetricValue);
+ });
}
configureGraph() {
@@ -263,12 +288,18 @@ class PrometheusGraph {
cpu_values: {
area_fill_color: '#edf3fc',
line_color: '#5b99f7',
- graph_legend_title: 'CPU utilization (%)',
+ graph_legend_title: 'CPU Usage (Cores)',
+ data: [],
+ xScale: {},
+ yScale: {},
},
memory_values: {
area_fill_color: '#fca326',
line_color: '#fc6d26',
- graph_legend_title: 'Memory usage (MB)',
+ graph_legend_title: 'Memory Usage (MB)',
+ data: [],
+ xScale: {},
+ yScale: {},
},
};
@@ -318,17 +349,17 @@ class PrometheusGraph {
}
transformData(metricsResponse) {
- const metricTypes = {};
Object.keys(metricsResponse.metrics).forEach((key) => {
if (key === 'cpu_values' || key === 'memory_values') {
const metricValues = (metricsResponse.metrics[key])[0];
- metricTypes[key] = metricValues.values.map(metric => ({
- time: new Date(metric[0] * 1000),
- value: metric[1],
- }));
+ if (typeof metricValues !== 'undefined') {
+ this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({
+ time: new Date(metric[0] * 1000),
+ value: metric[1],
+ }));
+ }
}
});
- this.data = metricTypes;
}
}
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index c38bc762675..4ccea0624ee 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -25,6 +25,7 @@
bindEvents() {
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
$('#user_notification_email').on('change', this.submitForm);
+ $('#user_notified_of_own_activity').on('change', this.submitForm);
$('.update-username').on('ajax:before', this.beforeUpdateUsername);
$('.update-username').on('ajax:complete', this.afterUpdateUsername);
$('.update-notifications').on('ajax:success', this.onUpdateNotifs);
diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
index 5cf28aa7a73..1d4bb8a13d6 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js
@@ -6,7 +6,7 @@ class ProtectedBranchDropdown {
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
- this.$protectedBranch = this.$dropdownContainer.find('.create-new-protected-branch');
+ this.$protectedBranch = this.$dropdownContainer.find('.js-create-new-protected-branch');
this.buildDropdown();
this.bindEvents();
@@ -46,7 +46,9 @@ class ProtectedBranchDropdown {
this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this));
}
- onClickCreateWildcard() {
+ onClickCreateWildcard(e) {
+ e.preventDefault();
+
// Refresh the dropdown's data, which ends up calling `getProtectedBranches`
this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex();
@@ -69,7 +71,7 @@ class ProtectedBranchDropdown {
if (branchName) {
this.$dropdownContainer
- .find('.create-new-protected-branch code')
+ .find('.js-create-new-protected-branch code')
.text(branchName);
}
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 64a68d56962..a9b3de281e1 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -56,14 +56,15 @@ import Cookies from 'js-cookie';
Sidebar.prototype.toggleTodo = function(e) {
var $btnText, $this, $todoLoading, ajaxType, url;
$this = $(e.currentTarget);
- $todoLoading = $('.js-issuable-todo-loading');
- $btnText = $('.js-issuable-todo-text', $this);
ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST';
if ($this.attr('data-delete-path')) {
url = "" + ($this.attr('data-delete-path'));
} else {
url = "" + ($this.data('url'));
}
+
+ $this.tooltip('hide');
+
return $.ajax({
url: url,
type: ajaxType,
@@ -74,34 +75,44 @@ import Cookies from 'js-cookie';
},
beforeSend: (function(_this) {
return function() {
- return _this.beforeTodoSend($this, $todoLoading);
+ $('.js-issuable-todo').disable()
+ .addClass('is-loading');
};
})(this)
}).done((function(_this) {
return function(data) {
- return _this.todoUpdateDone(data, $this, $btnText, $todoLoading);
+ return _this.todoUpdateDone(data);
};
})(this));
};
- Sidebar.prototype.beforeTodoSend = function($btn, $todoLoading) {
- $btn.disable();
- return $todoLoading.removeClass('hidden');
- };
+ Sidebar.prototype.todoUpdateDone = function(data) {
+ const deletePath = data.delete_path ? data.delete_path : null;
+ const attrPrefix = deletePath ? 'mark' : 'todo';
+ const $todoBtns = $('.js-issuable-todo');
- Sidebar.prototype.todoUpdateDone = function(data, $btn, $btnText, $todoLoading) {
$(document).trigger('todo:toggle', data.count);
- $btn.enable();
- $todoLoading.addClass('hidden');
+ $todoBtns.each((i, el) => {
+ const $el = $(el);
+ const $elText = $el.find('.js-issuable-todo-inner');
- if (data.delete_path != null) {
- $btn.attr('aria-label', $btn.data('mark-text')).attr('data-delete-path', data.delete_path);
- return $btnText.text($btn.data('mark-text'));
- } else {
- $btn.attr('aria-label', $btn.data('todo-text')).removeAttr('data-delete-path');
- return $btnText.text($btn.data('todo-text'));
- }
+ $el.removeClass('is-loading')
+ .enable()
+ .attr('aria-label', $el.data(`${attrPrefix}-text`))
+ .attr('data-delete-path', deletePath)
+ .attr('title', $el.data(`${attrPrefix}-text`));
+
+ if ($el.hasClass('has-tooltip')) {
+ $el.tooltip('fixTitle');
+ }
+
+ if ($el.data(`${attrPrefix}-icon`)) {
+ $elText.html($el.data(`${attrPrefix}-icon`));
+ } else {
+ $elText.text($el.data(`${attrPrefix}-text`));
+ }
+ });
};
Sidebar.prototype.sidebarDropdownLoading = function(e) {
@@ -198,7 +209,7 @@ import Cookies from 'js-cookie';
};
Sidebar.prototype.setSidebarHeight = function() {
- const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
+ const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + $('.sub-nav-scroll').outerHeight();
const $rightSidebar = $('.js-right-sidebar');
const diff = $navHeight - $(window).scrollTop();
if (diff > 0) {
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js
index 32067ed1fee..e62f429f1ae 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js
+++ b/app/assets/javascripts/templates/issuable_template_selector.js
@@ -1,7 +1,7 @@
/* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */
/* global Api */
-import TemplateSelector from '../blob/template_selectors/template_selector';
+import TemplateSelector from '../blob/template_selector';
((global) => {
class IssuableTemplateSelector extends TemplateSelector {
diff --git a/app/assets/javascripts/test_utils/index.js b/app/assets/javascripts/test_utils/index.js
new file mode 100644
index 00000000000..ef401abce2d
--- /dev/null
+++ b/app/assets/javascripts/test_utils/index.js
@@ -0,0 +1,4 @@
+import simulateDrag from './simulate_drag';
+
+// Export to global space for rspec to use
+window.simulateDrag = simulateDrag;
diff --git a/app/assets/javascripts/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js
index d48f2404fa5..e39213cb098 100644
--- a/app/assets/javascripts/test_utils/simulate_drag.js
+++ b/app/assets/javascripts/test_utils/simulate_drag.js
@@ -1,143 +1,137 @@
-/* eslint-disable wrap-iife, func-names, strict, no-var, vars-on-top, no-param-reassign, object-shorthand, no-shadow, comma-dangle, prefer-template, consistent-return, no-mixed-operators, no-unused-vars, no-unused-expressions, prefer-arrow-callback, max-len */
-(function () {
- 'use strict';
-
- function simulateEvent(el, type, options) {
- var event;
- if (!el) return;
- var ownerDocument = el.ownerDocument;
-
- options = options || {};
-
- if (/^mouse/.test(type)) {
- event = ownerDocument.createEvent('MouseEvents');
- event.initMouseEvent(type, true, true, ownerDocument.defaultView,
- options.button, options.screenX, options.screenY, options.clientX, options.clientY,
- options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
- } else {
- event = ownerDocument.createEvent('CustomEvent');
-
- event.initCustomEvent(type, true, true, ownerDocument.defaultView,
- options.button, options.screenX, options.screenY, options.clientX, options.clientY,
- options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
-
- event.dataTransfer = {
- data: {},
-
- setData: function (type, val) {
- this.data[type] = val;
- },
-
- getData: function (type) {
- return this.data[type];
- }
- };
- }
-
- if (el.dispatchEvent) {
- el.dispatchEvent(event);
- } else if (el.fireEvent) {
- el.fireEvent('on' + type, event);
- }
-
- return event;
- }
-
- function isLast(target) {
- var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
- var children = el.children;
-
- return children.length - 1 === target.index;
+function simulateEvent(el, type, options = {}) {
+ let event;
+ if (!el) return null;
+
+ if (/^mouse/.test(type)) {
+ event = el.ownerDocument.createEvent('MouseEvents');
+ event.initMouseEvent(type, true, true, el.ownerDocument.defaultView,
+ options.button, options.screenX, options.screenY, options.clientX, options.clientY,
+ options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
+ } else {
+ event = el.ownerDocument.createEvent('CustomEvent');
+
+ event.initCustomEvent(type, true, true, el.ownerDocument.defaultView,
+ options.button, options.screenX, options.screenY, options.clientX, options.clientY,
+ options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
+
+ event.dataTransfer = {
+ data: {},
+
+ setData(key, val) {
+ this.data[key] = val;
+ },
+
+ getData(key) {
+ return this.data[key];
+ },
+ };
}
- function getTarget(target) {
- var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
- var children = el.children;
-
- return (
- children[target.index] ||
- children[target.index === 'first' ? 0 : -1] ||
- children[target.index === 'last' ? children.length - 1 : -1] ||
- el
- );
+ if (el.dispatchEvent) {
+ el.dispatchEvent(event);
+ } else if (el.fireEvent) {
+ el.fireEvent(`on${type}`, event);
}
- function getRect(el) {
- var rect = el.getBoundingClientRect();
- var width = rect.right - rect.left;
- var height = rect.bottom - rect.top + 10;
-
- return {
- x: rect.left,
- y: rect.top,
- cx: rect.left + width / 2,
- cy: rect.top + height / 2,
- w: width,
- h: height,
- hw: width / 2,
- wh: height / 2
- };
+ return event;
+}
+
+function isLast(target) {
+ const el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
+ const children = el.children;
+
+ return children.length - 1 === target.index;
+}
+
+function getTarget(target) {
+ const el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
+ const children = el.children;
+
+ return (
+ children[target.index] ||
+ children[target.index === 'first' ? 0 : -1] ||
+ children[target.index === 'last' ? children.length - 1 : -1] ||
+ el
+ );
+}
+
+function getRect(el) {
+ const rect = el.getBoundingClientRect();
+ const width = rect.right - rect.left;
+ const height = (rect.bottom - rect.top) + 10;
+
+ return {
+ x: rect.left,
+ y: rect.top,
+ cx: rect.left + (width / 2),
+ cy: rect.top + (height / 2),
+ w: width,
+ h: height,
+ hw: width / 2,
+ wh: height / 2,
+ };
+}
+
+export default function simulateDrag(options) {
+ const { to, from } = options;
+ to.el = to.el || from.el;
+
+ const fromEl = getTarget(from);
+ const toEl = getTarget(to);
+ const firstEl = getTarget({
+ el: to.el,
+ index: 'first',
+ });
+ const lastEl = getTarget({
+ el: options.to.el,
+ index: 'last',
+ });
+
+ const fromRect = getRect(fromEl);
+ const toRect = getRect(toEl);
+ const firstRect = getRect(firstEl);
+ const lastRect = getRect(lastEl);
+
+ const startTime = new Date().getTime();
+ const duration = options.duration || 1000;
+
+ simulateEvent(fromEl, 'mousedown', {
+ button: 0,
+ clientX: fromRect.cx,
+ clientY: fromRect.cy,
+ });
+
+ if (options.ontap) options.ontap();
+ window.SIMULATE_DRAG_ACTIVE = 1;
+
+ if (options.to.index === 0) {
+ toRect.cy = firstRect.y;
+ } else if (isLast(options.to)) {
+ toRect.cy = lastRect.y + lastRect.h + 50;
}
- function simulateDrag(options, callback) {
- options.to.el = options.to.el || options.from.el;
+ const dragInterval = setInterval(() => {
+ const progress = (new Date().getTime() - startTime) / duration;
+ const x = (fromRect.cx + ((toRect.cx - fromRect.cx) * progress));
+ const y = (fromRect.cy + ((toRect.cy - fromRect.cy) * progress));
+ const overEl = fromEl.ownerDocument.elementFromPoint(x, y);
- var fromEl = getTarget(options.from);
- var toEl = getTarget(options.to);
- var firstEl = getTarget({
- el: options.to.el,
- index: 'first'
+ simulateEvent(overEl, 'mousemove', {
+ clientX: x,
+ clientY: y,
});
- var lastEl = getTarget({
- el: options.to.el,
- index: 'last'
- });
- var scrollable = options.scrollable;
-
- var fromRect = getRect(fromEl);
- var toRect = getRect(toEl);
- var firstRect = getRect(firstEl);
- var lastRect = getRect(lastEl);
-
- var startTime = new Date().getTime();
- var duration = options.duration || 1000;
- simulateEvent(fromEl, 'mousedown', { button: 0 });
- options.ontap && options.ontap();
- window.SIMULATE_DRAG_ACTIVE = 1;
-
- if (options.to.index === 0) {
- toRect.cy = firstRect.y;
- } else if (isLast(options.to)) {
- toRect.cy = lastRect.y + lastRect.h + 50;
- }
- var dragInterval = setInterval(function loop() {
- var progress = (new Date().getTime() - startTime) / duration;
- var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft;
- var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop;
- var overEl = fromEl.ownerDocument.elementFromPoint(x, y);
-
- simulateEvent(overEl, 'mousemove', {
- clientX: x,
- clientY: y
- });
-
- if (progress >= 1) {
- options.ondragend && options.ondragend();
- simulateEvent(toEl, 'mouseup');
- clearInterval(dragInterval);
- window.SIMULATE_DRAG_ACTIVE = 0;
- }
- }, 100);
-
- return {
- target: fromEl,
- fromList: fromEl.parentNode,
- toList: toEl.parentNode
- };
- }
-
- // Export
- window.simulateEvent = simulateEvent;
- window.simulateDrag = simulateDrag;
-})();
+ if (progress >= 1) {
+ if (options.ondragend) options.ondragend();
+ simulateEvent(toEl, 'mouseup');
+ clearInterval(dragInterval);
+ window.SIMULATE_DRAG_ACTIVE = 0;
+ }
+ }, 100);
+
+ return {
+ target: fromEl,
+ fromList: fromEl.parentNode,
+ toList: toEl.parentNode,
+ };
+}
diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js
index b27d252a3ef..fa078b48bf8 100644
--- a/app/assets/javascripts/user_callout.js
+++ b/app/assets/javascripts/user_callout.js
@@ -1,57 +1,26 @@
import Cookies from 'js-cookie';
-const userCalloutElementName = '.user-callout';
-const closeButton = '.close-user-callout';
-const userCalloutBtn = '.user-callout-btn';
-const userCalloutSvgAttrName = 'callout-svg';
-
const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
-const USER_CALLOUT_TEMPLATE = `
- <div class="bordered-box landing content-block">
- <button class="btn btn-default close close-user-callout" type="button">
- <i class="fa fa-times dismiss-icon"></i>
- </button>
- <div class="row">
- <div class="col-sm-3 col-xs-12 svg-container">
- </div>
- <div class="col-sm-8 col-xs-12 inner-content">
- <h4>
- Customize your experience
- </h4>
- <p>
- Change syntax themes, default project pages, and more in preferences.
- </p>
- <a class="btn user-callout-btn" href="/profile/preferences">Check it out</a>
- </div>
- </div>
-</div>`;
-
export default class UserCallout {
constructor() {
this.isCalloutDismissed = Cookies.get(USER_CALLOUT_COOKIE);
- this.userCalloutBody = $(userCalloutElementName);
- this.userCalloutSvg = $(userCalloutElementName).attr(userCalloutSvgAttrName);
- $(userCalloutElementName).removeAttr(userCalloutSvgAttrName);
+ this.userCalloutBody = $('.user-callout');
this.init();
}
init() {
- const $template = $(USER_CALLOUT_TEMPLATE);
if (!this.isCalloutDismissed || this.isCalloutDismissed === 'false') {
- $template.find('.svg-container').append(this.userCalloutSvg);
- this.userCalloutBody.append($template);
- $template.find(closeButton).on('click', e => this.dismissCallout(e));
- $template.find(userCalloutBtn).on('click', e => this.dismissCallout(e));
- } else {
- this.userCalloutBody.remove();
+ $('.js-close-callout').on('click', e => this.dismissCallout(e));
}
}
dismissCallout(e) {
- Cookies.set(USER_CALLOUT_COOKIE, 'true');
const $currentTarget = $(e.currentTarget);
- if ($currentTarget.hasClass('close-user-callout')) {
+
+ Cookies.set(USER_CALLOUT_COOKIE, 'true');
+
+ if ($currentTarget.hasClass('close')) {
this.userCalloutBody.remove();
}
}
diff --git a/app/assets/javascripts/vue_pipelines_index/components/async_button.js b/app/assets/javascripts/vue_pipelines_index/components/async_button.js
index aaebf29d8ae..58b8db4d519 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/async_button.js
+++ b/app/assets/javascripts/vue_pipelines_index/components/async_button.js
@@ -83,6 +83,7 @@ export default {
:class="buttonClass"
:title="title"
:aria-label="title"
+ data-container="body"
data-placement="top"
:disabled="isLoading">
<i :class="iconClass" aria-hidden="true"/>
diff --git a/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js b/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js
index b4480bd98c7..1626ae17a30 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js
+++ b/app/assets/javascripts/vue_pipelines_index/components/navigation_tabs.js
@@ -16,8 +16,12 @@ export default {
},
},
+ mounted() {
+ $(document).trigger('init.scrolling-tabs');
+ },
+
template: `
- <ul class="nav-links">
+ <ul class="nav-links scrolling-tabs">
<li
class="js-pipelines-tab-all"
:class="{ 'active': scope === 'all'}">
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js
index 3555040d60f..f18e2dfadaf 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js
@@ -21,6 +21,7 @@ export default {
<li v-for="artifact in artifacts">
<a
rel="nofollow"
+ download
:href="artifact.path">
<i class="fa fa-download" aria-hidden="true"></i>
<span>Download {{artifact.name}} artifacts</span>
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js b/app/assets/javascripts/vue_pipelines_index/pipelines.js
index 48f0e9036e8..9bdc232b7da 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipelines.js
+++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js
@@ -182,8 +182,14 @@ export default {
<div :class="cssClass">
<div
- class="top-area"
+ class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="!isLoading && !shouldRenderEmptyState">
+ <div class="fade-left">
+ <i class="fa fa-angle-left" aria-hidden="true"></i>
+ </div>
+ <div class="fade-right">
+ <i class="fa fa-angle-right" aria-hidden="true"></i>
+ </div>
<navigation-tabs
:scope="scope"
:count="state.count"
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js b/app/assets/javascripts/vue_shared/components/table_pagination.js
index b9cd28f6249..ebb14912b00 100644
--- a/app/assets/javascripts/vue_shared/components/table_pagination.js
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.js
@@ -3,8 +3,8 @@ const UI_LIMIT = 6;
const SPREAD = '...';
const PREV = 'Prev';
const NEXT = 'Next';
-const FIRST = '<< First';
-const LAST = 'Last >>';
+const FIRST = '« First';
+const LAST = 'Last »';
export default {
props: {
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 50849e95541..4369ae78bde 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -362,3 +362,13 @@
width: 100%;
}
}
+
+.btn-blank {
+ padding: 0;
+ background: transparent;
+ border: 0;
+
+ &:focus {
+ outline: 0;
+ }
+}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index da5b754aec7..2ede47e9de6 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -119,6 +119,46 @@
}
}
+@mixin dropdown-link {
+ display: block;
+ position: relative;
+ padding: 5px 8px;
+ color: $gl-text-color;
+ line-height: initial;
+ text-overflow: ellipsis;
+ border-radius: 2px;
+ white-space: nowrap;
+ overflow: hidden;
+
+ &:hover,
+ &:focus,
+ &.is-focused {
+ background-color: $dropdown-link-hover-bg;
+ text-decoration: none;
+
+ .badge {
+ background-color: darken($dropdown-link-hover-bg, 5%);
+ }
+ }
+
+ &.dropdown-menu-empty-link {
+ &.is-focused {
+ background-color: $dropdown-empty-row-bg;
+ }
+ }
+
+ &.dropdown-menu-user-link {
+ line-height: 16px;
+ }
+
+ .icon-play {
+ fill: $gl-text-color-secondary;
+ margin-right: 6px;
+ height: 12px;
+ width: 11px;
+ }
+}
+
.dropdown-menu,
.dropdown-menu-nav {
display: none;
@@ -178,43 +218,7 @@
}
a {
- display: block;
- position: relative;
- padding: 5px 8px;
- color: $gl-text-color;
- line-height: initial;
- text-overflow: ellipsis;
- border-radius: 2px;
- white-space: nowrap;
- overflow: hidden;
-
- &:hover,
- &:focus,
- &.is-focused {
- background-color: $dropdown-link-hover-bg;
- text-decoration: none;
-
- .badge {
- background-color: darken($dropdown-link-hover-bg, 5%);
- }
- }
-
- &.dropdown-menu-empty-link {
- &.is-focused {
- background-color: $dropdown-empty-row-bg;
- }
- }
-
- &.dropdown-menu-user-link {
- line-height: 16px;
- }
-
- .icon-play {
- fill: $gl-text-color-secondary;
- margin-right: 6px;
- height: 12px;
- width: 11px;
- }
+ @include dropdown-link;
}
.dropdown-header {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index fa02598760f..abb092623c0 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -26,7 +26,7 @@ header {
padding: 0 16px;
z-index: 100;
margin-bottom: 0;
- height: $header-height;
+ min-height: $header-height;
background-color: $gray-light;
border: none;
border-bottom: 1px solid $border-color;
@@ -48,10 +48,10 @@ header {
color: $gl-text-color-secondary;
font-size: 18px;
padding: 0;
- margin: ($header-height - 28) / 2 0;
+ margin: (($header-height - 28) / 2) 3px;
margin-left: 8px;
height: 28px;
- min-width: 28px;
+ min-width: 32px;
line-height: 28px;
text-align: center;
@@ -73,21 +73,29 @@ header {
background-color: $gray-light;
color: $gl-text-color;
- .todos-pending-count {
- background: darken($todo-alert-blue, 10%);
+ svg {
+ fill: $gl-text-color;
}
}
.fa-caret-down {
font-size: 14px;
}
+
+ svg {
+ position: relative;
+ top: 2px;
+ height: 17px;
+ // hack to get SVG to line up with FA icons
+ width: 23px;
+ fill: $gl-text-color-secondary;
+ }
}
.navbar-toggle {
color: $nav-toggle-gray;
- margin: 6px 0;
+ margin: 5px 0;
border-radius: 0;
- position: absolute;
right: -10px;
padding: 6px 10px;
@@ -135,14 +143,12 @@ header {
}
.header-content {
+ display: flex;
+ justify-content: space-between;
position: relative;
- height: $header-height;
+ min-height: $header-height;
padding-left: 30px;
- @media (min-width: $screen-sm-min) {
- padding-right: 0;
- }
-
.dropdown-menu {
margin-top: -5px;
}
@@ -165,8 +171,7 @@ header {
}
.group-name-toggle {
- margin: 0 5px;
- vertical-align: sub;
+ margin: 3px 5px;
}
.group-title {
@@ -177,39 +182,32 @@ header {
}
}
+ .title-container {
+ display: flex;
+ align-items: flex-start;
+ flex: 1 1 auto;
+ padding-top: (($header-height - 19) / 2);
+ overflow: hidden;
+ }
+
.title {
position: relative;
padding-right: 20px;
margin: 0;
font-size: 18px;
- max-width: 385px;
+ line-height: 22px;
display: inline-block;
- line-height: $header-height;
font-weight: normal;
color: $gl-text-color;
- overflow: hidden;
- text-overflow: ellipsis;
vertical-align: top;
white-space: nowrap;
- &.initializing {
- display: none;
- }
-
- @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
- max-width: 300px;
- }
-
- @media (max-width: $screen-xs-max) {
- max-width: 190px;
+ &.wrap {
+ white-space: normal;
}
- @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
- max-width: 428px;
- }
-
- @media (min-width: $screen-lg-min) {
- max-width: 685px;
+ &.initializing {
+ opacity: 0;
}
a {
@@ -226,10 +224,10 @@ header {
border: transparent;
background: transparent;
position: absolute;
+ top: 2px;
right: 3px;
width: 12px;
line-height: 19px;
- margin-top: (($header-height - 19) / 2);
padding: 0;
font-size: 10px;
text-align: center;
@@ -247,15 +245,12 @@ header {
}
.navbar-collapse {
- float: right;
+ flex: 0 0 auto;
border-top: none;
-
- @media (min-width: $screen-md-min) {
- padding: 0;
- }
+ padding: 0;
@media (max-width: $screen-xs-max) {
- float: none;
+ flex: 1 1 auto;
}
}
}
@@ -269,10 +264,30 @@ header {
}
}
-.page-sidebar-pinned.right-sidebar-expanded {
- @media (max-width: $screen-md-max) {
- .header-content .title {
- width: 300px;
+.navbar-nav {
+ li {
+ .badge {
+ position: inherit;
+ top: -3px;
+ font-weight: normal;
+ margin-left: -12px;
+ font-size: 11px;
+ color: $white-light;
+ padding: 1px 5px 2px;
+ border-radius: 7px;
+ box-shadow: 0 1px 0 rgba($gl-header-color, .2);
+
+ &.issues-count {
+ background-color: $green-500;
+ }
+
+ &.merge-requests-count {
+ background-color: $orange-600;
+ }
+
+ &.todos-count {
+ background-color: $blue-500;
+ }
}
}
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index df78bbdea51..b3340d41333 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -52,6 +52,18 @@
}
}
+@mixin basic-list-stats {
+ .stats {
+ float: right;
+ line-height: $list-text-height;
+ color: $gl-text-color;
+
+ span {
+ margin-right: 15px;
+ }
+ }
+}
+
@mixin bulleted-list {
> ul {
list-style-type: disc;
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 5ab505034b6..e6d808717f3 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -146,6 +146,10 @@
display: block;
}
+ &.scrolling-tabs {
+ float: left;
+ }
+
li a {
padding: 16px 15px 11px;
}
@@ -476,3 +480,44 @@
}
}
}
+
+.inner-page-scroll-tabs {
+ position: relative;
+
+ .nav-links {
+ padding-bottom: 1px;
+ }
+
+ .fade-right {
+ @include fade(left, $white-light);
+ right: 0;
+ text-align: right;
+
+ .fa {
+ right: 5px;
+ }
+ }
+
+ .fade-left {
+ @include fade(right, $white-light);
+ left: 0;
+ text-align: left;
+
+ .fa {
+ left: 5px;
+ }
+ }
+
+ .fade-right,
+ .fade-left {
+ top: 16px;
+ bottom: auto;
+ }
+
+ &.is-smaller {
+ .fade-right,
+ .fade-left {
+ top: 11px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 40e93032f59..746c9c25620 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -33,7 +33,7 @@
padding-right: 0;
@media (min-width: $screen-sm-min) {
- .content-wrapper {
+ &:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width;
}
@@ -55,7 +55,7 @@
padding-right: 0;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
- .content-wrapper {
+ &:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width;
}
}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 575d32b1a23..7c0fc1008d0 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -46,7 +46,7 @@
}
.issue-boards-page {
- .page-with-sidebar {
+ .content-wrapper {
padding-bottom: 0;
}
}
@@ -72,7 +72,7 @@
@media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS
- height: calc(100vh - 220px);
+ height: calc(100vh - 222px);
min-height: 475px;
transition: width .2s;
@@ -240,8 +240,13 @@
font-size: (14px / $issue-boards-font-size) * 1em;
}
+ .card-assignee {
+ margin-right: 5px;
+ }
+
.avatar {
margin-left: 0;
+ margin-right: 0;
}
}
@@ -296,7 +301,7 @@
}
}
-.issue-boards-sidebar {
+.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar {
&.right-sidebar {
top: 0;
bottom: 0;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index da8410eca66..0dad91ba128 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -142,7 +142,9 @@
border: 1px solid $border-gray-dark;
border-radius: $border-radius-default;
margin-left: 5px;
- line-height: 1;
+ font-size: $gl-font-size;
+ line-height: $gl-font-size;
+ outline: none;
&:hover {
background-color: darken($gray-light, 10%);
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index eab79c2a481..1aa1079903c 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -431,6 +431,21 @@
border-bottom: none;
}
+.diff-stats-summary-toggler {
+ padding: 0;
+ background-color: transparent;
+ border: 0;
+ color: $gl-link-color;
+ transition: color 0.1s linear;
+
+ &:hover,
+ &:focus {
+ outline: none;
+ text-decoration: underline;
+ color: $gl-link-hover-color;
+ }
+}
+
// Mobile
@media (max-width: 480px) {
.diff-title {
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 4af267403d8..f6b8c8ee2bc 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -1,4 +1,13 @@
.file-editor {
+ .nav-links {
+ border-top: 1px solid $border-color;
+ border-right: 1px solid $border-color;
+ border-left: 1px solid $border-color;
+ border-bottom: none;
+ border-radius: 2px;
+ background: $gray-normal;
+ }
+
#editor {
border: none;
border-radius: 0;
@@ -72,11 +81,7 @@
}
.encoding-selector,
- .soft-wrap-toggle,
- .license-selector,
- .gitignore-selector,
- .gitlab-ci-yml-selector,
- .dockerfile-selector {
+ .soft-wrap-toggle {
display: inline-block;
vertical-align: top;
font-family: $regular_font;
@@ -103,28 +108,9 @@
}
}
}
-
- .gitignore-selector,
- .license-selector,
- .gitlab-ci-yml-selector,
- .dockerfile-selector {
- .dropdown {
- line-height: 21px;
- }
-
- .dropdown-menu-toggle {
- vertical-align: top;
- width: 220px;
- }
- }
-
- .gitlab-ci-yml-selector {
- .dropdown-menu-toggle {
- width: 250px;
- }
- }
}
+
@media(max-width: $screen-xs-max){
.file-editor {
.file-title {
@@ -149,10 +135,7 @@
margin: 3px 0;
}
- .encoding-selector,
- .license-selector,
- .gitignore-selector,
- .gitlab-ci-yml-selector {
+ .encoding-selector {
display: block;
margin: 3px 0;
@@ -163,3 +146,104 @@
}
}
}
+
+.blob-new-page-title,
+.blob-edit-page-title {
+ margin: 19px 0 21px;
+ vertical-align: top;
+ display: inline-block;
+
+ @media(max-width: $screen-sm-max) {
+ display: block;
+ margin: 19px 0 12px;
+ }
+}
+
+.template-selectors-menu {
+ display: inline-block;
+ vertical-align: top;
+ margin: 14px 0 0 16px;
+ padding: 0 0 0 14px;
+ border-left: 1px solid $border-color;
+
+ @media(max-width: $screen-sm-max) {
+ display: block;
+ width: 100%;
+ margin: 5px 0;
+ padding: 0;
+ border-left: none;
+ }
+}
+
+.templates-selectors-label {
+ display: inline-block;
+ vertical-align: top;
+ margin-top: 6px;
+ line-height: 21px;
+
+ @media(max-width: $screen-sm-max) {
+ display: block;
+ margin: 5px 0;
+ }
+}
+
+.template-selector-dropdowns-wrap {
+ display: inline-block;
+ margin-left: 8px;
+ vertical-align: top;
+ margin: 5px 0 0 8px;
+
+ @media(max-width: $screen-sm-max) {
+ display: block;
+ width: 100%;
+ margin: 0 0 16px;
+ }
+
+ .license-selector,
+ .gitignore-selector,
+ .gitlab-ci-yml-selector,
+ .dockerfile-selector,
+ .template-type-selector {
+ display: inline-block;
+ vertical-align: top;
+ font-family: $regular_font;
+ margin-top: -5px;
+
+ @media(max-width: $screen-sm-max) {
+ display: block;
+ width: 100%;
+ margin: 5px 0;
+ }
+
+ .dropdown {
+ line-height: 21px;
+ }
+
+ .dropdown-menu-toggle {
+ width: 250px;
+ vertical-align: top;
+
+ @media(max-width: $screen-sm-max) {
+ display: block;
+ width: 100%;
+ margin: 5px 0;
+ }
+ }
+
+ }
+}
+
+.template-selectors-undo-menu {
+ display: inline-block;
+ margin: 7px 0 0 10px;
+
+ @media(max-width: $screen-sm-max) {
+ display: block;
+ width: 100%;
+ margin: 20px 0;
+ }
+
+ button {
+ margin: -4px 0 0 15px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 25be7f408d0..6faa3794c83 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -26,7 +26,7 @@
.table.ci-table {
.environments-actions {
- min-width: 200px;
+ min-width: 300px;
}
.environments-commit,
@@ -159,6 +159,16 @@
text {
fill: $stat-graph-axis-fill;
}
+
+ .label-axis-text,
+ .text-metric-usage {
+ fill: $black;
+ font-weight: 500;
+ }
+
+ .legend-axis-text {
+ fill: $black;
+ }
}
.x-axis path,
@@ -222,3 +232,12 @@
stroke: $black;
stroke-width: 1;
}
+
+.environments-actions {
+ .external-url,
+ .monitoring-url,
+ .terminal-button,
+ .stop-env-link {
+ width: 38px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 84d21e48463..73a5889867a 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -9,16 +9,15 @@
}
}
-.group-row {
- .stats {
- float: right;
- line-height: $list-text-height;
- color: $gl-text-color;
+.group-root-path {
+ max-width: 40vw;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ word-wrap: nowrap;
+}
- span {
- margin-right: 15px;
- }
- }
+.group-row {
+ @include basic-list-stats;
}
.ldap-group-links {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index c1a9bc4be28..e84a05e3e9e 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -243,6 +243,10 @@
font-size: 13px;
font-weight: normal;
}
+
+ .hide-expanded {
+ display: none;
+ }
}
&.right-sidebar-collapsed {
@@ -282,10 +286,11 @@
display: block;
width: 100%;
text-align: center;
- padding-bottom: 10px;
+ margin-bottom: 10px;
color: $issuable-sidebar-color;
- &:hover {
+ &:hover,
+ &:hover .todo-undone {
color: $gl-text-color;
}
@@ -294,6 +299,10 @@
margin-top: 0;
}
+ .todo-undone {
+ color: $gl-link-color;
+ }
+
.author {
display: none;
}
@@ -582,3 +591,21 @@
opacity: 0;
}
}
+
+.issuable-todo-btn {
+ .fa-spinner {
+ display: none;
+ }
+
+ &.is-loading {
+ .fa-spinner {
+ display: inline-block;
+ }
+
+ &.sidebar-collapsed-icon {
+ .issuable-todo-inner {
+ display: none;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 6630904ec92..566dcc64802 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -60,7 +60,17 @@
}
.modify-merge-commit-link {
+ padding: 0;
+
+ background-color: transparent;
+ border: 0;
+
color: $gl-text-color;
+
+ &:hover,
+ &:focus {
+ text-decoration: underline;
+ }
}
.merge-param-checkbox {
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index efbd9365fd9..335e587b8f4 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -52,66 +52,62 @@
}
}
-.milestone-summary {
- .milestone-stat {
- white-space: nowrap;
- margin-right: 10px;
+.milestone-sidebar {
+ .gutter-toggle {
+ margin-bottom: 10px;
+ }
- &.with-drilldown {
- margin-right: 2px;
+ .milestone-progress {
+ .title {
+ padding-top: 5px;
}
- }
- .remaining-days {
- color: $orange-600;
+ .progress {
+ height: 6px;
+ margin: 0;
+ }
}
- .milestone-stats-and-buttons {
- display: flex;
- justify-content: flex-start;
- flex-wrap: wrap;
+ .collapsed-milestone-date {
+ font-size: 12px;
+ }
- @media (min-width: $screen-xs-min) {
- justify-content: space-between;
- flex-wrap: nowrap;
- }
+ .milestone-date {
+ display: block;
}
- .milestone-progress-buttons {
- order: 1;
- margin-top: 10px;
+ .date-separator {
+ line-height: 5px;
+ }
- @media (min-width: $screen-xs-min) {
- order: 2;
- margin-top: 0;
- flex-shrink: 0;
- }
+ .remaining-days strong {
+ font-weight: normal;
+ }
- .btn {
- float: left;
- margin-right: $btn-side-margin;
+ .milestone-stat {
+ float: left;
+ margin-right: 14px;
+ }
- &:last-child {
- margin-right: 0;
- }
- }
+ .milestone-stat:last-child {
+ margin-right: 0;
}
- .milestone-stats {
- order: 2;
- width: 100%;
- padding: 7px 0;
- flex-shrink: 1;
+ .milestone-progress {
+ .sidebar-collapsed-icon {
+ clear: both;
+ padding: 15px 5px 5px;
- @media (min-width: $screen-xs-min) {
- // when displayed on one line stats go first, buttons second
- order: 1;
+ .progress {
+ margin: 5px 0;
+ }
}
}
- .progress {
- width: 100%;
- margin: 15px 0;
+ .right-sidebar-collapsed & {
+ .reference {
+ border-top: 1px solid $border-gray-normal;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index a2129722633..57cf8e136e2 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -243,22 +243,6 @@ ul.notes {
}
}
-.page-sidebar-pinned.right-sidebar-expanded {
- @media (max-width: $screen-md-max) {
- .note-header {
- .note-headline-light {
- display: block;
- }
-
- .note-actions {
- position: absolute;
- right: 0;
- top: 0;
- }
- }
- }
-}
-
// Diff code in discussion view
.discussion-body .diff-file {
.file-title {
@@ -426,8 +410,22 @@ ul.notes {
}
.discussion-toggle-button {
+ padding: 0;
+ background-color: transparent;
+ border: 0;
line-height: 20px;
font-size: 13px;
+ transition: color 0.1s linear;
+
+ &:hover {
+ color: $gl-link-color;
+ }
+
+ &:focus {
+ text-decoration: underline;
+ outline: none;
+ color: $gl-link-color;
+ }
.fa {
margin-right: 3px;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 949d52cffa2..0fa1f68e034 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -459,34 +459,13 @@ a.deploy-project-label {
flex-wrap: wrap;
.btn {
- margin: 0 10px 10px 0;
padding: 8px;
+ margin-left: 10px;
}
> div {
+ margin-bottom: 10px;
padding-left: 0;
-
- &:last-child {
- margin-bottom: 0;
-
- .btn {
- margin-right: 0;
- }
- }
- }
- }
-}
-
-.page-sidebar-pinned {
- .project-stats .nav > li.right {
- @media (min-width: $screen-lg-min) {
- float: none;
- }
- }
-
- .download-button {
- @media (min-width: $screen-lg-min) {
- margin-left: 0;
}
}
}
@@ -587,9 +566,19 @@ pre.light-well {
display: flex;
flex-direction: column;
+ // Disable Flexbox for admin page
+ &.admin-projects {
+ display: block;
+
+ .project-row {
+ display: block;
+ }
+ }
+
.project-row {
display: flex;
align-items: center;
+ @include basic-list-stats;
}
h3 {
@@ -746,6 +735,15 @@ pre.light-well {
}
}
+.create-new-protected-branch-button {
+ @include dropdown-link;
+
+ width: 100%;
+ background-color: transparent;
+ border: 0;
+ text-align: left;
+}
+
.protected-branches-list {
margin-bottom: 30px;
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index b071d7f18cd..a39815319f3 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -3,25 +3,6 @@
*
*/
-.navbar-nav {
- li {
- .badge.todos-pending-count {
- position: inherit;
- top: -6px;
- margin-top: -5px;
- font-weight: normal;
- background: $todo-alert-blue;
- margin-left: -17px;
- font-size: 11px;
- color: $white-light;
- padding: 3px;
- padding-top: 1px;
- padding-bottom: 1px;
- border-radius: 3px;
- }
- }
-}
-
.todos-list > .todo {
// workaround because we cannot use border-colapse
border-top: 1px solid transparent;
diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb
index 5055c318a5f..dc9a6df5f75 100644
--- a/app/controllers/admin/abuse_reports_controller.rb
+++ b/app/controllers/admin/abuse_reports_controller.rb
@@ -1,6 +1,7 @@
class Admin::AbuseReportsController < Admin::ApplicationController
def index
@abuse_reports = AbuseReport.order(id: :desc).page(params[:page])
+ @abuse_reports.includes(:reporter, :user)
end
def destroy
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 8d831ffdd70..515d8e1523b 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -45,15 +45,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
def application_setting_params
- restricted_levels = params[:application_setting][:restricted_visibility_levels]
- if restricted_levels.nil?
- params[:application_setting][:restricted_visibility_levels] = []
- else
- restricted_levels.map! do |level|
- level.to_i
- end
- end
-
import_sources = params[:application_setting][:import_sources]
if import_sources.nil?
params[:application_setting][:import_sources] = []
@@ -143,6 +134,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:unique_ips_limit_enabled,
:version_check_enabled,
:terminal_max_session_time,
+ :polling_interval_multiplier,
disabled_oauth_sign_in_sources: [],
import_sources: [],
diff --git a/app/controllers/admin/background_jobs_controller.rb b/app/controllers/admin/background_jobs_controller.rb
index c09095b9849..5f90ad7137d 100644
--- a/app/controllers/admin/background_jobs_controller.rb
+++ b/app/controllers/admin/background_jobs_controller.rb
@@ -1,7 +1,7 @@
class Admin::BackgroundJobsController < Admin::ApplicationController
def show
- ps_output, _ = Gitlab::Popen.popen(%W(ps -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command))
- @sidekiq_processes = ps_output.split("\n").grep(/sidekiq/)
+ ps_output, _ = Gitlab::Popen.popen(%W(ps ww -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command))
+ @sidekiq_processes = ps_output.split("\n").grep(/sidekiq \d+\.\d+\.\d+/)
@concurrency = Sidekiq.options[:concurrency]
end
end
diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb
index d496f08a598..4531657268c 100644
--- a/app/controllers/admin/labels_controller.rb
+++ b/app/controllers/admin/labels_controller.rb
@@ -16,10 +16,9 @@ class Admin::LabelsController < Admin::ApplicationController
end
def create
- @label = Label.new(label_params)
- @label.template = true
+ @label = Labels::CreateService.new(label_params).execute(template: true)
- if @label.save
+ if @label.persisted?
redirect_to admin_labels_url, notice: "Label was created"
else
render :new
@@ -27,7 +26,9 @@ class Admin::LabelsController < Admin::ApplicationController
end
def update
- if @label.update(label_params)
+ @label = Labels::UpdateService.new(label_params).execute(@label)
+
+ if @label.valid?
redirect_to admin_labels_path, notice: 'label was successfully updated.'
else
render :edit
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 24504685e48..563bcc65bd6 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -95,18 +95,14 @@ class Admin::UsersController < Admin::ApplicationController
def create
opts = {
- force_random_password: true,
- password_expires_at: nil
+ reset_password: true,
+ skip_confirmation: true
}
- @user = User.new(user_params.merge(opts))
- @user.created_by_id = current_user.id
- @user.generate_password
- @user.generate_reset_token
- @user.skip_confirmation!
+ @user = Users::CreateService.new(current_user, user_params.merge(opts)).execute
respond_to do |format|
- if @user.save
+ if @user.persisted?
format.html { redirect_to [:admin, @user], notice: 'User was successfully created.' }
format.json { render json: @user, status: :created, location: @user }
else
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 85ae4985e58..c8a501d7319 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -15,6 +15,9 @@ module IssuableCollections
# a new order into the collection.
# We cannot use reorder to not mess up the paginated collection.
issuable_ids = issuable_collection.map(&:id)
+
+ return {} if issuable_ids.empty?
+
issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type)
issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type)
issuable_merge_requests_count =
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 0cbf3eb58a3..00c50f9d0ad 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -14,6 +14,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
@members = @members.search(params[:search]) if params[:search].present?
@members = @members.sort(@sort)
@members = @members.page(params[:page]).per(50)
+ @members.includes(:user)
@requesters = AccessRequestsFinder.new(@group).execute(current_user)
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
index 587898a8634..facb25525b5 100644
--- a/app/controllers/groups/labels_controller.rb
+++ b/app/controllers/groups/labels_controller.rb
@@ -26,7 +26,7 @@ class Groups::LabelsController < Groups::ApplicationController
end
def create
- @label = @group.labels.create(label_params)
+ @label = Labels::CreateService.new(label_params).execute(group: group)
if @label.valid?
redirect_to group_labels_path(@group)
@@ -40,7 +40,9 @@ class Groups::LabelsController < Groups::ApplicationController
end
def update
- if @label.update_attributes(label_params)
+ @label = Labels::UpdateService.new(label_params).execute(@label)
+
+ if @label.valid?
redirect_back_or_group_labels_path
else
render :edit
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index 256c41e6145..9de0297ecfd 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -1,17 +1,27 @@
class Import::BaseController < ApplicationController
private
- def find_or_create_namespace(name, owner)
- return current_user.namespace if name == owner
+ def find_or_create_namespace(names, owner)
+ return current_user.namespace if names == owner
return current_user.namespace unless current_user.can_create_group?
- begin
- name = params[:target_namespace].presence || name
- namespace = Group.create!(name: name, path: name, owner: current_user)
- namespace.add_owner(current_user)
- namespace
- rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
- Namespace.find_by_path_or_name(name)
+ names = params[:target_namespace].presence || names
+ full_path_namespace = Namespace.find_by_full_path(names)
+
+ return full_path_namespace if full_path_namespace
+
+ names.split('/').inject(nil) do |parent, name|
+ begin
+ namespace = Group.create!(name: name,
+ path: name,
+ owner: current_user,
+ parent: parent)
+ namespace.add_owner(current_user)
+
+ namespace
+ rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
+ Namespace.where(parent: parent).find_by_path_or_name(name)
+ end
end
end
end
diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb
index 69959fe3687..7d1aa8d1ce0 100644
--- a/app/controllers/profiles/accounts_controller.rb
+++ b/app/controllers/profiles/accounts_controller.rb
@@ -1,11 +1,22 @@
class Profiles::AccountsController < Profiles::ApplicationController
+ include AuthHelper
+
def show
@user = current_user
end
def unlink
provider = params[:provider]
- current_user.identities.find_by(provider: provider).destroy unless provider.to_s == 'saml'
+ identity = current_user.identities.find_by(provider: provider)
+
+ return render_404 unless identity
+
+ if unlink_allowed?(provider)
+ identity.destroy
+ else
+ flash[:alert] = "You are not allowed to unlink your primary login account"
+ end
+
redirect_to profile_account_path
end
end
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index b8b71d295f6..a271e2dfc4b 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -17,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end
def user_params
- params.require(:user).permit(:notification_email)
+ params.require(:user).permit(:notification_email, :notified_of_own_activity)
end
end
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index f1e4246e7fb..3f3c90a49ab 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -74,7 +74,9 @@ class Projects::BuildsController < Projects::ApplicationController
end
def status
- render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha)
+ render json: BuildSerializer
+ .new(project: @project, user: @current_user)
+ .represent_status(@build)
end
def erase
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index 278098fcc58..37f6f637ff0 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -57,7 +57,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
def render_ok
set_workhorse_internal_api_content_type
- render json: Gitlab::Workhorse.git_http_ok(repository, user)
+ render json: Gitlab::Workhorse.git_http_ok(repository, user, action_name)
end
def render_http_not_allowed
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 0d6d9f492c1..d984e6d3918 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -260,4 +260,13 @@ class Projects::IssuesController < Projects::ApplicationController
:milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: []
)
end
+
+ def authenticate_user!
+ return if current_user
+
+ notice = "Please sign in to create the new issue."
+
+ store_location_for :user, request.fullpath
+ redirect_to new_user_session_path, notice: notice
+ end
end
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 1593b5c1afb..2f55ba4e700 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -29,7 +29,7 @@ class Projects::LabelsController < Projects::ApplicationController
end
def create
- @label = @project.labels.create(label_params)
+ @label = Labels::CreateService.new(label_params).execute(project: @project)
if @label.valid?
respond_to do |format|
@@ -48,7 +48,9 @@ class Projects::LabelsController < Projects::ApplicationController
end
def update
- if @label.update_attributes(label_params)
+ @label = Labels::UpdateService.new(label_params).execute(@label)
+
+ if @label.valid?
redirect_to namespace_project_labels_path(@project.namespace, @project)
else
render :edit
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 2fadf7c8c81..37e3ac05916 100755
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -10,7 +10,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled
before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check,
- :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
+ :ci_status, :pipeline_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines]
before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines]
@@ -39,6 +39,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@collection_type = "MergeRequest"
@merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page])
+ @merge_requests = @merge_requests.includes(merge_request_diff: :merge_request)
@issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
@@ -97,31 +98,31 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def diffs
apply_diff_view_cookie!
- @merge_request_diff =
- if params[:diff_id]
- @merge_request.merge_request_diffs.viewable.find(params[:diff_id])
- else
- @merge_request.merge_request_diff
- end
-
- @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
- @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
-
- if params[:start_sha].present?
- @start_sha = params[:start_sha]
- @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
-
- unless @start_version
- @start_sha = @merge_request_diff.head_commit_sha
- @start_version = @merge_request_diff
- end
- end
-
- @environment = @merge_request.environments_for(current_user).last
-
respond_to do |format|
format.html { define_discussion_vars }
format.json do
+ @merge_request_diff =
+ if params[:diff_id]
+ @merge_request.merge_request_diffs.viewable.find(params[:diff_id])
+ else
+ @merge_request.merge_request_diff
+ end
+
+ @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
+ @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
+
+ if params[:start_sha].present?
+ @start_sha = params[:start_sha]
+ @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
+
+ unless @start_version
+ @start_sha = @merge_request_diff.head_commit_sha
+ @start_version = @merge_request_diff
+ end
+ end
+
+ @environment = @merge_request.environments_for(current_user).last
+
if @start_sha
compared_diff_version
else
@@ -473,6 +474,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render json: response
end
+ def pipeline_status
+ render json: PipelineSerializer
+ .new(project: @project, user: @current_user)
+ .represent_status(@merge_request.head_pipeline)
+ end
+
def ci_environments_status
environments =
begin
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 5922e686cd0..408c0c60cb0 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -21,9 +21,9 @@ class Projects::MilestonesController < Projects::ApplicationController
@sort = params[:sort] || 'due_date_asc'
@milestones = @milestones.sort(@sort)
- @milestones = @milestones.includes(:project)
respond_to do |format|
format.html do
+ @milestones = @milestones.includes(:project)
@milestones = @milestones.page(params[:page])
end
format.json do
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 718d9e86bea..43a1abaa662 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -72,6 +72,12 @@ class Projects::PipelinesController < Projects::ApplicationController
end
end
+ def status
+ render json: PipelineSerializer
+ .new(project: @project, user: @current_user)
+ .represent_status(@pipeline)
+ end
+
def stage
@stage = pipeline.stage(params[:stage])
return not_found unless @stage
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index f210f7e61d2..c5e24b9e365 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -124,6 +124,6 @@ class Projects::WikisController < Projects::ApplicationController
end
def wiki_params
- params[:wiki].slice(:title, :content, :format, :message)
+ params.require(:wiki).permit(:title, :content, :format, :message)
end
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index b44f38d4a0c..8109427a45f 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -1,5 +1,4 @@
class RegistrationsController < Devise::RegistrationsController
- before_action :signup_enabled?
include Recaptcha::Verify
def new
@@ -21,15 +20,17 @@ class RegistrationsController < Devise::RegistrationsController
flash.delete :recaptcha_error
render action: 'new'
end
+ rescue Gitlab::Access::AccessDeniedError
+ redirect_to(new_user_session_path)
end
def destroy
- Users::DestroyService.new(current_user).execute(current_user)
+ DeleteUserWorker.perform_async(current_user.id, current_user.id)
respond_to do |format|
format.html do
session.try(:destroy)
- redirect_to new_user_session_path, notice: "Account successfully removed."
+ redirect_to new_user_session_path, notice: "Account scheduled for removal."
end
end
end
@@ -50,12 +51,6 @@ class RegistrationsController < Devise::RegistrationsController
private
- def signup_enabled?
- unless current_application_settings.signup_enabled?
- redirect_to(new_user_session_path)
- end
- end
-
def sign_up_params
params.require(:user).permit(:username, :email, :email_confirmation, :name, :password)
end
@@ -65,7 +60,7 @@ class RegistrationsController < Devise::RegistrationsController
end
def resource
- @resource ||= User.new(sign_up_params)
+ @resource ||= Users::CreateService.new(current_user, sign_up_params).build
end
def devise_mapping
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 612d69cf557..4a579601785 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -6,45 +6,19 @@ class SearchController < ApplicationController
layout 'search'
def show
- if params[:project_id].present?
- @project = Project.find_by(id: params[:project_id])
- @project = nil unless can?(current_user, :download_code, @project)
- end
+ search_service = SearchService.new(current_user, params)
- if params[:group_id].present?
- @group = Group.find_by(id: params[:group_id])
- @group = nil unless can?(current_user, :read_group, @group)
- end
+ @project = search_service.project
+ @group = search_service.group
return if params[:search].blank?
@search_term = params[:search]
- @scope = params[:scope]
- @show_snippets = params[:snippets].eql? 'true'
-
- @search_results =
- if @project
- unless %w(blobs notes issues merge_requests milestones wiki_blobs
- commits).include?(@scope)
- @scope = 'blobs'
- end
-
- Search::ProjectService.new(@project, current_user, params).execute
- elsif @show_snippets
- unless %w(snippet_blobs snippet_titles).include?(@scope)
- @scope = 'snippet_blobs'
- end
-
- Search::SnippetService.new(current_user, params).execute
- else
- unless %w(projects issues merge_requests milestones).include?(@scope)
- @scope = 'projects'
- end
- Search::GlobalService.new(current_user, params).execute
- end
-
- @search_objects = @search_results.objects(@scope, params[:page])
+ @scope = search_service.scope
+ @show_snippets = search_service.show_snippets?
+ @search_results = search_service.search_results
+ @search_objects = search_service.search_objects
check_single_commit_result
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 7d81c96262f..d8561871098 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -79,7 +79,7 @@ class SessionsController < Devise::SessionsController
if request.referer.present? && (params['redirect_to_referer'] == 'yes')
referer_uri = URI(request.referer)
if referer_uri.host == Gitlab.config.gitlab.host
- referer_uri.path
+ referer_uri.request_uri
else
request.fullpath
end
diff --git a/app/finders/group_finder.rb b/app/finders/group_finder.rb
new file mode 100644
index 00000000000..24c84d2d1aa
--- /dev/null
+++ b/app/finders/group_finder.rb
@@ -0,0 +1,17 @@
+class GroupFinder
+ include Gitlab::Allowable
+
+ def initialize(current_user)
+ @current_user = current_user
+ end
+
+ def execute(*params)
+ group = Group.find_by(*params)
+
+ if can?(@current_user, :read_group, group)
+ group
+ else
+ nil
+ end
+ end
+end
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index 08713272947..76715e5970d 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -9,7 +9,7 @@
# state: 'open' or 'closed' or 'all'
# group_id: integer
# project_id: integer
-# milestone_id: integer
+# milestone_title: string
# assignee_id: integer
# search: string
# label_name: string
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index fa0e2a5e3d8..e52083f86e4 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -20,8 +20,17 @@ class LabelsFinder < UnionFinder
if project?
if project
- label_ids << project.group.labels if project.group.present?
- label_ids << project.labels
+ if project.group.present?
+ labels_table = Label.arel_table
+
+ label_ids << Label.where(
+ labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].eq(project.group.id)).or(
+ labels_table[:type].eq('ProjectLabel').and(labels_table[:project_id].eq(project.id))
+ )
+ )
+ else
+ label_ids << project.labels
+ end
end
else
label_ids << Label.where(group_id: projects.group_ids)
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 1eec45d9cb5..42f0ebd774c 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -9,7 +9,7 @@
# state: 'open' or 'closed' or 'all'
# group_id: integer
# project_id: integer
-# milestone_id: integer
+# milestone_title: string
# assignee_id: integer
# search: string
# label_name: string
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index a3213581498..e5b811f3300 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -306,4 +306,8 @@ module ApplicationHelper
def active_when(condition)
'active' if condition
end
+
+ def show_user_callout?
+ cookies[:user_callout_dismissed] == 'true'
+ end
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index 1ee6c1d3afa..101fe579da2 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -76,5 +76,9 @@ module AuthHelper
(current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current
end
+ def unlink_allowed?(provider)
+ %w(saml cas3).exclude?(provider.to_s)
+ end
+
extend self
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index a777db2826b..ec57fec4f99 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -251,6 +251,21 @@ module IssuablesHelper
end
def selected_template(issuable)
- params[:issuable_template] if issuable_templates(issuable).include?(params[:issuable_template])
+ params[:issuable_template] if issuable_templates(issuable).any?{ |template| template[:name] == params[:issuable_template] }
+ end
+
+ def issuable_todo_button_data(issuable, todo, is_collapsed)
+ {
+ todo_text: "Add todo",
+ mark_text: "Mark done",
+ todo_icon: (is_collapsed ? icon('plus-square') : nil),
+ mark_icon: (is_collapsed ? icon('check-square', class: 'todo-undone') : nil),
+ issuable_id: issuable.id,
+ issuable_type: issuable.class.name.underscore,
+ url: namespace_project_todos_path(@project.namespace, @project),
+ delete_path: (dashboard_todo_path(todo) if todo),
+ placement: (is_collapsed ? 'left' : nil),
+ container: (is_collapsed ? 'body' : nil)
+ }
end
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index bd3f51fc658..c9e70faa52e 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -19,8 +19,8 @@ module MilestonesHelper
end
end
- def milestones_browse_issuables_path(milestone, type:)
- opts = { milestone_title: milestone.title }
+ def milestones_browse_issuables_path(milestone, state: nil, type:)
+ opts = { milestone_title: milestone.title, state: state }
if @project
polymorphic_path([@project.namespace.becomes(Namespace), @project, type], opts)
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 991fd949b94..17bfd07e00f 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -6,7 +6,8 @@ module NavHelper
current_path?('merge_requests#builds') ||
current_path?('merge_requests#conflicts') ||
current_path?('merge_requests#pipelines') ||
- current_path?('issues#show')
+ current_path?('issues#show') ||
+ current_path?('milestones#show')
if cookies[:collapsed_gutter] == 'true'
"page-gutter right-sidebar-collapsed"
else
diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb
index b5017080cfb..55f4da0ef85 100644
--- a/app/helpers/sidekiq_helper.rb
+++ b/app/helpers/sidekiq_helper.rb
@@ -3,9 +3,9 @@ module SidekiqHelper
(?<pid>\d+)\s+
(?<cpu>[\d\.,]+)\s+
(?<mem>[\d\.,]+)\s+
- (?<state>[DRSTWXZNLsl\+<]+)\s+
- (?<start>.+)\s+
- (?<command>sidekiq.*\])
+ (?<state>[DIEKNRSTVWXZNLpsl\+<>\/\d]+)\s+
+ (?<start>.+?)\s+
+ (?<command>(?:ruby\d+:\s+)?sidekiq.*\].*)
\z/x
def parse_sidekiq_ps(line)
diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb
index 79c3c2e62c5..a9b6b33eb5c 100644
--- a/app/mailers/base_mailer.rb
+++ b/app/mailers/base_mailer.rb
@@ -5,8 +5,8 @@ class BaseMailer < ActionMailer::Base
attr_accessor :current_user
helper_method :current_user, :can?
- default from: Proc.new { default_sender_address.format }
- default reply_to: Proc.new { default_reply_to_address.format }
+ default from: proc { default_sender_address.format }
+ default reply_to: proc { default_reply_to_address.format }
def can?
Ability.allowed?(current_user, action, subject)
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 671a0fe98cc..2961e16f5e0 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -131,6 +131,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :polling_interval_multiplier,
+ presence: true,
+ numericality: { greater_than_or_equal_to: 0 }
+
validates_each :restricted_visibility_levels do |record, attr, value|
value&.each do |level|
unless Gitlab::VisibilityLevel.options.has_value?(level)
@@ -233,7 +237,8 @@ class ApplicationSetting < ActiveRecord::Base
signup_enabled: Settings.gitlab['signup_enabled'],
terminal_max_session_time: 0,
two_factor_grace_period: 48,
- user_default_external: false
+ user_default_external: false,
+ polling_interval_multiplier: 1
}
end
diff --git a/app/models/blob.rb b/app/models/blob.rb
index 1376b86fdad..95d2111a992 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -46,6 +46,10 @@ class Blob < SimpleDelegator
text? && language && language.name == 'SVG'
end
+ def ipython_notebook?
+ text? && language&.name == 'Jupyter Notebook'
+ end
+
def size_within_svg_limits?
size <= MAXIMUM_SVG_SIZE
end
@@ -63,6 +67,8 @@ class Blob < SimpleDelegator
end
elsif image? || svg?
'image'
+ elsif ipython_notebook?
+ 'notebook'
elsif text?
'text'
else
diff --git a/app/models/board.rb b/app/models/board.rb
index 2780acc67c0..cf8317891b5 100644
--- a/app/models/board.rb
+++ b/app/models/board.rb
@@ -5,7 +5,7 @@ class Board < ActiveRecord::Base
validates :project, presence: true
- def done_list
- lists.merge(List.done).take
+ def closed_list
+ lists.merge(List.closed).take
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index ad0be70c32a..8431c5f228c 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -540,6 +540,8 @@ module Ci
end
def dependencies
+ return [] if empty_dependencies?
+
depended_jobs = depends_on_builds
return depended_jobs unless options[:dependencies].present?
@@ -549,6 +551,10 @@ module Ci
end
end
+ def empty_dependencies?
+ options[:dependencies]&.empty?
+ end
+
private
def update_artifacts_size
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index f12be98c80c..49dec770096 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -164,11 +164,6 @@ module Ci
builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
end
- # For now the only user who participates is the user who triggered
- def participants(_current_user = nil)
- Array(user)
- end
-
def valid_commit_sha
if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)")
@@ -210,7 +205,7 @@ module Ci
end
def stuck?
- builds.pending.any?(&:stuck?)
+ builds.pending.includes(:project).any?(&:stuck?)
end
def retryable?
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 8c71267da65..17b322b5ae3 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -105,6 +105,10 @@ class CommitStatus < ActiveRecord::Base
end
end
+ def locking_enabled?
+ status_changed?
+ end
+
def before_sha
pipeline.before_sha || Gitlab::Git::BLANK_SHA
end
diff --git a/app/models/concerns/importable.rb b/app/models/concerns/importable.rb
index 019ef755849..c9331eaf4cc 100644
--- a/app/models/concerns/importable.rb
+++ b/app/models/concerns/importable.rb
@@ -3,4 +3,7 @@ module Importable
attr_accessor :importing
alias_method :importing?, :importing
+
+ attr_accessor :imported
+ alias_method :imported?, :imported
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 4d54426b79e..b4dded7e27e 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -14,6 +14,7 @@ module Issuable
include Awardable
include Taskable
include TimeTrackable
+ include Importable
# This object is used to gather issuable meta data for displaying
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests
@@ -99,7 +100,7 @@ module Issuable
acts_as_paranoid
after_save :update_assignee_cache_counts, if: :assignee_id_changed?
- after_save :record_metrics
+ after_save :record_metrics, unless: :imported?
def update_assignee_cache_counts
# make sure we flush the cache for both the old *and* new assignees(if they exist)
diff --git a/app/models/concerns/repository_mirroring.rb b/app/models/concerns/repository_mirroring.rb
new file mode 100644
index 00000000000..fed336c29d6
--- /dev/null
+++ b/app/models/concerns/repository_mirroring.rb
@@ -0,0 +1,17 @@
+module RepositoryMirroring
+ def set_remote_as_mirror(name)
+ config = raw_repository.rugged.config
+
+ # This is used to define repository as equivalent as "git clone --mirror"
+ config["remote.#{name}.fetch"] = 'refs/*:refs/*'
+ config["remote.#{name}.mirror"] = true
+ config["remote.#{name}.prune"] = true
+ end
+
+ def fetch_mirror(remote, url)
+ add_remote(remote, url)
+ set_remote_as_mirror(remote)
+ fetch_remote(remote, forced: true)
+ remove_remote(remote)
+ end
+end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 10a5d9d2a24..472796df9df 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -59,10 +59,6 @@ class Issue < ActiveRecord::Base
before_transition any => :closed do |issue|
issue.closed_at = Time.zone.now
end
-
- before_transition closed: any do |issue|
- issue.closed_at = nil
- end
end
def hook_attrs
diff --git a/app/models/list.rb b/app/models/list.rb
index 1e5da7f4dd4..fbd19acd1f5 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -2,7 +2,7 @@ class List < ActiveRecord::Base
belongs_to :board
belongs_to :label
- enum list_type: { label: 1, done: 2 }
+ enum list_type: { label: 1, closed: 2 }
validates :board, :list_type, presence: true
validates :label, :position, presence: true, if: :label?
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 5ff83944d8c..8d740adb771 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -3,7 +3,6 @@ class MergeRequest < ActiveRecord::Base
include Issuable
include Referable
include Sortable
- include Importable
belongs_to :target_project, class_name: "Project"
belongs_to :source_project, class_name: "Project"
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index baee00b8fcd..6ad56b842b2 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -177,6 +177,16 @@ class MergeRequestDiff < ActiveRecord::Base
st_commits.count
end
+ def utf8_st_diffs
+ return [] if st_diffs.blank?
+
+ st_diffs.map do |diff|
+ diff.each do |k, v|
+ diff[k] = encode_utf8(v) if v.respond_to?(:encoding)
+ end
+ end
+ end
+
private
# Old GitLab implementations may have generated diffs as ["--broken-diff"].
@@ -270,14 +280,6 @@ class MergeRequestDiff < ActiveRecord::Base
project.merge_base_commit(head_commit_sha, start_commit_sha).try(:sha)
end
- def utf8_st_diffs
- st_diffs.map do |diff|
- diff.each do |k, v|
- diff[k] = encode_utf8(v) if v.respond_to?(:encoding)
- end
- end
- end
-
#
# #save or #update_attributes providing changes on serialized attributes do a lot of
# serialization and deserialization calls resulting in bad performance.
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index e85d5709624..ac205b9b738 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -30,7 +30,7 @@ class Milestone < ActiveRecord::Base
validates :title, presence: true, uniqueness: { scope: :project_id }
validates :project, presence: true
- validate :start_date_should_be_less_than_due_date, if: Proc.new { |m| m.start_date.present? && m.due_date.present? }
+ validate :start_date_should_be_less_than_due_date, if: proc { |m| m.start_date.present? && m.due_date.present? }
strip_attributes :title
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 826ded22ae5..1d4b1f7d590 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -120,10 +120,10 @@ class Namespace < ActiveRecord::Base
# Move the namespace directory in all storages paths used by member projects
repository_storage_paths.each do |repository_storage_path|
# Ensure old directory exists before moving it
- gitlab_shell.add_namespace(repository_storage_path, path_was)
+ gitlab_shell.add_namespace(repository_storage_path, full_path_was)
- unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
- Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"
+ unless gitlab_shell.mv_namespace(repository_storage_path, full_path_was, full_path)
+ Rails.logger.error "Exception moving path #{repository_storage_path} from #{full_path_was} to #{full_path}"
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
@@ -131,8 +131,8 @@ class Namespace < ActiveRecord::Base
end
end
- Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
- Gitlab::PagesTransfer.new.rename_namespace(path_was, path)
+ Gitlab::UploadsTransfer.new.rename_namespace(full_path_was, full_path)
+ Gitlab::PagesTransfer.new.rename_namespace(full_path_was, full_path)
remove_exports!
@@ -155,7 +155,7 @@ class Namespace < ActiveRecord::Base
def send_update_instructions
projects.each do |project|
- project.send_move_instructions("#{path_was}/#{project.path}")
+ project.send_move_instructions("#{full_path_was}/#{project.path}")
end
end
@@ -230,10 +230,10 @@ class Namespace < ActiveRecord::Base
old_repository_storage_paths.each do |repository_storage_path|
# Move namespace directory into trash.
# We will remove it later async
- new_path = "#{path}+#{id}+deleted"
+ new_path = "#{full_path}+#{id}+deleted"
- if gitlab_shell.mv_namespace(repository_storage_path, path, new_path)
- message = "Namespace directory \"#{path}\" moved to \"#{new_path}\""
+ if gitlab_shell.mv_namespace(repository_storage_path, full_path, new_path)
+ message = "Namespace directory \"#{full_path}\" moved to \"#{new_path}\""
Gitlab::AppLogger.info message
# Remove namespace directroy async with delay so
diff --git a/app/models/note.rb b/app/models/note.rb
index e22e96aec6f..16d66cb1427 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -37,6 +37,7 @@ class Note < ActiveRecord::Base
has_many :todos, dependent: :destroy
has_many :events, as: :target, dependent: :destroy
+ has_one :system_note_metadata
delegate :gfm_reference, :local_reference, to: :noteable
delegate :name, to: :project, prefix: true
@@ -70,7 +71,9 @@ class Note < ActiveRecord::Base
scope :fresh, ->{ order(created_at: :asc, id: :asc) }
scope :inc_author_project, ->{ includes(:project, :author) }
scope :inc_author, ->{ includes(:author) }
- scope :inc_relations_for_view, ->{ includes(:project, :author, :updated_by, :resolved_by, :award_emoji) }
+ scope :inc_relations_for_view, -> do
+ includes(:project, :author, :updated_by, :resolved_by, :award_emoji, :system_note_metadata)
+ end
scope :diff_notes, ->{ where(type: %w(LegacyDiffNote DiffNote)) }
scope :non_diff_notes, ->{ where(type: ['Note', nil]) }
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 52577bd52ea..e4726e62e93 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -60,16 +60,25 @@ class NotificationSetting < ActiveRecord::Base
def set_events
return if custom?
- EMAIL_EVENTS.each do |event|
- events[event] = false
- end
+ self.events = {}
end
# Validates store accessors values as boolean
# It is a text field so it does not cast correct boolean values in JSON
def events_to_boolean
EMAIL_EVENTS.each do |event|
- events[event] = ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(events[event])
+ bool = ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(public_send(event))
+
+ events[event] = bool
end
end
+
+ # Allow people to receive failed pipeline notifications if they already have
+ # custom notifications enabled, as these are more like mentions than the other
+ # custom settings.
+ def failed_pipeline
+ bool = super
+
+ bool.nil? || bool
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index f1bba56d32c..12fd0668ff8 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -114,6 +114,8 @@ class Project < ActiveRecord::Base
has_one :kubernetes_service, dependent: :destroy, inverse_of: :project
has_one :prometheus_service, dependent: :destroy, inverse_of: :project
has_one :mock_ci_service, dependent: :destroy
+ has_one :mock_deployment_service, dependent: :destroy
+ has_one :mock_monitoring_service, dependent: :destroy
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
has_one :forked_from_project, through: :forked_project_link
@@ -551,6 +553,10 @@ class Project < ActiveRecord::Base
import_type == 'gitea'
end
+ def github_import?
+ import_type == 'github'
+ end
+
def check_limit
unless creator.can_create_project? || namespace.kind == 'group'
projects_limit = creator.projects_limit
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index eef403dba92..3b90fd1c2c7 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -62,7 +62,7 @@ class JiraService < IssueTrackerService
def help
"You need to configure JIRA before enabling this service. For more details
read the
- [JIRA service documentation](#{help_page_url('project_services/jira')})."
+ [JIRA service documentation](#{help_page_url('user/project/integrations/jira')})."
end
def title
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index 02fbd5497fa..9c56518c991 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -22,22 +22,21 @@ class KubernetesService < DeploymentService
with_options presence: true, if: :activated? do
validates :api_url, url: true
validates :token
-
- validates :namespace,
- format: {
- with: Gitlab::Regex.kubernetes_namespace_regex,
- message: Gitlab::Regex.kubernetes_namespace_regex_message,
- },
- length: 1..63
end
+ validates :namespace,
+ allow_blank: true,
+ length: 1..63,
+ if: :activated?,
+ format: {
+ with: Gitlab::Regex.kubernetes_namespace_regex,
+ message: Gitlab::Regex.kubernetes_namespace_regex_message
+ }
+
after_save :clear_reactive_cache!
def initialize_properties
- if properties.nil?
- self.properties = {}
- self.namespace = "#{project.path}-#{project.id}" if project.present?
- end
+ self.properties = {} if properties.nil?
end
def title
@@ -62,7 +61,7 @@ class KubernetesService < DeploymentService
{ type: 'text',
name: 'namespace',
title: 'Kubernetes namespace',
- placeholder: 'Kubernetes namespace' },
+ placeholder: namespace_placeholder },
{ type: 'text',
name: 'api_url',
title: 'API URL',
@@ -92,7 +91,7 @@ class KubernetesService < DeploymentService
variables = [
{ key: 'KUBE_URL', value: api_url, public: true },
{ key: 'KUBE_TOKEN', value: token, public: false },
- { key: 'KUBE_NAMESPACE', value: namespace, public: true }
+ { key: 'KUBE_NAMESPACE', value: namespace_variable, public: true }
]
if ca_pem.present?
@@ -135,8 +134,26 @@ class KubernetesService < DeploymentService
{ pods: pods }
end
+ TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze
+
private
+ def namespace_placeholder
+ default_namespace || TEMPLATE_PLACEHOLDER
+ end
+
+ def namespace_variable
+ if namespace.present?
+ namespace
+ else
+ default_namespace
+ end
+ end
+
+ def default_namespace
+ "#{project.path}-#{project.id}" if project.present?
+ end
+
def build_kubeclient!(api_path: 'api', api_version: 'v1')
raise "Incomplete settings" unless api_url && namespace && token
diff --git a/app/models/project_services/mock_deployment_service.rb b/app/models/project_services/mock_deployment_service.rb
new file mode 100644
index 00000000000..59a3811ce5d
--- /dev/null
+++ b/app/models/project_services/mock_deployment_service.rb
@@ -0,0 +1,18 @@
+class MockDeploymentService < DeploymentService
+ def title
+ 'Mock deployment'
+ end
+
+ def description
+ 'Mock deployment service'
+ end
+
+ def self.to_param
+ 'mock_deployment'
+ end
+
+ # No terminals support
+ def terminals(environment)
+ []
+ end
+end
diff --git a/app/models/project_services/mock_monitoring_service.rb b/app/models/project_services/mock_monitoring_service.rb
new file mode 100644
index 00000000000..dd04e04e198
--- /dev/null
+++ b/app/models/project_services/mock_monitoring_service.rb
@@ -0,0 +1,17 @@
+class MockMonitoringService < MonitoringService
+ def title
+ 'Mock monitoring'
+ end
+
+ def description
+ 'Mock monitoring service'
+ end
+
+ def self.to_param
+ 'mock_monitoring'
+ end
+
+ def metrics(environment)
+ JSON.parse(File.read(Rails.root + 'spec/fixtures/metrics.json'))
+ end
+end
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index 5cff9a42484..6854d2243d7 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -31,7 +31,7 @@ class PrometheusService < MonitoringService
def help
<<-MD.strip_heredoc
- Retrieves the Kubernetes node metrics `container_cpu_usage_seconds_total`
+ Retrieves the Kubernetes node metrics `container_cpu_usage_seconds_total`
and `container_memory_usage_bytes` from the configured Prometheus server.
If you are not using [Auto-Deploy](https://docs.gitlab.com/ee/ci/autodeploy/index.html)
@@ -74,8 +74,8 @@ class PrometheusService < MonitoringService
def calculate_reactive_cache(environment_slug)
return unless active? && project && !project.pending_delete?
- memory_query = %{(sum(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"}) / count(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"})) /1024/1024}
- cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}) * 100}
+ memory_query = %{(sum(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / count(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"})) /1024/1024}
+ cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}) * 100}
{
success: true,
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 8a53e974b6f..6d6644053f8 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -169,6 +169,9 @@ class ProjectTeam
# Lookup 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).
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 6ab04440ca8..6b2383b73eb 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -2,6 +2,7 @@ require 'securerandom'
class Repository
include Gitlab::ShellAdapter
+ include RepositoryMirroring
attr_accessor :path_with_namespace, :project
@@ -64,7 +65,7 @@ class Repository
# Return absolute path to repository
def path_to_repo
@path_to_repo ||= File.expand_path(
- File.join(@project.repository_storage_path, path_with_namespace + ".git")
+ File.join(repository_storage_path, path_with_namespace + ".git")
)
end
@@ -401,10 +402,6 @@ class Repository
expire_tags_cache
end
- def before_import
- expire_content_cache
- end
-
# Runs code after the HEAD of a repository is changed.
def after_change_head
expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys)
@@ -981,7 +978,13 @@ class Repository
end
def is_ancestor?(ancestor_id, descendant_id)
- merge_base(ancestor_id, descendant_id) == ancestor_id
+ Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled|
+ if is_enabled
+ raw_repository.is_ancestor?(ancestor_id, descendant_id)
+ else
+ merge_base_commit(ancestor_id, descendant_id) == ancestor_id
+ end
+ end
end
def empty_repo?
@@ -1027,6 +1030,23 @@ class Repository
rugged.references.delete(tmp_ref) if tmp_ref
end
+ def add_remote(name, url)
+ raw_repository.remote_add(name, url)
+ rescue Rugged::ConfigError
+ raw_repository.remote_update(name, url: url)
+ end
+
+ def remove_remote(name)
+ raw_repository.remote_delete(name)
+ true
+ rescue Rugged::ConfigError
+ false
+ end
+
+ def fetch_remote(remote, forced: false, no_tags: false)
+ gitlab_shell.fetch_remote(repository_storage_path, path_with_namespace, remote, forced: forced, no_tags: no_tags)
+ end
+
def fetch_ref(source_path, source_ref, target_ref)
args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
Gitlab::Popen.popen(args, path_to_repo)
@@ -1144,4 +1164,8 @@ class Repository
def repository_event(event, tags = {})
Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags))
end
+
+ def repository_storage_path
+ @project.repository_storage_path
+ end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index e73f7e5d1a3..5a0ec58d193 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -25,7 +25,7 @@ class Service < ActiveRecord::Base
belongs_to :project, inverse_of: :services
has_one :service_hook
- validates :project_id, presence: true, unless: Proc.new { |service| service.template? }
+ validates :project_id, presence: true, unless: proc { |service| service.template? }
scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
scope :issue_trackers, -> { where(category: 'issue_tracker') }
@@ -238,7 +238,9 @@ class Service < ActiveRecord::Base
slack
teamcity
]
- service_names << 'mock_ci' if Rails.env.development?
+ if Rails.env.development?
+ service_names += %w[mock_ci mock_deployment mock_monitoring]
+ end
service_names.sort_by(&:downcase)
end
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
new file mode 100644
index 00000000000..1e6fc837a75
--- /dev/null
+++ b/app/models/system_note_metadata.rb
@@ -0,0 +1,11 @@
+class SystemNoteMetadata < ActiveRecord::Base
+ ICON_TYPES = %w[
+ commit merge confidential visible label assignee cross_reference
+ title time_tracking branch milestone discussion task moved opened closed merged
+ ].freeze
+
+ validates :note, presence: true
+ validates :action, inclusion: ICON_TYPES, allow_nil: true
+
+ belongs_to :note
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 5d19d873f43..95a766f2ede 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -22,6 +22,7 @@ class User < ActiveRecord::Base
default_value_for :hide_no_ssh_key, false
default_value_for :hide_no_password, false
default_value_for :project_view, :files
+ default_value_for :notified_of_own_activity, false
attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base,
@@ -115,7 +116,9 @@ class User < ActiveRecord::Base
validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email }
validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true
validates :bio, length: { maximum: 255 }, allow_blank: true
- validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 }
+ validates :projects_limit,
+ presence: true,
+ numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
validates :username,
namespace: true,
presence: true,
@@ -126,10 +129,9 @@ class User < ActiveRecord::Base
validate :unique_email, if: ->(user) { user.email_changed? }
validate :owns_notification_email, if: ->(user) { user.notification_email_changed? }
validate :owns_public_email, if: ->(user) { user.public_email_changed? }
+ validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
- before_validation :generate_password, on: :create
- before_validation :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id }
before_validation :sanitize_attrs
before_validation :set_notification_email, if: ->(user) { user.email_changed? }
before_validation :set_public_email, if: ->(user) { user.public_email_changed? }
@@ -139,8 +141,6 @@ class User < ActiveRecord::Base
before_save :ensure_external_user_rights
after_save :ensure_namespace_correct
after_initialize :set_projects_limit
- before_create :check_confirmation_email
- after_create :post_create_hook
after_destroy :post_destroy_hook
# User's Layout preference
@@ -384,10 +384,8 @@ class User < ActiveRecord::Base
"#{self.class.reference_prefix}#{username}"
end
- def generate_password
- if force_random_password
- self.password = self.password_confirmation = Devise.friendly_token.first(Devise.password_length.min)
- end
+ def skip_confirmation=(bool)
+ skip_confirmation! if bool
end
def generate_reset_token
@@ -399,10 +397,6 @@ class User < ActiveRecord::Base
@reset_token
end
- def check_confirmation_email
- skip_confirmation! unless current_application_settings.send_user_confirmation_email
- end
-
def recently_sent_password_reset?
reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
end
@@ -641,8 +635,10 @@ class User < ActiveRecord::Base
end
def fork_of(project)
- links = ForkedProjectLink.where(forked_from_project_id: project, forked_to_project_id: personal_projects)
-
+ links = ForkedProjectLink.where(
+ forked_from_project_id: project,
+ forked_to_project_id: personal_projects.unscope(:order)
+ )
if links.any?
links.first.forked_to_project
else
@@ -797,12 +793,6 @@ class User < ActiveRecord::Base
end
end
- def post_create_hook
- log_info("User \"#{name}\" (#{email}) was created")
- notification_service.new_user(self, @reset_token) if created_by_id
- system_hook_service.execute_hooks_for(self, :create)
- end
-
def post_destroy_hook
log_info("User \"#{name}\" (#{email}) was removed")
system_hook_service.execute_hooks_for(self, :destroy)
diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb
index 5bcbe285052..fadd6c5c597 100644
--- a/app/serializers/build_entity.rb
+++ b/app/serializers/build_entity.rb
@@ -18,10 +18,17 @@ class BuildEntity < Grape::Entity
expose :created_at
expose :updated_at
+ expose :detailed_status, as: :status, with: StatusEntity
private
+ alias_method :build, :object
+
def path_to(route, build)
send("#{route}_path", build.project.namespace, build.project, build)
end
+
+ def detailed_status
+ build.detailed_status(request.user)
+ end
end
diff --git a/app/serializers/build_serializer.rb b/app/serializers/build_serializer.rb
new file mode 100644
index 00000000000..79b67001199
--- /dev/null
+++ b/app/serializers/build_serializer.rb
@@ -0,0 +1,8 @@
+class BuildSerializer < BaseSerializer
+ entity BuildEntity
+
+ def represent_status(resource)
+ data = represent(resource, { only: [:status] })
+ data.fetch(:status, {})
+ end
+end
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index 4c017960628..4ff15a78115 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -9,6 +9,13 @@ class EnvironmentEntity < Grape::Entity
expose :last_deployment, using: DeploymentEntity
expose :stop_action?
+ expose :metrics_path, if: -> (environment, _) { environment.has_metrics? } do |environment|
+ metrics_namespace_project_environment_path(
+ environment.project.namespace,
+ environment.project,
+ environment)
+ end
+
expose :environment_path do |environment|
namespace_project_environment_path(
environment.project.namespace,
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index 61f0f11d7d2..3f16dd66d54 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -12,12 +12,7 @@ class PipelineEntity < Grape::Entity
end
expose :details do
- expose :status do |pipeline, options|
- StatusEntity.represent(
- pipeline.detailed_status(request.user),
- options)
- end
-
+ expose :detailed_status, as: :status, with: StatusEntity
expose :duration
expose :finished_at
expose :stages, using: StageEntity
@@ -82,4 +77,8 @@ class PipelineEntity < Grape::Entity
pipeline.cancelable? &&
can?(request.user, :update_pipeline, pipeline)
end
+
+ def detailed_status
+ pipeline.detailed_status(request.user)
+ end
end
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index ab2d3d5a3ec..7829df9fada 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -22,4 +22,11 @@ class PipelineSerializer < BaseSerializer
super(resource, opts)
end
end
+
+ def represent_status(resource)
+ return {} unless resource.present?
+
+ data = represent(resource, { only: [{ details: [:status] }] })
+ data.dig(:details, :status) || {}
+ end
end
diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb
index 47066bebfb1..dfd9d1584a1 100644
--- a/app/serializers/status_entity.rb
+++ b/app/serializers/status_entity.rb
@@ -1,7 +1,7 @@
class StatusEntity < Grape::Entity
include RequestAwareEntity
- expose :icon, :text, :label, :group
+ expose :icon, :favicon, :text, :label, :group
expose :has_details?, as: :has_details
expose :details_path
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index 745c2c4b681..a0cb00dba58 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -24,6 +24,10 @@ class BaseService
Gitlab::AppLogger.info message
end
+ def log_error(message)
+ Gitlab::AppLogger.error message
+ end
+
def system_hook_service
SystemHooksService.new
end
diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb
index f6275a63109..fd9ff115eab 100644
--- a/app/services/boards/create_service.rb
+++ b/app/services/boards/create_service.rb
@@ -12,7 +12,7 @@ module Boards
def create_board!
board = project.boards.create
- board.lists.create(list_type: :done)
+ board.lists.create(list_type: :closed)
board
end
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index cb6d30396ec..533e6787855 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -41,7 +41,7 @@ module Boards
end
def set_state
- params[:state] = list && list.done? ? 'closed' : 'opened'
+ params[:state] = list && list.closed? ? 'closed' : 'opened'
end
def board_label_ids
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 2a9981ab884..d5735f13c1e 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -48,8 +48,8 @@ module Boards
end
def issue_state
- return 'reopen' if moving_from_list.done?
- return 'close' if moving_to_list.done?
+ return 'reopen' if moving_from_list.closed?
+ return 'close' if moving_to_list.closed?
end
def add_label_ids
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 2935d00c075..33edcd60944 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -5,8 +5,6 @@ module Ci
def execute(pipeline)
@pipeline = pipeline
- ensure_created_builds! # TODO, remove me in 9.0
-
new_builds =
stage_indexes_of_created_builds.map do |index|
process_stage(index)
@@ -73,18 +71,5 @@ module Ci
def created_builds
pipeline.builds.created
end
-
- # This method is DEPRECATED and should be removed in 9.0.
- #
- # We need it to maintain backwards compatibility with previous versions
- # when builds were not created within one transaction with the pipeline.
- #
- def ensure_created_builds!
- return if created_builds.any?
-
- Ci::CreatePipelineBuildsService
- .new(project, current_user)
- .execute(pipeline)
- end
end
end
diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb
index 574561adc4c..f72ddbf690c 100644
--- a/app/services/ci/retry_pipeline_service.rb
+++ b/app/services/ci/retry_pipeline_service.rb
@@ -7,14 +7,14 @@ module Ci
raise Gitlab::Access::AccessDeniedError
end
- pipeline.builds.failed_or_canceled.find_each do |build|
+ pipeline.builds.latest.failed_or_canceled.find_each do |build|
next unless build.retryable?
Ci::RetryBuildService.new(project, current_user)
.reprocess(build)
end
- pipeline.builds.skipped.find_each do |skipped|
+ pipeline.builds.latest.skipped.find_each do |skipped|
retry_optimistic_lock(skipped) { |build| build.process }
end
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index 4e878ec556a..1d65c76d282 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -1,6 +1,8 @@
module Groups
class UpdateService < Groups::BaseService
def execute
+ reject_parent_id!
+
# check that user is allowed to set specified visibility_level
new_visibility = params[:visibility_level]
if new_visibility && new_visibility.to_i != group.visibility_level
@@ -22,5 +24,11 @@ module Groups
false
end
end
+
+ private
+
+ def reject_parent_id!
+ params.except!(:parent_id)
+ end
end
end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index a444c78b609..b7fe5cb168b 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -19,7 +19,7 @@ module Issues
if issue.previous_changes.include?('title') ||
issue.previous_changes.include?('description')
- todo_service.update_issue(issue, current_user)
+ todo_service.update_issue(issue, current_user, old_mentioned_users)
end
if issue.previous_changes.include?('milestone_id')
diff --git a/app/services/labels/base_service.rb b/app/services/labels/base_service.rb
new file mode 100644
index 00000000000..91d72a57b4e
--- /dev/null
+++ b/app/services/labels/base_service.rb
@@ -0,0 +1,161 @@
+module Labels
+ class BaseService < ::BaseService
+ COLOR_NAME_TO_HEX = {
+ black: '#000000',
+ silver: '#C0C0C0',
+ gray: '#808080',
+ white: '#FFFFFF',
+ maroon: '#800000',
+ red: '#FF0000',
+ purple: '#800080',
+ fuchsia: '#FF00FF',
+ green: '#008000',
+ lime: '#00FF00',
+ olive: '#808000',
+ yellow: '#FFFF00',
+ navy: '#000080',
+ blue: '#0000FF',
+ teal: '#008080',
+ aqua: '#00FFFF',
+ orange: '#FFA500',
+ aliceblue: '#F0F8FF',
+ antiquewhite: '#FAEBD7',
+ aquamarine: '#7FFFD4',
+ azure: '#F0FFFF',
+ beige: '#F5F5DC',
+ bisque: '#FFE4C4',
+ blanchedalmond: '#FFEBCD',
+ blueviolet: '#8A2BE2',
+ brown: '#A52A2A',
+ burlywood: '#DEB887',
+ cadetblue: '#5F9EA0',
+ chartreuse: '#7FFF00',
+ chocolate: '#D2691E',
+ coral: '#FF7F50',
+ cornflowerblue: '#6495ED',
+ cornsilk: '#FFF8DC',
+ crimson: '#DC143C',
+ darkblue: '#00008B',
+ darkcyan: '#008B8B',
+ darkgoldenrod: '#B8860B',
+ darkgray: '#A9A9A9',
+ darkgreen: '#006400',
+ darkgrey: '#A9A9A9',
+ darkkhaki: '#BDB76B',
+ darkmagenta: '#8B008B',
+ darkolivegreen: '#556B2F',
+ darkorange: '#FF8C00',
+ darkorchid: '#9932CC',
+ darkred: '#8B0000',
+ darksalmon: '#E9967A',
+ darkseagreen: '#8FBC8F',
+ darkslateblue: '#483D8B',
+ darkslategray: '#2F4F4F',
+ darkslategrey: '#2F4F4F',
+ darkturquoise: '#00CED1',
+ darkviolet: '#9400D3',
+ deeppink: '#FF1493',
+ deepskyblue: '#00BFFF',
+ dimgray: '#696969',
+ dimgrey: '#696969',
+ dodgerblue: '#1E90FF',
+ firebrick: '#B22222',
+ floralwhite: '#FFFAF0',
+ forestgreen: '#228B22',
+ gainsboro: '#DCDCDC',
+ ghostwhite: '#F8F8FF',
+ gold: '#FFD700',
+ goldenrod: '#DAA520',
+ greenyellow: '#ADFF2F',
+ grey: '#808080',
+ honeydew: '#F0FFF0',
+ hotpink: '#FF69B4',
+ indianred: '#CD5C5C',
+ indigo: '#4B0082',
+ ivory: '#FFFFF0',
+ khaki: '#F0E68C',
+ lavender: '#E6E6FA',
+ lavenderblush: '#FFF0F5',
+ lawngreen: '#7CFC00',
+ lemonchiffon: '#FFFACD',
+ lightblue: '#ADD8E6',
+ lightcoral: '#F08080',
+ lightcyan: '#E0FFFF',
+ lightgoldenrodyellow: '#FAFAD2',
+ lightgray: '#D3D3D3',
+ lightgreen: '#90EE90',
+ lightgrey: '#D3D3D3',
+ lightpink: '#FFB6C1',
+ lightsalmon: '#FFA07A',
+ lightseagreen: '#20B2AA',
+ lightskyblue: '#87CEFA',
+ lightslategray: '#778899',
+ lightslategrey: '#778899',
+ lightsteelblue: '#B0C4DE',
+ lightyellow: '#FFFFE0',
+ limegreen: '#32CD32',
+ linen: '#FAF0E6',
+ mediumaquamarine: '#66CDAA',
+ mediumblue: '#0000CD',
+ mediumorchid: '#BA55D3',
+ mediumpurple: '#9370DB',
+ mediumseagreen: '#3CB371',
+ mediumslateblue: '#7B68EE',
+ mediumspringgreen: '#00FA9A',
+ mediumturquoise: '#48D1CC',
+ mediumvioletred: '#C71585',
+ midnightblue: '#191970',
+ mintcream: '#F5FFFA',
+ mistyrose: '#FFE4E1',
+ moccasin: '#FFE4B5',
+ navajowhite: '#FFDEAD',
+ oldlace: '#FDF5E6',
+ olivedrab: '#6B8E23',
+ orangered: '#FF4500',
+ orchid: '#DA70D6',
+ palegoldenrod: '#EEE8AA',
+ palegreen: '#98FB98',
+ paleturquoise: '#AFEEEE',
+ palevioletred: '#DB7093',
+ papayawhip: '#FFEFD5',
+ peachpuff: '#FFDAB9',
+ peru: '#CD853F',
+ pink: '#FFC0CB',
+ plum: '#DDA0DD',
+ powderblue: '#B0E0E6',
+ rosybrown: '#BC8F8F',
+ royalblue: '#4169E1',
+ saddlebrown: '#8B4513',
+ salmon: '#FA8072',
+ sandybrown: '#F4A460',
+ seagreen: '#2E8B57',
+ seashell: '#FFF5EE',
+ sienna: '#A0522D',
+ skyblue: '#87CEEB',
+ slateblue: '#6A5ACD',
+ slategray: '#708090',
+ slategrey: '#708090',
+ snow: '#FFFAFA',
+ springgreen: '#00FF7F',
+ steelblue: '#4682B4',
+ tan: '#D2B48C',
+ thistle: '#D8BFD8',
+ tomato: '#FF6347',
+ turquoise: '#40E0D0',
+ violet: '#EE82EE',
+ wheat: '#F5DEB3',
+ whitesmoke: '#F5F5F5',
+ yellowgreen: '#9ACD32',
+ rebeccapurple: '#663399'
+ }.freeze
+
+ def convert_color_name_to_hex
+ color = params[:color]
+ color_name = color.strip.downcase
+
+ return color if color_name.start_with?('#')
+
+ COLOR_NAME_TO_HEX[color_name.to_sym] || color
+ end
+ end
+end
diff --git a/app/services/labels/create_service.rb b/app/services/labels/create_service.rb
new file mode 100644
index 00000000000..6c399c92377
--- /dev/null
+++ b/app/services/labels/create_service.rb
@@ -0,0 +1,25 @@
+module Labels
+ class CreateService < Labels::BaseService
+ def initialize(params = {})
+ @params = params.dup.with_indifferent_access
+ end
+
+ # returns the created label
+ def execute(target_params)
+ params[:color] = convert_color_name_to_hex if params[:color].present?
+
+ project_or_group = target_params[:project] || target_params[:group]
+
+ if project_or_group.present?
+ project_or_group.labels.create(params)
+ elsif target_params[:template]
+ label = Label.new(params)
+ label.template = true
+ label.save
+ label
+ else
+ Rails.logger.warn("target_params should contain :project or :group or :template, actual value: #{target_params}")
+ end
+ end
+ end
+end
diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb
index cf4f7606c94..940c8b333d3 100644
--- a/app/services/labels/find_or_create_service.rb
+++ b/app/services/labels/find_or_create_service.rb
@@ -3,7 +3,7 @@ module Labels
def initialize(current_user, project, params = {})
@current_user = current_user
@project = project
- @params = params.dup
+ @params = params.dup.with_indifferent_access
end
def execute(skip_authorization: false)
@@ -28,7 +28,7 @@ module Labels
new_label = available_labels.find_by(title: title)
if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project))
- new_label = project.labels.create(params)
+ new_label = Labels::CreateService.new(params).execute(project: project)
end
new_label
diff --git a/app/services/labels/update_service.rb b/app/services/labels/update_service.rb
new file mode 100644
index 00000000000..28dcabf9541
--- /dev/null
+++ b/app/services/labels/update_service.rb
@@ -0,0 +1,15 @@
+module Labels
+ class UpdateService < Labels::BaseService
+ def initialize(params = {})
+ @params = params.dup.with_indifferent_access
+ end
+
+ # returns the updated label
+ def execute(label)
+ params[:color] = convert_color_name_to_hex if params[:color].present?
+
+ label.update(params)
+ label
+ end
+ end
+end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 3cb9aae83f6..ab7fcf3b6e2 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -28,7 +28,7 @@ module MergeRequests
if merge_request.previous_changes.include?('title') ||
merge_request.previous_changes.include?('description')
- todo_service.update_merge_request(merge_request, current_user)
+ todo_service.update_merge_request(merge_request, current_user, old_mentioned_users)
end
if merge_request.previous_changes.include?('target_branch')
diff --git a/app/services/note_summary.rb b/app/services/note_summary.rb
new file mode 100644
index 00000000000..a6f6320d573
--- /dev/null
+++ b/app/services/note_summary.rb
@@ -0,0 +1,20 @@
+class NoteSummary
+ attr_reader :note
+ attr_reader :metadata
+
+ def initialize(noteable, project, author, body, action: nil, commit_count: nil)
+ @note = { noteable: noteable, project: project, author: author, note: body }
+ @metadata = { action: action, commit_count: commit_count }.compact
+
+ set_commit_params if note[:noteable].is_a?(Commit)
+ end
+
+ def metadata?
+ metadata.present?
+ end
+
+ def set_commit_params
+ note.merge!(noteable_type: 'Commit', commit_id: note[:noteable].id)
+ note[:noteable] = nil
+ end
+end
diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb
index 75a4b3ed826..75fd08ea0a9 100644
--- a/app/services/notes/update_service.rb
+++ b/app/services/notes/update_service.rb
@@ -3,11 +3,13 @@ module Notes
def execute(note)
return note unless note.editable?
+ old_mentioned_users = note.mentioned_users.to_a
+
note.update_attributes(params.merge(updated_by: current_user))
note.create_new_cross_references!(current_user)
if note.previous_changes.include?('note')
- TodoService.new.update_note(note, current_user)
+ TodoService.new.update_note(note, current_user, old_mentioned_users)
end
note
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index 44ae23fad18..8bb995158de 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -3,7 +3,7 @@
#
class NotificationRecipientService
attr_reader :project
-
+
def initialize(project)
@project = project
end
@@ -12,11 +12,7 @@ class NotificationRecipientService
custom_action = build_custom_key(action, target)
recipients = target.participants(current_user)
-
- unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
- recipients = add_project_watchers(recipients)
- end
-
+ recipients = add_project_watchers(recipients)
recipients = add_custom_notifications(recipients, custom_action)
recipients = reject_mention_users(recipients)
@@ -38,16 +34,38 @@ class NotificationRecipientService
recipients = reject_unsubscribed_users(recipients, target)
recipients = reject_users_without_access(recipients, target)
- recipients.delete(current_user) if skip_current_user
+ recipients.delete(current_user) if skip_current_user && !current_user.notified_of_own_activity?
recipients.uniq
end
+ def build_pipeline_recipients(target, current_user, action:)
+ return [] unless current_user
+
+ custom_action =
+ case action.to_s
+ when 'failed'
+ :failed_pipeline
+ when 'success'
+ :success_pipeline
+ end
+
+ notification_setting = notification_setting_for_user_project(current_user, target.project)
+
+ return [] if notification_setting.mention? || notification_setting.disabled?
+
+ return [] if notification_setting.custom? && !notification_setting.public_send(custom_action)
+
+ return [] if (notification_setting.watch? || notification_setting.participating?) && NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
+
+ reject_users_without_access([current_user], target)
+ end
+
def build_relabeled_recipients(target, current_user, labels:)
recipients = add_labels_subscribers([], target, labels: labels)
recipients = reject_unsubscribed_users(recipients, target)
recipients = reject_users_without_access(recipients, target)
- recipients.delete(current_user)
+ recipients.delete(current_user) unless current_user.notified_of_own_activity?
recipients.uniq
end
@@ -88,7 +106,7 @@ class NotificationRecipientService
recipients = reject_unsubscribed_users(recipients, note.noteable)
recipients = reject_users_without_access(recipients, note.noteable)
- recipients.delete(note.author)
+ recipients.delete(note.author) unless note.author.notified_of_own_activity?
recipients.uniq
end
@@ -290,4 +308,16 @@ class NotificationRecipientService
def build_custom_key(action, object)
"#{action}_#{object.class.model_name.name.underscore}".to_sym
end
+
+ def notification_setting_for_user_project(user, project)
+ project_setting = user.notification_settings_for(project)
+
+ return project_setting unless project_setting.global?
+
+ group_setting = user.notification_settings_for(project.group)
+
+ return group_setting unless group_setting.global?
+
+ user.global_notification_setting
+ end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index f9aa2346759..6b186263bd1 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -278,10 +278,11 @@ class NotificationService
return unless mailer.respond_to?(email_template)
- recipients ||= NotificationRecipientService.new(pipeline.project).build_recipients(
+ recipients ||= NotificationRecipientService.new(pipeline.project).build_pipeline_recipients(
pipeline,
- nil, # The acting user, who won't be added to recipients
- action: pipeline.status).map(&:notification_email)
+ pipeline.user,
+ action: pipeline.status,
+ ).map(&:notification_email)
if recipients.any?
mailer.public_send(email_template, pipeline, recipients).deliver_later
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index d484a96f785..4c72d5e117d 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -11,7 +11,7 @@ module Projects
success
rescue => e
- error(e.message)
+ error("Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}")
end
private
@@ -32,23 +32,40 @@ module Projects
end
def import_repository
+ raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url)
+
begin
- raise Error, "Blocked import URL." if Gitlab::UrlBlocker.blocked_url?(project.import_url)
- gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url)
- rescue => e
+ if project.github_import? || project.gitea_import?
+ fetch_repository
+ else
+ clone_repository
+ end
+ rescue Gitlab::Shell::Error => e
# Expire cache to prevent scenarios such as:
# 1. First import failed, but the repo was imported successfully, so +exists?+ returns true
# 2. Retried import, repo is broken or not imported but +exists?+ still returns true
- project.repository.before_import if project.repository_exists?
+ project.repository.expire_content_cache if project.repository_exists?
- raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}"
+ raise Error, e.message
end
end
+ def clone_repository
+ gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url)
+ end
+
+ def fetch_repository
+ project.create_repository
+ project.repository.add_remote(project.import_type, project.import_url)
+ project.repository.set_remote_as_mirror(project.import_type)
+ project.repository.fetch_remote(project.import_type, forced: true)
+ project.repository.remove_remote(project.import_type)
+ end
+
def import_data
return unless has_importer?
- project.repository.before_import unless project.gitlab_project_import?
+ project.repository.expire_content_cache unless project.gitlab_project_import?
unless importer.execute
raise Error, 'The remote data could not be imported.'
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 523b9f41916..17cf71cf098 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -46,6 +46,7 @@ module Projects
end
def error(message, http_status = nil)
+ log_error("Projects::UpdatePagesService: #{message}")
@status.allow_failure = !latest?
@status.description = message
@status.drop
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index 781cd13b44b..c1549df5ac6 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -16,5 +16,13 @@ module Search
Gitlab::SearchResults.new(current_user, projects, params[:search])
end
+
+ def scope
+ @scope ||= begin
+ allowed_scopes = %w[issues merge_requests milestones]
+
+ allowed_scopes.delete(params[:scope]) { 'projects' }
+ end
+ end
end
end
diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb
index 4b500914cfb..9a22abae635 100644
--- a/app/services/search/project_service.rb
+++ b/app/services/search/project_service.rb
@@ -12,5 +12,9 @@ module Search
params[:search],
params[:repository_ref])
end
+
+ def scope
+ @scope ||= %w[notes issues merge_requests milestones wiki_blobs commits].delete(params[:scope]) { 'blobs' }
+ end
end
end
diff --git a/app/services/search/snippet_service.rb b/app/services/search/snippet_service.rb
index 0b3e713e220..4f161beea4d 100644
--- a/app/services/search/snippet_service.rb
+++ b/app/services/search/snippet_service.rb
@@ -11,5 +11,9 @@ module Search
Gitlab::SnippetSearchResults.new(snippets, params[:search])
end
+
+ def scope
+ @scope ||= %w[snippet_titles].delete(params[:scope]) { 'snippet_blobs' }
+ end
end
end
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
new file mode 100644
index 00000000000..8d46a8dab3e
--- /dev/null
+++ b/app/services/search_service.rb
@@ -0,0 +1,63 @@
+class SearchService
+ include Gitlab::Allowable
+
+ def initialize(current_user, params = {})
+ @current_user = current_user
+ @params = params.dup
+ end
+
+ def project
+ return @project if defined?(@project)
+
+ @project =
+ if params[:project_id].present?
+ the_project = Project.find_by(id: params[:project_id])
+ can?(current_user, :download_code, the_project) ? the_project : nil
+ else
+ nil
+ end
+ end
+
+ def group
+ return @group if defined?(@group)
+
+ @group =
+ if params[:group_id].present?
+ the_group = Group.find_by(id: params[:group_id])
+ can?(current_user, :read_group, the_group) ? the_group : nil
+ else
+ nil
+ end
+ end
+
+ def show_snippets?
+ return @show_snippets if defined?(@show_snippets)
+
+ @show_snippets = params[:snippets] == 'true'
+ end
+
+ delegate :scope, to: :search_service
+
+ def search_results
+ @search_results ||= search_service.execute
+ end
+
+ def search_objects
+ @search_objects ||= search_results.objects(scope, params[:page])
+ end
+
+ private
+
+ def search_service
+ @search_service ||=
+ if project
+ Search::ProjectService.new(project, current_user, params)
+ elsif show_snippets?
+ Search::SnippetService.new(current_user, params)
+ else
+ Search::GlobalService.new(current_user, params)
+ end
+ end
+
+ attr_reader :current_user, :params
+end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 8e02fe3741a..35cfcc3682e 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -26,7 +26,7 @@ module SystemNoteService
body << new_commit_summary(new_commits).join("\n")
body << "\n\n[Compare with previous version](#{diff_comparison_url(noteable, project, oldrev)})"
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'commit', commit_count: total_count))
end
# Called when the assignee of a Noteable is changed or removed
@@ -46,7 +46,7 @@ module SystemNoteService
def change_assignee(noteable, project, author, assignee)
body = assignee.nil? ? 'removed assignee' : "assigned to #{assignee.to_reference}"
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'assignee'))
end
# Called when one or more labels on a Noteable are added and/or removed
@@ -86,7 +86,7 @@ module SystemNoteService
body << ' ' << 'label'.pluralize(labels_count)
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'label'))
end
# Called when the milestone of a Noteable is changed
@@ -106,7 +106,7 @@ module SystemNoteService
def change_milestone(noteable, project, author, milestone)
body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project)}"
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'milestone'))
end
# Called when the estimated time of a Noteable is changed
@@ -132,7 +132,7 @@ module SystemNoteService
"changed time estimate to #{parsed_time}"
end
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
end
# Called when the spent time of a Noteable is changed
@@ -161,7 +161,7 @@ module SystemNoteService
body = "#{action} #{parsed_time} of time spent"
end
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
end
# Called when the status of a Noteable is changed
@@ -183,53 +183,59 @@ module SystemNoteService
body = status.dup
body << " via #{source.gfm_reference(project)}" if source
- create_note(noteable: noteable, project: project, author: author, note: body)
+ action = status == 'reopened' ? 'opened' : status
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: action))
end
# Called when 'merge when pipeline succeeds' is executed
def merge_when_pipeline_succeeds(noteable, project, author, last_commit)
body = "enabled an automatic merge when the pipeline for #{last_commit.to_reference(project)} succeeds"
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
end
# Called when 'merge when pipeline succeeds' is canceled
def cancel_merge_when_pipeline_succeeds(noteable, project, author)
body = 'canceled the automatic merge'
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
end
def remove_merge_request_wip(noteable, project, author)
body = 'unmarked as a **Work In Progress**'
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
end
def add_merge_request_wip(noteable, project, author)
body = 'marked as a **Work In Progress**'
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
end
def add_merge_request_wip_from_commit(noteable, project, author, commit)
body = "marked as a **Work In Progress** from #{commit.to_reference(project)}"
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
end
def self.resolve_all_discussions(merge_request, project, author)
body = "resolved all discussions"
- create_note(noteable: merge_request, project: project, author: author, note: body)
+ create_note(NoteSummary.new(merge_request, project, author, body, action: 'discussion'))
end
def discussion_continued_in_issue(discussion, project, author, issue)
body = "created #{issue.to_reference} to continue this discussion"
- note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body)
- note_attributes[:type] = note_attributes.delete(:note_type)
- create_note(note_attributes)
+ note_params = discussion.reply_attributes.merge(project: project, author: author, note: body)
+ note_params[:type] = note_params.delete(:note_type)
+
+ note = Note.create(note_params.merge(system: true))
+ note.system_note_metadata = SystemNoteMetadata.new({ action: 'discussion' })
+
+ note
end
# Called when the title of a Noteable is changed
@@ -253,7 +259,8 @@ module SystemNoteService
marked_new_title = Gitlab::Diff::InlineDiffMarker.new(new_title).mark(new_diffs, mode: :addition, markdown: true)
body = "changed title from **#{marked_old_title}** to **#{marked_new_title}**"
- create_note(noteable: noteable, project: project, author: author, note: body)
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
end
# Called when the confidentiality changes
@@ -268,8 +275,15 @@ module SystemNoteService
#
# Returns the created Note object
def change_issue_confidentiality(issue, project, author)
- body = issue.confidential ? 'made the issue confidential' : 'made the issue visible to everyone'
- create_note(noteable: issue, project: project, author: author, note: body)
+ if issue.confidential
+ body = 'made the issue confidential'
+ action = 'confidential'
+ else
+ body = 'made the issue visible to everyone'
+ action = 'visible'
+ end
+
+ create_note(NoteSummary.new(issue, project, author, body, action: action))
end
# Called when a branch in Noteable is changed
@@ -288,7 +302,8 @@ module SystemNoteService
# Returns the created Note object
def change_branch(noteable, project, author, branch_type, old_branch, new_branch)
body = "changed #{branch_type} branch from `#{old_branch}` to `#{new_branch}`"
- create_note(noteable: noteable, project: project, author: author, note: body)
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'branch'))
end
# Called when a branch in Noteable is added or deleted
@@ -314,7 +329,8 @@ module SystemNoteService
end
body = "#{verb} #{branch_type} branch `#{branch}`"
- create_note(noteable: noteable, project: project, author: author, note: body)
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'branch'))
end
# Called when a branch is created from the 'new branch' button on a issue
@@ -325,7 +341,8 @@ module SystemNoteService
link = url_helpers.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
body = "created branch [`#{branch}`](#{link})"
- create_note(noteable: issue, project: project, author: author, note: body)
+
+ create_note(NoteSummary.new(issue, project, author, body, action: 'branch'))
end
# Called when a Mentionable references a Noteable
@@ -349,23 +366,12 @@ module SystemNoteService
return if cross_reference_disallowed?(noteable, mentioner)
gfm_reference = mentioner.gfm_reference(noteable.project)
-
- note_options = {
- project: noteable.project,
- author: author,
- note: cross_reference_note_content(gfm_reference)
- }
-
- if noteable.is_a?(Commit)
- note_options.merge!(noteable_type: 'Commit', commit_id: noteable.id)
- else
- note_options[:noteable] = noteable
- end
+ body = cross_reference_note_content(gfm_reference)
if noteable.is_a?(ExternalIssue)
noteable.project.issues_tracker.create_cross_reference_note(noteable, mentioner, author)
else
- create_note(note_options)
+ create_note(NoteSummary.new(noteable, noteable.project, author, body, action: 'cross_reference'))
end
end
@@ -444,7 +450,8 @@ module SystemNoteService
def change_task_status(noteable, project, author, new_task)
status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE
body = "marked the task **#{new_task.source}** as #{status_label}"
- create_note(noteable: noteable, project: project, author: author, note: body)
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'task'))
end
# Called when noteable has been moved to another project
@@ -466,7 +473,8 @@ module SystemNoteService
cross_reference = noteable_ref.to_reference(project)
body = "moved #{direction} #{cross_reference}"
- create_note(noteable: noteable, project: project, author: author, note: body)
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'moved'))
end
private
@@ -482,8 +490,11 @@ module SystemNoteService
end
end
- def create_note(args = {})
- Note.create(args.merge(system: true))
+ def create_note(note_summary)
+ note = Note.create(note_summary.note.merge(system: true))
+ note.system_note_metadata = SystemNoteMetadata.new(note_summary.metadata) if note_summary.metadata?
+
+ note
end
def cross_reference_note_prefix
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index bf7e76ec59e..b6e88b0280f 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -19,8 +19,8 @@ class TodoService
#
# * mark all pending todos related to the issue for the current user as done
#
- def update_issue(issue, current_user)
- update_issuable(issue, current_user)
+ def update_issue(issue, current_user, skip_users = [])
+ update_issuable(issue, current_user, skip_users)
end
# When close an issue we should:
@@ -60,8 +60,8 @@ class TodoService
#
# * create a todo for each mentioned user on merge request
#
- def update_merge_request(merge_request, current_user)
- update_issuable(merge_request, current_user)
+ def update_merge_request(merge_request, current_user, skip_users = [])
+ update_issuable(merge_request, current_user, skip_users)
end
# When close a merge request we should:
@@ -123,7 +123,7 @@ class TodoService
mark_pending_todos_as_done(merge_request, merge_request.author)
mark_pending_todos_as_done(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds?
end
-
+
# When a merge request could not be automatically merged due to its unmergeable state we should:
#
# * create a todo for a merge_user
@@ -131,7 +131,7 @@ class TodoService
def merge_request_became_unmergeable(merge_request)
create_unmergeable_todo(merge_request, merge_request.merge_user) if merge_request.merge_when_pipeline_succeeds?
end
-
+
# When create a note we should:
#
# * mark all pending todos related to the noteable for the note author as done
@@ -146,8 +146,8 @@ class TodoService
# * mark all pending todos related to the noteable for the current user as done
# * create a todo for each new user mentioned on note
#
- def update_note(note, current_user)
- handle_note(note, current_user)
+ def update_note(note, current_user, skip_users = [])
+ handle_note(note, current_user, skip_users)
end
# When an emoji is awarded we should:
@@ -204,7 +204,7 @@ class TodoService
# Only update those that are not really on that state
todos = todos.where.not(state: state)
todos_ids = todos.pluck(:id)
- todos.update_all(state: state)
+ todos.unscope(:order).update_all(state: state)
current_user.update_todos_count_cache
todos_ids
end
@@ -223,11 +223,11 @@ class TodoService
create_mention_todos(issuable.project, issuable, author)
end
- def update_issuable(issuable, author)
+ def update_issuable(issuable, author, skip_users = [])
# Skip toggling a task list item in a description
return if toggling_tasks?(issuable)
- create_mention_todos(issuable.project, issuable, author)
+ create_mention_todos(issuable.project, issuable, author, nil, skip_users)
end
def destroy_issuable(issuable, user)
@@ -239,7 +239,7 @@ class TodoService
issuable.tasks? && issuable.updated_tasks.any?
end
- def handle_note(note, author)
+ def handle_note(note, author, skip_users = [])
# Skip system notes, and notes on project snippet
return if note.system? || note.for_snippet?
@@ -247,7 +247,7 @@ class TodoService
target = note.noteable
mark_pending_todos_as_done(target, author)
- create_mention_todos(project, target, author, note)
+ create_mention_todos(project, target, author, note, skip_users)
end
def create_assignment_todo(issuable, author)
@@ -257,14 +257,14 @@ class TodoService
end
end
- def create_mention_todos(project, target, author, note = nil)
+ def create_mention_todos(project, target, author, note = nil, skip_users = [])
# Create Todos for directly addressed users
- directly_addressed_users = filter_directly_addressed_users(project, note || target, author)
+ directly_addressed_users = filter_directly_addressed_users(project, note || target, author, skip_users)
attributes = attributes_for_todo(project, target, author, Todo::DIRECTLY_ADDRESSED, note)
create_todos(directly_addressed_users, attributes)
# Create Todos for mentioned users
- mentioned_users = filter_mentioned_users(project, note || target, author)
+ mentioned_users = filter_mentioned_users(project, note || target, author, skip_users)
attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note)
create_todos(mentioned_users, attributes)
end
@@ -307,13 +307,13 @@ class TodoService
reject_users_without_access(users, project, target).uniq
end
- def filter_mentioned_users(project, target, author)
- mentioned_users = target.mentioned_users(author)
+ def filter_mentioned_users(project, target, author, skip_users = [])
+ mentioned_users = target.mentioned_users(author) - skip_users
filter_todo_users(mentioned_users, project, target)
end
- def filter_directly_addressed_users(project, target, author)
- directly_addressed_users = target.directly_addressed_users(author)
+ def filter_directly_addressed_users(project, target, author, skip_users = [])
+ directly_addressed_users = target.directly_addressed_users(author) - skip_users
filter_todo_users(directly_addressed_users, project, target)
end
diff --git a/app/services/users/create_service.rb b/app/services/users/create_service.rb
new file mode 100644
index 00000000000..a847a71a66a
--- /dev/null
+++ b/app/services/users/create_service.rb
@@ -0,0 +1,112 @@
+module Users
+ # Service for creating a new user.
+ class CreateService < BaseService
+ def initialize(current_user, params = {})
+ @current_user = current_user
+ @params = params.dup
+ end
+
+ def build
+ raise Gitlab::Access::AccessDeniedError unless can_create_user?
+
+ user = User.new(build_user_params)
+
+ if current_user&.is_admin?
+ if params[:reset_password]
+ @reset_token = user.generate_reset_token
+ params[:force_random_password] = true
+ end
+
+ if params[:force_random_password]
+ random_password = Devise.friendly_token.first(Devise.password_length.min)
+ user.password = user.password_confirmation = random_password
+ end
+ end
+
+ identity_attrs = params.slice(:extern_uid, :provider)
+
+ if identity_attrs.any?
+ user.identities.build(identity_attrs)
+ end
+
+ user
+ end
+
+ def execute
+ user = build
+
+ if user.save
+ log_info("User \"#{user.name}\" (#{user.email}) was created")
+ notification_service.new_user(user, @reset_token) if @reset_token
+ system_hook_service.execute_hooks_for(user, :create)
+ end
+
+ user
+ end
+
+ private
+
+ def can_create_user?
+ (current_user.nil? && current_application_settings.signup_enabled?) || current_user&.is_admin?
+ end
+
+ # Allowed params for creating a user (admins only)
+ def admin_create_params
+ [
+ :access_level,
+ :admin,
+ :avatar,
+ :bio,
+ :can_create_group,
+ :color_scheme_id,
+ :email,
+ :external,
+ :force_random_password,
+ :password_automatically_set,
+ :hide_no_password,
+ :hide_no_ssh_key,
+ :key_id,
+ :linkedin,
+ :name,
+ :password,
+ :password_expires_at,
+ :projects_limit,
+ :remember_me,
+ :skip_confirmation,
+ :skype,
+ :theme_id,
+ :twitter,
+ :username,
+ :website_url
+ ]
+ end
+
+ # Allowed params for user signup
+ def signup_params
+ [
+ :email,
+ :email_confirmation,
+ :password_automatically_set,
+ :name,
+ :password,
+ :username
+ ]
+ end
+
+ def build_user_params
+ if current_user&.is_admin?
+ user_params = params.slice(*admin_create_params)
+ user_params[:created_by_id] = current_user&.id
+
+ if params[:reset_password]
+ user_params.merge!(force_random_password: true, password_expires_at: nil)
+ end
+ else
+ user_params = params.slice(*signup_params)
+ user_params[:skip_confirmation] = !current_application_settings.send_user_confirmation_email
+ end
+
+ user_params
+ end
+ end
+end
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
index 833da5bc5d1..a3b32a71a64 100644
--- a/app/services/users/destroy_service.rb
+++ b/app/services/users/destroy_service.rb
@@ -20,10 +20,10 @@ module Users
Groups::DestroyService.new(group, current_user).execute
end
- user.personal_projects.each do |project|
+ user.personal_projects.with_deleted.each do |project|
# Skip repository removal because we remove directory with namespace
# that contain all this repositories
- ::Projects::DestroyService.new(project, current_user, skip_repo: true).async_execute
+ ::Projects::DestroyService.new(project, current_user, skip_repo: true).execute
end
move_issues_to_ghost_user(user)
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 3eab065bb9f..5d51a2b5cbc 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -558,5 +558,19 @@
Maximum time for web terminal websocket connection (in seconds).
0 for unlimited.
+ %fieldset
+ %legend Real-time features
+ .form-group
+ = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :polling_interval_multiplier, class: 'form-control'
+ .help-block
+ Change this value to influence how frequently the GitLab UI polls for updates.
+ If you set the value to 2 all polling intervals are multiplied
+ by 2, which means that polling happens half as frequently.
+ The multiplier can also have a decimal value.
+ The default value (1) is a reasonable choice for the majority of GitLab
+ installations. Set to 0 to completely disable polling.
+
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml
index c1a9f8d6ddd..596f367a00d 100644
--- a/app/views/admin/projects/_projects.html.haml
+++ b/app/views/admin/projects/_projects.html.haml
@@ -1,15 +1,16 @@
.js-projects-list-holder
- if @projects.any?
- %ul.projects-list.content-list
+ %ul.projects-list.content-list.admin-projects
- @projects.each_with_index do |project|
- %li.project-row
+ %li.project-row{ class: ('no-description' if project.description.blank?) }
.controls
- - if project.archived
- %span.label.label-warning archived
- %span.badge
- = storage_counter(project.statistics.storage_size)
= link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn"
= link_to 'Delete', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-remove"
+ .stats
+ %span.badge
+ = storage_counter(project.statistics.storage_size)
+ - if project.archived
+ %span.label.label-warning archived
.title
= link_to [:admin, project.namespace.becomes(Namespace), project] do
.dash-project-avatar
@@ -20,7 +21,7 @@
- if project.namespace
= project.namespace.human_name
\/
- %span.project-name.filter-title
+ %span.project-name
= project.name
- if project.description.present?
diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml
index 7855239dfe5..794aaec89bd 100644
--- a/app/views/admin/users/_access_levels.html.haml
+++ b/app/views/admin/users/_access_levels.html.haml
@@ -2,7 +2,7 @@
%legend Access
.form-group
= f.label :projects_limit, class: 'control-label'
- .col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control'
+ .col-sm-10= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control'
.form-group
= f.label :can_create_group, class: 'control-label'
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index 600ee63a5c0..4679b9549d1 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -1,7 +1,9 @@
= content_for :flash_message do
= render 'shared/project_limit'
-.top-area
- %ul.nav-links
+.top-area.scrolling-tabs-container.inner-page-scroll-tabs
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ %ul.nav-links.scrolling-tabs
= nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
Your projects
diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml
index 60c84a26420..2129920afd2 100644
--- a/app/views/dashboard/milestones/show.html.haml
+++ b/app/views/dashboard/milestones/show.html.haml
@@ -1,5 +1,5 @@
- header_title "Milestones", dashboard_milestones_path
= render 'shared/milestones/top', milestone: @milestone
-= render 'shared/milestones/summary', milestone: @milestone
= render 'shared/milestones/tabs', milestone: @milestone, show_full_project_name: true
+= render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 51
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index eef794dbd51..596499230f9 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -4,7 +4,9 @@
- page_title "Projects"
- header_title "Projects", dashboard_projects_path
-.user-callout{ 'callout-svg' => custom_icon('icon_customization') }
+- unless show_user_callout?
+ = render 'shared/user_callout'
+
- if @projects.any? || params[:name]
= render 'dashboard/projects_head'
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index 6f5d4bf2a2f..2d78c55211e 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -8,7 +8,7 @@
.discussion.js-toggle-container{ class: discussion.id, data: { discussion_id: discussion.id } }
.discussion-header
.discussion-actions
- = link_to "#", class: "note-action-button discussion-toggle-button js-toggle-button" do
+ %button.note-action-button.discussion-toggle-button.js-toggle-button{ type: "button" }
- if expanded
= icon("chevron-up")
- else
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index 56f463572bb..f630f1effdc 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -17,24 +17,3 @@
= link_to filter_projects_path(visibility_level: level) do
= visibility_level_icon(level)
= visibility_level_label(level)
-
-- if @tags.present?
- .dropdown
- %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" }
- = icon('tags')
- %span.light Tags:
- - if params[:tag].present?
- = params[:tag]
- - else
- Any
- = icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- %li
- = link_to filter_projects_path(tag: nil) do
- Any
-
- - @tags.each do |tag|
- %li{ class: active_when(tag.name == params[:tag]) || 'light' }
- = link_to filter_projects_path(tag: tag.name) do
- = icon('tag')
- = tag.name
diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index e66a8e0a3b3..33e68bc766e 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -1,8 +1,4 @@
= render "header_title"
-
-- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
-
= render 'shared/milestones/top', milestone: @milestone, group: @group
-= render 'shared/milestones/summary', milestone: @milestone
= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true
+= render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 102
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 7ddee0e5244..23abf6897d4 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -11,9 +11,13 @@
= render 'layouts/nav/dashboard'
- else
= render 'layouts/nav/explore'
- %button.navbar-toggle{ type: 'button' }
- %span.sr-only Toggle navigation
- = icon('ellipsis-v')
+
+ .header-logo
+ = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
+ = brand_header_logo
+
+ .title-container
+ %h1.title{ class: ('initializing' if @has_group_title) }= title
.navbar-collapse.collapse
%ul.nav.navbar-nav
@@ -31,11 +35,6 @@
%li
= link_to admin_root_path, title: 'Admin Area', aria: { label: "Admin Area" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('wrench fw')
- %li
- = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('bell fw')
- %span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) }
- = todos_count_format(todos_pending_count)
- if current_user.can_create_project?
%li
= link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
@@ -45,6 +44,21 @@
= link_to sherlock_transactions_path, title: 'Sherlock Transactions',
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('tachometer fw')
+ %li
+ = link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('hashtag fw')
+ %span.badge.issues-count
+ = number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened))
+ %li
+ = link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = custom_icon('mr_bold')
+ %span.badge.merge-requests-count
+ = number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened))
+ %li
+ = link_to dashboard_todos_path, title: 'Todos', aria: { label: "Todos" }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('check-circle fw')
+ %span.badge.todos-count
+ = todos_count_format(todos_pending_count)
%li.header-user.dropdown
= link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do
= image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar"
@@ -63,11 +77,9 @@
%div
= link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
- .header-logo
- = link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
- = brand_header_logo
-
- %h1.title{ class: ('initializing' if @has_group_title) }= title
+ %button.navbar-toggle{ type: 'button' }
+ %span.sr-only Toggle navigation
+ = icon('ellipsis-v')
= yield :header_content
diff --git a/app/views/notify/project_was_exported_email.html.haml b/app/views/notify/project_was_exported_email.html.haml
index b28fea35ad5..76440926a2b 100644
--- a/app/views/notify/project_was_exported_email.html.haml
+++ b/app/views/notify/project_was_exported_email.html.haml
@@ -2,7 +2,7 @@
Project #{@project.name} was exported successfully.
%p
The project export can be downloaded from:
- = link_to download_export_namespace_project_url(@project.namespace, @project) do
+ = link_to download_export_namespace_project_url(@project.namespace, @project), rel: 'nofollow', download: '', do
= @project.name_with_namespace + " export"
%p
The download link will expire in 24 hours.
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 8a994f6d600..5ce2220c907 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -75,12 +75,12 @@
.provider-btn-image
= provider_image_tag(provider)
- if auth_active?(provider)
- - if provider.to_s == 'saml'
- %a.provider-btn
- Active
- - else
+ - if unlink_allowed?(provider)
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
Disconnect
+ - else
+ %a.provider-btn
+ Active
- else
= link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do
Connect
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 5c5e5940365..51c4e8e5a73 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -34,6 +34,11 @@
.clearfix
+ = form_for @user, url: profile_notifications_path, method: :put do |f|
+ %label{ for: 'user_notified_of_own_activity' }
+ = f.check_box :notified_of_own_activity
+ %span Receive notifications about your own activity
+
%hr
%h5
Groups (#{@group_notifications.count})
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 79a0dc1b959..0fd19780570 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,6 +1,6 @@
- empty_repo = @project.empty_repo?
.project-home-panel.text-center{ class: ("empty-project" if empty_repo) }
- %div{ class: container_class }
+ .limit-container-width{ class: container_class }
.avatar-container.s70.project-avatar
= project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile')
%h1.project-title
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
index edf55d59f28..de8c173f26f 100644
--- a/app/views/projects/artifacts/browse.html.haml
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -3,7 +3,7 @@
.top-block.row-content-block.clearfix
.pull-right
= link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build),
- class: 'btn btn-default download' do
+ rel: 'nofollow', download: '', class: 'btn btn-default download' do
= icon('download')
Download artifacts archive
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index e7adef5558a..4b344b2edb9 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -1,29 +1,23 @@
+- action = current_action?(:edit) || current_action?(:update) ? 'edit' : 'create'
+
.file-holder.file.append-bottom-default
- .js-file-title.file-title.clearfix
+ .js-file-title.file-title.clearfix{ data: { current_action: action } }
.editor-ref
= icon('code-fork')
= ref
%span.editor-file-name
- if current_action?(:edit) || current_action?(:update)
= text_field_tag 'file_path', (params[:file_path] || @path),
- class: 'form-control new-file-path'
+ class: 'form-control new-file-path js-file-path-name-input'
- if current_action?(:new) || current_action?(:create)
%span.editor-file-name
\/
= text_field_tag 'file_name', params[:file_name], placeholder: "File name",
- required: true, class: 'form-control new-file-name'
+ required: true, class: 'form-control new-file-name js-file-path-name-input'
.pull-right.file-buttons
- .license-selector.js-license-selector-wrap.hidden
- = dropdown_tag("Choose a License template", options: { toggle_class: 'btn js-license-selector', title: "Choose a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
- .gitignore-selector.js-gitignore-selector-wrap.hidden
- = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
- .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden
- = dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
- .dockerfile-selector.js-dockerfile-selector-wrap.hidden
- = dropdown_tag("Choose a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } )
- = button_tag class: 'soft-wrap-toggle btn', type: 'button' do
+ = button_tag class: 'soft-wrap-toggle btn', type: 'button', tabindex: '-1' do
%span.no-wrap
= custom_icon('icon_no_wrap')
No wrap
@@ -31,7 +25,7 @@
= custom_icon('icon_soft_wrap')
Soft wrap
.encoding-selector
- = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
+ = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2', tabindex: '-1'
.file-editor.code
%pre.js-edit-mode-pane#editor= params[:content] || local_assigns[:blob_data]
diff --git a/app/views/projects/blob/_notebook.html.haml b/app/views/projects/blob/_notebook.html.haml
new file mode 100644
index 00000000000..ab1cf933944
--- /dev/null
+++ b/app/views/projects/blob/_notebook.html.haml
@@ -0,0 +1,5 @@
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('common_vue')
+ = page_specific_javascript_bundle_tag('notebook_viewer')
+
+.file-content#js-notebook-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml
new file mode 100644
index 00000000000..d52733d2bd6
--- /dev/null
+++ b/app/views/projects/blob/_template_selectors.html.haml
@@ -0,0 +1,17 @@
+.template-selectors-menu
+ .templates-selectors-label
+ Template
+ .template-selector-dropdowns-wrap
+ .template-type-selector.js-template-type-selector-wrap.hidden
+ = dropdown_tag("Choose type", options: { toggle_class: 'btn js-template-type-selector', title: "Choose a template type" } )
+ .license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden
+ = dropdown_tag("Apply a License template", options: { toggle_class: 'btn js-license-selector', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
+ .gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden
+ = dropdown_tag("Apply a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
+ .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden
+ = dropdown_tag("Apply a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
+ .dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden
+ = dropdown_tag("Apply a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } )
+ .template-selectors-undo-menu.hidden
+ %span.text-info Template applied
+ %button.btn.btn-sm.btn-info Undo
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index afe0b5dba45..4b26f944733 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -11,12 +11,15 @@
Someone edited the file the same time you did. Please check out
= link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank", rel: 'noopener noreferrer'
and make sure your changes will not unintentionally remove theirs.
-
+ .editor-title-row
+ %h3.page-title.blob-edit-page-title
+ Edit file
+ = render 'template_selectors'
.file-editor
%ul.nav-links.no-bottom.js-edit-mode
%li.active
= link_to '#editor' do
- Edit File
+ Write
%li
= link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index 4c449e040ee..2afb909572a 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -2,10 +2,10 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
= page_specific_javascript_bundle_tag('blob')
-
-%h3.page-title
- New File
-
+.editor-title-row
+ %h3.page-title.blob-new-page-title
+ New file
+ = render 'template_selectors'
.file-editor
= form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do
= render 'projects/blob/editor', ref: @ref
diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml
index added3f669b..7ca0ec8ed2b 100644
--- a/app/views/projects/boards/_show.html.haml
+++ b/app/views/projects/boards/_show.html.haml
@@ -6,10 +6,8 @@
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('filtered_search')
= page_specific_javascript_bundle_tag('boards')
- = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
%script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
- %script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list"
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
= render "projects/issues/head"
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index 0bca6a786cb..5a4eaf92b16 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -7,12 +7,12 @@
data: { container: "body", placement: "bottom" } }
{{ list.title }}
.board-issue-count-holder.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' }
- %span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "done" && !disabled }' }
+ %span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
{{ list.issuesSize }}
- if can?(current_user, :admin_issue, @project)
%button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
"@click" => "showNewIssueForm",
- "v-if" => 'list.type !== "done"',
+ "v-if" => 'list.type !== "closed"',
"aria-label" => "Add an issue",
"title" => "Add an issue",
data: { placement: "top", container: "body" } }
diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml
deleted file mode 100644
index 4a4dd84d5d2..00000000000
--- a/app/views/projects/boards/components/_board_list.html.haml
+++ /dev/null
@@ -1,26 +0,0 @@
-.board-list-component
- .board-list-loading.text-center{ "v-if" => "loading" }
- = icon("spinner spin")
- - if can? current_user, :create_issue, @project
- %board-new-issue{ ":list" => "list",
- "v-if" => 'list.type !== "done" && showIssueForm' }
- %ul.board-list{ "ref" => "list",
- "v-show" => "!loading",
- ":data-board" => "list.id",
- ":class" => '{ "is-smaller": showIssueForm }' }
- %board-card{ "v-for" => "(issue, index) in issues",
- "ref" => "issue",
- ":index" => "index",
- ":list" => "list",
- ":issue" => "issue",
- ":issue-link-base" => "issueLinkBase",
- ":root-path" => "rootPath",
- ":disabled" => "disabled",
- ":key" => "issue.id" }
- %li.board-list-count.text-center{ "v-if" => "showCount",
- "data-issue-id" => "-1" }
- = icon("spinner spin", "v-show" => "list.loadingMore" )
- %span{ "v-if" => "list.issues.length === list.issuesSize" }
- Showing all issues
- %span{ "v-else" => true }
- Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index b597c7f7a12..6f45d5b0689 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -33,7 +33,7 @@
= link_to keep_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post do
Keep
- = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default' do
+ = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build), rel: 'nofollow', download: '', class: 'btn btn-sm btn-default' do
Download
- if @build.artifacts_metadata?
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 09286a1b3c6..aeed293a724 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -94,7 +94,7 @@
%td
.pull-right
- if can?(current_user, :read_build, build) && build.artifacts?
- = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), title: 'Download artifacts', class: 'btn btn-build' do
+ = link_to download_namespace_project_build_artifacts_path(build.project.namespace, build.project, build), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do
= icon('download')
- if can?(current_user, :update_build, build)
- if build.active?
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 6ab9a80e083..4b1ff75541a 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -24,7 +24,7 @@
.visible-xs-inline
= render_commit_status(commit, ref: ref)
- if commit.description?
- %a.text-expander.hidden-xs.js-toggle-button ...
+ %button.text-expander.hidden-xs.js-toggle-button{ type: "button" } ...
- if commit.description?
%pre.commit-row-description.js-toggle-content
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index 8e24e28765f..fd4f3c8d3cc 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -1,7 +1,7 @@
.js-toggle-container
.commit-stat-summary
Showing
- = link_to '#', class: 'js-toggle-button' do
+ %button.diff-stats-summary-toggler.js-toggle-button{ type: "button" }
%strong= pluralize(diff_files.size, "changed file")
with
%strong.cgreen #{diff_files.sum(&:added_lines)} additions
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 82e0d0025ec..160345cfaa5 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -163,7 +163,7 @@
- if @project.export_project_path
= link_to 'Download export', download_export_namespace_project_path(@project.namespace, @project),
- method: :get, class: "btn btn-default"
+ rel: 'nofollow', download: '', method: :get, class: "btn btn-default"
= link_to 'Generate new export', generate_new_export_namespace_project_path(@project.namespace, @project),
method: :post, class: "btn btn-default"
- else
@@ -238,6 +238,8 @@
%ul
%li Be careful. Renaming a project's repository can have unintended side effects.
%li You will need to update your local repositories to point to the new location.
+ - if @project.deployment_services.any?
+ %li Your deployment services will be broken, you will need to manually fix the services after renaming.
= f.submit 'Rename project', class: "btn btn-warning"
- if can?(current_user, :change_namespace, @project)
%hr
diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml
index bf0f1819073..a82ef5ee5bb 100644
--- a/app/views/projects/environments/_external_url.html.haml
+++ b/app/views/projects/environments/_external_url.html.haml
@@ -1,3 +1,4 @@
- if environment.external_url && can?(current_user, :read_environment, environment)
= link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do
= icon('external-link')
+ View deployment
diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml
index acbac1869fd..e27281d6917 100644
--- a/app/views/projects/environments/_metrics_button.html.haml
+++ b/app/views/projects/environments/_metrics_button.html.haml
@@ -4,3 +4,4 @@
= link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do
= icon('area-chart')
+ Monitoring
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index 3b45162df52..92dc58cd38d 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -11,7 +11,7 @@
.col-sm-6
%h3.page-title
Environment:
- = @environment.name
+ = link_to @environment.name, environment_path(@environment)
.col-sm-6
.nav-controls
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index f463a429f65..ff6aaebda22 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -4,9 +4,9 @@
%div{ class: container_class }
.top-area.adjust
- .col-md-9
+ .col-md-7
%h3.page-title= @environment.name
- .col-md-3
+ .col-md-5
.nav-controls
= render 'projects/environments/metrics_button', environment: @environment
= render 'projects/environments/terminal_button', environment: @environment
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 8d4a91cb64c..29f861c09c6 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -3,9 +3,6 @@
- hide_class = ''
= render "projects/issues/head"
-- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
-
- if @labels.exists? || @prioritized_labels.exists?
%div{ class: container_class }
.top-area.adjust
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 6682a85ffa6..881ee9fd596 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -52,8 +52,10 @@
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true
.merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
- .merge-request-tabs-container
- %ul.merge-request-tabs.nav-links.no-top.no-bottom
+ .merge-request-tabs-container.scrolling-tabs-container.inner-page-scroll-tabs
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ %ul.merge-request-tabs.nav-links.scrolling-tabs
%li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
Discussion
diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml
index c94c7944c0b..e5ec151a61d 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -37,7 +37,7 @@
= check_box_tag :should_remove_source_branch
Remove source branch
.accept-control
- = link_to "#", class: "modify-merge-commit-link js-toggle-button" do
+ %button.modify-merge-commit-link.js-toggle-button{ type: "button" }
= icon('edit')
Modify commit message
.js-toggle-content.hide.prepend-top-default
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index d16f49bd33a..5249d752585 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -3,9 +3,6 @@
- page_description @milestone.description
= render "projects/issues/head"
-- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
-
%div{ class: container_class }
.detail-page-header.milestone-page-header
.status-box{ class: status_box_class(@milestone) }
@@ -36,6 +33,9 @@
= link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-danger" do
Delete
+ %a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" }
+ = icon('angle-double-left')
+
.detail-page-description.milestone-detail{ class: ('hide-bottom-border' unless @milestone.description.present? ) }
%h2.title
= markdown_field(@milestone, :title)
@@ -53,5 +53,5 @@
.alert.alert-success.prepend-top-default
%span All issues for this milestone are closed. You may close this milestone now.
- = render 'shared/milestones/summary', milestone: @milestone, project: @project
= render 'shared/milestones/tabs', milestone: @milestone
+ = render 'shared/milestones/sidebar', milestone: @milestone, project: @project, affix_offset: 153
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 34a1214a350..0c7b53e5a9a 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -76,9 +76,9 @@
Gitea
%div
- if git_import_enabled?
- = link_to "#", class: 'btn js-toggle-button import_git' do
+ %button.btn.js-toggle-button.import_git{ type: "button" }
= icon('git', text: 'Repo by URL')
- .import_gitlab_project
+ .import_gitlab_project.has-tooltip{ data: { container: 'body' } }
- if gitlab_project_import_enabled?
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export')
@@ -109,6 +109,9 @@
%p Please wait a moment, this page will automatically refresh when ready.
:javascript
+ var importBtnTooltip = "Please enter a valid project name.";
+ var $importBtnWrapper = $('.import_gitlab_project');
+
$('.how_to_import_link').bind('click', function (e) {
e.preventDefault();
var import_modal = $(this).next(".modal").show();
@@ -123,15 +126,8 @@
$(".btn_import_gitlab_project").attr("href", _href + '?namespace_id=' + $("#project_namespace_id").val() + '&path=' + $("#project_path").val());
});
- $('.btn_import_gitlab_project').attr('disabled',true)
- $('.import_gitlab_project').attr('title', 'Project path and name required.');
-
- $('.import_gitlab_project').click(function( event ) {
- if($('.btn_import_gitlab_project').attr('disabled')) {
- event.preventDefault();
- new Flash("Please enter path and name for the project to be imported to.");
- }
- });
+ $('.btn_import_gitlab_project').attr('disabled', $('#project_path').val().trim().length === 0);
+ $importBtnWrapper.attr('title', importBtnTooltip);
$('#new_project').submit(function(){
var $path = $('#project_path');
@@ -139,13 +135,13 @@
});
$('#project_path').keyup(function(){
- if($(this).val().length !=0) {
+ if($(this).val().trim().length !== 0) {
$('.btn_import_gitlab_project').attr('disabled', false);
- $('.import_gitlab_project').attr('title','');
- $(".flash-container").html("")
+ $importBtnWrapper.attr('title','');
+ $importBtnWrapper.removeClass('has-tooltip');
} else {
$('.btn_import_gitlab_project').attr('disabled',true);
- $('.import_gitlab_project').attr('title', 'Project path and name required.');
+ $importBtnWrapper.addClass('has-tooltip');
}
});
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index b02fef638ff..bc57f7f1c46 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -10,13 +10,13 @@
Pipelines
- if project_nav_tab? :builds
- = nav_link(path: 'builds#index', controller: :builds) do
+ = nav_link(controller: :builds) do
= link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do
%span
Jobs
- if project_nav_tab? :environments
- = nav_link(path: 'environments#index', controller: :environments) do
+ = nav_link(controller: :environments) do
= link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
%span
Environments
diff --git a/app/views/projects/protected_branches/_dropdown.html.haml b/app/views/projects/protected_branches/_dropdown.html.haml
index a9e27df5a87..5af0cc7a2f3 100644
--- a/app/views/projects/protected_branches/_dropdown.html.haml
+++ b/app/views/projects/protected_branches/_dropdown.html.haml
@@ -10,6 +10,6 @@
%ul.dropdown-footer-list
%li
- = link_to '#', title: "New Protected Branch", class: "create-new-protected-branch" do
+ %button{ class: "create-new-protected-branch-button js-create-new-protected-branch", title: "New Protected Branch" }
Create wildcard
%code
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index de1229d58aa..edfe6da1816 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -13,7 +13,7 @@
= render "home_panel"
- if current_user && can?(current_user, :download_code, @project)
- %nav.project-stats{ class: container_class }
+ %nav.project-stats.limit-container-width{ class: container_class }
%ul.nav
%li
= link_to project_files_path(@project) do
@@ -74,11 +74,11 @@
Set up auto deploy
- if @repository.commit
- %div{ class: container_class }
+ .limit-container-width{ class: container_class }
.project-last-commit
= render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
-%div{ class: container_class }
+.limit-container-width{ class: container_class }
- if @project.archived?
.text-warning.center.prepend-top-20
%p
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index c52527332bc..0d2cd4a7476 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -1,3 +1,5 @@
+- commit_message = @page.persisted? ? "Update #{@page.title}" : "Create #{@page.title}"
+
= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f|
= form_errors(@page)
@@ -28,7 +30,7 @@
.form-group
= f.label :commit_message, class: 'control-label'
- .col-sm-10= f.text_field :message, class: 'form-control', rows: 18
+ .col-sm-10= f.text_field :message, class: 'form-control', rows: 18, value: commit_message
.form-actions
- if @page && @page.persisted?
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index 5afb95ac430..059a0d1ac78 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -1,71 +1,74 @@
-%ul.nav-links.search-filter
- - if @project
- %li{ class: active_when(@scope == 'blobs') }
- = link_to search_filter_path(scope: 'blobs') do
- Code
- %span.badge
- = @search_results.blobs_count
- %li{ class: active_when(@scope == 'issues') }
- = link_to search_filter_path(scope: 'issues') do
- Issues
- %span.badge
- = @search_results.issues_count
- %li{ class: active_when(@scope == 'merge_requests') }
- = link_to search_filter_path(scope: 'merge_requests') do
- Merge requests
- %span.badge
- = @search_results.merge_requests_count
- %li{ class: active_when(@scope == 'milestones') }
- = link_to search_filter_path(scope: 'milestones') do
- Milestones
- %span.badge
- = @search_results.milestones_count
- %li{ class: active_when(@scope == 'notes') }
- = link_to search_filter_path(scope: 'notes') do
- Comments
- %span.badge
- = @search_results.notes_count
- %li{ class: active_when(@scope == 'wiki_blobs') }
- = link_to search_filter_path(scope: 'wiki_blobs') do
- Wiki
- %span.badge
- = @search_results.wiki_blobs_count
- %li{ class: active_when(@scope == 'commits') }
- = link_to search_filter_path(scope: 'commits') do
- Commits
- %span.badge
- = @search_results.commits_count
+.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ %ul.nav-links.search-filter.scrolling-tabs
+ - if @project
+ %li{ class: active_when(@scope == 'blobs') }
+ = link_to search_filter_path(scope: 'blobs') do
+ Code
+ %span.badge
+ = @search_results.blobs_count
+ %li{ class: active_when(@scope == 'issues') }
+ = link_to search_filter_path(scope: 'issues') do
+ Issues
+ %span.badge
+ = @search_results.issues_count
+ %li{ class: active_when(@scope == 'merge_requests') }
+ = link_to search_filter_path(scope: 'merge_requests') do
+ Merge requests
+ %span.badge
+ = @search_results.merge_requests_count
+ %li{ class: active_when(@scope == 'milestones') }
+ = link_to search_filter_path(scope: 'milestones') do
+ Milestones
+ %span.badge
+ = @search_results.milestones_count
+ %li{ class: active_when(@scope == 'notes') }
+ = link_to search_filter_path(scope: 'notes') do
+ Comments
+ %span.badge
+ = @search_results.notes_count
+ %li{ class: active_when(@scope == 'wiki_blobs') }
+ = link_to search_filter_path(scope: 'wiki_blobs') do
+ Wiki
+ %span.badge
+ = @search_results.wiki_blobs_count
+ %li{ class: active_when(@scope == 'commits') }
+ = link_to search_filter_path(scope: 'commits') do
+ Commits
+ %span.badge
+ = @search_results.commits_count
- - elsif @show_snippets
- %li{ class: active_when(@scope == 'snippet_blobs') }
- = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do
- Snippet Contents
- %span.badge
- = @search_results.snippet_blobs_count
- %li{ class: active_when(@scope == 'snippet_titles') }
- = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do
- Titles and Filenames
- %span.badge
- = @search_results.snippet_titles_count
+ - elsif @show_snippets
+ %li{ class: active_when(@scope == 'snippet_blobs') }
+ = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do
+ Snippet Contents
+ %span.badge
+ = @search_results.snippet_blobs_count
+ %li{ class: active_when(@scope == 'snippet_titles') }
+ = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do
+ Titles and Filenames
+ %span.badge
+ = @search_results.snippet_titles_count
- - else
- %li{ class: active_when(@scope == 'projects') }
- = link_to search_filter_path(scope: 'projects') do
- Projects
- %span.badge
- = @search_results.projects_count
- %li{ class: active_when(@scope == 'issues') }
- = link_to search_filter_path(scope: 'issues') do
- Issues
- %span.badge
- = @search_results.issues_count
- %li{ class: active_when(@scope == 'merge_requests') }
- = link_to search_filter_path(scope: 'merge_requests') do
- Merge requests
- %span.badge
- = @search_results.merge_requests_count
- %li{ class: active_when(@scope == 'milestones') }
- = link_to search_filter_path(scope: 'milestones') do
- Milestones
- %span.badge
- = @search_results.milestones_count
+ - else
+ %li{ class: active_when(@scope == 'projects') }
+ = link_to search_filter_path(scope: 'projects') do
+ Projects
+ %span.badge
+ = @search_results.projects_count
+ %li{ class: active_when(@scope == 'issues') }
+ = link_to search_filter_path(scope: 'issues') do
+ Issues
+ %span.badge
+ = @search_results.issues_count
+ %li{ class: active_when(@scope == 'merge_requests') }
+ = link_to search_filter_path(scope: 'merge_requests') do
+ Merge requests
+ %span.badge
+ = @search_results.merge_requests_count
+ %li{ class: active_when(@scope == 'milestones') }
+ = link_to search_filter_path(scope: 'milestones') do
+ Milestones
+ %span.badge
+ = @search_results.milestones_count
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index c2d9ac87b20..8869d510aef 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -1,4 +1,6 @@
-- parent = Group.find_by(id: params[:parent_id] || @group.parent_id)
+- parent = GroupFinder.new(current_user).execute(id: params[:parent_id] || @group.parent_id)
+- group_path = root_url
+- group_path << parent.full_path + '/' if parent
- if @group.persisted?
.form-group
= f.label :name, class: 'control-label' do
@@ -11,7 +13,7 @@
Group path
.col-sm-10
.input-group.gl-field-error-anchor
- .input-group-addon
+ .group-root-path.input-group-addon.has-tooltip{ title: group_path, :'data-placement' => 'bottom' }
%span>= root_url
- if parent
%strong= parent.full_path + '/'
diff --git a/app/views/shared/_user_callout.html.haml b/app/views/shared/_user_callout.html.haml
new file mode 100644
index 00000000000..8f1293adcb1
--- /dev/null
+++ b/app/views/shared/_user_callout.html.haml
@@ -0,0 +1,14 @@
+.user-callout
+ .bordered-box.landing.content-block
+ %button.btn.btn-default.close.js-close-callout{ type: 'button',
+ 'aria-label' => 'Dismiss customize experience box' }
+ = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
+ .row
+ .col-sm-3.col-xs-12.svg-container
+ = custom_icon('icon_customization')
+ .col-sm-8.col-xs-12.inner-content
+ %h4
+ Customize your experience
+ %p
+ Change syntax themes, default project pages, and more in preferences.
+ = link_to 'Check it out', profile_preferences_path, class: 'btn btn-default js-close-callout'
diff --git a/app/views/shared/icons/_activity.svg b/app/views/shared/icons/_activity.svg
deleted file mode 100644
index d465504b154..00000000000
--- a/app/views/shared/icons/_activity.svg
+++ /dev/null
@@ -1,16 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
- <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
- <title>path-1</title>
- <desc>Created with Sketch.</desc>
- <defs></defs>
- <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
- <g id="_activity" fill="#7E7D7D">
- <g id="Page-1">
- <g id="path-1">
- <path d="M5,0 C4.448,0 4,0.448 4,1 L4,3 L1,3 C0.448,3 0,3.448 0,4 L0,9 C0,9.552 0.448,10 1,10 L5,10 L5,8 L11,8 L11,10 L15,10 C15.552,10 16,9.552 16,9 L16,4 C16,3.448 15.552,3 15,3 L12,3 L12,1 C12,0.448 11.552,0 11,0 L5,0 L5,0 L5,0 L5,0 Z M6,2.5 C6,2.224 6.224,2 6.5,2 L9.5,2 C9.776,2 10,2.224 10,2.5 C10,2.776 9.776,3 9.5,3 L6.5,3 C6.224,3 6,2.776 6,2.5 L6,2.5 L6,2.5 L6,2.5 Z M6,11 L10.001,11 L10.001,9 L6,9 L6,11 L6,11 L6,11 L6,11 Z M11,11 L11,12 L5,12 L5,11 L1,11 C0.448,11 0,11.448 0,12 L0,15 C0,15.552 0.448,16 1,16 L15,16 C15.552,16 16,15.552 16,15 L16,12 C16,11.448 15.552,11 15,11 L11,11 L11,11 L11,11 L11,11 Z"></path>
- </g>
- </g>
- </g>
- </g>
-</svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_commits.svg b/app/views/shared/icons/_commits.svg
deleted file mode 100644
index ba9bb89935e..00000000000
--- a/app/views/shared/icons/_commits.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
- <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
- <title>Pasted Image 240</title>
- <desc>Created with Sketch.</desc>
- <defs></defs>
- <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
- <path d="M3,8 C3,5.951 4.236,4.194 6,3.422 L6,0 L1,0 C0.448,0 0,0.448 0,1 L0,15 C0,15.552 0.448,16 1,16 L6,16 L6,12.578 C4.236,11.806 3,10.049 3,8 M7,12.899 L7,16 L9,16 L9,12.899 C8.677,12.965 8.343,13 8,13 C7.657,13 7.323,12.965 7,12.899 M15,0 L10,0 L10,3.422 C11.764,4.194 13,5.951 13,8 C13,10.049 11.764,11.806 10,12.578 L10,16 L15,16 C15.552,16 16,15.552 16,15 L16,1 C16,0.448 15.552,0 15,0 M10,8 C10,9.105 9.105,10 8,10 C6.895,10 6,9.105 6,8 C6,6.895 6.895,6 8,6 C9.105,6 10,6.895 10,8 M4,8 C4,10.209 5.791,12 8,12 C10.209,12 12,10.209 12,8 C12,5.791 10.209,4 8,4 C5.791,4 4,5.791 4,8 M9,3.101 L9,0 L7,0 L7,3.101 C7.323,3.035 7.657,3 8,3 C8.343,3 8.677,3.035 9,3.101" id="Pasted-Image-240" fill="#7E7D7D"></path>
- </g>
-</svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_contributionanalytics.svg b/app/views/shared/icons/_contributionanalytics.svg
deleted file mode 100644
index adf09a14964..00000000000
--- a/app/views/shared/icons/_contributionanalytics.svg
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
- <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
- <title>Group</title>
- <desc>Created with Sketch.</desc>
- <defs></defs>
- <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
- <g id="Group">
- <path d="M8,0 C3.581,0 0,3.581 0,8 C0,12.419 3.581,16 8,16 C12.419,16 16,12.419 16,8 C16,3.581 12.419,0 8,0 M8,2 C11.308,2 14,4.692 14,8 C14,11.308 11.308,14 8,14 C4.692,14 2,11.308 2,8 C2,4.692 4.692,2 8,2" id="Fill-1" fill="#7E7C7C"></path>
- <polygon id="Stroke-6" fill="#7E7C7C" points="2.0197351 9.86809696 6.4567351 6.52409696 5.79233671 6.46815759 9.53233671 10.4271576 9.87070552 10.78534 10.2338016 10.4522494 15.0258016 6.05624938 14.3497984 5.31935062 9.55779844 9.71535062 10.2592633 9.74044241 6.51926329 5.78144241 6.21208651 5.45627854 5.8548649 5.72550304 1.4178649 9.06950304"></polygon>
- <path d="M7.0313,6.3928 C7.0313,6.9448 6.5833,7.3928 6.0313,7.3928 C5.4793,7.3928 5.0313,6.9448 5.0313,6.3928 C5.0313,5.8408 5.4793,5.3928 6.0313,5.3928 C6.5833,5.3928 7.0313,5.8408 7.0313,6.3928" id="Fill-8" fill="#FEFEFE"></path>
- <path d="M6.5313,6.3928 C6.5313,6.66865763 6.30715763,6.8928 6.0313,6.8928 C5.75544237,6.8928 5.5313,6.66865763 5.5313,6.3928 C5.5313,6.11694237 5.75544237,5.8928 6.0313,5.8928 C6.30715763,5.8928 6.5313,6.11694237 6.5313,6.3928 L6.5313,6.3928 Z M7.5313,6.3928 C7.5313,5.56465763 6.85944237,4.8928 6.0313,4.8928 C5.20315763,4.8928 4.5313,5.56465763 4.5313,6.3928 C4.5313,7.22094237 5.20315763,7.8928 6.0313,7.8928 C6.85944237,7.8928 7.5313,7.22094237 7.5313,6.3928 L7.5313,6.3928 Z" id="Stroke-10" fill="#7E7C7C"></path>
- <path d="M10.8854,9.8715 C10.8854,10.4235 10.4374,10.8715 9.8854,10.8715 C9.3334,10.8715 8.8854,10.4235 8.8854,9.8715 C8.8854,9.3195 9.3334,8.8715 9.8854,8.8715 C10.4374,8.8715 10.8854,9.3195 10.8854,9.8715" id="Fill-12" fill="#FEFEFE"></path>
- <path d="M10.3854,9.8715 C10.3854,10.1473576 10.1612576,10.3715 9.8854,10.3715 C9.60954237,10.3715 9.3854,10.1473576 9.3854,9.8715 C9.3854,9.59564237 9.60954237,9.3715 9.8854,9.3715 C10.1612576,9.3715 10.3854,9.59564237 10.3854,9.8715 L10.3854,9.8715 Z M11.3854,9.8715 C11.3854,9.04335763 10.7135424,8.3715 9.8854,8.3715 C9.05725763,8.3715 8.3854,9.04335763 8.3854,9.8715 C8.3854,10.6996424 9.05725763,11.3715 9.8854,11.3715 C10.7135424,11.3715 11.3854,10.6996424 11.3854,9.8715 L11.3854,9.8715 Z" id="Stroke-14" fill="#7E7C7C"></path>
- </g>
- </g>
-</svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_delta.svg b/app/views/shared/icons/_delta.svg
deleted file mode 100644
index 7c0c0d3999c..00000000000
--- a/app/views/shared/icons/_delta.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-<svg width="14px" height="10px" viewBox="322 21 14 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
- <path d="M330.078605,22.8166945 L335.259532,29.6235062 C335.615145,30.0907182 335.412062,30.4694683 334.822641,30.4694683 L331.657805,30.4694683 L324.04678,30.4694683 C323.449879,30.4694683 323.260751,30.0822112 323.609889,29.6235062 L328.790816,22.8166945 C329.146429,22.3494825 329.729467,22.3579895 330.078605,22.8166945 Z" id="delta" stroke="#5C5C5C" stroke-width="1" fill="none"></path>
-</svg>
diff --git a/app/views/shared/icons/_files.svg b/app/views/shared/icons/_files.svg
deleted file mode 100644
index fc378d81e40..00000000000
--- a/app/views/shared/icons/_files.svg
+++ /dev/null
@@ -1,17 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
- <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
- <title>Pasted Image 237</title>
- <desc>Created with Sketch.</desc>
- <defs></defs>
- <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
- <g id="Pasted-Image-237">
- <path d="M15.1111,16 C15.6021,16 16.0001,15.602 16.0001,15.111 L16.0001,4.444 C15.5341,3.983 12.0671,0.378 11.5551,0 L0.8891,0 C0.3981,0 0.0001,0.398 0.0001,0.889 L0.0001,15.111 C0.0001,15.602 0.3981,16 0.8891,16 L15.1111,16 M14.0001,14.111 L1.8891,14.111 L1.8891,2 L10.8131,2 C11.4451,2.42 13.5811,4.555 14.0001,5.187 L14.0001,14.111" id="Fill-1" fill="#7E7D7D"></path>
- <path d="M0.889,0 C0.398,0 0,0.398 0,0.889 L0,15.111 C0,15.602 0.398,16 0.889,16 L15.111,16 C15.602,16 16,15.602 16,15.111 L16,4.445 C15.534,3.983 12.068,0.377 11.555,0 L0.889,0 L0.889,0 Z M1.889,2 L10.813,2 C11.446,2.42 13.581,4.554 14,5.187 L14,14.111 L1.889,14.111 L1.889,2 L1.889,2 Z" id="Clip-4"></path>
- <polygon id="Fill-6" fill="#7E7D7D" points="9 7 11 7 11 2 9 2"></polygon>
- <polygon id="Clip-9" points="9 7 11 7 11 2.001 9 2.001"></polygon>
- <polygon id="Fill-11" fill="#7E7D7D" points="10 7 15.444 7 15.444 5 10 5"></polygon>
- <polygon id="Clip-14" points="10 7 15.444 7 15.444 5 10 5"></polygon>
- </g>
- </g>
-</svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_icon_close.svg b/app/views/shared/icons/_icon_close.svg
index 9d62012518b..59a6cb32d18 100644
--- a/app/views/shared/icons/_icon_close.svg
+++ b/app/views/shared/icons/_icon_close.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><path d="M9,7.5l5.83-5.91a.48.48,0,0,0,0-.69L14.11.15a.46.46,0,0,0-.68,0l-5.93,6L1.57.15a.46.46,0,0,0-.68,0L.15.9a.48.48,0,0,0,0,.69L6,7.5.15,13.41a.48.48,0,0,0,0,.69l.74.75a.46.46,0,0,0,.68,0l5.93-6,5.93,6a.46.46,0,0,0,.68,0l.74-.75a.48.48,0,0,0,0-.69Z"/></svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><path d="M9,7.5l5.83-5.91a.48.48,0,0,0,0-.69L14.11.15a.46.46,0,0,0-.68,0l-5.93,6L1.57.15a.46.46,0,0,0-.68,0L.15.9a.48.48,0,0,0,0,.69L6,7.5.15,13.41a.48.48,0,0,0,0,.69l.74.75a.46.46,0,0,0,.68,0l5.93-6,5.93,6a.46.46,0,0,0,.68,0l.74-.75a.48.48,0,0,0,0-.69Z"/></svg>
diff --git a/app/views/shared/icons/_icon_empty_groups.svg b/app/views/shared/icons/_icon_empty_groups.svg
index 9228be05f03..cf378145e59 100644
--- a/app/views/shared/icons/_icon_empty_groups.svg
+++ b/app/views/shared/icons/_icon_empty_groups.svg
@@ -1 +1 @@
-<svg width="249" height="368" viewBox="891 156 249 368" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="131" height="162" rx="10"/><mask id="e" x="0" y="0" width="131" height="162" fill="#fff"><use xlink:href="#a"/></mask><path d="M223.616 127.958V108.96c0-4.416-3.584-8-8.005-8h-23.985c-2.778 0-5.98 2.014-7.18 4.5l-5.07 10.5h-49.763c-5.527 0-9.996 4.475-9.996 9.997v53.005c0 5.513 4.475 9.997 9.996 9.997h84.01c5.525 0 9.994-4.477 9.994-9.998v-51.004z" id="b"/><mask id="f" x="0" y="0" width="104" height="88" fill="#fff"><use xlink:href="#b"/></mask><path d="M47 25h.996C53.52 25 58 29.472 58 34.99v20.02C58 60.526 53.52 65 47.996 65H10.004C4.48 65 0 60.528 0 55.01V34.99C0 29.474 4.48 25 10.004 25H11v-7c0-9.94 8.06-18 18-18s18 8.06 18 18v7zm-6 0H17v-7c0-6.627 5.373-12 12-12s12 5.373 12 12v7z" id="c"/><mask id="g" x="0" y="0" width="58" height="65" fill="#fff"><use xlink:href="#c"/></mask><path d="M0 10.008C0 4.48 4.476 0 10 0h218c5.523 0 10 4.473 10 10.008v140.94c0 5.53-4.062 11.882-9.08 14.196l-100.84 46.5c-5.015 2.31-13.142 2.312-18.16 0l-100.84-46.5C4.064 162.832 0 156.484 0 150.95V10.007z" id="d"/><mask id="h" x="0" y="0" width="238" height="213.417" fill="#fff"><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(891 156)"><g transform="rotate(8 -266.528 490.3)"><use stroke="#E5E5E5" mask="url(#e)" stroke-width="8" fill="#FFF" xlink:href="#a"/><rect fill="#FC8A51" x="20" y="31" width="12" height="4" rx="2"/><rect fill="#FC8A51" x="60" y="31" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="36" y="31" width="20" height="4" rx="2"/><rect fill="#6B4FBB" x="20" y="65" width="20" height="4" rx="2"/><rect fill="#FDE5D8" x="44" y="65" width="20" height="4" rx="2"/><rect fill="#FC8A51" x="36" y="80" width="20" height="4" rx="2"/><rect fill="#FDE5D8" x="20" y="80" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="20" y="48" width="12" height="4" rx="2"/><rect fill="#FC8A51" x="36" y="48" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="60" y="80" width="12" height="4" rx="2"/><rect fill="#6B4FBB" x="52" y="48" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="68" y="48" width="12" height="4" rx="2"/></g><use stroke="#B5A7DD" mask="url(#f)" stroke-width="8" fill="#FFF" transform="rotate(5 171.616 144.96)" xlink:href="#b"/><path d="M58 132c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#C1E7D0"/><path d="M90.143 132c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M74.686 133.875l-3.18-3.18c-.29-.29-.77-.296-1.06-.005l-1.55 1.55c-.287.287-.29.766.004 1.06l4.92 4.92c.504.504 1.32.504 1.823 0l.654-.653 7.804-7.804c.3-.3.29-.77-.005-1.067l-1.578-1.58c-.302-.3-.775-.298-1.068-.004l-6.764 6.763z" fill="#31AF64"/><path d="M4 66c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18S4 75.94 4 66z" fill="#D5ECF7"/><path d="M36.143 66c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M22 55.714c5.68 0 10.286 4.605 10.286 10.286 0 5.68-4.605 10.286-10.286 10.286-3.45 0-6.505-1.7-8.37-4.307L22 66V55.714z" fill="#2D9FD8"/><g transform="rotate(-8 748.533 18.147)"><use stroke="#FDE5D8" mask="url(#g)" stroke-width="8" fill="#FFF" xlink:href="#c"/><path d="M31 46.584c1.766-.772 3-2.534 3-4.584 0-2.76-2.24-5-5-5s-5 2.24-5 5c0 2.05 1.234 3.812 3 4.584v3.42c0 1.1.895 1.996 2 1.996 1.112 0 2-.894 2-1.997v-3.42z" fill="#FC8A51"/></g><g transform="translate(0 154)"><use stroke="#E5E5E5" mask="url(#h)" stroke-width="8" fill="#FFF" xlink:href="#d"/><g opacity=".3"><path d="M141.837 104.53l-2.56-7.993-5.074-15.843c-.26-.815-1.398-.815-1.66 0l-5.074 15.843h-16.85l-5.075-15.843c-.26-.815-1.398-.815-1.66 0l-5.073 15.843-2.56 7.993c-.234.73.022 1.528.633 1.98l22.16 16.33 22.16-16.33c.61-.452.866-1.25.632-1.98" fill="#A1A1A1"/><path fill="#5C5C5C" d="M119.044 122.84l8.425-26.303h-16.85l8.424 26.304"/><path fill="#787878" d="M119.044 122.84l-8.425-26.303H98.81l20.232 26.304"/><path fill="#787878" d="M119.044 122.84l8.425-26.303h11.807l-20.233 26.304"/><path d="M98.812 96.537l-2.56 7.993c-.234.73.022 1.528.633 1.98l22.16 16.33L98.81 96.538z" fill="#A1A1A1"/><path d="M98.812 96.537h11.807l-5.075-15.843c-.26-.815-1.398-.815-1.66 0l-5.073 15.843z" fill="#5C5C5C"/><path d="M139.277 96.537l2.56 7.993c.234.73-.022 1.528-.634 1.98l-22.16 16.33 20.234-26.303z" fill="#A1A1A1"/><path d="M139.277 96.537H127.47l5.074-15.843c.26-.815 1.398-.815 1.66 0l5.073 15.843z" fill="#5C5C5C"/></g><path d="M57 18.29c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83H41c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83H77c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm17 24.693c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V28.35c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.633zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V28.35c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.633zm202 32.923c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V61.274c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V61.274c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm202 32.923c0 1.01.895 1.828 2 1.828s2-.82 2-1.83V94.2c0-1.012-.895-1.83-2-1.83s-2 .818-2 1.83v14.63zm-202 0c0 1.01.895 1.828 2 1.828s2-.82 2-1.83V94.2c0-1.012-.895-1.83-2-1.83s-2 .818-2 1.83v14.63zm202 32.922c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V127.12c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V127.12c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm179.023 19.555c-.988.452-1.388 1.55-.894 2.454.493.904 1.694 1.27 2.682.82l14.31-6.545c.99-.452 1.39-1.55.896-2.454-.494-.902-1.696-1.27-2.684-.817l-14.31 6.544zm-32.2 14.723c-.987.452-1.388 1.55-.894 2.454.493.904 1.695 1.27 2.683.818l14.31-6.544c.99-.45 1.39-1.55.895-2.454-.494-.903-1.695-1.27-2.683-.818l-14.31 6.544zm-32.2 14.724c-.987.45-1.387 1.55-.893 2.454.494.903 1.695 1.27 2.683.818l14.31-6.544c.99-.452 1.39-1.55.896-2.454-.495-.904-1.697-1.27-2.685-.818l-14.31 6.544zm-23.67-2.023l-12.186-5.57c-.987-.452-2.19-.086-2.683.817-.494.904-.093 2.003.895 2.454l12.185 5.573c.754.345 1.57.645 2.438.898 1.052.307 2.177-.224 2.513-1.187.335-.962-.246-1.99-1.298-2.298-.677-.197-1.302-.426-1.864-.684zM62.57 168.437c-.988-.452-2.19-.086-2.683.818-.494.903-.094 2.002.894 2.454l14.31 6.544c.988.45 2.19.085 2.683-.818.494-.904.094-2.003-.894-2.454l-14.312-6.544zm-32.2-14.723c-.988-.452-2.19-.086-2.683.818-.494.904-.093 2.003.895 2.454l14.31 6.544c.988.452 2.19.086 2.684-.818.494-.903.093-2.002-.895-2.454l-14.312-6.543z" fill="#EEE"/></g><g><path d="M104 18c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#FADFD9"/><path d="M136.143 18c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M119.43 8.994c0-.707.57-1.28 1.283-1.28h2.574c.71 0 1.284.57 1.284 1.28v10.298c0 .706-.57 1.28-1.283 1.28h-2.574c-.71 0-1.284-.57-1.284-1.28V8.994zm0 15.433c0-.71.57-1.284 1.283-1.284h2.574c.71 0 1.284.57 1.284 1.284V27c0 .71-.57 1.286-1.283 1.286h-2.574c-.71 0-1.284-.57-1.284-1.285v-2.573z" fill="#E75E40"/></g><g><path d="M213 89c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#F6D4DC"/><path d="M245.143 89c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M231 86.348l-3.603-3.602c-.288-.29-.766-.286-1.063.01l-1.578 1.578c-.3.302-.3.773-.01 1.063L228.348 89l-3.602 3.603c-.29.288-.286.766.01 1.063l1.578 1.578c.302.3.773.3 1.063.01L231 91.652l3.603 3.602c.288.29.766.286 1.063-.01l1.578-1.578c.3-.302.3-.773.01-1.063L233.652 89l3.602-3.603c.29-.288.286-.766-.01-1.063l-1.578-1.578c-.302-.3-.773-.3-1.063-.01L231 86.348z" fill="#D22852"/></g></g></svg> \ No newline at end of file
+<svg width="249" height="368" viewBox="891 156 249 368" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="131" height="162" rx="10"/><mask id="e" x="0" y="0" width="131" height="162" fill="#fff"><use xlink:href="#a"/></mask><path d="M223.616 127.958V108.96c0-4.416-3.584-8-8.005-8h-23.985c-2.778 0-5.98 2.014-7.18 4.5l-5.07 10.5h-49.763c-5.527 0-9.996 4.475-9.996 9.997v53.005c0 5.513 4.475 9.997 9.996 9.997h84.01c5.525 0 9.994-4.477 9.994-9.998v-51.004z" id="b"/><mask id="f" x="0" y="0" width="104" height="88" fill="#fff"><use xlink:href="#b"/></mask><path d="M47 25h.996C53.52 25 58 29.472 58 34.99v20.02C58 60.526 53.52 65 47.996 65H10.004C4.48 65 0 60.528 0 55.01V34.99C0 29.474 4.48 25 10.004 25H11v-7c0-9.94 8.06-18 18-18s18 8.06 18 18v7zm-6 0H17v-7c0-6.627 5.373-12 12-12s12 5.373 12 12v7z" id="c"/><mask id="g" x="0" y="0" width="58" height="65" fill="#fff"><use xlink:href="#c"/></mask><path d="M0 10.008C0 4.48 4.476 0 10 0h218c5.523 0 10 4.473 10 10.008v140.94c0 5.53-4.062 11.882-9.08 14.196l-100.84 46.5c-5.015 2.31-13.142 2.312-18.16 0l-100.84-46.5C4.064 162.832 0 156.484 0 150.95V10.007z" id="d"/><mask id="h" x="0" y="0" width="238" height="213.417" fill="#fff"><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(891 156)"><g transform="rotate(8 -266.528 490.3)"><use stroke="#E5E5E5" mask="url(#e)" stroke-width="8" fill="#FFF" xlink:href="#a"/><rect fill="#FC8A51" x="20" y="31" width="12" height="4" rx="2"/><rect fill="#FC8A51" x="60" y="31" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="36" y="31" width="20" height="4" rx="2"/><rect fill="#6B4FBB" x="20" y="65" width="20" height="4" rx="2"/><rect fill="#FDE5D8" x="44" y="65" width="20" height="4" rx="2"/><rect fill="#FC8A51" x="36" y="80" width="20" height="4" rx="2"/><rect fill="#FDE5D8" x="20" y="80" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="20" y="48" width="12" height="4" rx="2"/><rect fill="#FC8A51" x="36" y="48" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="60" y="80" width="12" height="4" rx="2"/><rect fill="#6B4FBB" x="52" y="48" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="68" y="48" width="12" height="4" rx="2"/></g><use stroke="#B5A7DD" mask="url(#f)" stroke-width="8" fill="#FFF" transform="rotate(5 171.616 144.96)" xlink:href="#b"/><path d="M58 132c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#C1E7D0"/><path d="M90.143 132c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M74.686 133.875l-3.18-3.18c-.29-.29-.77-.296-1.06-.005l-1.55 1.55c-.287.287-.29.766.004 1.06l4.92 4.92c.504.504 1.32.504 1.823 0l.654-.653 7.804-7.804c.3-.3.29-.77-.005-1.067l-1.578-1.58c-.302-.3-.775-.298-1.068-.004l-6.764 6.763z" fill="#31AF64"/><path d="M4 66c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18S4 75.94 4 66z" fill="#D5ECF7"/><path d="M36.143 66c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M22 55.714c5.68 0 10.286 4.605 10.286 10.286 0 5.68-4.605 10.286-10.286 10.286-3.45 0-6.505-1.7-8.37-4.307L22 66V55.714z" fill="#2D9FD8"/><g transform="rotate(-8 748.533 18.147)"><use stroke="#FDE5D8" mask="url(#g)" stroke-width="8" fill="#FFF" xlink:href="#c"/><path d="M31 46.584c1.766-.772 3-2.534 3-4.584 0-2.76-2.24-5-5-5s-5 2.24-5 5c0 2.05 1.234 3.812 3 4.584v3.42c0 1.1.895 1.996 2 1.996 1.112 0 2-.894 2-1.997v-3.42z" fill="#FC8A51"/></g><g transform="translate(0 154)"><use stroke="#E5E5E5" mask="url(#h)" stroke-width="8" fill="#FFF" xlink:href="#d"/><g opacity=".3"><path d="M141.837 104.53l-2.56-7.993-5.074-15.843c-.26-.815-1.398-.815-1.66 0l-5.074 15.843h-16.85l-5.075-15.843c-.26-.815-1.398-.815-1.66 0l-5.073 15.843-2.56 7.993c-.234.73.022 1.528.633 1.98l22.16 16.33 22.16-16.33c.61-.452.866-1.25.632-1.98" fill="#A1A1A1"/><path fill="#5C5C5C" d="M119.044 122.84l8.425-26.303h-16.85l8.424 26.304"/><path fill="#787878" d="M119.044 122.84l-8.425-26.303H98.81l20.232 26.304"/><path fill="#787878" d="M119.044 122.84l8.425-26.303h11.807l-20.233 26.304"/><path d="M98.812 96.537l-2.56 7.993c-.234.73.022 1.528.633 1.98l22.16 16.33L98.81 96.538z" fill="#A1A1A1"/><path d="M98.812 96.537h11.807l-5.075-15.843c-.26-.815-1.398-.815-1.66 0l-5.073 15.843z" fill="#5C5C5C"/><path d="M139.277 96.537l2.56 7.993c.234.73-.022 1.528-.634 1.98l-22.16 16.33 20.234-26.303z" fill="#A1A1A1"/><path d="M139.277 96.537H127.47l5.074-15.843c.26-.815 1.398-.815 1.66 0l5.073 15.843z" fill="#5C5C5C"/></g><path d="M57 18.29c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83H41c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83H77c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm17 24.693c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V28.35c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.633zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V28.35c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.633zm202 32.923c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V61.274c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V61.274c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm202 32.923c0 1.01.895 1.828 2 1.828s2-.82 2-1.83V94.2c0-1.012-.895-1.83-2-1.83s-2 .818-2 1.83v14.63zm-202 0c0 1.01.895 1.828 2 1.828s2-.82 2-1.83V94.2c0-1.012-.895-1.83-2-1.83s-2 .818-2 1.83v14.63zm202 32.922c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V127.12c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V127.12c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm179.023 19.555c-.988.452-1.388 1.55-.894 2.454.493.904 1.694 1.27 2.682.82l14.31-6.545c.99-.452 1.39-1.55.896-2.454-.494-.902-1.696-1.27-2.684-.817l-14.31 6.544zm-32.2 14.723c-.987.452-1.388 1.55-.894 2.454.493.904 1.695 1.27 2.683.818l14.31-6.544c.99-.45 1.39-1.55.895-2.454-.494-.903-1.695-1.27-2.683-.818l-14.31 6.544zm-32.2 14.724c-.987.45-1.387 1.55-.893 2.454.494.903 1.695 1.27 2.683.818l14.31-6.544c.99-.452 1.39-1.55.896-2.454-.495-.904-1.697-1.27-2.685-.818l-14.31 6.544zm-23.67-2.023l-12.186-5.57c-.987-.452-2.19-.086-2.683.817-.494.904-.093 2.003.895 2.454l12.185 5.573c.754.345 1.57.645 2.438.898 1.052.307 2.177-.224 2.513-1.187.335-.962-.246-1.99-1.298-2.298-.677-.197-1.302-.426-1.864-.684zM62.57 168.437c-.988-.452-2.19-.086-2.683.818-.494.903-.094 2.002.894 2.454l14.31 6.544c.988.45 2.19.085 2.683-.818.494-.904.094-2.003-.894-2.454l-14.312-6.544zm-32.2-14.723c-.988-.452-2.19-.086-2.683.818-.494.904-.093 2.003.895 2.454l14.31 6.544c.988.452 2.19.086 2.684-.818.494-.903.093-2.002-.895-2.454l-14.312-6.543z" fill="#EEE"/></g><g><path d="M104 18c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#FADFD9"/><path d="M136.143 18c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M119.43 8.994c0-.707.57-1.28 1.283-1.28h2.574c.71 0 1.284.57 1.284 1.28v10.298c0 .706-.57 1.28-1.283 1.28h-2.574c-.71 0-1.284-.57-1.284-1.28V8.994zm0 15.433c0-.71.57-1.284 1.283-1.284h2.574c.71 0 1.284.57 1.284 1.284V27c0 .71-.57 1.286-1.283 1.286h-2.574c-.71 0-1.284-.57-1.284-1.285v-2.573z" fill="#E75E40"/></g><g><path d="M213 89c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#F6D4DC"/><path d="M245.143 89c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M231 86.348l-3.603-3.602c-.288-.29-.766-.286-1.063.01l-1.578 1.578c-.3.302-.3.773-.01 1.063L228.348 89l-3.602 3.603c-.29.288-.286.766.01 1.063l1.578 1.578c.302.3.773.3 1.063.01L231 91.652l3.603 3.602c.288.29.766.286 1.063-.01l1.578-1.578c.3-.302.3-.773.01-1.063L233.652 89l3.602-3.603c.29-.288.286-.766-.01-1.063l-1.578-1.578c-.302-.3-.773-.3-1.063-.01L231 86.348z" fill="#D22852"/></g></g></svg>
diff --git a/app/views/shared/icons/_icon_mr_issue.svg b/app/views/shared/icons/_icon_mr_issue.svg
index ae219a3ded2..a56af9c556c 100644
--- a/app/views/shared/icons/_icon_mr_issue.svg
+++ b/app/views/shared/icons/_icon_mr_issue.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill-rule="evenodd"><path d="m8.411 1.012c-.136-.008-.273-.012-.411-.012-3.866 0-7 3.134-7 7 0 3.866 3.134 7 7 7 3.866 0 7-3.134 7-7 0-.138-.004-.275-.012-.411-.464.201-.964.334-1.488.386 0 .008 0 .016 0 .025 0 3.038-2.462 5.5-5.5 5.5-3.038 0-5.5-2.462-5.5-5.5 0-3.038 2.462-5.5 5.5-5.5.008 0 .016 0 .025 0 .052-.524.185-1.024.386-1.488"/><path d="m12 2h-1.01c-.54 0-.991.448-.991 1 0 .556.444 1 .991 1h1.01v1.01c0 .54.448.991 1 .991.556 0 1-.444 1-.991v-1.01h1.01c.54 0 .991-.448.991-1 0-.556-.444-1-.991-1h-1.01v-1.01c0-.54-.448-.991-1-.991-.556 0-1 .444-1 .991v1.01m-5 4.01c0-.557.444-1.01 1-1.01.552 0 1 .443 1 1.01v1.981c0 .557-.444 1.01-1 1.01-.552 0-1-.443-1-1.01v-1.981m1 5.991c.552 0 1-.448 1-1 0-.552-.448-1-1-1-.552 0-1 .448-1 1 0 .552.448 1 1 1"/></g></svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill-rule="evenodd"><path d="m8.411 1.012c-.136-.008-.273-.012-.411-.012-3.866 0-7 3.134-7 7 0 3.866 3.134 7 7 7 3.866 0 7-3.134 7-7 0-.138-.004-.275-.012-.411-.464.201-.964.334-1.488.386 0 .008 0 .016 0 .025 0 3.038-2.462 5.5-5.5 5.5-3.038 0-5.5-2.462-5.5-5.5 0-3.038 2.462-5.5 5.5-5.5.008 0 .016 0 .025 0 .052-.524.185-1.024.386-1.488"/><path d="m12 2h-1.01c-.54 0-.991.448-.991 1 0 .556.444 1 .991 1h1.01v1.01c0 .54.448.991 1 .991.556 0 1-.444 1-.991v-1.01h1.01c.54 0 .991-.448.991-1 0-.556-.444-1-.991-1h-1.01v-1.01c0-.54-.448-.991-1-.991-.556 0-1 .444-1 .991v1.01m-5 4.01c0-.557.444-1.01 1-1.01.552 0 1 .443 1 1.01v1.981c0 .557-.444 1.01-1 1.01-.552 0-1-.443-1-1.01v-1.981m1 5.991c.552 0 1-.448 1-1 0-.552-.448-1-1-1-.552 0-1 .448-1 1 0 .552.448 1 1 1"/></g></svg>
diff --git a/app/views/shared/icons/_icon_play.svg b/app/views/shared/icons/_icon_play.svg
index e965afa9a56..4c69fc99a9e 100644
--- a/app/views/shared/icons/_icon_play.svg
+++ b/app/views/shared/icons/_icon_play.svg
@@ -1,3 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11" class="icon-play">
- <path fill-rule="evenodd" d="m9.283 6.47l-7.564 4.254c-.949.534-1.719.266-1.719-.576v-9.292c0-.852.756-1.117 1.719-.576l7.564 4.254c.949.534.963 1.392 0 1.934"/>
- </svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11" class="icon-play"><path fill-rule="evenodd" d="m9.283 6.47l-7.564 4.254c-.949.534-1.719.266-1.719-.576v-9.292c0-.852.756-1.117 1.719-.576l7.564 4.254c.949.534.963 1.392 0 1.934"/></svg>
diff --git a/app/views/shared/icons/_icon_stopwatch.svg b/app/views/shared/icons/_icon_stopwatch.svg
index f20de04538e..6c2a8b2773f 100644
--- a/app/views/shared/icons/_icon_stopwatch.svg
+++ b/app/views/shared/icons/_icon_stopwatch.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14" enable-background="new 0 0 12 14"><path d="m11.5 2.4l-1.3-1.1-1 1.1 1.4 1.1.9-1.1"/><path d="m6.8 2v-.5h.5v-1.5h-2.6v1.5h.5v.5c-2.9.4-5.2 2.9-5.2 6 0 3.3 2.7 6 6 6s6-2.7 6-6c0-3-2.3-5.6-5.2-6m-.8 10.5c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5 4.5 2 4.5 4.5-2 4.5-4.5 4.5"/><path d="m6.2 8.9h-.5c-.1 0-.2-.1-.2-.2v-3.5c0-.1.1-.2.2-.2h.5c.1 0 .2.1.2.2v3.5c0 .1-.1.2-.2.2"/></svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14" enable-background="new 0 0 12 14"><path d="m11.5 2.4l-1.3-1.1-1 1.1 1.4 1.1.9-1.1"/><path d="m6.8 2v-.5h.5v-1.5h-2.6v1.5h.5v.5c-2.9.4-5.2 2.9-5.2 6 0 3.3 2.7 6 6 6s6-2.7 6-6c0-3-2.3-5.6-5.2-6m-.8 10.5c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5 4.5 2 4.5 4.5-2 4.5-4.5 4.5"/><path d="m6.2 8.9h-.5c-.1 0-.2-.1-.2-.2v-3.5c0-.1.1-.2.2-.2h.5c.1 0 .2.1.2.2v3.5c0 .1-.1.2-.2.2"/></svg>
diff --git a/app/views/shared/icons/_icon_timer.svg b/app/views/shared/icons/_icon_timer.svg
index 0b1e5804427..572a31ebcca 100644
--- a/app/views/shared/icons/_icon_timer.svg
+++ b/app/views/shared/icons/_icon_timer.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><g fill="#8F8F8F" fill-rule="evenodd"><path d="M29.513 10.134A15.922 15.922 0 0 0 23 7.28V6h2.993C26.55 6 27 5.552 27 5V2a1 1 0 0 0-1.007-1H14.007C13.45 1 13 1.448 13 2v3a1 1 0 0 0 1.007 1H17v1.28C9.597 8.686 4 15.19 4 23c0 8.837 7.163 16 16 16s16-7.163 16-16c0-3.461-1.099-6.665-2.967-9.283l1.327-1.58a2.498 2.498 0 0 0-.303-3.53 2.499 2.499 0 0 0-3.528.315l-1.016 1.212zM20 34c6.075 0 11-4.925 11-11s-4.925-11-11-11S9 16.925 9 23s4.925 11 11 11z"/><path d="M19 21h-4.002c-.552 0-.998.452-.998 1.01v1.98c0 .567.447 1.01.998 1.01h7.004c.274 0 .521-.111.701-.291a.979.979 0 0 0 .297-.704v-8.01c0-.54-.452-.995-1.01-.995h-1.98a.997.997 0 0 0-1.01.995V21z"/></g></svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><g fill="#8F8F8F" fill-rule="evenodd"><path d="M29.513 10.134A15.922 15.922 0 0 0 23 7.28V6h2.993C26.55 6 27 5.552 27 5V2a1 1 0 0 0-1.007-1H14.007C13.45 1 13 1.448 13 2v3a1 1 0 0 0 1.007 1H17v1.28C9.597 8.686 4 15.19 4 23c0 8.837 7.163 16 16 16s16-7.163 16-16c0-3.461-1.099-6.665-2.967-9.283l1.327-1.58a2.498 2.498 0 0 0-.303-3.53 2.499 2.499 0 0 0-3.528.315l-1.016 1.212zM20 34c6.075 0 11-4.925 11-11s-4.925-11-11-11S9 16.925 9 23s4.925 11 11 11z"/><path d="M19 21h-4.002c-.552 0-.998.452-.998 1.01v1.98c0 .567.447 1.01.998 1.01h7.004c.274 0 .521-.111.701-.291a.979.979 0 0 0 .297-.704v-8.01c0-.54-.452-.995-1.01-.995h-1.98a.997.997 0 0 0-1.01.995V21z"/></g></svg>
diff --git a/app/views/shared/icons/_illustration_no_commits.svg b/app/views/shared/icons/_illustration_no_commits.svg
index 4f9d9add60d..34f177d7efa 100644
--- a/app/views/shared/icons/_illustration_no_commits.svg
+++ b/app/views/shared/icons/_illustration_no_commits.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 168 107" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="#eee" fill-rule="evenodd"><path d="m4.01 2h1.102c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-1.102c-2.218 0-4.01 1.788-4.01 4 0 .552.448 1 1 1 .552 0 1-.448 1-1 0-1.108.892-2 2.01-2m12.702 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m8.088 0c.822 0 1.554.503 1.86 1.254.208.512.791.758 1.303.55.512-.208.758-.791.55-1.303-.609-1.497-2.069-2.5-3.712-2.5h-2.188c-.552 0-1 .448-1 1 0 .552.448 1 1 1h2.188m2.01 12.518c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 6.282c0 1.108-.892 2-2.01 2h-.72c-.552 0-1 .448-1 1 0 .552.448 1 1 1h.72c2.218 0 4.01-1.788 4.01-4v-.382c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.382m-14.325 2c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-8.47 0c-.755 0-1.438-.424-1.782-1.085-.255-.49-.859-.681-1.349-.426-.49.255-.681.859-.426 1.349.684 1.316 2.046 2.162 3.556 2.162h2.57c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-2.57m-2.01-12.136c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-6.664c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.764c0 .552.448 1 1 1 .552 0 1-.448 1-1v-.764" id="0"/><circle cx="21" cy="24" r="10"/><rect width="33" height="3" x="37" y="18" rx="1.5" id="1"/><rect width="53" height="3" x="37" y="27" rx="1.5" id="2"/><path d="m131 29c0 .552.447.999.996.999h22.01c.545 0 .996-.451.996-.999v-9c0-.552-.447-.999-.996-.999h-22.01c-.545 0-.996.451-.996.999v9m.996-12h22.01c1.655 0 2.996 1.344 2.996 2.999v9c0 1.657-1.35 2.999-2.996 2.999h-22.01c-1.655 0-2.996-1.344-2.996-2.999v-9c0-1.657 1.35-2.999 2.996-2.999" id="3"/><g transform="translate(0 59)"><use xlink:href="#0"/><circle cx="21" cy="24" r="10"/><use xlink:href="#1"/><use xlink:href="#2"/><use xlink:href="#3"/></g></g></svg> \ No newline at end of file
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 168 107" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="#eee" fill-rule="evenodd"><path d="m4.01 2h1.102c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-1.102c-2.218 0-4.01 1.788-4.01 4 0 .552.448 1 1 1 .552 0 1-.448 1-1 0-1.108.892-2 2.01-2m12.702 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m8.088 0c.822 0 1.554.503 1.86 1.254.208.512.791.758 1.303.55.512-.208.758-.791.55-1.303-.609-1.497-2.069-2.5-3.712-2.5h-2.188c-.552 0-1 .448-1 1 0 .552.448 1 1 1h2.188m2.01 12.518c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 6.282c0 1.108-.892 2-2.01 2h-.72c-.552 0-1 .448-1 1 0 .552.448 1 1 1h.72c2.218 0 4.01-1.788 4.01-4v-.382c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.382m-14.325 2c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-8.47 0c-.755 0-1.438-.424-1.782-1.085-.255-.49-.859-.681-1.349-.426-.49.255-.681.859-.426 1.349.684 1.316 2.046 2.162 3.556 2.162h2.57c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-2.57m-2.01-12.136c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-6.664c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.764c0 .552.448 1 1 1 .552 0 1-.448 1-1v-.764" id="0"/><circle cx="21" cy="24" r="10"/><rect width="33" height="3" x="37" y="18" rx="1.5" id="1"/><rect width="53" height="3" x="37" y="27" rx="1.5" id="2"/><path d="m131 29c0 .552.447.999.996.999h22.01c.545 0 .996-.451.996-.999v-9c0-.552-.447-.999-.996-.999h-22.01c-.545 0-.996.451-.996.999v9m.996-12h22.01c1.655 0 2.996 1.344 2.996 2.999v9c0 1.657-1.35 2.999-2.996 2.999h-22.01c-1.655 0-2.996-1.344-2.996-2.999v-9c0-1.657 1.35-2.999 2.996-2.999" id="3"/><g transform="translate(0 59)"><use xlink:href="#0"/><circle cx="21" cy="24" r="10"/><use xlink:href="#1"/><use xlink:href="#2"/><use xlink:href="#3"/></g></g></svg>
diff --git a/app/views/shared/icons/_members.svg b/app/views/shared/icons/_members.svg
deleted file mode 100644
index f8043b31fe8..00000000000
--- a/app/views/shared/icons/_members.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg width="22px" height="16px" viewBox="0 0 22 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
- <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
- <title>Group</title>
- <desc>Created with Sketch.</desc>
- <defs></defs>
- <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
- <g id="Group" fill="#7E7C7C">
- <path d="M6.4357,11.8588 C7.1487,11.2798 7.8797,10.7808 8.5357,10.3708 C8.5837,10.3008 8.6187,10.2338 8.6187,10.1768 L8.6187,8.8088 C8.9197,8.5218 9.0927,8.1248 9.0927,7.7028 L9.0927,5.3748 C9.0927,3.9478 7.9187,2.7858 6.4757,2.7858 L5.9687,2.7858 C4.5247,2.7858 3.3507,3.9478 3.3507,5.3748 L3.3507,7.7028 C3.3507,8.1248 3.5247,8.5218 3.8247,8.8088 L3.8247,10.5838 C3.2537,10.8738 1.8797,11.6198 0.5967,12.6618 C0.2177,12.9698 -0.0003,13.4258 -0.0003,13.9138 L-0.0003,15.5088 C-0.0003,15.5438 0.0857,15.7668 0.3467,15.7778 C1.3257,15.8198 3.8417,15.8328 5.9617,15.9038 C5.8337,15.8148 5.7447,15.6748 5.7447,15.5088 L5.7447,13.5498 C5.7447,12.9848 5.9967,12.2158 6.4357,11.8588" id="Fill-1"></path>
- <path d="M21.3092,12.1 C19.6932,10.787 17.9592,9.86 17.3042,9.53 L17.3042,7.235 C17.6722,6.9 17.8862,6.428 17.8862,5.925 L17.8862,3.066 C17.8862,1.376 16.4952,0 14.7852,0 L14.1632,0 C12.4532,0 11.0622,1.376 11.0622,3.066 L11.0622,5.925 C11.0622,6.428 11.2752,6.9 11.6442,7.235 L11.6442,9.53 C10.9892,9.86 9.2542,10.787 7.6392,12.1 C7.2002,12.457 6.9482,12.985 6.9482,13.55 L6.9482,15.509 C6.9482,15.78 7.1702,16 7.4442,16 L14.1172,16 L14.1172,11.704 C12.6812,11.595 11.5652,10.853 11.5652,9.945 C11.5652,9.804 11.5982,9.669 11.6482,9.538 C11.9502,10.326 13.0982,10.913 14.4762,10.913 C15.8532,10.913 17.0012,10.326 17.3032,9.538 C17.3532,9.669 17.3862,9.804 17.3862,9.945 C17.3862,10.793 16.4152,11.5 15.1172,11.679 L15.1172,16 L21.5032,16 C21.7772,16 22.0002,15.78 22.0002,15.509 L22.0002,13.55 C22.0002,12.985 21.7482,12.457 21.3092,12.1" id="Fill-4"></path>
- </g>
- </g>
-</svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_milestones.svg b/app/views/shared/icons/_milestones.svg
deleted file mode 100644
index 3d62ecc0631..00000000000
--- a/app/views/shared/icons/_milestones.svg
+++ /dev/null
@@ -1,15 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg width="16px" height="17px" viewBox="0 0 16 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
- <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
- <title>Group</title>
- <desc>Created with Sketch.</desc>
- <defs></defs>
- <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
- <g id="Group" fill="#7E7C7C">
- <path d="M15.1111,1 L0.8891,1 C0.3981,1 0.0001,1.446 0.0001,1.996 L0.0001,15.945 C0.0001,16.495 0.3981,16.941 0.8891,16.941 L15.1111,16.941 C15.6021,16.941 16.0001,16.495 16.0001,15.945 L16.0001,1.996 C16.0001,1.446 15.6021,1 15.1111,1 L15.1111,1 L15.1111,1 Z M14.0001,6.0002 L14.0001,14.949 L2.0001,14.949 L2.0001,6.0002 L14.0001,6.0002 Z M14.0001,4.0002 L14.0001,2.993 L2.0001,2.993 L2.0001,4.0002 L14.0001,4.0002 Z" id="Combined-Shape"></path>
- <polygon id="Fill-11" points="3 2.0002 5 2.0002 5 0.0002 3 0.0002"></polygon>
- <polygon id="Fill-16" points="11 2.0002 13 2.0002 13 0.0002 11 0.0002"></polygon>
- <path d="M5.37709616,11.5511984 L6.92309616,12.7821984 C7.35112915,13.123019 7.97359761,13.0565604 8.32002627,12.6330535 L10.7740263,9.63305349 C11.1237073,9.20557058 11.0606364,8.57555475 10.6331535,8.22587373 C10.2056706,7.87619272 9.57565475,7.93926361 9.22597373,8.36674651 L6.77197373,11.3667465 L8.16890384,11.2176016 L6.62290384,9.98660159 C6.19085236,9.6425813 5.56172188,9.71394467 5.21770159,10.1459962 C4.8736813,10.5780476 4.94504467,11.2071781 5.37709616,11.5511984 L5.37709616,11.5511984 Z" id="Stroke-21"></path>
- </g>
- </g>
-</svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_mr.svg b/app/views/shared/icons/_mr.svg
deleted file mode 100644
index dd3dbcc4473..00000000000
--- a/app/views/shared/icons/_mr.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
- <!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
- <title>Group</title>
- <desc>Created with Sketch.</desc>
- <defs></defs>
- <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
- <g id="Group" fill="#7E7C7C">
- <path d="M15.1111,0 L0.8891,0 C0.3981,0 0.0001,0.446 0.0001,0.996 L0.0001,14.945 C0.0001,15.495 0.3981,15.941 0.8891,15.941 L15.1111,15.941 C15.6021,15.941 16.0001,15.495 16.0001,14.945 L16.0001,0.996 C16.0001,0.446 15.6021,0 15.1111,0 L15.1111,0 L15.1111,0 Z M2.0001,13.949 L14.0001,13.949 L14.0001,1.993 L2.0001,1.993 L2.0001,13.949 Z M2,5.0002 L14,5.0002 L14,3.0002 L2,3.0002 L2,5.0002 Z" id="Combined-Shape"></path>
- <path d="M8.547,12.0002 L12,12.0002 L12,10.0002 L8.547,10.0002 L8.547,12.0002 Z M5.2029,12 L3.9999,10.867 L5.2029,9.501 L3.9999,8.181 L5.2029,7 L7.4529,9.499 L5.2029,12 Z" id="Combined-Shape"></path>
- </g>
- </g>
-</svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_mr_bold.svg b/app/views/shared/icons/_mr_bold.svg
new file mode 100644
index 00000000000..2daa55a8652
--- /dev/null
+++ b/app/views/shared/icons/_mr_bold.svg
@@ -0,0 +1 @@
+<svg width="15" height="20" viewBox="0 0 12 14" xmlns="http://www.w3.org/2000/svg"><path d="M1 4.967a2.15 2.15 0 1 1 2.3 0v5.066a2.15 2.15 0 1 1-2.3 0V4.967zm7.85 5.17V5.496c0-.745-.603-1.346-1.35-1.346V6l-3-3 3-3v1.85c2.016 0 3.65 1.63 3.65 3.646v4.45a2.15 2.15 0 1 1-2.3.191z" fill-rule="nonzero"/></svg>
diff --git a/app/views/shared/icons/_pipelines.svg b/app/views/shared/icons/_pipelines.svg
deleted file mode 100644
index 794e8a27025..00000000000
--- a/app/views/shared/icons/_pipelines.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
- <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
- <title>Pasted Image 246</title>
- <desc>Created with Sketch.</desc>
- <defs></defs>
- <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
- <path d="M12.5,14 C11.672,14 11,13.328 11,12.5 C11,11.672 11.672,11 12.5,11 C13.328,11 14,11.672 14,12.5 C14,13.328 13.328,14 12.5,14 M12.5,9 L3.5,9 C1.567,9 0,10.567 0,12.5 C0,14.433 1.567,16 3.5,16 L12.5,16 C14.433,16 16,14.433 16,12.5 C16,10.567 14.433,9 12.5,9 M3.5,2 C4.328,2 5,2.672 5,3.5 C5,4.328 4.328,5 3.5,5 C2.672,5 2,4.328 2,3.5 C2,2.672 2.672,2 3.5,2 M3.5,7 L12.5,7 C14.433,7 16,5.433 16,3.5 C16,1.567 14.433,0 12.5,0 L3.5,0 C1.567,0 0,1.567 0,3.5 C0,5.433 1.567,7 3.5,7" id="Pasted-Image-246" fill="#303030"></path>
- </g>
-</svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_wiki.svg b/app/views/shared/icons/_wiki.svg
deleted file mode 100644
index 182d91e23aa..00000000000
--- a/app/views/shared/icons/_wiki.svg
+++ /dev/null
@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
- <!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
- <title>Pasted Image 241</title>
- <desc>Created with Sketch.</desc>
- <defs></defs>
- <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
- <path d="M2.004,12.9999459 L3.939,12.9999459 L3.939,4.99994585 L2.004,4.99994585 L2.004,12.9999459 Z M7.017,9.99994585 L13.018,9.99994585 L13.018,8.99994585 L7.017,8.99994585 L7.017,9.99994585 Z M7.017,7.99994585 L13.018,7.99994585 L13.018,6.99994585 L7.017,6.99994585 L7.017,7.99994585 Z M7.017,5.99994585 L13.018,5.99994585 L13.018,4.99994585 L7.017,4.99994585 L7.017,5.99994585 Z M14.754,-5.41499267e-05 L4.938,-5.41499267e-05 C4.386,-5.41499267e-05 3.938,0.44794585 3.938,0.99994585 L3.938,2.99994585 L1,2.99994585 C0.448,2.99994585 0,3.44794585 0,3.99994585 L0,12.9999459 C0.037,13.4999459 -0.25,16.0509459 3.938,15.9999459 L12.408,15.9999459 C12.408,15.9999459 15.754,15.9169459 15.754,13.9999459 L15.754,0.99994585 C15.754,0.44794585 15.306,-5.41499267e-05 14.754,-5.41499267e-05 L14.754,-5.41499267e-05 Z" id="Pasted-Image-241" fill="#7E7D7D"></path>
- </g>
-</svg> \ No newline at end of file
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index e6cac8cd214..4c3c81a2f56 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -25,7 +25,7 @@
%button.btn.btn-link
= icon('search')
%span
- Keep typing and press Enter
+ Press Enter or click to search
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 884bd3ca9ca..92d2d93a732 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -13,15 +13,12 @@
%a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" }
= sidebar_gutter_toggle_icon
- if current_user
- %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", "aria-label" => (todo.nil? ? "Add todo" : "Mark done"), data: { todo_text: "Add todo", mark_text: "Mark done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } }
- %span.js-issuable-todo-text
- - if todo
- Mark done
- - else
- Add todo
- = icon('spin spinner', class: 'hidden js-issuable-todo-loading', 'aria-hidden': 'true')
+ = render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
+ - if current_user
+ .block.todo.hide-expanded
+ = render "shared/issuable/sidebar_todo", todo: todo, issuable: issuable, is_collapsed: true
.block.assignee
.sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee.name if issuable.assignee) }
- if issuable.assignee
@@ -121,7 +118,7 @@
- selected_labels = issuable.labels
.block.labels
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
- = icon('tags', class: 'hidden', 'aria-hidden': 'true')
+ = icon('tags', 'aria-hidden': 'true')
%span
= selected_labels.size
.title.hide-collapsed
diff --git a/app/views/shared/issuable/_sidebar_todo.html.haml b/app/views/shared/issuable/_sidebar_todo.html.haml
new file mode 100644
index 00000000000..574e2958ae8
--- /dev/null
+++ b/app/views/shared/issuable/_sidebar_todo.html.haml
@@ -0,0 +1,15 @@
+- is_collapsed = local_assigns.fetch(:is_collapsed, false)
+- mark_content = is_collapsed ? icon('check-square', class: 'todo-undone') : 'Mark done'
+- todo_content = is_collapsed ? icon('plus-square') : 'Add todo'
+
+%button.issuable-todo-btn.js-issuable-todo{ type: 'button',
+ class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn pull-right'),
+ title: (todo.nil? ? 'Add todo' : 'Mark done'),
+ 'aria-label' => (todo.nil? ? 'Add todo' : 'Mark done'),
+ data: issuable_todo_button_data(issuable, todo, is_collapsed) }
+ %span.issuable-todo-inner.js-issuable-todo-inner<
+ - if todo
+ = mark_content
+ - else
+ = todo_content
+ = icon('spin spinner', 'aria-hidden': 'true')
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 8e721c9c8dd..a5aa768b1b2 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -31,7 +31,7 @@
Joined #{time_ago_with_tooltip(member.created_at)}
- if member.expires?
·
- %span{ class: ('text-warning' if member.expires_soon?) }
+ %span{ class: "#{"text-warning" if member.expires_soon?} has-tooltip", title: member.expires_at.to_time.in_time_zone.to_s(:medium) }
Expires in #{distance_of_time_in_words_to_now(member.expires_at)}
- else
@@ -47,7 +47,7 @@
- current_resource = @project || @group
.controls.member-controls
- if show_controls && member.source == current_resource
- - if user != current_user
+ - if user != current_user && can_admin_member
= form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f|
= f.hidden_field :access_level
.member-form-control.dropdown.append-right-5
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
new file mode 100644
index 00000000000..2810f1377b2
--- /dev/null
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -0,0 +1,131 @@
+- affix_offset = local_assigns.fetch(:affix_offset, "102")
+- project = local_assigns[:project]
+
+%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix" }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
+ .issuable-sidebar.milestone-sidebar
+ .block.milestone-progress.issuable-sidebar-header
+ %a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => "Toggle sidebar" }
+ = sidebar_gutter_toggle_icon
+
+ .sidebar-collapsed-icon
+ %span== #{milestone.percent_complete(current_user)}%
+ = milestone_progress_bar(milestone)
+ .title.hide-collapsed
+ %strong.bold== #{milestone.percent_complete(current_user)}%
+ %span.hide-collapsed
+ complete
+ .value.hide-collapsed
+ = milestone_progress_bar(milestone)
+
+ .block.start_date.hide-collapsed
+ .title
+ Start date
+ - if @project && can?(current_user, :admin_milestone, @project)
+ = link_to 'Edit', edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: 'edit-link pull-right'
+ .value
+ %span.value-content
+ - if milestone.start_date
+ %span.bold= milestone.start_date.to_s(:medium)
+ - else
+ %span.no-value No start date
+
+ .block.due_date
+ .sidebar-collapsed-icon
+ = icon('calendar', 'aria-hidden': 'true')
+ %span.collapsed-milestone-date
+ - if milestone.start_date && milestone.due_date
+ - if milestone.start_date.year == milestone.due_date.year
+ .milestone-date= milestone.start_date.strftime('%b %-d')
+ - else
+ .milestone-date= milestone.start_date.strftime('%b %-d %Y')
+ .date-separator -
+ .due_date= milestone.due_date.strftime('%b %-d %Y')
+ - elsif milestone.start_date
+ From
+ .milestone-date= milestone.start_date.strftime('%b %-d %Y')
+ - elsif milestone.due_date
+ Until
+ .milestone-date= milestone.due_date.strftime('%b %-d %Y')
+ - else
+ None
+ .title.hide-collapsed
+ Due date
+ - if @project && can?(current_user, :admin_milestone, @project)
+ = link_to 'Edit', edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: 'edit-link pull-right'
+ .value.hide-collapsed
+ %span.value-content
+ - if milestone.due_date
+ %span.bold= milestone.due_date.to_s(:medium)
+ - else
+ %span.no-value No due date
+ - remaining_days = milestone_remaining_days(milestone)
+ - if remaining_days.present?
+ = surround '(', ')' do
+ %span.remaining-days= remaining_days
+
+ - if !project || can?(current_user, :read_issue, project)
+ .block
+ .sidebar-collapsed-icon
+ %strong
+ = icon('hashtag', 'aria-hidden': 'true')
+ %span= milestone.issues_visible_to_user(current_user).count
+ .title.hide-collapsed
+ Issues
+ %span.badge= milestone.issues_visible_to_user(current_user).count
+ - if project && can?(current_user, :create_issue, project)
+ = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "pull-right", title: "New Issue" do
+ New issue
+ .value.hide-collapsed.bold
+ %span.milestone-stat
+ = link_to milestones_browse_issuables_path(milestone, type: :issues) do
+ Open:
+ = milestone.issues_visible_to_user(current_user).opened.count
+ %span.milestone-stat
+ = link_to milestones_browse_issuables_path(milestone, type: :issues, state: 'closed') do
+ Closed:
+ = milestone.issues_visible_to_user(current_user).closed.count
+
+ .block
+ .sidebar-collapsed-icon
+ %strong
+ = icon('exclamation', 'aria-hidden': 'true')
+ %span= milestone.issues_visible_to_user(current_user).count
+ .title.hide-collapsed
+ Merge requests
+ %span.badge= milestone.merge_requests.count
+ .value.hide-collapsed.bold
+ - if !project || can?(current_user, :read_merge_request, project)
+ %span.milestone-stat
+ = link_to milestones_browse_issuables_path(milestone, type: :merge_requests) do
+ Open:
+ = milestone.merge_requests.opened.count
+ %span.milestone-stat
+ = link_to milestones_browse_issuables_path(milestone, type: :merge_requests, state: 'closed') do
+ Closed:
+ = milestone.merge_requests.closed.count
+ %span.milestone-stat
+ = link_to milestones_browse_issuables_path(milestone, type: :merge_requests, state: 'merged') do
+ Merged:
+ = milestone.merge_requests.merged.count
+ - else
+ %span.milestone-stat
+ Open:
+ = milestone.merge_requests.opened.count
+ %span.milestone-stat
+ Closed:
+ = milestone.merge_requests.closed.count
+ %span.milestone-stat
+ Merged:
+ = milestone.merge_requests.merged.count
+
+ - milestone_ref = milestone.try(:to_reference, full: true)
+ - if milestone_ref.present?
+ .block.reference
+ .sidebar-collapsed-icon.dont-change-state
+ = clipboard_button(clipboard_text: milestone_ref, title: "Copy reference to clipboard", placement: "left")
+ .cross-project-reference.hide-collapsed
+ %span
+ Reference:
+ %cite{ title: milestone_ref }
+ = milestone_ref
+ = clipboard_button(clipboard_text: milestone_ref, title: "Copy reference to clipboard", placement: "left")
diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml
deleted file mode 100644
index 78079f633d5..00000000000
--- a/app/views/shared/milestones/_summary.html.haml
+++ /dev/null
@@ -1,45 +0,0 @@
-- project = local_assigns[:project]
-
-.context.prepend-top-default
- .milestone-summary
- %h4 Progress
-
- .milestone-stats-and-buttons
- .milestone-stats
- - if !project || can?(current_user, :read_issue, project)
- %span.milestone-stat.with-drilldown
- %strong= milestone.issues_visible_to_user(current_user).size
- issues:
- %span.milestone-stat
- %strong= milestone.issues_visible_to_user(current_user).opened.size
- open and
- %strong= milestone.issues_visible_to_user(current_user).closed.size
- closed
- %span.milestone-stat.with-drilldown
- %strong= milestone.merge_requests.size
- merge requests:
- %span.milestone-stat
- %strong= milestone.merge_requests.opened.size
- open and
- %strong= milestone.merge_requests.merged.size
- merged
- %span.milestone-stat
- %strong== #{milestone.percent_complete(current_user)}%
- complete
- - remaining_days = milestone_remaining_days(milestone)
- - if remaining_days.present?
- %span.milestone-stat
- %span.remaining-days= remaining_days
-
- .milestone-progress-buttons
- %span.tab-issues-buttons
- - if project
- - if can?(current_user, :create_issue, project)
- = link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn", title: "New Issue" do
- New Issue
- - if can?(current_user, :read_issue, project)
- = link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn"
- %span.tab-merge-requests-buttons.hidden
- = link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn"
-
- = milestone_progress_bar(milestone)
diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml
index a0e9ec46220..9a4502873ef 100644
--- a/app/views/shared/milestones/_tabs.html.haml
+++ b/app/views/shared/milestones/_tabs.html.haml
@@ -1,26 +1,29 @@
-%ul.nav-links.no-top.no-bottom
- - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project)
- %li.active
- = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
- Issues
- %span.badge= milestone.issues_visible_to_user(current_user).size
+.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ %ul.nav-links.scrolling-tabs
+ - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project)
+ %li.active
+ = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
+ Issues
+ %span.badge= milestone.issues_visible_to_user(current_user).size
+ %li
+ = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
+ Merge Requests
+ %span.badge= milestone.merge_requests.size
+ - else
+ %li.active
+ = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
+ Merge Requests
+ %span.badge= milestone.merge_requests.size
%li
- = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
- Merge Requests
- %span.badge= milestone.merge_requests.size
- - else
- %li.active
- = link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
- Merge Requests
- %span.badge= milestone.merge_requests.size
- %li
- = link_to '#tab-participants', 'data-toggle' => 'tab' do
- Participants
- %span.badge= milestone.participants.count
- %li
- = link_to '#tab-labels', 'data-toggle' => 'tab' do
- Labels
- %span.badge= milestone.labels.count
+ = link_to '#tab-participants', 'data-toggle' => 'tab' do
+ Participants
+ %span.badge= milestone.participants.count
+ %li
+ = link_to '#tab-labels', 'data-toggle' => 'tab' do
+ Labels
+ %span.badge= milestone.labels.count
- show_project_name = local_assigns.fetch(:show_project_name, false)
- show_full_project_name = local_assigns.fetch(:show_full_project_name, false)
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 497446c1ef3..2562f085338 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -3,6 +3,9 @@
- group = local_assigns[:group]
.detail-page-header
+ %a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" }
+ = icon('angle-double-left')
+
.status-box{ class: "status-box-#{milestone.closed? ? 'closed' : 'open'}" }
- if milestone.closed?
Closed
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
index a736bfd91e2..708adbc38f1 100644
--- a/app/views/shared/notifications/_custom_notifications.html.haml
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -25,7 +25,7 @@
.form-group
.checkbox{ class: ("prepend-top-0" if index == 0) }
%label{ for: field_id }
- = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.events[event])
+ = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.public_send(event))
%strong
= notification_event_name(event)
= icon("spinner spin", class: "custom-notification-event-loading")
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 059aeebaf34..761f0b606b5 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -24,7 +24,7 @@
- if project.namespace && !skip_namespace
= project.namespace.human_name
\/
- %span.project-name.filter-title
+ %span.project-name
= project.name
- if show_last_commit_as_description
diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml
index 4afd31f788b..d1e88274878 100644
--- a/app/views/users/calendar_activities.html.haml
+++ b/app/views/users/calendar_activities.html.haml
@@ -18,9 +18,9 @@
= event_action_name(event)
%strong
- if event.note?
- = link_to event.note_target.to_reference, event_note_target_path(event)
+ = link_to event.note_target.to_reference, event_note_target_path(event), class: 'has-tooltip', title: event.target_title
- elsif event.target
- = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target]
+ = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title
at
%strong
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 601187455b3..969ea7ab9e6 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -79,26 +79,29 @@
%p.profile-user-bio
= @user.bio
- %ul.nav-links.center.user-profile-nav
- %li.js-activity-tab
- = link_to user_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
- Activity
- %li.js-groups-tab
- = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do
- Groups
- %li.js-contributed-tab
- = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do
- Contributed projects
- %li.js-projects-tab
- = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do
- Personal projects
- %li.js-snippets-tab
- = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do
- Snippets
+ .scrolling-tabs-container
+ .fade-left= icon('angle-left')
+ .fade-right= icon('angle-right')
+ %ul.nav-links.center.user-profile-nav.scrolling-tabs
+ %li.js-activity-tab
+ = link_to user_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
+ Activity
+ %li.js-groups-tab
+ = link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do
+ Groups
+ %li.js-contributed-tab
+ = link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do
+ Contributed projects
+ %li.js-projects-tab
+ = link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do
+ Personal projects
+ %li.js-snippets-tab
+ = link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do
+ Snippets
%div{ class: container_class }
- - if @user == current_user
- .user-callout{ 'callout-svg' => custom_icon('icon_customization') }
+ - if @user == current_user && !show_user_callout?
+ = render 'shared/user_callout'
.tab-content
#activity.tab-pane
.row-content-block.calender-block.white.second-block.hidden-xs
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 2cd87895c55..015a41b6e82 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -3,20 +3,16 @@ class PostReceive
include DedicatedSidekiqQueue
def perform(repo_path, identifier, changes)
- if repository_storage = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1]['path'].to_s) }
- repo_path.gsub!(repository_storage[1]['path'].to_s, "")
- else
- log("Check gitlab.yml config for correct repositories.storages values. No repository storage path matches \"#{repo_path}\"")
- end
+ repo_relative_path = Gitlab::RepoPath.strip_storage_path(repo_path)
changes = Base64.decode64(changes) unless changes.include?(' ')
# Use Sidekiq.logger so arguments can be correlated with execution
# time and thread ID's.
Sidekiq.logger.info "changes: #{changes.inspect}" if ENV['SIDEKIQ_LOG_ARGUMENTS']
- post_received = Gitlab::GitPostReceive.new(repo_path, identifier, changes)
+ post_received = Gitlab::GitPostReceive.new(repo_relative_path, identifier, changes)
if post_received.project.nil?
- log("Triggered hook for non-existing project with full path \"#{repo_path}\"")
+ log("Triggered hook for non-existing project with full path \"#{repo_relative_path}\"")
return false
end
@@ -25,7 +21,7 @@ class PostReceive
elsif post_received.regular_project?
process_project_changes(post_received)
else
- log("Triggered hook for unidentifiable repository type with full path \"#{repo_path}\"")
+ log("Triggered hook for unidentifiable repository type with full path \"#{repo_relative_path}\"")
false
end
end
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index e9a5bd7f24e..2f7967cf531 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -53,6 +53,8 @@ class ProcessCommitWorker
def update_issue_metrics(commit, author)
mentioned_issues = commit.all_references(author).issues
+ return if mentioned_issues.empty?
+
Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil).
update_all(first_mentioned_in_commit_at: commit.committed_date)
end
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index c8a77e21c12..68a6fd76e70 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -1,6 +1,5 @@
class RepositoryImportWorker
include Sidekiq::Worker
- include Gitlab::ShellAdapter
include DedicatedSidekiqQueue
attr_accessor :project, :current_user
diff --git a/changelogs/unreleased/12818-expose-simple-cicd-status-endpoints-with-status-serializer-gitlab-ci-status-for-pipeline-job-and-merge-request.yml b/changelogs/unreleased/12818-expose-simple-cicd-status-endpoints-with-status-serializer-gitlab-ci-status-for-pipeline-job-and-merge-request.yml
new file mode 100644
index 00000000000..953009213df
--- /dev/null
+++ b/changelogs/unreleased/12818-expose-simple-cicd-status-endpoints-with-status-serializer-gitlab-ci-status-for-pipeline-job-and-merge-request.yml
@@ -0,0 +1,5 @@
+---
+title: Expose CI/CD status API endpoints with Gitlab::Ci::Status facility on pipeline,
+ job and merge request for favicon
+merge_request: 9561
+author: dosuken123
diff --git a/changelogs/unreleased/17325-rugged-gem-update.yml b/changelogs/unreleased/17325-rugged-gem-update.yml
new file mode 100644
index 00000000000..7ca619439c4
--- /dev/null
+++ b/changelogs/unreleased/17325-rugged-gem-update.yml
@@ -0,0 +1,4 @@
+---
+title: Update rugged to 0.25.1.1
+merge_request: 10286
+author: Elan Ruusamäe
diff --git a/changelogs/unreleased/20914-project-home-width.yml b/changelogs/unreleased/20914-project-home-width.yml
new file mode 100644
index 00000000000..323a614f3c8
--- /dev/null
+++ b/changelogs/unreleased/20914-project-home-width.yml
@@ -0,0 +1,4 @@
+---
+title: Limit line length for project home page
+merge_request:
+author:
diff --git a/changelogs/unreleased/22303-symbolic-in-tree.yml b/changelogs/unreleased/22303-symbolic-in-tree.yml
new file mode 100644
index 00000000000..02444f571d0
--- /dev/null
+++ b/changelogs/unreleased/22303-symbolic-in-tree.yml
@@ -0,0 +1,4 @@
+---
+title: Fix symlink icon in project tree
+merge_request: 9780
+author: mhasbini
diff --git a/changelogs/unreleased/22850-404-when-requesting-build-trace.yml b/changelogs/unreleased/22850-404-when-requesting-build-trace.yml
deleted file mode 100644
index 6b442130d9b..00000000000
--- a/changelogs/unreleased/22850-404-when-requesting-build-trace.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Resolve "404 when requesting build trace"
-merge_request: 9759
-author: dosuken123
diff --git a/changelogs/unreleased/23363-use-strong-params-in-wikis-controller.yml b/changelogs/unreleased/23363-use-strong-params-in-wikis-controller.yml
new file mode 100644
index 00000000000..dd342d38fef
--- /dev/null
+++ b/changelogs/unreleased/23363-use-strong-params-in-wikis-controller.yml
@@ -0,0 +1,4 @@
+---
+title: Update wikis_controller.rb to use strong params
+merge_request:
+author:
diff --git a/changelogs/unreleased/23655-api-group-issues.yml b/changelogs/unreleased/23655-api-group-issues.yml
new file mode 100644
index 00000000000..e19e588d09e
--- /dev/null
+++ b/changelogs/unreleased/23655-api-group-issues.yml
@@ -0,0 +1,4 @@
+---
+title: Fix API group/issues default state filter
+merge_request:
+author: Alexander Randa
diff --git a/changelogs/unreleased/23674-simplify-milestone-summary.yml b/changelogs/unreleased/23674-simplify-milestone-summary.yml
new file mode 100644
index 00000000000..7a315c25151
--- /dev/null
+++ b/changelogs/unreleased/23674-simplify-milestone-summary.yml
@@ -0,0 +1,4 @@
+---
+title: Move milestone summary content into the sidebar
+merge_request: 10096
+author:
diff --git a/changelogs/unreleased/24187-set-git-terminal-prompt-env-var-in-initializer.yml b/changelogs/unreleased/24187-set-git-terminal-prompt-env-var-in-initializer.yml
new file mode 100644
index 00000000000..7fe5c8a84af
--- /dev/null
+++ b/changelogs/unreleased/24187-set-git-terminal-prompt-env-var-in-initializer.yml
@@ -0,0 +1,4 @@
+---
+title: Set GIT_TERMINAL_PROMPT env variable in initializer
+merge_request: 10372
+author:
diff --git a/changelogs/unreleased/24784-system-notes-meta-data.yml b/changelogs/unreleased/24784-system-notes-meta-data.yml
new file mode 100644
index 00000000000..757ae9e0527
--- /dev/null
+++ b/changelogs/unreleased/24784-system-notes-meta-data.yml
@@ -0,0 +1,4 @@
+---
+title: Add metadata to system notes
+merge_request: 9964
+author:
diff --git a/changelogs/unreleased/24861-stringify-group-member-details.yml b/changelogs/unreleased/24861-stringify-group-member-details.yml
new file mode 100644
index 00000000000..f56a1060862
--- /dev/null
+++ b/changelogs/unreleased/24861-stringify-group-member-details.yml
@@ -0,0 +1,4 @@
+---
+title: Hide form inputs for group member without editing rights
+merge_request: 7816
+author:
diff --git a/changelogs/unreleased/25332-make-file-templates-easy-to-use-and-discover.yml b/changelogs/unreleased/25332-make-file-templates-easy-to-use-and-discover.yml
new file mode 100644
index 00000000000..fc95858f783
--- /dev/null
+++ b/changelogs/unreleased/25332-make-file-templates-easy-to-use-and-discover.yml
@@ -0,0 +1,4 @@
+---
+title: Remove no-new annotation from file_template_mediator.js.
+merge_request: !9782
+author:
diff --git a/changelogs/unreleased/25556-prevent-users-from-disconnecting-gitlab-account-from-cas.yml b/changelogs/unreleased/25556-prevent-users-from-disconnecting-gitlab-account-from-cas.yml
new file mode 100644
index 00000000000..17e38ba6243
--- /dev/null
+++ b/changelogs/unreleased/25556-prevent-users-from-disconnecting-gitlab-account-from-cas.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent users from disconnecting GitLab account from CAS
+merge_request: 10282
+author:
diff --git a/changelogs/unreleased/27293-remove-repeated-labels.yml b/changelogs/unreleased/27293-remove-repeated-labels.yml
new file mode 100644
index 00000000000..60caa6e971a
--- /dev/null
+++ b/changelogs/unreleased/27293-remove-repeated-labels.yml
@@ -0,0 +1,4 @@
+---
+title: Remove duplicated tokens in issuable search bar
+merge_request:
+author:
diff --git a/changelogs/unreleased/27878-new-service-for-creating-user.yml b/changelogs/unreleased/27878-new-service-for-creating-user.yml
new file mode 100644
index 00000000000..c07f0cef8db
--- /dev/null
+++ b/changelogs/unreleased/27878-new-service-for-creating-user.yml
@@ -0,0 +1,4 @@
+---
+title: Implement user create service
+merge_request: 9220
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/28424-labels-support-color-names-in-backend.yml b/changelogs/unreleased/28424-labels-support-color-names-in-backend.yml
new file mode 100644
index 00000000000..00da1e0fa60
--- /dev/null
+++ b/changelogs/unreleased/28424-labels-support-color-names-in-backend.yml
@@ -0,0 +1,4 @@
+---
+title: Labels support color names in backend
+merge_request: 9725
+author: Dongqing Hu
diff --git a/changelogs/unreleased/28732-expandable-folders.yml b/changelogs/unreleased/28732-expandable-folders.yml
new file mode 100644
index 00000000000..9ae30ba6253
--- /dev/null
+++ b/changelogs/unreleased/28732-expandable-folders.yml
@@ -0,0 +1,4 @@
+---
+title: Add back expandable folder behavior
+merge_request:
+author:
diff --git a/changelogs/unreleased/28799-todo-creation.yml b/changelogs/unreleased/28799-todo-creation.yml
new file mode 100644
index 00000000000..c6e05468568
--- /dev/null
+++ b/changelogs/unreleased/28799-todo-creation.yml
@@ -0,0 +1,4 @@
+---
+title: Create todos only for new mentions
+merge_request:
+author:
diff --git a/changelogs/unreleased/29034-fix-github-importer.yml b/changelogs/unreleased/29034-fix-github-importer.yml
deleted file mode 100644
index 6d08db3d55d..00000000000
--- a/changelogs/unreleased/29034-fix-github-importer.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix name colision when importing GitHub pull requests from forked repositories
-merge_request: 9719
-author:
diff --git a/changelogs/unreleased/29116-maxint-error.yml b/changelogs/unreleased/29116-maxint-error.yml
new file mode 100644
index 00000000000..06e976617d5
--- /dev/null
+++ b/changelogs/unreleased/29116-maxint-error.yml
@@ -0,0 +1,4 @@
+---
+title: Fix projects_limit RangeError on user create
+merge_request:
+author: Alexander Randa
diff --git a/changelogs/unreleased/29341-add-metrics-button-env-overview.yml b/changelogs/unreleased/29341-add-metrics-button-env-overview.yml
new file mode 100644
index 00000000000..16b69235dff
--- /dev/null
+++ b/changelogs/unreleased/29341-add-metrics-button-env-overview.yml
@@ -0,0 +1,4 @@
+---
+title: Add metrics button to environments overview page
+merge_request: 10234
+author:
diff --git a/changelogs/unreleased/29414-fix-toggle-discussion-link-jump.yml b/changelogs/unreleased/29414-fix-toggle-discussion-link-jump.yml
new file mode 100644
index 00000000000..04342f5359d
--- /dev/null
+++ b/changelogs/unreleased/29414-fix-toggle-discussion-link-jump.yml
@@ -0,0 +1,4 @@
+---
+title: Update toggle buttons to be <button>
+merge_request:
+author:
diff --git a/changelogs/unreleased/29432-prevent-click-disabled-btn.yml b/changelogs/unreleased/29432-prevent-click-disabled-btn.yml
new file mode 100644
index 00000000000..f30570cf68b
--- /dev/null
+++ b/changelogs/unreleased/29432-prevent-click-disabled-btn.yml
@@ -0,0 +1,4 @@
+---
+title: Fix project title validation, prevent clicking on disabled button
+merge_request: 9931
+author:
diff --git a/changelogs/unreleased/29492-useless-queries.yml b/changelogs/unreleased/29492-useless-queries.yml
new file mode 100644
index 00000000000..266a04be352
--- /dev/null
+++ b/changelogs/unreleased/29492-useless-queries.yml
@@ -0,0 +1,4 @@
+---
+title: Remove useless queries with false conditions (e.g 1=0)
+merge_request: 10141
+author: mhasbini
diff --git a/changelogs/unreleased/29669-redirect-referer-params.yml b/changelogs/unreleased/29669-redirect-referer-params.yml
new file mode 100644
index 00000000000..d8fc7f33049
--- /dev/null
+++ b/changelogs/unreleased/29669-redirect-referer-params.yml
@@ -0,0 +1,4 @@
+---
+title: Fix redirection after login when the referer have params
+merge_request:
+author: mhasbini
diff --git a/changelogs/unreleased/29670-jira-integration-documentation-improvment.yml b/changelogs/unreleased/29670-jira-integration-documentation-improvment.yml
new file mode 100644
index 00000000000..8975f0b6ef3
--- /dev/null
+++ b/changelogs/unreleased/29670-jira-integration-documentation-improvment.yml
@@ -0,0 +1,4 @@
+---
+title: Added clarification to the Jira integration documentation.
+merge_request: 10066
+author: Matthew Bender
diff --git a/changelogs/unreleased/29828-change-search-hint-in-new-filters.yml b/changelogs/unreleased/29828-change-search-hint-in-new-filters.yml
new file mode 100644
index 00000000000..a9322693ca4
--- /dev/null
+++ b/changelogs/unreleased/29828-change-search-hint-in-new-filters.yml
@@ -0,0 +1,4 @@
+---
+title: Change hint on first row of filters dropdown to `Press Enter or click to search`
+merge_request: 10138
+author:
diff --git a/changelogs/unreleased/29830-build-scroll-indicator.yml b/changelogs/unreleased/29830-build-scroll-indicator.yml
new file mode 100644
index 00000000000..e899a828de7
--- /dev/null
+++ b/changelogs/unreleased/29830-build-scroll-indicator.yml
@@ -0,0 +1,4 @@
+---
+title: fix sidebar padding for build and wiki pages
+merge_request:
+author:
diff --git a/changelogs/unreleased/29843-project-subgroup-transfer.yml b/changelogs/unreleased/29843-project-subgroup-transfer.yml
new file mode 100644
index 00000000000..1cf83517591
--- /dev/null
+++ b/changelogs/unreleased/29843-project-subgroup-transfer.yml
@@ -0,0 +1,4 @@
+---
+title: Correctly update paths when changing a child group
+merge_request:
+author:
diff --git a/changelogs/unreleased/29866-navbar-counters.yml b/changelogs/unreleased/29866-navbar-counters.yml
new file mode 100644
index 00000000000..c67dff6cffa
--- /dev/null
+++ b/changelogs/unreleased/29866-navbar-counters.yml
@@ -0,0 +1,4 @@
+---
+title: Add shortcuts and counters to MRs and issues in navbar
+merge_request:
+author:
diff --git a/changelogs/unreleased/29929-jira-doc.yml b/changelogs/unreleased/29929-jira-doc.yml
new file mode 100644
index 00000000000..f79dcd84634
--- /dev/null
+++ b/changelogs/unreleased/29929-jira-doc.yml
@@ -0,0 +1,4 @@
+---
+title: Fix link to Jira service documentation
+merge_request:
+author:
diff --git a/changelogs/unreleased/29950-vue-pagination-icons.yml b/changelogs/unreleased/29950-vue-pagination-icons.yml
new file mode 100644
index 00000000000..e03092b8dba
--- /dev/null
+++ b/changelogs/unreleased/29950-vue-pagination-icons.yml
@@ -0,0 +1,4 @@
+---
+title: consistent icons in vue and kaminari pagers
+merge_request:
+author:
diff --git a/changelogs/unreleased/30035-milestone-with-due-date-shows-escaped-html.yml b/changelogs/unreleased/30035-milestone-with-due-date-shows-escaped-html.yml
deleted file mode 100644
index 651c299ac66..00000000000
--- a/changelogs/unreleased/30035-milestone-with-due-date-shows-escaped-html.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix escaped html appearing in milestone page
-merge_request: 10224
-author:
diff --git a/changelogs/unreleased/30112-fix-pipelines-sub-nav-highlight.yml b/changelogs/unreleased/30112-fix-pipelines-sub-nav-highlight.yml
new file mode 100644
index 00000000000..deca629be83
--- /dev/null
+++ b/changelogs/unreleased/30112-fix-pipelines-sub-nav-highlight.yml
@@ -0,0 +1,4 @@
+---
+title: Fix sub-nav highlighting for `Environments` and `Jobs` pages
+merge_request: 10254
+author:
diff --git a/changelogs/unreleased/30400-fix-blob-highlighting-in-search.yml b/changelogs/unreleased/30400-fix-blob-highlighting-in-search.yml
new file mode 100644
index 00000000000..942258450c0
--- /dev/null
+++ b/changelogs/unreleased/30400-fix-blob-highlighting-in-search.yml
@@ -0,0 +1,4 @@
+---
+title: Fix blob highlighting in search
+merge_request: 10420
+author:
diff --git a/changelogs/unreleased/add-mock-deployment-and-monitoring-service-for-development.yaml b/changelogs/unreleased/add-mock-deployment-and-monitoring-service-for-development.yaml
new file mode 100644
index 00000000000..4c81d21a94b
--- /dev/null
+++ b/changelogs/unreleased/add-mock-deployment-and-monitoring-service-for-development.yaml
@@ -0,0 +1,4 @@
+---
+title: Added mock deployment and monitoring service with environments fixtures
+merge_request:
+author:
diff --git a/changelogs/unreleased/better-priority-sorting-2.yml b/changelogs/unreleased/better-priority-sorting-2.yml
deleted file mode 100644
index ca0d14718dc..00000000000
--- a/changelogs/unreleased/better-priority-sorting-2.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow filtering by all started milestones
-merge_request:
-author:
diff --git a/changelogs/unreleased/better-priority-sorting.yml b/changelogs/unreleased/better-priority-sorting.yml
deleted file mode 100644
index a44cd090ceb..00000000000
--- a/changelogs/unreleased/better-priority-sorting.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Allow sorting by due date and priority
-merge_request:
-author:
diff --git a/changelogs/unreleased/bug-api_milestone_merge_requests_scope.yml b/changelogs/unreleased/bug-api_milestone_merge_requests_scope.yml
new file mode 100644
index 00000000000..a1e1c29165e
--- /dev/null
+++ b/changelogs/unreleased/bug-api_milestone_merge_requests_scope.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes milestone/merge_requests endpoint to actually scope the result
+merge_request:
+author: Joren De Groof
diff --git a/changelogs/unreleased/calendar-tooltips.yml b/changelogs/unreleased/calendar-tooltips.yml
new file mode 100644
index 00000000000..d1517bbab58
--- /dev/null
+++ b/changelogs/unreleased/calendar-tooltips.yml
@@ -0,0 +1,4 @@
+---
+title: Add tooltip to user's calendar activities
+merge_request: 10123
+author: Alex Argunov
diff --git a/changelogs/unreleased/create-collapsed-todo-button.yml b/changelogs/unreleased/create-collapsed-todo-button.yml
new file mode 100644
index 00000000000..6da6c070bf7
--- /dev/null
+++ b/changelogs/unreleased/create-collapsed-todo-button.yml
@@ -0,0 +1,5 @@
+---
+title: adds todo functionality to closed issuable sidebar and changes todo bell icon
+ to check-square
+merge_request:
+author:
diff --git a/changelogs/unreleased/environment-performance-improvements.yml b/changelogs/unreleased/environment-performance-improvements.yml
new file mode 100644
index 00000000000..43e8f0afcee
--- /dev/null
+++ b/changelogs/unreleased/environment-performance-improvements.yml
@@ -0,0 +1,4 @@
+---
+title: Improved UX for the environments metrics view
+merge_request: 9946
+author:
diff --git a/changelogs/unreleased/feature-use-gitaly-for-commit-is-ancestor.yml b/changelogs/unreleased/feature-use-gitaly-for-commit-is-ancestor.yml
new file mode 100644
index 00000000000..733e3643ce5
--- /dev/null
+++ b/changelogs/unreleased/feature-use-gitaly-for-commit-is-ancestor.yml
@@ -0,0 +1,4 @@
+---
+title: Use Gitaly for Repository#is_ancestor
+merge_request: 9864
+author:
diff --git a/changelogs/unreleased/filter-bar-fix-ie.yml b/changelogs/unreleased/filter-bar-fix-ie.yml
deleted file mode 100644
index f1fa7d9b177..00000000000
--- a/changelogs/unreleased/filter-bar-fix-ie.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fixed filtered search not working in IE
-merge_request:
-author:
diff --git a/changelogs/unreleased/fix-admin-projects.yml b/changelogs/unreleased/fix-admin-projects.yml
new file mode 100644
index 00000000000..d192f07004c
--- /dev/null
+++ b/changelogs/unreleased/fix-admin-projects.yml
@@ -0,0 +1,4 @@
+---
+title: Fix layout of projects page on admin area
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-ci-api-regression-for-after-script.yml b/changelogs/unreleased/fix-ci-api-regression-for-after-script.yml
deleted file mode 100644
index cdd7d1e6945..00000000000
--- a/changelogs/unreleased/fix-ci-api-regression-for-after-script.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix after_script processing for Runners APIv4
-merge_request: 10185
-author:
diff --git a/changelogs/unreleased/fix-gb-remove-deprecated-pipeline-processing-code.yml b/changelogs/unreleased/fix-gb-remove-deprecated-pipeline-processing-code.yml
new file mode 100644
index 00000000000..32862b527fd
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-remove-deprecated-pipeline-processing-code.yml
@@ -0,0 +1,4 @@
+---
+title: Drop support for correctly processing legacy pipelines
+merge_request: 10266
+author:
diff --git a/changelogs/unreleased/fix-github-importer-slowness.yml b/changelogs/unreleased/fix-github-importer-slowness.yml
new file mode 100644
index 00000000000..c1f8d0e02d5
--- /dev/null
+++ b/changelogs/unreleased/fix-github-importer-slowness.yml
@@ -0,0 +1,4 @@
+---
+title: Improve performance of GitHub importer for large repositories.
+merge_request: 10273
+author:
diff --git a/changelogs/unreleased/fix-import-fork.yml b/changelogs/unreleased/fix-import-fork.yml
new file mode 100644
index 00000000000..ff8dd131995
--- /dev/null
+++ b/changelogs/unreleased/fix-import-fork.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Import/Export MR diffs not showing and missing forked MRs
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-import-namespace.yml b/changelogs/unreleased/fix-import-namespace.yml
new file mode 100644
index 00000000000..9a2fa5e425f
--- /dev/null
+++ b/changelogs/unreleased/fix-import-namespace.yml
@@ -0,0 +1,4 @@
+---
+title: Create subgroups if they don't exist while importing projects
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix_admin_monitoring_background.yml b/changelogs/unreleased/fix_admin_monitoring_background.yml
new file mode 100644
index 00000000000..3a9a1c88672
--- /dev/null
+++ b/changelogs/unreleased/fix_admin_monitoring_background.yml
@@ -0,0 +1,4 @@
+---
+title: Handle parsing OpenBSD ps output properly to display sidekiq infos on admin->monitoring->background
+merge_request: 10303
+author: Sebastian Reitenbach
diff --git a/changelogs/unreleased/fix_rake_gitlab_check_sidekiq.yml b/changelogs/unreleased/fix_rake_gitlab_check_sidekiq.yml
new file mode 100644
index 00000000000..4752ed34ae6
--- /dev/null
+++ b/changelogs/unreleased/fix_rake_gitlab_check_sidekiq.yml
@@ -0,0 +1,4 @@
+---
+title: Force unlimited terminal size when checking processes via call to ps
+merge_request: 10246
+author: Sebastian Reitenbach
diff --git a/changelogs/unreleased/fix_wiki_commit_message.yml b/changelogs/unreleased/fix_wiki_commit_message.yml
new file mode 100644
index 00000000000..e5cd398b4b5
--- /dev/null
+++ b/changelogs/unreleased/fix_wiki_commit_message.yml
@@ -0,0 +1,4 @@
+---
+title: Fix wiki commit message
+merge_request: 10464
+author: blackst0ne
diff --git a/changelogs/unreleased/introduce-polling-interval-multiplier.yml b/changelogs/unreleased/introduce-polling-interval-multiplier.yml
new file mode 100644
index 00000000000..3ccae8e327f
--- /dev/null
+++ b/changelogs/unreleased/introduce-polling-interval-multiplier.yml
@@ -0,0 +1,4 @@
+---
+title: Introduce "polling_interval_multiplier" as application setting
+merge_request: 10280
+author:
diff --git a/changelogs/unreleased/issue_91_ee_backport.yml b/changelogs/unreleased/issue_91_ee_backport.yml
new file mode 100644
index 00000000000..17bc0e435f3
--- /dev/null
+++ b/changelogs/unreleased/issue_91_ee_backport.yml
@@ -0,0 +1,4 @@
+---
+title: Do not set closed_at to nil when issue is reopened
+merge_request:
+author:
diff --git a/changelogs/unreleased/jej-group-name-disclosure.yml b/changelogs/unreleased/jej-group-name-disclosure.yml
new file mode 100644
index 00000000000..9b8ab7082ef
--- /dev/null
+++ b/changelogs/unreleased/jej-group-name-disclosure.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed private group name disclosure via new/update forms
+merge_request:
+author:
diff --git a/changelogs/unreleased/make_user_mentions_case_insensitive.yml b/changelogs/unreleased/make_user_mentions_case_insensitive.yml
new file mode 100644
index 00000000000..ab114494802
--- /dev/null
+++ b/changelogs/unreleased/make_user_mentions_case_insensitive.yml
@@ -0,0 +1,4 @@
+---
+title: Make user mentions case-insensitive
+merge_request: 10285
+author: blackst0ne
diff --git a/changelogs/unreleased/mr-diffs-speed-up.yml b/changelogs/unreleased/mr-diffs-speed-up.yml
new file mode 100644
index 00000000000..ccc7a99d05e
--- /dev/null
+++ b/changelogs/unreleased/mr-diffs-speed-up.yml
@@ -0,0 +1,4 @@
+---
+title: Speed up initial rendering of MR diffs page
+merge_request:
+author:
diff --git a/changelogs/unreleased/namespace-race-condition.yml b/changelogs/unreleased/namespace-race-condition.yml
new file mode 100644
index 00000000000..2a76b6c74e8
--- /dev/null
+++ b/changelogs/unreleased/namespace-race-condition.yml
@@ -0,0 +1,4 @@
+---
+title: Fix project creation failure due to race condition in namespace directory creation
+merge_request: 10268
+author: Robin Bobbitt
diff --git a/changelogs/unreleased/option-to-be-notified-of-own-activity.yml b/changelogs/unreleased/option-to-be-notified-of-own-activity.yml
new file mode 100644
index 00000000000..542287a09be
--- /dev/null
+++ b/changelogs/unreleased/option-to-be-notified-of-own-activity.yml
@@ -0,0 +1,4 @@
+---
+title: Add option to receive email notifications about your own activity
+merge_request: 10032
+author: Richard Macklin
diff --git a/changelogs/unreleased/pages-debug-log.yml b/changelogs/unreleased/pages-debug-log.yml
new file mode 100644
index 00000000000..328c8e4615b
--- /dev/null
+++ b/changelogs/unreleased/pages-debug-log.yml
@@ -0,0 +1,4 @@
+---
+title: Log errors during generating of Gitlab Pages to debug log
+merge_request: 10335
+author: Danilo Bargen
diff --git a/changelogs/unreleased/quiet-pipelines.yml b/changelogs/unreleased/quiet-pipelines.yml
new file mode 100644
index 00000000000..c02eb59b824
--- /dev/null
+++ b/changelogs/unreleased/quiet-pipelines.yml
@@ -0,0 +1,5 @@
+---
+title: Only email pipeline creators; only email for successful pipelines with custom
+ settings
+merge_request:
+author:
diff --git a/changelogs/unreleased/remove_index_for_users-current_sign_in_at.yml b/changelogs/unreleased/remove_index_for_users-current_sign_in_at.yml
new file mode 100644
index 00000000000..ec3a2c8e2bf
--- /dev/null
+++ b/changelogs/unreleased/remove_index_for_users-current_sign_in_at.yml
@@ -0,0 +1,4 @@
+---
+title: Remove index for users.current sign in at
+merge_request: 10401
+author: blackst0ne
diff --git a/changelogs/unreleased/rename_done_to_closed.yml b/changelogs/unreleased/rename_done_to_closed.yml
new file mode 100644
index 00000000000..6de112c4b0d
--- /dev/null
+++ b/changelogs/unreleased/rename_done_to_closed.yml
@@ -0,0 +1,4 @@
+---
+title: Change Done column to Closed in issue boards
+merge_request: 10198
+author: blackst0ne
diff --git a/changelogs/unreleased/scrollable-secondary-tabs.yml b/changelogs/unreleased/scrollable-secondary-tabs.yml
new file mode 100644
index 00000000000..963d5d325dc
--- /dev/null
+++ b/changelogs/unreleased/scrollable-secondary-tabs.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed tabs not scrolling on mobile
+merge_request:
+author:
diff --git a/changelogs/unreleased/sh-fix-ssh-keys-with-spaces.yml b/changelogs/unreleased/sh-fix-ssh-keys-with-spaces.yml
new file mode 100644
index 00000000000..fe75d7e1156
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-ssh-keys-with-spaces.yml
@@ -0,0 +1,4 @@
+---
+title: Handle SSH keys that have multiple spaces between each marker
+merge_request:
+author:
diff --git a/changelogs/unreleased/sh-relax-wiki-slug-constraint.yml b/changelogs/unreleased/sh-relax-wiki-slug-constraint.yml
new file mode 100644
index 00000000000..08395b0d28c
--- /dev/null
+++ b/changelogs/unreleased/sh-relax-wiki-slug-constraint.yml
@@ -0,0 +1,4 @@
+---
+title: Relax constraint on Wiki IDs, since subdirectories can contain spaces
+merge_request:
+author:
diff --git a/changelogs/unreleased/sh-remove-tags-from-explore.yml b/changelogs/unreleased/sh-remove-tags-from-explore.yml
new file mode 100644
index 00000000000..b76ec89a006
--- /dev/null
+++ b/changelogs/unreleased/sh-remove-tags-from-explore.yml
@@ -0,0 +1,4 @@
+---
+title: Remove Tags filter from Projects Explore dropdown
+merge_request:
+author:
diff --git a/changelogs/unreleased/slow-search-changelog.yml b/changelogs/unreleased/slow-search-changelog.yml
deleted file mode 100644
index d50cf1f94cd..00000000000
--- a/changelogs/unreleased/slow-search-changelog.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Simplify search queries for projects and merge requests
-merge_request: 10053
-author: mhasbini
diff --git a/changelogs/unreleased/style-proc-cop.yml b/changelogs/unreleased/style-proc-cop.yml
new file mode 100644
index 00000000000..25acab740bd
--- /dev/null
+++ b/changelogs/unreleased/style-proc-cop.yml
@@ -0,0 +1,4 @@
+---
+title: Enable Style/Proc cop for rubocop
+merge_request:
+author: mhasbini
diff --git a/changelogs/unreleased/update-test-bundle-ignored-files.yml b/changelogs/unreleased/update-test-bundle-ignored-files.yml
new file mode 100644
index 00000000000..1235d4ced6c
--- /dev/null
+++ b/changelogs/unreleased/update-test-bundle-ignored-files.yml
@@ -0,0 +1,4 @@
+---
+title: update test_bundle.js ignored files
+merge_request:
+author:
diff --git a/changelogs/unreleased/zj-kube-service-auto-fill.yml b/changelogs/unreleased/zj-kube-service-auto-fill.yml
new file mode 100644
index 00000000000..7a2c7a5085b
--- /dev/null
+++ b/changelogs/unreleased/zj-kube-service-auto-fill.yml
@@ -0,0 +1,4 @@
+---
+title: Don't fill in the default kubernetes namespace
+merge_request:
+author:
diff --git a/config/application.rb b/config/application.rb
index f9f01b66473..f2ecc4ce77c 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -150,6 +150,7 @@ module Gitlab
# This is needed for gitlab-shell
ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH']
+ ENV['GIT_TERMINAL_PROMPT'] = '0'
config.generators do |g|
g.factory_girl false
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 3747baf4c3b..4314e902564 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -443,14 +443,10 @@ production: &base
# Gitaly settings
gitaly:
- # The socket_path setting is optional and obsolete. When this is set
- # GitLab assumes it can reach a Gitaly services via a Unix socket at
- # this path. When this is commented out GitLab will not use Gitaly.
- #
- # This setting is obsolete because we expect it to be moved under
- # repositories/storages in GitLab 9.1.
- #
- # socket_path: tmp/sockets/private/gitaly.socket
+ # This setting controls whether GitLab uses Gitaly (new component
+ # introduced in 9.0). Eventually Gitaly use will become mandatory and
+ # this option will disappear.
+ enabled: false
#
# 4. Advanced settings
@@ -465,6 +461,7 @@ production: &base
storages: # You must have at least a `default` storage path.
default:
path: /home/git/repositories/
+ gitaly_address: unix:/home/git/gitlab/tmp/sockets/private/gitaly.socket # TCP connections are supported too (e.g. tcp://host:port)
## Backup settings
backup:
@@ -573,10 +570,15 @@ test:
# In order to setup it correctly you need to specify
# your system username you use to run GitLab
# user: YOUR_USERNAME
+ pages:
+ path: tmp/tests/pages
repositories:
storages:
default:
path: tmp/tests/repositories/
+ gitaly_address: unix:<%= Rails.root.join('tmp/sockets/private/gitaly.socket') %>
+ gitaly:
+ enabled: false
backup:
path: tmp/tests/backups
gitlab_shell:
diff --git a/config/initializers/0_inflections.rb b/config/initializers/0_inflections.rb
index d4197da3fa9..f977104ff9d 100644
--- a/config/initializers/0_inflections.rb
+++ b/config/initializers/0_inflections.rb
@@ -10,5 +10,5 @@
# end
#
ActiveSupport::Inflector.inflections do |inflect|
- inflect.uncountable %w(award_emoji project_statistics)
+ inflect.uncountable %w(award_emoji project_statistics system_note_metadata)
end
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 62020fa9a75..e8fef0000c1 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -79,6 +79,10 @@ class Settings < Settingslogic
value
end
+ def absolute(path)
+ File.expand_path(path, Rails.root)
+ end
+
private
def base_url(config)
@@ -178,7 +182,7 @@ if github_settings
end
Settings['shared'] ||= Settingslogic.new({})
-Settings.shared['path'] = File.expand_path(Settings.shared['path'] || "shared", Rails.root)
+Settings.shared['path'] = Settings.absolute(Settings.shared['path'] || "shared")
Settings['issues_tracker'] ||= {}
@@ -237,7 +241,7 @@ Settings['gitlab_ci'] ||= Settingslogic.new({})
Settings.gitlab_ci['shared_runners_enabled'] = true if Settings.gitlab_ci['shared_runners_enabled'].nil?
Settings.gitlab_ci['all_broken_builds'] = true if Settings.gitlab_ci['all_broken_builds'].nil?
Settings.gitlab_ci['add_pusher'] = false if Settings.gitlab_ci['add_pusher'].nil?
-Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_ci['builds_path'] || "builds/", Rails.root)
+Settings.gitlab_ci['builds_path'] = Settings.absolute(Settings.gitlab_ci['builds_path'] || "builds/")
Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url)
#
@@ -251,7 +255,7 @@ Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled']
#
Settings['artifacts'] ||= Settingslogic.new({})
Settings.artifacts['enabled'] = true if Settings.artifacts['enabled'].nil?
-Settings.artifacts['path'] = File.expand_path(Settings.artifacts['path'] || File.join(Settings.shared['path'], "artifacts"), Rails.root)
+Settings.artifacts['path'] = Settings.absolute(Settings.artifacts['path'] || File.join(Settings.shared['path'], "artifacts"))
Settings.artifacts['max_size'] ||= 100 # in megabytes
#
@@ -265,14 +269,14 @@ Settings.registry['api_url'] ||= "http://localhost:5000/"
Settings.registry['key'] ||= nil
Settings.registry['issuer'] ||= nil
Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.registry['port']].compact.join(':')
-Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], 'registry'), Rails.root)
+Settings.registry['path'] = Settings.absolute(Settings.registry['path'] || File.join(Settings.shared['path'], 'registry'))
#
# Pages
#
Settings['pages'] ||= Settingslogic.new({})
Settings.pages['enabled'] = false if Settings.pages['enabled'].nil?
-Settings.pages['path'] = File.expand_path(Settings.pages['path'] || File.join(Settings.shared['path'], "pages"), Rails.root)
+Settings.pages['path'] = Settings.absolute(Settings.pages['path'] || File.join(Settings.shared['path'], "pages"))
Settings.pages['https'] = false if Settings.pages['https'].nil?
Settings.pages['host'] ||= "example.com"
Settings.pages['port'] ||= Settings.pages.https ? 443 : 80
@@ -286,7 +290,7 @@ Settings.pages['external_https'] ||= false unless Settings.pages['external_http
#
Settings['lfs'] ||= Settingslogic.new({})
Settings.lfs['enabled'] = true if Settings.lfs['enabled'].nil?
-Settings.lfs['storage_path'] = File.expand_path(Settings.lfs['storage_path'] || File.join(Settings.shared['path'], "lfs-objects"), Rails.root)
+Settings.lfs['storage_path'] = Settings.absolute(Settings.lfs['storage_path'] || File.join(Settings.shared['path'], "lfs-objects"))
#
# Mattermost
@@ -350,8 +354,8 @@ Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['job_class'] = 'Rem
# GitLab Shell
#
Settings['gitlab_shell'] ||= Settingslogic.new({})
-Settings.gitlab_shell['path'] ||= Settings.gitlab['user_home'] + '/gitlab-shell/'
-Settings.gitlab_shell['hooks_path'] ||= Settings.gitlab['user_home'] + '/gitlab-shell/hooks/'
+Settings.gitlab_shell['path'] = Settings.absolute(Settings.gitlab_shell['path'] || Settings.gitlab['user_home'] + '/gitlab-shell/')
+Settings.gitlab_shell['hooks_path'] = Settings.absolute(Settings.gitlab_shell['hooks_path'] || Settings.gitlab['user_home'] + '/gitlab-shell/hooks/')
Settings.gitlab_shell['secret_file'] ||= Rails.root.join('.gitlab_shell_secret')
Settings.gitlab_shell['receive_pack'] = true if Settings.gitlab_shell['receive_pack'].nil?
Settings.gitlab_shell['upload_pack'] = true if Settings.gitlab_shell['upload_pack'].nil?
@@ -374,6 +378,11 @@ unless Settings.repositories.storages['default']
Settings.repositories.storages['default']['path'] ||= Settings.gitlab['user_home'] + '/repositories/'
end
+Settings.repositories.storages.values.each do |storage|
+ # Expand relative paths
+ storage['path'] = Settings.absolute(storage['path'])
+end
+
#
# The repository_downloads_path is used to remove outdated repository
# archives, if someone has it configured incorrectly, and it points
@@ -395,7 +404,7 @@ end
Settings['backup'] ||= Settingslogic.new({})
Settings.backup['keep_time'] ||= 0
Settings.backup['pg_schema'] = nil
-Settings.backup['path'] = File.expand_path(Settings.backup['path'] || "tmp/backups/", Rails.root)
+Settings.backup['path'] = Settings.absolute(Settings.backup['path'] || "tmp/backups/")
Settings.backup['archive_permissions'] ||= 0600
Settings.backup['upload'] ||= Settingslogic.new({ 'remote_directory' => nil, 'connection' => nil })
# Convert upload connection settings to use symbol keys, to make Fog happy
@@ -418,7 +427,7 @@ Settings.git['timeout'] ||= 10
# least. This setting is fed to 'rm -rf' in
# db/migrate/20151023144219_remove_satellites.rb
Settings['satellites'] ||= Settingslogic.new({})
-Settings.satellites['path'] = File.expand_path(Settings.satellites['path'] || "tmp/repo_satellites/", Rails.root)
+Settings.satellites['path'] = Settings.absolute(Settings.satellites['path'] || "tmp/repo_satellites/")
#
# Extra customization
@@ -440,7 +449,7 @@ Settings.rack_attack.git_basic_auth['bantime'] ||= 1.hour
# Gitaly
#
Settings['gitaly'] ||= Settingslogic.new({})
-Settings.gitaly['socket_path'] ||= ENV['GITALY_SOCKET_PATH']
+Settings.gitaly['enabled'] ||= false
#
# Webpack settings
diff --git a/config/initializers/8_gitaly.rb b/config/initializers/8_gitaly.rb
index 07dd30f0a24..c7f27c78535 100644
--- a/config/initializers/8_gitaly.rb
+++ b/config/initializers/8_gitaly.rb
@@ -1,2 +1,18 @@
-# Make sure we initialize a Gitaly channel before Sidekiq starts multi-threaded execution.
-Gitlab::GitalyClient.channel unless Rails.env.test?
+require 'uri'
+
+# Make sure we initialize our Gitaly channels before Sidekiq starts multi-threaded execution.
+if Gitlab.config.gitaly.enabled || Rails.env.test?
+ Gitlab.config.repositories.storages.each do |name, params|
+ address = params['gitaly_address']
+
+ unless address.present?
+ raise "storage #{name.inspect} is missing a gitaly_address"
+ end
+
+ unless URI(address).scheme.in?(%w(tcp unix))
+ raise "Unsupported Gitaly address: #{address.inspect}"
+ end
+
+ Gitlab::GitalyClient.configure_channel(name, address)
+ end
+end
diff --git a/config/initializers/bullet.rb b/config/initializers/bullet.rb
index 95e82966c7a..0ade7109420 100644
--- a/config/initializers/bullet.rb
+++ b/config/initializers/bullet.rb
@@ -1,6 +1,11 @@
-if ENV['ENABLE_BULLET']
- require 'bullet'
+if defined?(Bullet) && ENV['ENABLE_BULLET']
+ Rails.application.configure do
+ config.after_initialize do
+ Bullet.enable = true
- Bullet.enable = true
- Bullet.console = true
+ Bullet.bullet_logger = true
+ Bullet.console = true
+ Bullet.raise = Rails.env.test?
+ end
+ end
end
diff --git a/config/initializers/rspec_profiling.rb b/config/initializers/rspec_profiling.rb
index 70177995356..764c067c6f0 100644
--- a/config/initializers/rspec_profiling.rb
+++ b/config/initializers/rspec_profiling.rb
@@ -7,7 +7,11 @@ module RspecProfilingExt
module Git
def branch
- ENV['CI_COMMIT_REF_NAME'] || super
+ if ENV['CI_COMMIT_REF_NAME']
+ "#{defined?(Gitlab::License) ? 'ee' : 'ce'}:#{ENV['CI_COMMIT_REF_NAME']}"
+ else
+ super
+ end
end
end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 44b8ae7aedd..7244f851869 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -102,6 +102,7 @@ constraints(ProjectUrlConstrainer.new) do
get :merge_widget_refresh
post :cancel_merge_when_pipeline_succeeds
get :ci_status
+ get :pipeline_status
get :ci_environments_status
post :toggle_subscription
post :remove_wip
@@ -152,6 +153,7 @@ constraints(ProjectUrlConstrainer.new) do
post :cancel
post :retry
get :builds
+ get :status
end
end
@@ -164,7 +166,7 @@ constraints(ProjectUrlConstrainer.new) do
end
collection do
- get :folder, path: 'folders/:id'
+ get :folder, path: 'folders/*id', constraints: { format: /(html|json)/ }
end
end
diff --git a/config/routes/wiki.rb b/config/routes/wiki.rb
index a6b3f5d4693..c2da84ff6f2 100644
--- a/config/routes/wiki.rb
+++ b/config/routes/wiki.rb
@@ -1,5 +1,3 @@
-WIKI_SLUG_ID = { id: /\S+/ }.freeze unless defined? WIKI_SLUG_ID
-
scope(controller: :wikis) do
scope(path: 'wikis', as: :wikis) do
get :git_access
@@ -8,7 +6,7 @@ scope(controller: :wikis) do
post '/', to: 'wikis#create'
end
- scope(path: 'wikis/*id', as: :wiki, constraints: WIKI_SLUG_ID, format: false) do
+ scope(path: 'wikis/*id', as: :wiki, format: false) do
get :edit
get :history
post :preview_markdown
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 0859c8416c8..36c09c14d56 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -23,7 +23,6 @@ var config = {
main: './main.js',
blob: './blob_edit/blob_bundle.js',
boards: './boards/boards_bundle.js',
- simulate_drag: './test_utils/simulate_drag.js',
cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js',
commit_pipelines: './commit/pipelines/pipelines_bundle.js',
diff_notes: './diff_notes/diff_notes_bundle.js',
@@ -37,6 +36,7 @@ var config = {
merge_request_widget: './merge_request_widget/ci_bundle.js',
monitoring: './monitoring/monitoring_bundle.js',
network: './network/network_bundle.js',
+ notebook_viewer: './blob/notebook_viewer.js',
profile: './profile/profile_bundle.js',
protected_branches: './protected_branches/protected_branches_bundle.js',
snippet: './snippet/snippet_bundle.js',
@@ -52,7 +52,7 @@ var config = {
filename: IS_PRODUCTION ? '[name].[chunkhash].bundle.js' : '[name].bundle.js'
},
- devtool: 'inline-source-map',
+ devtool: 'cheap-module-source-map',
module: {
rules: [
@@ -105,6 +105,7 @@ var config = {
'environments_folder',
'issuable',
'merge_conflicts',
+ 'notebook_viewer',
'vue_pipelines',
],
minChunks: function(module, count) {
@@ -115,7 +116,11 @@ var config = {
// create cacheable common library bundle for all d3 chunks
new webpack.optimize.CommonsChunkPlugin({
name: 'common_d3',
- chunks: ['graphs', 'users', 'monitoring'],
+ chunks: [
+ 'graphs',
+ 'users',
+ 'monitoring',
+ ],
}),
// create cacheable common library bundles
@@ -158,6 +163,7 @@ if (IS_PRODUCTION) {
}
if (IS_DEV_SERVER) {
+ config.devtool = 'cheap-module-eval-source-map';
config.devServer = {
port: DEV_SERVER_PORT,
headers: { 'Access-Control-Allow-Origin': '*' },
diff --git a/db/fixtures/development/18_abuse_reports.rb b/db/fixtures/development/18_abuse_reports.rb
index 8618d10387a..88d2f784852 100644
--- a/db/fixtures/development/18_abuse_reports.rb
+++ b/db/fixtures/development/18_abuse_reports.rb
@@ -1,5 +1,27 @@
-require 'factory_girl_rails'
+module Db
+ module Fixtures
+ module Development
+ class AbuseReport
+ def self.seed
+ Gitlab::Seeder.quiet do
+ (::AbuseReport.default_per_page + 3).times do |i|
+ reported_user =
+ ::User.create!(
+ username: "reported_user_#{i}",
+ name: FFaker::Name.name,
+ email: FFaker::Internet.email,
+ confirmed_at: DateTime.now,
+ password: '12345678'
+ )
-(AbuseReport.default_per_page + 3).times do
- FactoryGirl.create(:abuse_report)
+ ::AbuseReport.create(reporter: ::User.take, user: reported_user, message: 'User sends spam')
+ print '.'
+ end
+ end
+ end
+ end
+ end
+ end
end
+
+Db::Fixtures::Development::AbuseReport.seed
diff --git a/db/fixtures/development/19_environments.rb b/db/fixtures/development/19_environments.rb
new file mode 100644
index 00000000000..93214b9d3e7
--- /dev/null
+++ b/db/fixtures/development/19_environments.rb
@@ -0,0 +1,70 @@
+require './spec/support/sidekiq'
+
+class Gitlab::Seeder::Environments
+ def initialize(project)
+ @project = project
+ end
+
+ def seed!
+ @project.create_mock_deployment_service!(active: true)
+ @project.create_mock_monitoring_service!(active: true)
+
+ create_master_deployments!('production')
+ create_master_deployments!('staging')
+ create_merge_request_review_deployments!
+ end
+
+ private
+
+ def create_master_deployments!(name)
+ @project.repository.commits('master', limit: 4).map do |commit|
+ create_deployment!(
+ @project,
+ name,
+ 'master',
+ commit.id
+ )
+ end
+ end
+
+ def create_merge_request_review_deployments!
+ @project.merge_requests.sample(4).map do |merge_request|
+ next unless merge_request.diff_head_sha
+
+ create_deployment!(
+ merge_request.source_project,
+ "review/#{merge_request.source_branch}",
+ merge_request.source_branch,
+ merge_request.diff_head_sha
+ )
+ end
+ end
+
+ def create_deployment!(project, name, ref, sha)
+ environment = find_or_create_environment!(project, name)
+ environment.deployments.create!(
+ project: project,
+ ref: ref,
+ sha: sha,
+ tag: false,
+ deployable: find_deployable(project, name)
+ )
+ end
+
+ def find_or_create_environment!(project, name)
+ project.environments.find_or_create_by!(name: name).tap do |environment|
+ environment.update(external_url: "https://google.com/#{name}")
+ end
+ end
+
+ def find_deployable(project, environment)
+ project.builds.where(environment: environment).sample
+ end
+end
+
+Gitlab::Seeder.quiet do
+ Project.all.sample(5).each do |project|
+ project_environments = Gitlab::Seeder::Environments.new(project)
+ project_environments.seed!
+ end
+end
diff --git a/db/fixtures/development/19_nested_groups.rb b/db/fixtures/development/20_nested_groups.rb
index d8dddc3fee9..d8dddc3fee9 100644
--- a/db/fixtures/development/19_nested_groups.rb
+++ b/db/fixtures/development/20_nested_groups.rb
diff --git a/db/fixtures/production/001_admin.rb b/db/fixtures/production/001_admin.rb
index b37dc794015..1c7c89f7bbd 100644
--- a/db/fixtures/production/001_admin.rb
+++ b/db/fixtures/production/001_admin.rb
@@ -12,10 +12,12 @@ else
user_args[:password] = ENV['GITLAB_ROOT_PASSWORD']
end
-user = User.new(user_args)
-user.skip_confirmation!
+# Only admins can create other admin users in Users::CreateService so to solve
+# the chicken-and-egg problem, we pass a non-persisted admin user to the service.
+transient_admin = User.new(admin: true)
+user = Users::CreateService.new(transient_admin, user_args.merge!(skip_confirmation: true)).execute
-if user.save
+if user.persisted?
puts "Administrator account created:".color(:green)
puts
puts "login: root".color(:green)
diff --git a/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
index 1e2abea5254..69dd15b8b4e 100644
--- a/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
+++ b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
@@ -17,7 +17,7 @@ class RemoveUnusedCiTablesAndColumns < ActiveRecord::Migration
end
remove_column :ci_pipelines, :push_data, :text
- remove_column :ci_builds, :job_id, :integer
+ remove_column :ci_builds, :job_id, :integer if column_exists?(:ci_builds, :job_id)
remove_column :ci_builds, :deploy, :boolean
end
diff --git a/db/migrate/20170314082049_create_system_note_metadata.rb b/db/migrate/20170314082049_create_system_note_metadata.rb
new file mode 100644
index 00000000000..dd1e6cf8172
--- /dev/null
+++ b/db/migrate/20170314082049_create_system_note_metadata.rb
@@ -0,0 +1,23 @@
+class CreateSystemNoteMetadata < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ create_table :system_note_metadata do |t|
+ t.references :note, null: false
+ t.integer :commit_count
+ t.string :action
+
+ t.timestamps null: false
+ end
+
+ add_concurrent_foreign_key :system_note_metadata, :notes, column: :note_id
+ end
+
+ def down
+ drop_table :system_note_metadata
+ end
+end
diff --git a/db/migrate/20170316061730_readd_notified_of_own_activity_to_users.rb b/db/migrate/20170316061730_readd_notified_of_own_activity_to_users.rb
new file mode 100644
index 00000000000..524eb2557ce
--- /dev/null
+++ b/db/migrate/20170316061730_readd_notified_of_own_activity_to_users.rb
@@ -0,0 +1,10 @@
+class ReaddNotifiedOfOwnActivityToUsers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def change
+ add_column :users, :notified_of_own_activity, :boolean
+ end
+end
diff --git a/db/migrate/20170329124448_add_polling_interval_multiplier_to_application_settings.rb b/db/migrate/20170329124448_add_polling_interval_multiplier_to_application_settings.rb
new file mode 100644
index 00000000000..a8affd19a0b
--- /dev/null
+++ b/db/migrate/20170329124448_add_polling_interval_multiplier_to_application_settings.rb
@@ -0,0 +1,33 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPollingIntervalMultiplierToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :application_settings, :polling_interval_multiplier, :decimal, default: 1, allow_null: false
+ end
+
+ def down
+ remove_column :application_settings, :polling_interval_multiplier
+ end
+end
diff --git a/db/migrate/20170402231018_remove_index_for_users_current_sign_in_at.rb b/db/migrate/20170402231018_remove_index_for_users_current_sign_in_at.rb
new file mode 100644
index 00000000000..8316ee9eb9f
--- /dev/null
+++ b/db/migrate/20170402231018_remove_index_for_users_current_sign_in_at.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 RemoveIndexForUsersCurrentSignInAt < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ if index_exists? :users, :current_sign_in_at
+ if Gitlab::Database.postgresql?
+ execute 'DROP INDEX CONCURRENTLY index_users_on_current_sign_in_at;'
+ else
+ remove_index :users, :current_sign_in_at
+ end
+ end
+ end
+
+ def down
+ add_concurrent_index :users, :current_sign_in_at
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 904fef4a381..ccf18d07179 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: 20170317203554) do
+ActiveRecord::Schema.define(version: 20170402231018) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -115,6 +115,7 @@ ActiveRecord::Schema.define(version: 20170317203554) do
t.integer "unique_ips_limit_per_user"
t.integer "unique_ips_limit_time_window"
t.boolean "unique_ips_limit_enabled", default: false, null: false
+ t.decimal "polling_interval_multiplier", default: 1.0, null: false
end
create_table "audit_events", force: :cascade do |t|
@@ -1075,6 +1076,14 @@ ActiveRecord::Schema.define(version: 20170317203554) do
add_index "subscriptions", ["subscribable_id", "subscribable_type", "user_id", "project_id"], name: "index_subscriptions_on_subscribable_and_user_id_and_project_id", unique: true, using: :btree
+ create_table "system_note_metadata", force: :cascade do |t|
+ t.integer "note_id", null: false
+ t.integer "commit_count"
+ t.string "action"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
create_table "taggings", force: :cascade do |t|
t.integer "tag_id"
t.integer "taggable_id"
@@ -1236,13 +1245,13 @@ ActiveRecord::Schema.define(version: 20170317203554) do
t.string "organization"
t.boolean "authorized_projects_populated"
t.boolean "ghost"
+ t.boolean "notified_of_own_activity"
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree
add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
add_index "users", ["created_at"], name: "index_users_on_created_at", using: :btree
- add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"}
add_index "users", ["ghost"], name: "index_users_on_ghost", using: :btree
@@ -1307,6 +1316,7 @@ ActiveRecord::Schema.define(version: 20170317203554) do
add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
add_foreign_key "protected_branch_push_access_levels", "protected_branches"
add_foreign_key "subscriptions", "projects", on_delete: :cascade
+ add_foreign_key "system_note_metadata", "notes", name: "fk_d83a918cb1", on_delete: :cascade
add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade
add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index 57d85d770e7..df11d5e49a8 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -1,8 +1,17 @@
-# GitLab Community Edition documentation
+# GitLab Community Edition
-## University
+All technical content published by GitLab lives in the documentation, including:
-[University](university/README.md) contain guides to learn Git and GitLab through courses and videos.
+- **General Documentation**
+ - [User docs](#user-documentation): general documentation dedicated to regular users of GitLab
+ - [Admin docs](#administrator-documentation): general documentation dedicated to administrators of GitLab instances
+ - [Contributor docs](#contributor-documentation): general documentation on how to develop and contribute to GitLab
+- [Topics](topics/index.md): pages organized per topic, gathering all the
+ resources already published by GitLab related to a specific subject, including
+ general docs, [technical articles](development/writing_documentation.md#technical-articles),
+ blog posts and video tutorials.
+- [GitLab University](university/README.md): guides to learn Git and GitLab
+ through courses and videos.
## User documentation
diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md
index cf3aca106e9..c22b1af8bfb 100644
--- a/doc/administration/high_availability/database.md
+++ b/doc/administration/high_availability/database.md
@@ -13,8 +13,8 @@ Database Service (RDS) that runs PostgreSQL.
If you use a cloud-managed service, or provide your own PostgreSQL:
-1. Setup PostgreSQL according to the
- [database requirements document](doc/install/requirements.md#database).
+1. Setup PostgreSQL according to the
+ [database requirements document](../../install/requirements.md#database).
1. Set up a `gitlab` username with a password of your choice. The `gitlab` user
needs privileges to create the `gitlabhq_production` database.
1. Configure the GitLab application servers with the appropriate details.
diff --git a/doc/api/boards.md b/doc/api/boards.md
index a74e82335eb..b2106463639 100644
--- a/doc/api/boards.md
+++ b/doc/api/boards.md
@@ -63,7 +63,7 @@ Example response:
## List board lists
Get a list of the board's lists.
-Does not include `backlog` and `done` lists
+Does not include `backlog` and `closed` lists
```
GET /projects/:id/boards/:board_id/lists
diff --git a/doc/api/labels.md b/doc/api/labels.md
index e8c220f6809..839000a4f48 100644
--- a/doc/api/labels.md
+++ b/doc/api/labels.md
@@ -90,7 +90,7 @@ POST /projects/:id/labels
| ------------- | ------- | -------- | ---------------------------- |
| `id` | integer | yes | The ID of the project |
| `name` | string | yes | The name of the label |
-| `color` | string | yes | The color of the label in 6-digit hex notation with leading `#` sign |
+| `color` | string | yes | The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the [CSS color names](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords) |
| `description` | string | no | The description of the label |
| `priority` | integer | no | The priority of the label. Must be greater or equal than zero or `null` to remove the priority. |
@@ -145,7 +145,7 @@ PUT /projects/:id/labels
| `id` | integer | yes | The ID of the project |
| `name` | string | yes | The name of the existing label |
| `new_name` | string | yes if `color` is not provided | The new name of the label |
-| `color` | string | yes if `new_name` is not provided | The new color of the label in 6-digit hex notation with leading `#` sign |
+| `color` | string | yes if `new_name` is not provided | The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the [CSS color names](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords) |
| `description` | string | no | The new description of the label |
| `priority` | integer | no | The new priority of the label. Must be greater or equal than zero or `null` to remove the priority. |
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 6ef06b2c2e9..5e927143714 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -9,13 +9,13 @@ Notes are comments on snippets, issues or merge requests.
Gets a list of all notes for a single issue.
```
-GET /projects/:id/issues/:issue_id/notes
+GET /projects/:id/issues/:issue_iid/notes
```
Parameters:
- `id` (required) - The ID of a project
-- `issue_id` (required) - The ID of an issue
+- `issue_iid` (required) - The IID of an issue
```json
[
@@ -63,13 +63,13 @@ Parameters:
Returns a single note for a specific project issue
```
-GET /projects/:id/issues/:issue_id/notes/:note_id
+GET /projects/:id/issues/:issue_iid/notes/:note_id
```
Parameters:
- `id` (required) - The ID of a project
-- `issue_id` (required) - The ID of a project issue
+- `issue_iid` (required) - The IID of a project issue
- `note_id` (required) - The ID of an issue note
### Create new issue note
@@ -78,13 +78,13 @@ Creates a new note to a single project issue. If you create a note where the bod
only contains an Award Emoji, you'll receive this object back.
```
-POST /projects/:id/issues/:issue_id/notes
+POST /projects/:id/issues/:issue_iid/notes
```
Parameters:
- `id` (required) - The ID of a project
-- `issue_id` (required) - The ID of an issue
+- `issue_id` (required) - The IID of an issue
- `body` (required) - The content of a note
- `created_at` (optional) - Date time string, ISO 8601 formatted, e.g. 2016-03-11T03:45:40Z
@@ -93,13 +93,13 @@ Parameters:
Modify existing note of an issue.
```
-PUT /projects/:id/issues/:issue_id/notes/:note_id
+PUT /projects/:id/issues/:issue_iid/notes/:note_id
```
Parameters:
- `id` (required) - The ID of a project
-- `issue_id` (required) - The ID of an issue
+- `issue_iid` (required) - The IID of an issue
- `note_id` (required) - The ID of a note
- `body` (required) - The content of a note
@@ -108,7 +108,7 @@ Parameters:
Deletes an existing note of an issue.
```
-DELETE /projects/:id/issues/:issue_id/notes/:note_id
+DELETE /projects/:id/issues/:issue_iid/notes/:note_id
```
Parameters:
@@ -116,7 +116,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project |
-| `issue_id` | integer | yes | The ID of an issue |
+| `issue_iid` | integer | yes | The IID of an issue |
| `note_id` | integer | yes | The ID of a note |
```bash
@@ -228,26 +228,26 @@ curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://git
Gets a list of all notes for a single merge request.
```
-GET /projects/:id/merge_requests/:merge_request_id/notes
+GET /projects/:id/merge_requests/:merge_request_iid/notes
```
Parameters:
- `id` (required) - The ID of a project
-- `merge_request_id` (required) - The ID of a project merge request
+- `merge_request_iid` (required) - The IID of a project merge request
### Get single merge request note
Returns a single note for a given merge request.
```
-GET /projects/:id/merge_requests/:merge_request_id/notes/:note_id
+GET /projects/:id/merge_requests/:merge_request_iid/notes/:note_id
```
Parameters:
- `id` (required) - The ID of a project
-- `merge_request_id` (required) - The ID of a project merge request
+- `merge_request_iid` (required) - The IID of a project merge request
- `note_id` (required) - The ID of a merge request note
```json
@@ -278,13 +278,13 @@ If you create a note where the body only contains an Award Emoji, you'll receive
this object back.
```
-POST /projects/:id/merge_requests/:merge_request_id/notes
+POST /projects/:id/merge_requests/:merge_request_iid/notes
```
Parameters:
- `id` (required) - The ID of a project
-- `merge_request_id` (required) - The ID of a merge request
+- `merge_request_iid` (required) - The IID of a merge request
- `body` (required) - The content of a note
### Modify existing merge request note
@@ -292,13 +292,13 @@ Parameters:
Modify existing note of a merge request.
```
-PUT /projects/:id/merge_requests/:merge_request_id/notes/:note_id
+PUT /projects/:id/merge_requests/:merge_request_iid/notes/:note_id
```
Parameters:
- `id` (required) - The ID of a project
-- `merge_request_id` (required) - The ID of a merge request
+- `merge_request_iid` (required) - The IID of a merge request
- `note_id` (required) - The ID of a note
- `body` (required) - The content of a note
@@ -307,7 +307,7 @@ Parameters:
Deletes an existing note of a merge request.
```
-DELETE /projects/:id/merge_requests/:merge_request_id/notes/:note_id
+DELETE /projects/:id/merge_requests/:merge_request_iid/notes/:note_id
```
Parameters:
@@ -315,7 +315,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project |
-| `merge_request_id` | integer | yes | The ID of a merge request |
+| `merge_request_iid` | integer | yes | The IID of a merge request |
| `note_id` | integer | yes | The ID of a note |
```bash
diff --git a/doc/api/settings.md b/doc/api/settings.md
index ad975e2e325..d99695ca986 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -48,7 +48,8 @@ Example response:
"koding_url": null,
"plantuml_enabled": false,
"plantuml_url": null,
- "terminal_max_session_time": 0
+ "terminal_max_session_time": 0,
+ "polling_interval_multiplier": 1.0
}
```
@@ -88,6 +89,7 @@ PUT /application/settings
| `plantuml_enabled` | boolean | no | Enable PlantUML integration. Default is `false`. |
| `plantuml_url` | string | yes (if `plantuml_enabled` is `true`) | The PlantUML instance URL for integration. |
| `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time. |
+| `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling. |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal
@@ -124,6 +126,7 @@ Example response:
"koding_url": null,
"plantuml_enabled": false,
"plantuml_url": null,
- "terminal_max_session_time": 0
+ "terminal_max_session_time": 0,
+ "polling_interval_multiplier": 1.0
}
```
diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md
index 7f4426ee85d..8e002fe0022 100644
--- a/doc/api/v3_to_v4.md
+++ b/doc/api/v3_to_v4.md
@@ -80,3 +80,4 @@ Below are the changes made between V3 and V4.
- `GET /projects/:id/repository/blobs/:sha` now returns JSON attributes for the blob identified by `:sha`, instead of finding the commit identified by `:sha` and returning the raw content of the blob in that commit identified by the required `?filepath=:filepath`
- Moved `GET /projects/:id/repository/commits/:sha/blob?file_path=:file_path` and `GET /projects/:id/repository/blobs/:sha?file_path=:file_path` to `GET /projects/:id/repository/files/:file_path/raw?ref=:sha`
- `GET /projects/:id/repository/tree` parameter `ref_name` has been renamed to `ref` for consistency
+- `confirm` parameter for `POST /users` has been deprecated in favor of `skip_confirmation` parameter
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index b3c9fe275c4..edb315d5b84 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -318,7 +318,7 @@ variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
before_script:
- - docker login -u gitlab-ci-token -p $CI_COMMIT_TOKEN $CI_REGISTRY
+ - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
build:
stage: build
diff --git a/doc/ci/examples/deployment/README.md b/doc/ci/examples/deployment/README.md
index d28aa282825..7b0995597c4 100644
--- a/doc/ci/examples/deployment/README.md
+++ b/doc/ci/examples/deployment/README.md
@@ -1,20 +1,30 @@
-## Using Dpl as deployment tool
-Dpl (dee-pee-ell) is a deploy tool made for continuous deployment that's developed and used by Travis CI, but can also be used with GitLab CI.
+# Using Dpl as deployment tool
-**We recommend to use Dpl, if you're deploying to any of these of these services: https://github.com/travis-ci/dpl#supported-providers**.
+[Dpl](https://github.com/travis-ci/dpl) (dee-pee-ell) is a deploy tool made for
+continuous deployment that's developed and used by Travis CI, but can also be
+used with GitLab CI.
-### Requirements
-To use Dpl you need at least Ruby 1.8.7 with ability to install gems.
+>**Note:**
+We recommend to use Dpl if you're deploying to any of these of these services:
+https://github.com/travis-ci/dpl#supported-providers.
+
+## Requirements
+
+To use Dpl you need at least Ruby 1.9.3 with ability to install gems.
+
+## Basic usage
+
+Dpl can be installed on any machine with:
-### Basic usage
-The Dpl can be installed on any machine with:
```
gem install dpl
```
-This allows you to test all commands from your shell, rather than having to test it on a CI server.
+This allows you to test all commands from your local terminal, rather than
+having to test it on a CI server.
If you don't have Ruby installed you can do it on Debian-compatible Linux with:
+
```
apt-get update
apt-get install ruby-dev
@@ -26,9 +36,10 @@ To use it simply define provider and any additional parameters required by the p
For example if you want to use it to deploy your application to heroku, you need to specify `heroku` as provider, specify `api-key` and `app`.
There's more and all possible parameters can be found here: https://github.com/travis-ci/dpl#heroku
-```
+```yaml
staging:
- type: deploy
+ stage: deploy
+ script:
- gem install dpl
- dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY
```
@@ -37,14 +48,17 @@ In the above example we use Dpl to deploy `my-app-staging` to Heroku server with
To use different provider take a look at long list of [Supported Providers](https://github.com/travis-ci/dpl#supported-providers).
-### Using Dpl with Docker
+## Using Dpl with Docker
+
When you use GitLab Runner you most likely configured it to use your server's shell commands.
This means that all commands are run in context of local user (ie. gitlab_runner or gitlab_ci_multi_runner).
It also means that most probably in your Docker container you don't have the Ruby runtime installed.
You will have to install it:
-```
+
+```yaml
staging:
- type: deploy
+ stage: deploy
+ script:
- apt-get update -yq
- apt-get install -y ruby-dev
- gem install dpl
@@ -53,24 +67,31 @@ staging:
- master
```
-The first line `apt-get update -yq` updates the list of available packages, where second `apt-get install -y ruby-dev` install `Ruby` runtime on system.
+The first line `apt-get update -yq` updates the list of available packages,
+where second `apt-get install -y ruby-dev` installs the Ruby runtime on system.
The above example is valid for all Debian-compatible systems.
-### Usage in staging and production
-It's pretty common in developer workflow to have staging (development) and production environment.
-If we consider above example: we would like to deploy `master` branch to `staging` and `all tags` to `production` environment.
+## Usage in staging and production
+
+It's pretty common in the development workflow to have staging (development) and
+production environments
+
+Let's consider the following example: we would like to deploy the `master`
+branch to `staging` and all tags to the `production` environment.
The final `.gitlab-ci.yml` for that setup would look like this:
-```
+```yaml
staging:
- type: deploy
+ stage: deploy
+ script:
- gem install dpl
- dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY
only:
- master
-
+
production:
- type: deploy
+ stage: deploy
+ script:
- gem install dpl
- dpl --provider=heroku --app=my-app-production --api-key=$HEROKU_PRODUCTION_API_KEY
only:
@@ -78,21 +99,28 @@ production:
```
We created two deploy jobs that are executed on different events:
+
1. `staging` is executed for all commits that were pushed to `master` branch,
2. `production` is executed for all pushed tags.
We also use two secure variables:
+
1. `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app,
2. `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app.
-### Storing API keys
-In GitLab CI 7.12 a new feature was introduced: Secure Variables.
-Secure Variables can added by going to `Project > Variables > Add Variable`.
-**This feature requires `gitlab-runner` with version equal or greater than 0.4.0.**
-The variables that are defined in the project settings are sent along with the build script to the runner.
-The secure variables are stored out of the repository. Never store secrets in your projects' .gitlab-ci.yml.
-It is also important that secret's value is hidden in the job log.
+## Storing API keys
+
+Secure Variables can added by going to your project's
+**Settings ➔ CI/CD Pipelines ➔ Secret variables**. The variables that are defined
+in the project settings are sent along with the build script to the Runner.
+The secure variables are stored out of the repository. Never store secrets in
+your project's `.gitlab-ci.yml`. It is also important that the secret's value
+is hidden in the job log.
+
+You access added variable by prefixing it's name with `$` (on non-Windows runners)
+or `%` (for Windows Batch runners):
-You access added variable by prefixing it's name with `$` (on non-Windows runners) or `%` (for Windows Batch runners):
1. `$SECRET_VARIABLE` - use it for non-Windows runners
2. `%SECRET_VARIABLE%` - use it for Windows Batch runners
+
+Read more about the [CI variables](../../variables/README.md).
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index ccaee33dc92..e380282f910 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -4,6 +4,7 @@
- [Introduced][ci-229] in GitLab CE 7.14.
- GitLab 8.12 has a completely redesigned job permissions system. Read all
about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#job-triggers).
+- GitLab 9.0 introduced a trigger ownership to solve permission problems.
Triggers can be used to force a rebuild of a specific `ref` (branch or tag)
with an API call.
@@ -21,13 +22,30 @@ overview of the time the triggers were last used.
![Triggers page overview](img/triggers_page.png)
+## Take ownership
+
+Each created trigger when run will impersonate their associated user including
+their access to projects and their project permissions.
+
+You can take ownership of existing triggers by clicking *Take ownership*.
+From now on the trigger will be run as you.
+
+## Legacy triggers
+
+Old triggers, created before 9.0 will be marked as Legacy. Triggers with
+the legacy label do not have an associated user and only have access
+to the current project.
+
+Legacy trigger are considered deprecated and will be removed
+with one of the future versions of GitLab.
+
## Revoke a trigger
You can revoke a trigger any time by going at your project's
**Settings > Triggers** and hitting the **Revoke** button. The action is
irreversible.
-## Trigger a job
+## Trigger a pipeline
> **Note**:
Valid refs are only the branches and tags. If you pass a commit SHA as a ref,
@@ -63,7 +81,7 @@ below.
See the [Examples](#examples) section for more details on how to actually
trigger a rebuild.
-## Trigger a job from webhook
+## Trigger a pipeline from webhook
> Introduced in GitLab 8.14.
@@ -117,7 +135,7 @@ curl --request POST \
"https://gitlab.example.com/api/v4/projects/9/trigger/pipeline?token=TOKEN&ref=master"
```
-### Triggering a job within `.gitlab-ci.yml`
+### Triggering a pipeline within `.gitlab-ci.yml`
You can also benefit by using triggers in your `.gitlab-ci.yml`. Let's say that
you have two projects, A and B, and you want to trigger a rebuild on the `master`
diff --git a/doc/ci/triggers/img/triggers_page.png b/doc/ci/triggers/img/triggers_page.png
index 8ebf68d0384..eafd8519a23 100644
--- a/doc/ci/triggers/img/triggers_page.png
+++ b/doc/ci/triggers/img/triggers_page.png
Binary files differ
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 4e9094cb0f1..53c29a4fd98 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -352,7 +352,7 @@ Example values:
export CI_JOB_ID="50"
export CI_COMMIT_SHA="1ecfd275763eff1d6b4844ea3168962458c9f27a"
export CI_COMMIT_REF_NAME="master"
-export CI_REPOSITORY="https://gitab-ci-token:abcde-1234ABCD5678ef@example.com/gitlab-org/gitlab-ce.git"
+export CI_REPOSITORY_URL="https://gitlab-ci-token:abcde-1234ABCD5678ef@example.com/gitlab-org/gitlab-ce.git"
export CI_COMMIT_TAG="1.0.0"
export CI_JOB_NAME="spec:other"
export CI_JOB_STAGE="test"
diff --git a/doc/development/README.md b/doc/development/README.md
index e27e7fc7d19..3c797505aa9 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -12,6 +12,8 @@
contributing to the API.
- [Documentation styleguide](doc_styleguide.md) Use this styleguide if you are
contributing to documentation.
+- [Writing documentation](writing_documentation.md)
+ - [Distinction between general documentation and technical articles](writing_documentation.md#distinction-between-general-documentation-and-technical-articles)
- [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations
- [Testing standards and style guidelines](testing.md)
- [UX guide](ux_guide/index.md) for building GitLab with existing CSS styles and elements
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 9bed441c131..bb78a0de0c5 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -3,6 +3,8 @@
This styleguide recommends best practices to improve documentation and to keep
it organized and easy to find.
+See also [writing documentation](writing_documentation.md).
+
## Location and naming of documents
>**Note:**
@@ -27,6 +29,7 @@ The table below shows what kind of documentation goes where.
| `doc/legal/` | Legal documents about contributing to GitLab. |
| `doc/install/`| Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). |
| `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. |
+| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`); Technical Articles: user guides, admin guides, technical overviews, tutorials (`doc/topics/topic-name/`). |
---
@@ -56,6 +59,10 @@ The table below shows what kind of documentation goes where.
own document located at `doc/user/admin_area/settings/`. For example,
the **Visibility and Access Controls** category should have a document
located at `doc/user/admin_area/settings/visibility_and_access_controls.md`.
+1. The `doc/topics/` directory holds topic-related technical content. Create
+ `doc/topics/topic-name/subtopic-name/index.md` when subtopics become necessary.
+ Note that `topics` holds the index page per topic, and technical articles. General
+ user- and admin- related documentation, should be placed accordingly.
---
diff --git a/doc/development/fe_guide/performance.md b/doc/development/fe_guide/performance.md
index 2d76bb18cff..9437a5f7a6e 100644
--- a/doc/development/fe_guide/performance.md
+++ b/doc/development/fe_guide/performance.md
@@ -17,8 +17,7 @@ polling rate.
A `Poll-Interval: -1` means you should disable polling, and this must be implemented.
1. A response with HTTP status `4XX` or `5XX` should disable polling as well.
1. Use a common library for polling.
-1. Poll on active tabs only. Use a common library to find out which tab currently has eyes on it.
-Please use [Focus](https://gitlab.com/andrewn/focus). Specifically [Eyeballs Detector](https://gitlab.com/andrewn/focus/blob/master/lib/eyeballs-detector.js).
+1. Poll on active tabs only. Please use [Visibility](https://github.com/ai/visibilityjs).
1. Use regular polling intervals, do not use backoff polling, or jitter, as the interval will be
controlled by the server.
1. The backend code will most likely be using etags. You do not and should not check for status
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index 034cfe73d33..abd241c0bc8 100644
--- a/doc/development/fe_guide/style_guide_js.md
+++ b/doc/development/fe_guide/style_guide_js.md
@@ -200,7 +200,6 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
#### Naming
- **Extensions**: Use `.vue` extension for Vue components.
- **Reference Naming**: Use PascalCase for Vue components and camelCase for their instances:
-
```javascript
// bad
import cardBoard from 'cardBoard';
@@ -218,15 +217,23 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
cardBoard: CardBoard
};
```
-- **Props Naming**: Avoid using DOM component prop names.
+- **Props Naming:**
+- Avoid using DOM component prop names.
+- Use kebab-case instead of camelCase to provide props in templates.
```javascript
// bad
<component class="btn">
// good
- <component cssClass="btn">
- ```
+ <component css-class="btn">
+
+ // bad
+ <component myProp="prop" />
+
+ // good
+ <component my-prop="prop" />
+```
#### Alignment
- Follow these alignment styles for the template method:
diff --git a/doc/development/fe_guide/testing.md b/doc/development/fe_guide/testing.md
index bb6adeacc4c..8d3513d3566 100644
--- a/doc/development/fe_guide/testing.md
+++ b/doc/development/fe_guide/testing.md
@@ -7,7 +7,7 @@ feature tests with Capybara for integration testing.
Feature tests need to be written for all new features. Regression tests ought
to be written for all bug fixes to prevent them from recurring in the future.
-See [the Testing Standards and Style Guidelines](/doc/development/testing.md)
+See [the Testing Standards and Style Guidelines](../testing.md)
for more information on general testing practices at GitLab.
## Karma test suite
@@ -48,7 +48,7 @@ remove these directives when you commit your code.
Information on setting up and running RSpec integration tests with
[Capybara][capybara] can be found in the
-[general testing guide](/doc/development/testing.md).
+[general testing guide](../testing.md).
## Gotchas
diff --git a/doc/development/img/cache-hit.svg b/doc/development/img/cache-hit.svg
new file mode 100644
index 00000000000..1c37693df2d
--- /dev/null
+++ b/doc/development/img/cache-hit.svg
@@ -0,0 +1,21 @@
+<svg version="1.1" id="mscgen_js-svg-__svg" class="mscgen_js-svg-__svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="976" height="310" viewBox="0 0 976 310"><desc>
+
+# Generated by mscgen_js - https://sverweij.github.io/mscgen_js
+msc {
+ # options
+ hscale="1.5";
+
+ # entities
+ c [label="Client", textbgcolor="lime"],
+ rails [label="Rails", textbgcolor="cyan"],
+ etag [label="EtagCaching", textbgcolor="orange"],
+ redis [label="Redis", textbgcolor="white"];
+
+ # arcs
+ c =&gt; rails [label="GET /projects/5/pipelines"];
+ rails =&gt; etag [label="GET /projects/5/pipelines"];
+ etag =&gt; redis [label="read(key = 'GET &lt;Etag&gt;')"];
+ redis =&gt; etag [label="cache hit", linecolor="green", textcolor="green"];
+ |||;
+ etag =&gt; c [label="304 Not Modified", linecolor="blue", textcolor="blue"];
+}</desc><defs><style type="text/css">svg.mscgen_js-svg-__svg{font-family:Helvetica,sans-serif;font-size:12px;font-weight:normal;font-style:normal;text-decoration:none;background-color:white;stroke:black;stroke-width:2;color:black}.mscgen_js-svg-__svg path, .mscgen_js-svg-__svg rect{fill:none;color:black;stroke:black}.mscgen_js-svg-__svg .label-text-background{fill:white;stroke:white;stroke-width:0}.mscgen_js-svg-__svg .bglayer{fill:white;stroke:white;stroke-width:0}.mscgen_js-svg-__svg line{stroke:black}.mscgen_js-svg-__svg .return, .mscgen_js-svg-__svg .comment{stroke-dasharray:5,3}.mscgen_js-svg-__svg .inline_expression_divider{stroke-dasharray:10,5}.mscgen_js-svg-__svg text{color:inherit;stroke:none;text-anchor:middle}.mscgen_js-svg-__svg text.entity-text{text-decoration:underline}.mscgen_js-svg-__svg text.anchor-start{text-anchor:start}.mscgen_js-svg-__svg .arrow-marker{overflow:visible}.mscgen_js-svg-__svg .arrow-style{stroke-width:1}.mscgen_js-svg-__svg .arcrow, .mscgen_js-svg-__svg .arcrowomit, .mscgen_js-svg-__svg .emphasised{stroke-linecap:butt}.mscgen_js-svg-__svg .arcrowomit{stroke-dasharray:2,2;}.mscgen_js-svg-__svg .box, .mscgen_js-svg-__svg .entity{fill:white;stroke-linejoin:round}.mscgen_js-svg-__svg .inherit{stroke:inherit;color:inherit}.mscgen_js-svg-__svg .inherit-fill{fill:inherit}.mscgen_js-svg-__svg .watermark{stroke:black;color:black;fill:black;font-size:48pt;font-weight:bold;opacity:0.14}</style><marker orient="auto" id="mscgen_js-svg-__svgmethod-black" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="1,1 9,3 1,5" class="arrow-style" stroke="black" fill="black"></polygon></marker><marker orient="auto" id="mscgen_js-svg-__svgmethod-l-black" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="17,1 9,3 17,5" class="arrow-style" stroke="black" fill="black"></polygon></marker><marker orient="auto" id="mscgen_js-svg-__svgmethod-blue" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="1,1 9,3 1,5" class="arrow-style" stroke="blue" fill="blue"></polygon></marker><marker orient="auto" id="mscgen_js-svg-__svgmethod-l-blue" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="17,1 9,3 17,5" class="arrow-style" stroke="blue" fill="blue"></polygon></marker><marker orient="auto" id="mscgen_js-svg-__svgmethod-green" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="1,1 9,3 1,5" class="arrow-style" stroke="green" fill="green"></polygon></marker><marker orient="auto" id="mscgen_js-svg-__svgmethod-l-green" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="17,1 9,3 17,5" class="arrow-style" stroke="green" fill="green"></polygon></marker></defs><g id="mscgen_js-svg-__svg__body" transform="translate(53,3) scale(1,1)"><g id="mscgen_js-svg-__svg__background"><rect width="976" height="310" x="-53" y="-3" class="bglayer"></rect></g><g id="mscgen_js-svg-__svg__arcspanlayer"></g><g id="mscgen_js-svg-__svg__lifelinelayer"><line x1="75" y1="38" x2="75" y2="76" class="arcrow"></line><line x1="315" y1="38" x2="315" y2="76" class="arcrow"></line><line x1="555" y1="38" x2="555" y2="76" class="arcrow"></line><line x1="795" y1="38" x2="795" y2="76" class="arcrow"></line><line x1="75" y1="76" x2="75" y2="114" class="arcrow"></line><line x1="315" y1="76" x2="315" y2="114" class="arcrow"></line><line x1="555" y1="76" x2="555" y2="114" class="arcrow"></line><line x1="795" y1="76" x2="795" y2="114" class="arcrow"></line><line x1="75" y1="114" x2="75" y2="152" class="arcrow"></line><line x1="315" y1="114" x2="315" y2="152" class="arcrow"></line><line x1="555" y1="114" x2="555" y2="152" class="arcrow"></line><line x1="795" y1="114" x2="795" y2="152" class="arcrow"></line><line x1="75" y1="152" x2="75" y2="190" class="arcrow"></line><line x1="315" y1="152" x2="315" y2="190" class="arcrow"></line><line x1="555" y1="152" x2="555" y2="190" class="arcrow"></line><line x1="795" y1="152" x2="795" y2="190" class="arcrow"></line><line x1="75" y1="190" x2="75" y2="228" class="arcrow"></line><line x1="315" y1="190" x2="315" y2="228" class="arcrow"></line><line x1="555" y1="190" x2="555" y2="228" class="arcrow"></line><line x1="795" y1="190" x2="795" y2="228" class="arcrow"></line><line x1="75" y1="228" x2="75" y2="266" class="arcrow"></line><line x1="315" y1="228" x2="315" y2="266" class="arcrow"></line><line x1="555" y1="228" x2="555" y2="266" class="arcrow"></line><line x1="795" y1="228" x2="795" y2="266" class="arcrow"></line><line x1="75" y1="266" x2="75" y2="304" class="arcrow"></line><line x1="315" y1="266" x2="315" y2="304" class="arcrow"></line><line x1="555" y1="266" x2="555" y2="304" class="arcrow"></line><line x1="795" y1="266" x2="795" y2="304" class="arcrow"></line></g><g id="mscgen_js-svg-__svg__sequencelayer"><g id="mscgen_js-svg-__svgentities"><g><rect width="150" height="38" class="entity" style="fill:lime;"></rect><g><text x="75" y="22.5" class="entity-text "><tspan>Client</tspan></text></g></g><g><rect width="150" height="38" x="240" class="entity" style="fill:cyan;"></rect><g><text x="315" y="22.5" class="entity-text "><tspan>Rails</tspan></text></g></g><g><rect width="150" height="38" x="480" class="entity" style="fill:orange;"></rect><g><text x="555" y="22.5" class="entity-text "><tspan>EtagCaching</tspan></text></g></g><g><rect width="150" height="38" x="720" class="entity" style="fill:white;"></rect><g><text x="795" y="22.5" class="entity-text "><tspan>Redis</tspan></text></g></g></g><g><line x1="75" y1="95" x2="315" y2="95" class="arc directional method" style="stroke:black" marker-end="url(#mscgen_js-svg-__svgmethod-black)"></line><g><rect width="134.06" height="14" x="127.97" y="79.5" class="label-text-background"></rect><text x="195" y="90.5" class="directional-text method-text "><tspan>GET /projects/5/pipelines</tspan></text></g></g><g><line x1="315" y1="133" x2="555" y2="133" class="arc directional method" style="stroke:black" marker-end="url(#mscgen_js-svg-__svgmethod-black)"></line><g><rect width="134.06" height="14" x="367.97" y="117.5" class="label-text-background"></rect><text x="435" y="128.5" class="directional-text method-text "><tspan>GET /projects/5/pipelines</tspan></text></g></g><g><line x1="555" y1="171" x2="795" y2="171" class="arc directional method" style="stroke:black" marker-end="url(#mscgen_js-svg-__svgmethod-black)"></line><g><rect width="135.64" height="14" x="607.17" y="155.5" class="label-text-background"></rect><text x="675" y="166.5" class="directional-text method-text "><tspan>read(key = 'GET &lt;Etag&gt;')</tspan></text></g></g><g><line x1="795" y1="209" x2="555" y2="209" class="arc directional method" style="stroke:green" marker-end="url(#mscgen_js-svg-__svgmethod-green)"></line><g><rect width="48.02" height="14" x="650.98" y="193.5" class="label-text-background"></rect><text x="675" y="204.5" class="directional-text method-text " style="fill:green;"><tspan>cache hit</tspan></text></g></g><g></g><g><line x1="555" y1="285" x2="75" y2="285" class="arc directional method" style="stroke:blue" marker-end="url(#mscgen_js-svg-__svgmethod-blue)"></line><g><rect width="90.72" height="14" x="269.63" y="269.5" class="label-text-background"></rect><text x="315" y="280.5" class="directional-text method-text " style="fill:blue;"><tspan>304 Not Modified</tspan></text></g></g></g><g id="mscgen_js-svg-__svg__notelayer"></g><g id="mscgen_js-svg-__svg__watermark"></g></g></svg> \ No newline at end of file
diff --git a/doc/development/img/cache-miss.svg b/doc/development/img/cache-miss.svg
new file mode 100644
index 00000000000..8429e6a1918
--- /dev/null
+++ b/doc/development/img/cache-miss.svg
@@ -0,0 +1,24 @@
+<svg version="1.1" id="mscgen_js-svg-__svg" class="mscgen_js-svg-__svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="976" height="386" viewBox="0 0 976 386"><desc>
+
+# Generated by mscgen_js - https://sverweij.github.io/mscgen_js
+msc {
+ # options
+ hscale="1.5";
+
+ # entities
+ c [label="Client", textbgcolor="lime"],
+ rails [label="Rails", textbgcolor="cyan"],
+ etag [label="EtagCaching", textbgcolor="orange"],
+ redis [label="Redis", textbgcolor="white"];
+
+ # arcs
+ c =&gt; rails [label="GET /projects/5/pipelines"];
+ rails =&gt; etag [label="GET /projects/5/pipelines"];
+ etag =&gt; redis [label="read(key = 'GET &lt;Etag&gt;')"];
+ redis =&gt; etag [label="cache miss", linecolor="red", textcolor="red"];
+ |||;
+ etag =&gt; redis [label="write('&lt;New Etag&gt;')"];
+ etag =&gt; rails [label="GET /projects/5/pipelines"];
+ rails =&gt; c [label="JSON response w/ ETag", linecolor="blue", textcolor="blue"];
+}
+</desc><defs><style type="text/css">svg.mscgen_js-svg-__svg{font-family:Helvetica,sans-serif;font-size:12px;font-weight:normal;font-style:normal;text-decoration:none;background-color:white;stroke:black;stroke-width:2}.mscgen_js-svg-__svg path, .mscgen_js-svg-__svg rect{fill:none}.mscgen_js-svg-__svg .label-text-background{fill:white;stroke:white;stroke-width:0}.mscgen_js-svg-__svg .bglayer{fill:white;stroke:white;stroke-width:0}.mscgen_js-svg-__svg line{}.mscgen_js-svg-__svg .return, .mscgen_js-svg-__svg .comment{stroke-dasharray:5,3}.mscgen_js-svg-__svg .inline_expression_divider{stroke-dasharray:10,5}.mscgen_js-svg-__svg text{color:inherit;stroke:none;text-anchor:middle}.mscgen_js-svg-__svg text.entity-text{text-decoration:underline}.mscgen_js-svg-__svg text.anchor-start{text-anchor:start}.mscgen_js-svg-__svg .arrow-marker{overflow:visible}.mscgen_js-svg-__svg .arrow-style{stroke-width:1}.mscgen_js-svg-__svg .arcrow, .mscgen_js-svg-__svg .arcrowomit, .mscgen_js-svg-__svg .emphasised{stroke-linecap:butt}.mscgen_js-svg-__svg .arcrowomit{stroke-dasharray:2,2}.mscgen_js-svg-__svg .box, .mscgen_js-svg-__svg .entity{fill:white;stroke-linejoin:round}.mscgen_js-svg-__svg .inherit{stroke:inherit;color:inherit}.mscgen_js-svg-__svg .inherit-fill{fill:inherit}.mscgen_js-svg-__svg .watermark{font-size:48pt;font-weight:bold;opacity:0.14}</style><marker orient="auto" id="mscgen_js-svg-__svgmethod-black" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="1,1 9,3 1,5" class="arrow-style" stroke="black" fill="black"></polygon></marker><marker orient="auto" id="mscgen_js-svg-__svgmethod-l-black" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="17,1 9,3 17,5" class="arrow-style" stroke="black" fill="black"></polygon></marker><marker orient="auto" id="mscgen_js-svg-__svgmethod-blue" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="1,1 9,3 1,5" class="arrow-style" stroke="blue" fill="blue"></polygon></marker><marker orient="auto" id="mscgen_js-svg-__svgmethod-l-blue" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="17,1 9,3 17,5" class="arrow-style" stroke="blue" fill="blue"></polygon></marker><marker orient="auto" id="mscgen_js-svg-__svgmethod-red" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="1,1 9,3 1,5" class="arrow-style" stroke="red" fill="red"></polygon></marker><marker orient="auto" id="mscgen_js-svg-__svgmethod-l-red" class="arrow-marker" viewBox="0 0 10 10" refX="9" refY="3" markerUnits="strokeWidth" markerWidth="10" markerHeight="10"><polygon points="17,1 9,3 17,5" class="arrow-style" stroke="red" fill="red"></polygon></marker></defs><g id="mscgen_js-svg-__svg__body" transform="translate(53,3) scale(1,1)"><g id="mscgen_js-svg-__svg__background"><rect width="976" height="386" x="-53" y="-3" class="bglayer"></rect></g><g id="mscgen_js-svg-__svg__arcspanlayer"></g><g id="mscgen_js-svg-__svg__lifelinelayer"><line x1="75" y1="38" x2="75" y2="76" class="arcrow"></line><line x1="315" y1="38" x2="315" y2="76" class="arcrow"></line><line x1="555" y1="38" x2="555" y2="76" class="arcrow"></line><line x1="795" y1="38" x2="795" y2="76" class="arcrow"></line><line x1="75" y1="76" x2="75" y2="114" class="arcrow"></line><line x1="315" y1="76" x2="315" y2="114" class="arcrow"></line><line x1="555" y1="76" x2="555" y2="114" class="arcrow"></line><line x1="795" y1="76" x2="795" y2="114" class="arcrow"></line><line x1="75" y1="114" x2="75" y2="152" class="arcrow"></line><line x1="315" y1="114" x2="315" y2="152" class="arcrow"></line><line x1="555" y1="114" x2="555" y2="152" class="arcrow"></line><line x1="795" y1="114" x2="795" y2="152" class="arcrow"></line><line x1="75" y1="152" x2="75" y2="190" class="arcrow"></line><line x1="315" y1="152" x2="315" y2="190" class="arcrow"></line><line x1="555" y1="152" x2="555" y2="190" class="arcrow"></line><line x1="795" y1="152" x2="795" y2="190" class="arcrow"></line><line x1="75" y1="190" x2="75" y2="228" class="arcrow"></line><line x1="315" y1="190" x2="315" y2="228" class="arcrow"></line><line x1="555" y1="190" x2="555" y2="228" class="arcrow"></line><line x1="795" y1="190" x2="795" y2="228" class="arcrow"></line><line x1="75" y1="228" x2="75" y2="266" class="arcrow"></line><line x1="315" y1="228" x2="315" y2="266" class="arcrow"></line><line x1="555" y1="228" x2="555" y2="266" class="arcrow"></line><line x1="795" y1="228" x2="795" y2="266" class="arcrow"></line><line x1="75" y1="266" x2="75" y2="304" class="arcrow"></line><line x1="315" y1="266" x2="315" y2="304" class="arcrow"></line><line x1="555" y1="266" x2="555" y2="304" class="arcrow"></line><line x1="795" y1="266" x2="795" y2="304" class="arcrow"></line><line x1="75" y1="304" x2="75" y2="342" class="arcrow"></line><line x1="315" y1="304" x2="315" y2="342" class="arcrow"></line><line x1="555" y1="304" x2="555" y2="342" class="arcrow"></line><line x1="795" y1="304" x2="795" y2="342" class="arcrow"></line><line x1="75" y1="342" x2="75" y2="380" class="arcrow"></line><line x1="315" y1="342" x2="315" y2="380" class="arcrow"></line><line x1="555" y1="342" x2="555" y2="380" class="arcrow"></line><line x1="795" y1="342" x2="795" y2="380" class="arcrow"></line></g><g id="mscgen_js-svg-__svg__sequencelayer"><g id="mscgen_js-svg-__svgentities"><g><rect width="150" height="38" class="entity" style="fill:lime;"></rect><g><text x="75" y="22.5" class="entity-text "><tspan>Client</tspan></text></g></g><g><rect width="150" height="38" x="240" class="entity" style="fill:cyan;"></rect><g><text x="315" y="22.5" class="entity-text "><tspan>Rails</tspan></text></g></g><g><rect width="150" height="38" x="480" class="entity" style="fill:orange;"></rect><g><text x="555" y="22.5" class="entity-text "><tspan>EtagCaching</tspan></text></g></g><g><rect width="150" height="38" x="720" class="entity" style="fill:white;"></rect><g><text x="795" y="22.5" class="entity-text "><tspan>Redis</tspan></text></g></g></g><g><line x1="75" y1="95" x2="315" y2="95" class="arc directional method" style="stroke:black" marker-end="url(#mscgen_js-svg-__svgmethod-black)"></line><g><rect width="134.06" height="14" x="127.97" y="79.5" class="label-text-background"></rect><text x="195" y="90.5" class="directional-text method-text "><tspan>GET /projects/5/pipelines</tspan></text></g></g><g><line x1="315" y1="133" x2="555" y2="133" class="arc directional method" style="stroke:black" marker-end="url(#mscgen_js-svg-__svgmethod-black)"></line><g><rect width="134.06" height="14" x="367.97" y="117.5" class="label-text-background"></rect><text x="435" y="128.5" class="directional-text method-text "><tspan>GET /projects/5/pipelines</tspan></text></g></g><g><line x1="555" y1="171" x2="795" y2="171" class="arc directional method" style="stroke:black" marker-end="url(#mscgen_js-svg-__svgmethod-black)"></line><g><rect width="135.64" height="14" x="607.17" y="155.5" class="label-text-background"></rect><text x="675" y="166.5" class="directional-text method-text "><tspan>read(key = 'GET &lt;Etag&gt;')</tspan></text></g></g><g><line x1="795" y1="209" x2="555" y2="209" class="arc directional method" style="stroke:red" marker-end="url(#mscgen_js-svg-__svgmethod-red)"></line><g><rect width="60.02" height="14" x="644.98" y="193.5" class="label-text-background"></rect><text x="675" y="204.5" class="directional-text method-text " style="fill:red;"><tspan>cache miss</tspan></text></g></g><g></g><g><line x1="555" y1="285" x2="795" y2="285" class="arc directional method" style="stroke:black" marker-end="url(#mscgen_js-svg-__svgmethod-black)"></line><g><rect width="103.94" height="14" x="623.02" y="269.5" class="label-text-background"></rect><text x="675" y="280.5" class="directional-text method-text "><tspan>write('&lt;New Etag&gt;')</tspan></text></g></g><g><line x1="555" y1="323" x2="315" y2="323" class="arc directional method" style="stroke:black" marker-end="url(#mscgen_js-svg-__svgmethod-black)"></line><g><rect width="134.06" height="14" x="367.97" y="307.5" class="label-text-background"></rect><text x="435" y="318.5" class="directional-text method-text "><tspan>GET /projects/5/pipelines</tspan></text></g></g><g><line x1="315" y1="361" x2="75" y2="361" class="arc directional method" style="stroke:blue" marker-end="url(#mscgen_js-svg-__svgmethod-blue)"></line><g><rect width="130.72" height="14" x="129.63" y="345.5" class="label-text-background"></rect><text x="195" y="356.5" class="directional-text method-text " style="fill:blue;"><tspan>JSON response w/ ETag</tspan></text></g></g></g><g id="mscgen_js-svg-__svg__notelayer"></g><g id="mscgen_js-svg-__svg__watermark"></g></g></svg> \ No newline at end of file
diff --git a/doc/development/polling.md b/doc/development/polling.md
index a7f2962acf0..e5a717f712b 100644
--- a/doc/development/polling.md
+++ b/doc/development/polling.md
@@ -22,6 +22,9 @@ Instead you should use polling mechanism with ETag caching in Redis.
## How it works
+![Cache miss](img/cache-miss.svg)
+![Cache hit](img/cache-hit.svg)
+
1. Whenever a resource changes we generate a random value and store it in
Redis.
1. When a client makes a request we set the `ETag` response header to the value
diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md
index 18d0647c798..ac7c1b6207d 100644
--- a/doc/development/ux_guide/components.md
+++ b/doc/development/ux_guide/components.md
@@ -19,14 +19,24 @@
---
## Tooltips
+Tooltips identify elements or provide additional, useful information about the referring elements. Tooltips are different from ALT-attributes, which are intended primarily for static images. Tooltips are summoned by:
+
+* Hovering over an element with a cursor
+* Focusing on an element with a keyboard (usually the tab key)
+* Upon touch
### Usage
-A tooltip should only be added if additional information is required.
+A tooltip should be used:
+* When there isn’t enough space to show the information
+* When it isn’t critical for the user to see the information
+* For icons that don’t have a label
+
+Tooltips shouldn’t repeat information that is shown near the referring element. However, they can show the same data in a different format (e.g. date or timestamps).
![Tooltip usage](img/tooltip-usage.png)
### Placement
-By default, tooltips should be placed below the element that they refer to. However, if there is not enough space in the viewpoint, the tooltip should be moved to the side as needed.
+By default, tooltips should be placed below the referring element. However, if there isn’t enough space in the viewport, the tooltip should be moved to the side as needed.
![Tooltip placement location](img/tooltip-placement.png)
diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md
new file mode 100644
index 00000000000..482ec54207b
--- /dev/null
+++ b/doc/development/writing_documentation.md
@@ -0,0 +1,72 @@
+# Writing documentation
+
+ - **General Documentation**: written by the developers responsible by creating features. Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers.
+ - **Technical Articles**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/).
+ - **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs, in the same merge request containing code.
+
+## Distinction between General Documentation and Technical Articles
+
+### General documentation
+
+General documentation is categorized by _User_, _Admin_, and _Contributor_, and describe what that feature is, what it does, and its available settings.
+
+### Technical Articles
+
+Technical articles replace technical content that once lived in the [GitLab Blog](https://about.gitlab.com/blog/), where they got out-of-date and weren't easily found.
+
+They are topic-related documentation, written with an user-friendly approach and language, aiming to provide the community with guidance on specific processes to achieve certain objectives.
+
+A technical article guides users and/or admins to achieve certain objectives (within guides and tutorials), or provide an overview of that particular topic or feature (within technical overviews). It can also describe the use, implementation, or integration of third-party tools with GitLab.
+
+They live under `doc/topics/topic-name/`, and can be searched per topic, within "Indexes per Topic" pages. The topics are listed on the main [Indexes per Topic](../topics/index.md) page.
+
+#### Types of Technical Articles
+
+- **User guides**: technical content to guide regular users from point A to point B
+- **Admin guides**: technical content to guide administrators of GitLab instances from point A to point B
+- **Technical Overviews**: technical content describing features, solutions, and third-party integrations
+- **Tutorials**: technical content provided step-by-step on how to do things, or how to reach very specific objectives
+
+#### Understanding guides, tutorials, and technical overviews
+
+Suppose there's a process to go from point A to point B in 5 steps: `(A) 1 > 2 > 3 > 4 > 5 (B)`.
+
+A **guide** can be understood as a description of certain processes to achieve a particular objective. A guide brings you from A to B describing the characteristics of that process, but not necessarily going over each step. It can mention, for example, steps 2 and 3, but does not necessarily explain how to accomplish them.
+
+- Live example: "GitLab Pages from A to Z - [Part 1](../user/project/pages/getting_started_part_one.md) to [Part 4](../user/project/pages/getting_started_part_four.md)"
+
+A **tutorial** requires a clear **step-by-step** guidance to achieve a singular objective. It brings you from A to B, describing precisely all the necessary steps involved in that process, showing each of the 5 steps to go from A to B.
+It does not only describes steps 2 and 3, but also shows you how to accomplish them.
+
+- Live example (on the blog): [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/)
+
+A **technical overview** is a description of what a certain feature is, and what it does, but does not walk
+through the process of how to use it systematically.
+
+- Live example (on the blog): [GitLab Workflow, an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/)
+
+#### Special format
+
+Every **Technical Article** contains, in the very beginning, a blockquote with the following information:
+
+- A reference to the **type of article** (user guide, admin guide, tech overview, tutorial)
+- A reference to the **knowledge level** expected from the reader to be able to follow through (beginner, intermediate, advanced)
+- A reference to the **author's name** and **GitLab.com handle**
+
+```md
+> **Type:** tutorial ||
+> **Level:** intermediary ||
+> **Author:** [Name Surname](https://gitlab.com/username)
+```
+
+#### Technical Articles - Writing Method
+
+Use the [writing method](https://about.gitlab.com/handbook/product/technical-writing/#writing-method) defined by the Technical Writing team.
+
+## Documentation style guidelines
+
+All the docs follow the same [styleguide](doc_styleguide.md).
+
+### Markdown
+
+Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index a6b10176450..a2248a38435 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -477,12 +477,12 @@ with setting up Gitaly until you upgrade to GitLab 9.1 or later.
# Enable Gitaly in the init script
echo 'gitaly_enabled=true' | sudo tee -a /etc/default/gitlab
-Next, edit `/home/git/gitlab/config/gitlab.yml` and make sure `socket_path` in
+Next, edit `/home/git/gitlab/config/gitlab.yml` and make sure `enabled: true` in
the `gitaly:` section is uncommented.
# <- gitlab.yml indentation starts here
gitaly:
- socket_path: tmp/sockets/private/gitaly.socket
+ enabled: true
For more information about configuring Gitaly see
[doc/administration/gitaly](../administration/gitaly).
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 7b586138f42..35586091f74 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -129,6 +129,9 @@ If you want to run the database separately, expect a size of about 1 MB per user
### PostgreSQL Requirements
+As of GitLab 9.0, PostgreSQL 9.6 is recommended. Lower versions of PostgreSQL
+may work but primary testing and developement takes place using PostgreSQL 9.6.
+
Users using PostgreSQL must ensure the `pg_trgm` extension is loaded into every
GitLab database. This extension can be enabled (using a PostgreSQL super user)
by running the following query for every database:
diff --git a/doc/topics/index.md b/doc/topics/index.md
new file mode 100644
index 00000000000..6de13d79554
--- /dev/null
+++ b/doc/topics/index.md
@@ -0,0 +1,16 @@
+# Topics
+
+Welcome to Topics! We have organized our content resources into topics
+to get you started on areas of your interest. Each topic page
+consists of an index listing all related content. It will guide
+you through better understanding GitLab's concepts
+through our regular docs, and, when available, through articles (guides,
+tutorials, technical overviews, blog posts) and videos.
+
+- [GitLab Installation](../install/README.md)
+- [Continuous Integration (GitLab CI)](../ci/README.md)
+- [GitLab Pages](../user/project/pages/index.md)
+
+>**Note:**
+Non-linked topics are currently under development and subjected to change.
+More topics will be available soon.
diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md
index f28896c2227..4b3c5bf6d64 100644
--- a/doc/update/8.2-to-8.3.md
+++ b/doc/update/8.2-to-8.3.md
@@ -120,6 +120,14 @@ There are new configuration options available for [`gitlab.yml`][yaml]. View the
git diff origin/8-2-stable:config/gitlab.yml.example origin/8-3-stable:config/gitlab.yml.example
```
+#### GitLab default file
+
+The value of the `gitlab_workhorse_options` variable should be updated within the default gitlab file (`/etc/default/gitlab`) according to the following diff:
+
+```sh
+git diff origin/8-2-stable:lib/support/init.d/gitlab.default.example origin/8-3-stable:lib/support/init.d/gitlab.default.example
+```
+
#### Nginx configuration
GitLab 8.3 introduces major changes in the NGINX configuration.
diff --git a/doc/update/9.0-to-9.1.md b/doc/update/9.0-to-9.1.md
new file mode 100644
index 00000000000..53cddb3f290
--- /dev/null
+++ b/doc/update/9.0-to-9.1.md
@@ -0,0 +1,366 @@
+# From 9.0 to 9.1
+
+** TODO: **
+
+# TODO clean out 9.0-specific stuff
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be
+sure to upgrade your interpreter if necessary.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
+echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
+cd ruby-2.3.3
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and
+it has a minimum requirement of node v4.3.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v4.3.0` you will need to update to a newer version. You
+can find instructions to install from community maintained packages or compile
+from source at the nodejs.org website.
+
+<https://nodejs.org/en/download/>
+
+
+Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
+JavaScript dependencies.
+
+```bash
+curl --location https://yarnpkg.com/install.sh | bash -
+```
+
+More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
+
+### 5. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 9-1-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 9-1-stable-ee
+```
+
+### 6. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
+```
+
+### 7. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires
+[Go 1.5](https://golang.org/dl) which should already be on your system from
+GitLab 8.1. GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-workhorse
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
+sudo -u git -H make
+```
+
+### 8. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/9-0-stable:config/gitlab.yml.example origin/9-1-stable:config/gitlab.yml.example
+```
+
+#### Configuration changes for repository storages
+
+This version introduces a new configuration structure for repository storages.
+Update your current configuration as follows, replacing with your storages names and paths:
+
+**For installations from source**
+
+1. Update your `gitlab.yml`, from
+
+ ```yaml
+ repositories:
+ storages: # You must have at least a 'default' storage path.
+ default: /home/git/repositories
+ nfs: /mnt/nfs/repositories
+ cephfs: /mnt/cephfs/repositories
+ ```
+
+ to
+
+ ```yaml
+ repositories:
+ storages: # You must have at least a 'default' storage path.
+ default:
+ path: /home/git/repositories
+ nfs:
+ path: /mnt/nfs/repositories
+ cephfs:
+ path: /mnt/cephfs/repositories
+ ```
+
+**For Omnibus installations**
+
+1. Update your `/etc/gitlab/gitlab.rb`, from
+
+ ```ruby
+ git_data_dirs({
+ "default" => "/var/opt/gitlab/git-data",
+ "nfs" => "/mnt/nfs/git-data",
+ "cephfs" => "/mnt/cephfs/git-data"
+ })
+ ```
+
+ to
+
+ ```ruby
+ git_data_dirs({
+ "default" => { "path" => "/var/opt/gitlab/git-data" },
+ "nfs" => { "path" => "/mnt/nfs/git-data" },
+ "cephfs" => { "path" => "/mnt/cephfs/git-data" }
+ })
+ ```
+
+#### Git configuration
+
+Configure Git to generate packfile bitmaps (introduced in Git 2.0) on
+the GitLab server during `git gc`.
+
+```sh
+cd /home/git/gitlab
+
+sudo -u git -H git config --global repack.writeBitmaps true
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/9-0-stable:lib/support/nginx/gitlab-ssl origin/9-1-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/9-0-stable:lib/support/nginx/gitlab origin/9-1-stable:lib/support/nginx/gitlab
+```
+
+If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
+configuration as GitLab application no longer handles setting it.
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-1-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/9-0-stable:lib/support/init.d/gitlab.default.example origin/9-1-stable:lib/support/init.d/gitlab.default.example
+```
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 9. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Update node dependencies and recompile assets
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+
+# Clean up cache
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 10. Optional: install Gitaly
+
+Gitaly is still an optional component of GitLab. If you want to save time
+during your 9.1 upgrade **you can skip this step**.
+
+If you have not yet set up Gitaly then follow [Gitaly section of the installation
+guide](../install/installation.md#install-gitaly).
+
+If you installed Gitaly in GitLab 9.0 you need to make some changes in gitlab.yml.
+
+Look for `socket_path:` the `gitaly:` section. Its value is usually
+`/home/git/gitlab/tmp/sockets/private/gitaly.socket`. Note what socket
+path your gitlab.yml is using. Now go to the `repositories:` section,
+and for each entry under `storages:`, add a `gitaly_address:` based on
+the socket path, but with `unix:` in front.
+
+```yaml
+ repositories:
+ storages:
+ default:
+ path: /home/git/repositories
+ gitaly_address: unix:/home/git/gitlab/tmp/sockets/private/gitaly.socket
+ other_storage:
+ path: /home/git/other-repositories
+ gitaly_address: unix:/home/git/gitlab/tmp/sockets/private/gitaly.socket
+```
+
+Each entry under `storages:` should use the same `gitaly_address`.
+
+### 11. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 12. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (9.0)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.17 to 9.0](8.17-to-9.0.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-1-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-1-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index 154a0f817da..1c493599cf8 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -57,7 +57,7 @@ sudo -u git -H bundle clean
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
# Clean up assets and cache
-sudo -u git -H bundle exec rake gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production
```
### 4. Update gitlab-workhorse to the corresponding version
diff --git a/doc/user/project/integrations/img/jira_project_settings.png b/doc/user/project/integrations/img/jira_project_settings.png
new file mode 100644
index 00000000000..cb6a6ba14ce
--- /dev/null
+++ b/doc/user/project/integrations/img/jira_project_settings.png
Binary files differ
diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md
index 4c64d1e0907..e02f81fd972 100644
--- a/doc/user/project/integrations/jira.md
+++ b/doc/user/project/integrations/jira.md
@@ -157,6 +157,11 @@ the same goal:
where `PROJECT-1` is the issue ID of the JIRA project.
+>**Note:**
+- Only commits and merges into the project's default branch (usually **master**) will
+ close an issue in Jira. You can change your projects default branch under
+ [project settings](img/jira_project_settings.png).
+
### JIRA issue closing example
Let's consider the following example:
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index 676a21e85c4..12d7700176c 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -153,8 +153,8 @@ The queries utilized by GitLab are shown in the following table.
| Metric | Query |
| ------ | ----- |
-| Average Memory (MB) | `(sum(container_memory_usage_bytes{container_name="app",environment="$CI_ENVIRONMENT_SLUG"}) / count(container_memory_usage_bytes{container_name="app",environment="$CI_ENVIRONMENT_SLUG"})) /1024/1024` |
-| Average CPU Utilization (%) | `sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="$CI_ENVIRONMENT_SLUG"}[2m])) / count(container_cpu_usage_seconds_total{container_name="app",environment="$CI_ENVIRONMENT_SLUG"}) * 100` |
+| Average Memory (MB) | `(sum(container_memory_usage_bytes{container_name!="POD",environment="$CI_ENVIRONMENT_SLUG"}) / count(container_memory_usage_bytes{container_name!="POD",environment="$CI_ENVIRONMENT_SLUG"})) /1024/1024` |
+| Average CPU Utilization (%) | `sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="$CI_ENVIRONMENT_SLUG"}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",environment="$CI_ENVIRONMENT_SLUG"}) * 100` |
## Monitoring CI/CD Environments
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index b559d132590..f846736028f 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -87,12 +87,12 @@ your Runners in the most possible secure way, by avoiding the following:
By using an insecure GitLab Runner configuration, you allow the rogue developers
to steal the tokens of other jobs.
-## job triggers
+## Pipeline triggers
-[job triggers][triggers] do not support the new permission model.
-They continue to use the old authentication mechanism where the CI job
-can access only its own sources. We plan to remove that limitation in one of
-the upcoming releases.
+Since 9.0 [pipelnie triggers][triggers] do support the new permission model.
+The new triggers do impersonate their associated user including their access
+to projects and their project permissions. To migrate trigger to use new permisison
+model use **Take ownership**.
## Before GitLab 8.12
@@ -141,6 +141,7 @@ with GitLab 8.12.
With the new job permissions model, there is now an easy way to access all
dependent source code in a project. That way, we can:
+1. Access a project's dependent repositories
1. Access a project's [Git submodules][gitsub]
1. Access private container images
1. Access project's and submodule LFS objects
@@ -177,6 +178,22 @@ As a user:
access to. As an Administrator, you can verify that by impersonating the user
and retry the failing job in order to verify that everything is correct.
+### Dependent repositories
+
+The [Job environment variable][jobenv] `CI_JOB_TOKEN` can be used to
+authenticate any clones of dependent repositories. For example:
+
+```
+git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/myuser/mydependentrepo
+```
+
+It can also be used for system-wide authentication
+(only do this in a docker container, it will overwrite ~/.netrc):
+
+```
+echo -e "machine gitlab.com\nlogin gitlab-ci-token\npassword ${CI_JOB_TOKEN}" > ~/.netrc
+```
+
### Git submodules
To properly configure submodules with GitLab CI, read the
@@ -221,3 +238,4 @@ test:
[triggers]: ../../ci/triggers/README.md
[update-docs]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update
[workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse
+[jobenv]: ../../ci/variables/README.md#predefined-variables-environment-variables
diff --git a/doc/user/project/pages/getting_started_part_four.md b/doc/user/project/pages/getting_started_part_four.md
index 35af48724f2..50767095aa0 100644
--- a/doc/user/project/pages/getting_started_part_four.md
+++ b/doc/user/project/pages/getting_started_part_four.md
@@ -1,5 +1,9 @@
# GitLab Pages from A to Z: Part 4
+> **Type**: user guide ||
+> **Level**: intermediate ||
+> **Author**: [Marcia Ramos](https://gitlab.com/marcia)
+
- [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)
diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md
index 582a4afbab4..e92549aa0df 100644
--- a/doc/user/project/pages/getting_started_part_one.md
+++ b/doc/user/project/pages/getting_started_part_one.md
@@ -1,5 +1,9 @@
# GitLab Pages from A to Z: Part 1
+> **Type**: user guide ||
+> **Level**: beginner ||
+> **Author**: [Marcia Ramos](https://gitlab.com/marcia)
+
- **Part 1: Static sites and GitLab Pages domains**
- [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)
diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md
index 55fcd5f00f2..80f16e43e20 100644
--- a/doc/user/project/pages/getting_started_part_three.md
+++ b/doc/user/project/pages/getting_started_part_three.md
@@ -1,5 +1,9 @@
# GitLab Pages from A to Z: Part 3
+> **Type**: user guide ||
+> **Level**: beginner ||
+> **Author**: [Marcia Ramos](https://gitlab.com/marcia)
+
- [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**
diff --git a/doc/user/project/pages/getting_started_part_two.md b/doc/user/project/pages/getting_started_part_two.md
index d0e2c467fee..578ad13f5df 100644
--- a/doc/user/project/pages/getting_started_part_two.md
+++ b/doc/user/project/pages/getting_started_part_two.md
@@ -1,5 +1,9 @@
# GitLab Pages from A to Z: Part 2
+> **Type**: user guide ||
+> **Level**: beginner ||
+> **Author**: [Marcia Ramos](https://gitlab.com/marcia)
+
- [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md)
- **Part 2: Quick start guide - Setting up GitLab Pages**
- [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md)
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index 4c52974e103..e91d36987a9 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -66,14 +66,13 @@ Below is the table of events users can be notified of:
In all of the below cases, the notification will be sent to:
- Participants:
- the author and assignee of the issue/merge request
- - the author of the pipeline
- authors of comments on the issue/merge request
- anyone mentioned by `@username` in the issue/merge request title or description
- anyone mentioned by `@username` in any of the comments on the issue/merge request
...with notification level "Participating" or higher
-- Watchers: users with notification level "Watch" (however successful pipeline would be off for watchers)
+- Watchers: users with notification level "Watch"
- Subscribers: anyone who manually subscribed to the issue/merge request
- Custom: Users with notification level "custom" who turned on notifications for any of the events present in the table below
@@ -89,8 +88,8 @@ In all of the below cases, the notification will be sent to:
| Reopen merge request | |
| Merge merge request | |
| New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher |
-| Failed pipeline | The above, plus the author of the pipeline |
-| Successful pipeline | The above, plus the author of the pipeline |
+| Failed pipeline | The author of the pipeline |
+| Successful pipeline | The author of the pipeline, if they have the custom notification setting for successful pipelines set |
In addition, if the title or description of an Issue or Merge Request is
diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb
index cb36d6ae1a9..d4a04f693b8 100644
--- a/features/steps/dashboard/new_project.rb
+++ b/features/steps/dashboard/new_project.rb
@@ -19,7 +19,7 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
expect(page).to have_link('Bitbucket')
expect(page).to have_link('GitLab.com')
expect(page).to have_link('Google Code')
- expect(page).to have_link('Repo by URL')
+ expect(page).to have_button('Repo by URL')
expect(page).to have_link('GitLab export')
end
diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb
index 9f01dff776f..3225e19995b 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -3,6 +3,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
include SharedPaths
include SharedProject
include SharedUser
+ include WaitForAjax
step '"John Doe" is a developer of project "Shop"' do
project.team << [john_doe, :developer]
@@ -28,7 +29,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
merge_request_reference = merge_request.to_reference(full: true)
issue_reference = issue.to_reference(full: true)
- page.within('.todos-pending-count') { expect(page).to have_content '4' }
+ page.within('.todos-count') { expect(page).to have_content '4' }
expect(page).to have_content 'To do 4'
expect(page).to have_content 'Done 0'
@@ -44,7 +45,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
click_link 'Done'
end
- page.within('.todos-pending-count') { expect(page).to have_content '3' }
+ page.within('.todos-count') { expect(page).to have_content '3' }
expect(page).to have_content 'To do 3'
expect(page).to have_content 'Done 1'
should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference(full: true)}", merge_request.title, state: :done_reversible)
@@ -56,7 +57,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
click_link 'Mark all as done'
- page.within('.todos-pending-count') { expect(page).to have_content '0' }
+ page.within('.todos-count') { expect(page).to have_content '0' }
expect(page).to have_content 'To do 0'
expect(page).to have_content 'Done 4'
expect(page).to have_content "You're all done!"
@@ -138,6 +139,8 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
step 'I should be directed to the corresponding page' do
page.should have_css('.identifier', text: 'Merge Request !1')
+ # Merge request page loads and issues a number of Ajax requests
+ wait_for_ajax
end
def should_see_todo(position, title, body, state: :pending)
diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb
index 20204ad8654..9996f3baf0d 100644
--- a/features/steps/group/milestones.rb
+++ b/features/steps/group/milestones.rb
@@ -36,7 +36,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
step 'I should see group milestone with all issues and MRs assigned to that milestone' do
expect(page).to have_content('Milestone GL-113')
- expect(page).to have_content('3 issues: 3 open and 0 closed')
+ expect(page).to have_content('Issues 3 Open: 3 Closed: 0')
issue = Milestone.find_by(name: 'GL-113').issues.first
expect(page).to have_link(issue.title, href: namespace_project_issue_path(issue.project.namespace, issue.project, issue))
end
diff --git a/features/steps/project/hooks.rb b/features/steps/project/hooks.rb
index 37b608ffbd3..0a71833a8a1 100644
--- a/features/steps/project/hooks.rb
+++ b/features/steps/project/hooks.rb
@@ -23,13 +23,13 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps
end
step 'I submit new hook' do
- @url = FFaker::Internet.uri("http")
+ @url = 'http://example.org/1'
fill_in "hook_url", with: @url
expect { click_button "Add Webhook" }.to change(ProjectHook, :count).by(1)
end
step 'I submit new hook with SSL verification enabled' do
- @url = FFaker::Internet.uri("http")
+ @url = 'http://example.org/2'
fill_in "hook_url", with: @url
check "hook_enable_ssl_verification"
expect { click_button "Add Webhook" }.to change(ProjectHook, :count).by(1)
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 9f0057cace7..c9c4f537fad 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -382,7 +382,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I modify merge commit message' do
- find('.modify-merge-commit-link').click
+ click_button "Modify commit message"
fill_in 'commit_message', with: 'wow such merge'
end
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
index c0c489d2775..6da8aaac6cb 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -1,9 +1,8 @@
-require 'spinach/capybara'
require 'capybara/poltergeist'
require 'capybara-screenshot/spinach'
# Give CI some extra time
-timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 30 : 10
+timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
@@ -25,5 +24,5 @@ Capybara.ignore_hidden_elements = false
Capybara::Screenshot.prune_strategy = :keep_last_run
Spinach.hooks.before_run do
- TestEnv.warm_asset_cache unless ENV['CI'] || ENV['CI_SERVER']
+ TestEnv.eager_load_driver_server
end
diff --git a/features/support/env.rb b/features/support/env.rb
index f394c30d52f..06c804b1db7 100644
--- a/features/support/env.rb
+++ b/features/support/env.rb
@@ -5,10 +5,6 @@ ENV['RAILS_ENV'] = 'test'
require './config/environment'
require 'rspec/expectations'
-require_relative 'capybara'
-require_relative 'db_cleaner'
-require_relative 'rerun'
-
if ENV['CI']
require 'knapsack'
Knapsack::Adapters::SpinachAdapter.bind
@@ -33,3 +29,19 @@ Spinach.hooks.before_run do
include FactoryGirl::Syntax::Methods
end
+
+module StdoutReporterWithScenarioLocation
+ # Override the standard reporter to show filename and line number next to each
+ # scenario for easy, focused re-runs
+ def before_scenario_run(scenario, step_definitions = nil)
+ @max_step_name_length = scenario.steps.map(&:name).map(&:length).max if scenario.steps.any?
+ name = scenario.name
+
+ # This number has no significance, it's just to line things up
+ max_length = @max_step_name_length + 19
+ out.puts "\n #{'Scenario:'.green} #{name.light_green.ljust(max_length)}" \
+ " # #{scenario.feature.filename}:#{scenario.line}"
+ end
+end
+
+Spinach::Reporter::Stdout.prepend(StdoutReporterWithScenarioLocation)
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index 409cb5b924f..9fcf04efa38 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -121,7 +121,7 @@ module API
end
def oauth2_bearer_token_error_handler
- Proc.new do |e|
+ proc do |e|
response =
case e
when MissingTokenError
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 5954aea8041..00d44821e3f 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -204,7 +204,7 @@ module API
expose :id, :name, :type, :path
expose :mode do |obj, options|
- filemode = obj.mode.to_s(8)
+ filemode = obj.mode
filemode = "0" + filemode if filemode.length < 6
filemode
end
@@ -581,6 +581,7 @@ module API
expose :plantuml_enabled
expose :plantuml_url
expose :terminal_max_session_time
+ expose :polling_interval_multiplier
end
class Release < Grape::Entity
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index bd22b82476b..61527c1e20b 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -90,6 +90,11 @@ module API
MergeRequestsFinder.new(current_user, project_id: user_project.id).find_by!(iid: iid)
end
+ def find_project_snippet(id)
+ finder_params = { filter: :by_project, project: user_project }
+ SnippetsFinder.new.execute(current_user, finder_params).find(id)
+ end
+
def find_merge_request_with_access(iid, access_level = :read_merge_request)
merge_request = user_project.merge_requests.find_by!(iid: iid)
authorize! access_level, merge_request
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
index 74848a6e144..1369b021ea4 100644
--- a/lib/api/helpers/runner.rb
+++ b/lib/api/helpers/runner.rb
@@ -50,10 +50,14 @@ module API
forbidden!('Job has been erased!') if job.erased?
end
- def authenticate_job!(job)
+ def authenticate_job!
+ job = Ci::Build.find_by_id(params[:id])
+
validate_job!(job) do
forbidden! unless job_token_valid?(job)
end
+
+ job
end
def job_token_valid?(job)
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 7eed93aba00..523f38d129e 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -139,7 +139,7 @@ module API
return unless Gitlab::GitalyClient.enabled?
begin
- Gitlab::GitalyClient::Notifications.new.post_receive(params[:repo_path])
+ Gitlab::GitalyClient::Notifications.new(params[:repo_path]).post_receive
rescue GRPC::Unavailable => e
render_api_error(e, 500)
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index fd2674910d2..4dce5dd130a 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -63,14 +63,14 @@ module API
success Entities::IssueBasic
end
params do
- optional :state, type: String, values: %w[opened closed all], default: 'opened',
+ optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
use :issues_params
end
get ":id/issues" do
group = find_group!(params[:id])
- issues = find_issues(group_id: group.id, state: params[:state] || 'opened')
+ issues = find_issues(group_id: group.id)
present paginate(issues), with: Entities::IssueBasic, current_user: current_user
end
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index d9a3cb7bb6b..20b25529d0c 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -23,7 +23,7 @@ module API
end
params do
requires :name, type: String, desc: 'The name of the label to be created'
- requires :color, type: String, desc: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB)"
+ requires :color, type: String, desc: "The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the allowed CSS color names"
optional :description, type: String, desc: 'The description of label to be created'
optional :priority, type: Integer, desc: 'The priority of the label', allow_blank: true
end
@@ -34,7 +34,7 @@ module API
conflict!('Label already exists') if label
priority = params.delete(:priority)
- label = user_project.labels.create(declared_params(include_missing: false))
+ label = ::Labels::CreateService.new(declared_params(include_missing: false)).execute(project: user_project)
if label.valid?
label.prioritize!(user_project, priority) if priority
@@ -65,7 +65,7 @@ module API
params do
requires :name, type: String, desc: 'The name of the label to be updated'
optional :new_name, type: String, desc: 'The new name of the label'
- optional :color, type: String, desc: "The new color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB)"
+ optional :color, type: String, desc: "The new color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the allowed CSS color names"
optional :description, type: String, desc: 'The new description of label'
optional :priority, type: Integer, desc: 'The priority of the label', allow_blank: true
at_least_one_of :new_name, :color, :description, :priority
@@ -82,7 +82,8 @@ module API
# Rename new name to the actual label attribute name
label_params[:name] = label_params.delete(:new_name) if label_params.key?(:new_name)
- render_validation_error!(label) unless label.update(label_params)
+ label = ::Labels::UpdateService.new(label_params).execute(label)
+ render_validation_error!(label) unless label.valid?
if update_priority
if priority.nil?
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 5cc807d5bff..c8033664133 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -226,41 +226,6 @@ module API
.cancel(merge_request)
end
- desc 'Get the comments of a merge request' do
- success Entities::MRNote
- end
- params do
- use :pagination
- end
- get ':id/merge_requests/:merge_request_iid/comments' do
- merge_request = find_merge_request_with_access(params[:merge_request_iid])
- present paginate(merge_request.notes.fresh), with: Entities::MRNote
- end
-
- desc 'Post a comment to a merge request' do
- success Entities::MRNote
- end
- params do
- requires :note, type: String, desc: 'The text of the comment'
- end
- post ':id/merge_requests/:merge_request_iid/comments' do
- merge_request = find_merge_request_with_access(params[:merge_request_iid], :create_note)
-
- opts = {
- note: params[:note],
- noteable_type: 'MergeRequest',
- noteable_id: merge_request.id
- }
-
- note = ::Notes::CreateService.new(user_project, current_user, opts).execute
-
- if note.save
- present note, with: Entities::MRNote
- else
- render_api_error!("Failed to save note #{note.errors.messages}", 400)
- end
- end
-
desc 'List issues that will be closed on merge' do
success Entities::MRNote
end
diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb
index e7ab82f08db..a3ea619a2fb 100644
--- a/lib/api/milestones.rb
+++ b/lib/api/milestones.rb
@@ -139,7 +139,7 @@ module API
finder_params = {
project_id: user_project.id,
- milestone_id: milestone.id,
+ milestone_title: milestone.title,
sort: 'position_asc'
}
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 29ceffdbd2d..de39e579ac3 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -21,7 +21,7 @@ module API
use :pagination
end
get ":id/#{noteables_str}/:noteable_id/notes" do
- noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+ noteable = find_project_noteable(noteables_str, params[:noteable_id])
if can?(current_user, noteable_read_ability_name(noteable), noteable)
# We exclude notes that are cross-references and that cannot be viewed
@@ -49,7 +49,7 @@ module API
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
end
get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
- noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
+ noteable = find_project_noteable(noteables_str, params[:noteable_id])
note = noteable.notes.find(params[:note_id])
can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user)
@@ -69,14 +69,14 @@ module API
optional :created_at, type: String, desc: 'The creation date of the note'
end
post ":id/#{noteables_str}/:noteable_id/notes" do
+ noteable = find_project_noteable(noteables_str, params[:noteable_id])
+
opts = {
note: params[:body],
noteable_type: noteables_str.classify,
- noteable_id: params[:noteable_id]
+ noteable_id: noteable.id
}
- noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
-
if can?(current_user, noteable_read_ability_name(noteable), noteable)
if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user)
opts[:created_at] = params[:created_at]
@@ -137,6 +137,10 @@ module API
end
helpers do
+ def find_project_noteable(noteables_str, noteable_id)
+ public_send("find_project_#{noteables_str.singularize}", noteable_id)
+ end
+
def noteable_read_ability_name(noteable)
"read_#{noteable.class.to_s.underscore}".to_sym
end
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 4c9db2c8716..d288369e362 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -113,8 +113,7 @@ module API
optional :state, type: String, desc: %q(Job's status: success, failed)
end
put '/:id' do
- job = Ci::Build.find_by_id(params[:id])
- authenticate_job!(job)
+ job = authenticate_job!
job.update_attributes(trace: params[:trace]) if params[:trace]
@@ -140,8 +139,7 @@ module API
optional :token, type: String, desc: %q(Job's authentication token)
end
patch '/:id/trace' do
- job = Ci::Build.find_by_id(params[:id])
- authenticate_job!(job)
+ job = authenticate_job!
error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range')
content_range = request.headers['Content-Range']
@@ -175,8 +173,7 @@ module API
require_gitlab_workhorse!
Gitlab::Workhorse.verify_api_request!(headers)
- job = Ci::Build.find_by_id(params[:id])
- authenticate_job!(job)
+ job = authenticate_job!
forbidden!('Job is not running') unless job.running?
if params[:filesize]
@@ -212,8 +209,7 @@ module API
not_allowed! unless Gitlab.config.artifacts.enabled
require_gitlab_workhorse!
- job = Ci::Build.find_by_id(params[:id])
- authenticate_job!(job)
+ job = authenticate_job!
forbidden!('Job is not running!') unless job.running?
artifacts_upload_path = ArtifactUploader.artifacts_upload_path
@@ -245,8 +241,7 @@ module API
optional :token, type: String, desc: %q(Job's authentication token)
end
get '/:id/artifacts' do
- job = Ci::Build.find_by_id(params[:id])
- authenticate_job!(job)
+ job = authenticate_job!
artifacts_file = job.artifacts_file
unless artifacts_file.file_storage?
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 4e0c9cb1f63..6802a99311e 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -562,8 +562,14 @@ module API
desc: 'URL to the mock service'
}
]
+ services['mock-deployment'] = []
+ services['mock-monitoring'] = []
- service_classes << MockCiService
+ service_classes += [
+ MockCiService,
+ MockDeploymentService,
+ MockMonitoringService,
+ ]
end
trigger_services = {
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index d4d3229f0d1..c7f97ad2aab 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -110,6 +110,7 @@ module API
requires :housekeeping_gc_period, type: Integer, desc: "Number of Git pushes after which 'git gc' is run."
end
optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.'
+ optional :polling_interval_multiplier, type: BigDecimal, desc: 'Interval multiplier used by endpoints that perform polling. Set to 0 to disable polling.'
at_least_one_of :default_branch_protection, :default_project_visibility, :default_snippet_visibility,
:default_group_visibility, :restricted_visibility_levels, :import_sources,
:enabled_git_access_protocol, :gravatar_enabled, :default_projects_limit,
@@ -125,7 +126,7 @@ module API
:akismet_enabled, :admin_notification_email, :sentry_enabled,
:repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled,
:version_check_enabled, :email_author_in_body, :html_emails_enabled,
- :housekeeping_enabled, :terminal_max_session_time
+ :housekeeping_enabled, :terminal_max_session_time, :polling_interval_multiplier
end
put "application/settings" do
attrs = declared_params(include_missing: false)
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 2d4d5a25221..530ca0b5235 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -27,7 +27,7 @@ module API
optional :location, type: String, desc: 'The location of the user'
optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator'
optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups'
- optional :confirm, type: Boolean, desc: 'Flag indicating the account needs to be confirmed'
+ optional :skip_confirmation, type: Boolean, default: false, desc: 'Flag indicating the account is confirmed'
optional :external, type: Boolean, desc: 'Flag indicating the user is an external user'
all_or_none_of :extern_uid, :provider
end
@@ -97,29 +97,10 @@ module API
post do
authenticated_as_admin!
- # Filter out params which are used later
- user_params = declared_params(include_missing: false)
- identity_attrs = user_params.slice(:provider, :extern_uid)
- confirm = user_params.delete(:confirm)
- user = User.new(user_params.except(:extern_uid, :provider, :reset_password))
-
- if user_params.delete(:reset_password)
- user.attributes = {
- force_random_password: true,
- password_expires_at: nil,
- created_by_id: current_user.id
- }
- user.generate_password
- user.generate_reset_token
- end
-
- user.skip_confirmation! unless confirm
-
- if identity_attrs.any?
- user.identities.build(identity_attrs)
- end
+ params = declared_params(include_missing: false)
+ user = ::Users::CreateService.new(current_user, params).execute
- if user.save
+ if user.persisted?
present user, with: Entities::UserPublic
else
conflict!('Email has already been taken') if User.
@@ -312,7 +293,7 @@ module API
user = User.find_by(id: params[:id])
not_found!('User') unless user
- ::Users::DestroyService.new(current_user).execute(user)
+ DeleteUserWorker.perform_async(current_user.id, user.id)
end
desc 'Block a user. Available only for admins.'
diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb
index 54c6a8060b8..715083fc4f8 100644
--- a/lib/api/v3/issues.rb
+++ b/lib/api/v3/issues.rb
@@ -73,14 +73,14 @@ module API
success ::API::Entities::Issue
end
params do
- optional :state, type: String, values: %w[opened closed all], default: 'opened',
+ optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
use :issues_params
end
get ":id/issues" do
group = find_group!(params[:id])
- issues = find_issues(group_id: group.id, state: params[:state] || 'opened', match_all_labels: true)
+ issues = find_issues(group_id: group.id, match_all_labels: true)
present paginate(issues), with: ::API::Entities::Issue, current_user: current_user
end
diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb
index 14f54731730..5e18cecc431 100644
--- a/lib/api/v3/users.rb
+++ b/lib/api/v3/users.rb
@@ -9,6 +9,59 @@ module API
end
resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
+ helpers do
+ params :optional_attributes do
+ optional :skype, type: String, desc: 'The Skype username'
+ optional :linkedin, type: String, desc: 'The LinkedIn username'
+ optional :twitter, type: String, desc: 'The Twitter username'
+ optional :website_url, type: String, desc: 'The website of the user'
+ optional :organization, type: String, desc: 'The organization of the user'
+ optional :projects_limit, type: Integer, desc: 'The number of projects a user can create'
+ optional :extern_uid, type: String, desc: 'The external authentication provider UID'
+ optional :provider, type: String, desc: 'The external provider'
+ optional :bio, type: String, desc: 'The biography of the user'
+ optional :location, type: String, desc: 'The location of the user'
+ optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator'
+ optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups'
+ optional :confirm, type: Boolean, default: true, desc: 'Flag indicating the account needs to be confirmed'
+ optional :external, type: Boolean, desc: 'Flag indicating the user is an external user'
+ all_or_none_of :extern_uid, :provider
+ end
+ end
+
+ desc 'Create a user. Available only for admins.' do
+ success ::API::Entities::UserPublic
+ end
+ params do
+ requires :email, type: String, desc: 'The email of the user'
+ optional :password, type: String, desc: 'The password of the new user'
+ optional :reset_password, type: Boolean, desc: 'Flag indicating the user will be sent a password reset token'
+ at_least_one_of :password, :reset_password
+ requires :name, type: String, desc: 'The name of the user'
+ requires :username, type: String, desc: 'The username of the user'
+ use :optional_attributes
+ end
+ post do
+ authenticated_as_admin!
+
+ params = declared_params(include_missing: false)
+ user = ::Users::CreateService.new(current_user, params.merge!(skip_confirmation: !params[:confirm])).execute
+
+ if user.persisted?
+ present user, with: ::API::Entities::UserPublic
+ else
+ conflict!('Email has already been taken') if User.
+ where(email: user.email).
+ count > 0
+
+ conflict!('Username has already been taken') if User.
+ where(username: user.username).
+ count > 0
+
+ render_validation_error!(user)
+ end
+ end
+
desc 'Get the SSH keys of a specified user. Available only for admins.' do
success ::API::Entities::SSHKey
end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index cd745d35e7c..6b29600a751 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -182,7 +182,9 @@ module Backup
dir_entries = Dir.entries(path)
- yield('custom_hooks') if dir_entries.include?('custom_hooks')
+ if dir_entries.include?('custom_hooks') || dir_entries.include?('custom_hooks.tar')
+ yield('custom_hooks')
+ end
end
def prepare
diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb
index ac5216d9cfb..3888acf935e 100644
--- a/lib/banzai/filter/merge_request_reference_filter.rb
+++ b/lib/banzai/filter/merge_request_reference_filter.rb
@@ -11,8 +11,8 @@ module Banzai
MergeRequest
end
- def find_object(project, id)
- project.merge_requests.find_by(iid: id)
+ def find_object(project, iid)
+ merge_requests_per_project[project][iid]
end
def url_for_object(mr, project)
@@ -21,6 +21,31 @@ module Banzai
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
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index fe1f0923136..a798927823f 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -60,7 +60,7 @@ module Banzai
self.class.references_in(text) do |match, username|
if username == 'all' && !skip_project_check?
link_to_all(link_content: link_content)
- elsif namespace = namespaces[username]
+ elsif namespace = namespaces[username.downcase]
link_to_namespace(namespace, link_content: link_content) || match
else
match
@@ -74,7 +74,7 @@ module Banzai
# The keys of this Hash are the namespace paths, the values the
# corresponding Namespace objects.
def namespaces
- @namespaces ||= Namespace.where_full_path_in(usernames).index_by(&:full_path)
+ @namespaces ||= Namespace.where_full_path_in(usernames).index_by(&:full_path).transform_keys(&:downcase)
end
# Returns all usernames referenced in the current document.
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 746e76a1b1f..95cc6308c3b 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -86,8 +86,7 @@ module Ci
# Example Request:
# PATCH /builds/:id/trace.txt
patch ":id/trace.txt" do
- build = Ci::Build.find_by_id(params[:id])
- authenticate_build!(build)
+ build = authenticate_build!
error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range')
content_range = request.headers['Content-Range']
@@ -117,8 +116,7 @@ module Ci
require_gitlab_workhorse!
Gitlab::Workhorse.verify_api_request!(headers)
not_allowed! unless Gitlab.config.artifacts.enabled
- build = Ci::Build.find_by_id(params[:id])
- authenticate_build!(build)
+ build = authenticate_build!
forbidden!('build is not running') unless build.running?
if params[:filesize]
@@ -154,8 +152,7 @@ module Ci
post ":id/artifacts" do
require_gitlab_workhorse!
not_allowed! unless Gitlab.config.artifacts.enabled
- build = Ci::Build.find_by_id(params[:id])
- authenticate_build!(build)
+ build = authenticate_build!
forbidden!('Build is not running!') unless build.running?
artifacts_upload_path = ArtifactUploader.artifacts_upload_path
@@ -189,8 +186,7 @@ module Ci
# Example Request:
# GET /builds/:id/artifacts
get ":id/artifacts" do
- build = Ci::Build.find_by_id(params[:id])
- authenticate_build!(build)
+ build = authenticate_build!
artifacts_file = build.artifacts_file
unless artifacts_file.file_storage?
@@ -214,8 +210,7 @@ module Ci
# Example Request:
# DELETE /builds/:id/artifacts
delete ":id/artifacts" do
- build = Ci::Build.find_by_id(params[:id])
- authenticate_build!(build)
+ build = authenticate_build!
status(200)
build.erase_artifacts!
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
index 996990b464f..5109dc9670f 100644
--- a/lib/ci/api/helpers.rb
+++ b/lib/ci/api/helpers.rb
@@ -13,10 +13,14 @@ module Ci
forbidden! unless current_runner
end
- def authenticate_build!(build)
+ def authenticate_build!
+ build = Ci::Build.find_by_id(params[:id])
+
validate_build!(build) do
forbidden! unless build_token_valid?(build)
end
+
+ build
end
def validate_build!(build)
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index 44323b47dca..f4efa20374a 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -130,8 +130,13 @@ module Gitlab
end
def create_labels
- LABELS.each do |label|
- @labels[label[:title]] = project.labels.create!(label)
+ LABELS.each do |label_params|
+ label = ::Labels::CreateService.new(label_params).execute(project: project)
+ if label.valid?
+ @labels[label_params[:title]] = label
+ else
+ raise "Failed to create label \"#{label_params[:title]}\" for project \"#{project.name_with_namespace}\""
+ end
end
end
diff --git a/lib/gitlab/ci/status/canceled.rb b/lib/gitlab/ci/status/canceled.rb
index dd6d99e9075..97c121ce7b9 100644
--- a/lib/gitlab/ci/status/canceled.rb
+++ b/lib/gitlab/ci/status/canceled.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_canceled'
end
+
+ def favicon
+ 'favicon_status_canceled'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/core.rb b/lib/gitlab/ci/status/core.rb
index 3dd2b9e01f6..d4fd83b93f8 100644
--- a/lib/gitlab/ci/status/core.rb
+++ b/lib/gitlab/ci/status/core.rb
@@ -18,6 +18,10 @@ module Gitlab
raise NotImplementedError
end
+ def favicon
+ raise NotImplementedError
+ end
+
def label
raise NotImplementedError
end
diff --git a/lib/gitlab/ci/status/created.rb b/lib/gitlab/ci/status/created.rb
index 6596d7e01ca..0721bf6ec7c 100644
--- a/lib/gitlab/ci/status/created.rb
+++ b/lib/gitlab/ci/status/created.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_created'
end
+
+ def favicon
+ 'favicon_status_created'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/failed.rb b/lib/gitlab/ci/status/failed.rb
index c5b5e3203ad..cb75e9383a8 100644
--- a/lib/gitlab/ci/status/failed.rb
+++ b/lib/gitlab/ci/status/failed.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_failed'
end
+
+ def favicon
+ 'favicon_status_failed'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/manual.rb b/lib/gitlab/ci/status/manual.rb
index 5f28521901d..f8f6c2903ba 100644
--- a/lib/gitlab/ci/status/manual.rb
+++ b/lib/gitlab/ci/status/manual.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_manual'
end
+
+ def favicon
+ 'favicon_status_manual'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/pending.rb b/lib/gitlab/ci/status/pending.rb
index d30f35a59a2..f40cc1314dc 100644
--- a/lib/gitlab/ci/status/pending.rb
+++ b/lib/gitlab/ci/status/pending.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_pending'
end
+
+ def favicon
+ 'favicon_status_pending'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/running.rb b/lib/gitlab/ci/status/running.rb
index 2aba3c373c7..1237cd47dc8 100644
--- a/lib/gitlab/ci/status/running.rb
+++ b/lib/gitlab/ci/status/running.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_running'
end
+
+ def favicon
+ 'favicon_status_running'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/skipped.rb b/lib/gitlab/ci/status/skipped.rb
index 16282aefd03..28005d91503 100644
--- a/lib/gitlab/ci/status/skipped.rb
+++ b/lib/gitlab/ci/status/skipped.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_skipped'
end
+
+ def favicon
+ 'favicon_status_skipped'
+ end
end
end
end
diff --git a/lib/gitlab/ci/status/success.rb b/lib/gitlab/ci/status/success.rb
index c09c5f006e3..88f7758a270 100644
--- a/lib/gitlab/ci/status/success.rb
+++ b/lib/gitlab/ci/status/success.rb
@@ -13,6 +13,10 @@ module Gitlab
def icon
'icon_status_success'
end
+
+ def favicon
+ 'favicon_status_success'
+ end
end
end
end
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index 0829c1c318e..496ee0bdcb0 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -125,7 +125,7 @@ module Gitlab
end
puts
- puts applies_cleanly_msg(ee_branch)
+ puts applies_cleanly_msg(ee_branch_found)
end
def check_patch(patch_path)
@@ -215,7 +215,7 @@ module Gitlab
end
def ee_patch_name
- @ee_patch_name ||= patch_name_from_branch(ee_branch)
+ @ee_patch_name ||= patch_name_from_branch(ee_branch_found)
end
def ee_patch_full_path
diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb
index ffbc6e17dc5..9c98f0d1a30 100644
--- a/lib/gitlab/etag_caching/middleware.rb
+++ b/lib/gitlab/etag_caching/middleware.rb
@@ -18,8 +18,7 @@ module Gitlab
if_none_match = env['HTTP_IF_NONE_MATCH']
if if_none_match == etag
- Gitlab::Metrics.add_event(:etag_caching_cache_hit)
- [304, { 'ETag' => etag }, ['']]
+ handle_cache_hit(etag)
else
track_cache_miss(if_none_match, cached_value_present)
@@ -52,6 +51,14 @@ module Gitlab
%Q{W/"#{value}"}
end
+ def handle_cache_hit(etag)
+ Gitlab::Metrics.add_event(:etag_caching_cache_hit)
+
+ status_code = Gitlab::PollingInterval.polling_enabled? ? 304 : 429
+
+ [status_code, { 'ETag' => etag }, ['']]
+ end
+
def track_cache_miss(if_none_match, cached_value_present)
if if_none_match.blank?
Gitlab::Metrics.add_event(:etag_caching_header_missing)
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 2187dd70ff4..32aebb6f6f0 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -320,7 +320,7 @@ module Gitlab
def log_by_walk(sha, options)
walk_options = {
show: sha,
- sort: Rugged::SORT_DATE,
+ sort: Rugged::SORT_NONE,
limit: options[:limit],
offset: options[:offset]
}
@@ -346,7 +346,12 @@ module Gitlab
cmd << "--after=#{options[:after].iso8601}" if options[:after]
cmd << "--before=#{options[:before].iso8601}" if options[:before]
cmd << sha
- cmd += %W[-- #{options[:path]}] if options[:path].present?
+
+ # :path can be a string or an array of strings
+ if options[:path].present?
+ cmd << '--'
+ cmd += Array(options[:path])
+ end
raw_output = IO.popen(cmd) { |io| io.read }
lines = offset_in_ruby ? raw_output.lines.drop(offset) : raw_output.lines
@@ -382,7 +387,7 @@ module Gitlab
# a detailed list of valid arguments.
def commits_between(from, to)
walker = Rugged::Walker.new(rugged)
- walker.sorting(Rugged::SORT_DATE | Rugged::SORT_REVERSE)
+ walker.sorting(Rugged::SORT_NONE | Rugged::SORT_REVERSE)
sha_from = sha_from_ref(from)
sha_to = sha_from_ref(to)
@@ -406,6 +411,11 @@ module Gitlab
rugged.merge_base(from, to)
end
+ # Returns true is +from+ is direct ancestor to +to+, otherwise false
+ def is_ancestor?(from, to)
+ Gitlab::GitalyClient::Commit.is_ancestor(self, from, to)
+ end
+
# Return an array of Diff objects that represent the diff
# between +from+ and +to+. See Diff::filter_diff_options for the allowed
# diff options. The +options+ hash can also include :break_rewrites to
@@ -460,7 +470,7 @@ module Gitlab
if actual_options[:order] == :topo
walker.sorting(Rugged::SORT_TOPO)
else
- walker.sorting(Rugged::SORT_DATE)
+ walker.sorting(Rugged::SORT_NONE)
end
commits = []
@@ -828,23 +838,6 @@ module Gitlab
Rugged::Commit.create(rugged, actual_options)
end
- def commits_since(from_date)
- walker = Rugged::Walker.new(rugged)
- walker.sorting(Rugged::SORT_DATE | Rugged::SORT_REVERSE)
-
- rugged.references.each("refs/heads/*") do |ref|
- walker.push(ref.target_id)
- end
-
- commits = []
- walker.each do |commit|
- break if commit.author[:time].to_date < from_date
- commits.push(commit)
- end
-
- commits
- end
-
AUTOCRLF_VALUES = {
"true" => true,
"false" => false,
diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb
index f7450e8b58f..b722d8a9f56 100644
--- a/lib/gitlab/git/tree.rb
+++ b/lib/gitlab/git/tree.rb
@@ -33,7 +33,7 @@ module Gitlab
root_id: root_tree.oid,
name: entry[:name],
type: entry[:type],
- mode: entry[:filemode],
+ mode: entry[:filemode].to_s(8),
path: path ? File.join(path, entry[:name]) : entry[:name],
commit_id: sha,
)
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 1ce47ef2b05..fe15fb12adb 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -4,28 +4,32 @@ module Gitlab
module GitalyClient
SERVER_VERSION_FILE = 'GITALY_SERVER_VERSION'.freeze
- def self.gitaly_address
- if Gitlab.config.gitaly.socket_path
- "unix://#{Gitlab.config.gitaly.socket_path}"
- end
+ def self.configure_channel(storage, address)
+ @addresses ||= {}
+ @addresses[storage] = address
+ @channels ||= {}
+ @channels[storage] = new_channel(address)
+ end
+
+ def self.new_channel(address)
+ address = address.sub(%r{^tcp://}, '') if URI(address).scheme == 'tcp'
+ # NOTE: When Gitaly runs on a Unix socket, permissions are
+ # handled using the file system and no additional authentication is
+ # required (therefore the :this_channel_is_insecure flag)
+ # TODO: Add authentication support when Gitaly is running on a TCP socket.
+ GRPC::Core::Channel.new(address, {}, :this_channel_is_insecure)
end
- def self.channel
- return @channel if defined?(@channel)
+ def self.get_channel(storage)
+ @channels[storage]
+ end
- @channel =
- if enabled?
- # NOTE: Gitaly currently runs on a Unix socket, so permissions are
- # handled using the file system and no additional authentication is
- # required (therefore the :this_channel_is_insecure flag)
- GRPC::Core::Channel.new(gitaly_address, {}, :this_channel_is_insecure)
- else
- nil
- end
+ def self.get_address(storage)
+ @addresses[storage]
end
def self.enabled?
- gitaly_address.present?
+ Gitlab.config.gitaly.enabled
end
def self.feature_enabled?(feature)
diff --git a/lib/gitlab/gitaly_client/commit.rb b/lib/gitlab/gitaly_client/commit.rb
index 525b8d680e9..f15faebe27e 100644
--- a/lib/gitlab/gitaly_client/commit.rb
+++ b/lib/gitlab/gitaly_client/commit.rb
@@ -7,8 +7,10 @@ module Gitlab
class << self
def diff_from_parent(commit, options = {})
- stub = Gitaly::Diff::Stub.new(nil, nil, channel_override: GitalyClient.channel)
- repo = Gitaly::Repository.new(path: commit.project.repository.path_to_repo)
+ project = commit.project
+ channel = GitalyClient.get_channel(project.repository_storage)
+ stub = Gitaly::Diff::Stub.new(nil, nil, channel_override: channel)
+ repo = Gitaly::Repository.new(path: project.repository.path_to_repo)
parent = commit.parents[0]
parent_id = parent ? parent.id : EMPTY_TREE_ID
request = Gitaly::CommitDiffRequest.new(
@@ -19,6 +21,20 @@ module Gitlab
Gitlab::Git::DiffCollection.new(stub.commit_diff(request), options)
end
+
+ def is_ancestor(repository, ancestor_id, child_id)
+ project = Project.find_by_path(repository.path)
+ channel = GitalyClient.get_channel(project.repository_storage)
+ stub = Gitaly::Commit::Stub.new(nil, nil, channel_override: channel)
+ repo = Gitaly::Repository.new(path: repository.path_to_repo)
+ request = Gitaly::CommitIsAncestorRequest.new(
+ repository: repo,
+ ancestor_id: ancestor_id,
+ child_id: child_id
+ )
+
+ stub.commit_is_ancestor(request).value
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/notifications.rb b/lib/gitlab/gitaly_client/notifications.rb
index b827a56207f..cbfb129c002 100644
--- a/lib/gitlab/gitaly_client/notifications.rb
+++ b/lib/gitlab/gitaly_client/notifications.rb
@@ -3,14 +3,19 @@ module Gitlab
class Notifications
attr_accessor :stub
- def initialize
- @stub = Gitaly::Notifications::Stub.new(nil, nil, channel_override: GitalyClient.channel)
+ def initialize(repo_path)
+ full_path = Gitlab::RepoPath.strip_storage_path(repo_path).
+ sub(/\.git\z/, '').sub(/\.wiki\z/, '')
+ @project = Project.find_by_full_path(full_path)
+
+ channel = GitalyClient.get_channel(@project.repository_storage)
+ @stub = Gitaly::Notifications::Stub.new(nil, nil, channel_override: channel)
end
- def post_receive(repo_path)
- repository = Gitaly::Repository.new(path: repo_path)
+ def post_receive
+ repository = Gitaly::Repository.new(path: @project.repository.path_to_repo)
request = Gitaly::PostReceiveRequest.new(repository: repository)
- stub.post_receive(request)
+ @stub.post_receive(request)
end
end
end
diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb
index 5d29e698b27..8aa885fb811 100644
--- a/lib/gitlab/github_import/branch_formatter.rb
+++ b/lib/gitlab/github_import/branch_formatter.rb
@@ -11,6 +11,14 @@ module Gitlab
sha.present? && ref.present?
end
+ def user
+ raw_data.user&.login || 'unknown'
+ end
+
+ def short_sha
+ Commit.truncate_sha(sha)
+ end
+
private
def branch_exists?
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index eea4a91f17d..a8c0b47e786 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -157,7 +157,7 @@ module Gitlab
end
def restore_source_branch(pull_request)
- project.repository.fetch_ref(repo_url, "pull/#{pull_request.number}/head", pull_request.source_branch_name)
+ project.repository.create_branch(pull_request.source_branch_name, pull_request.source_branch_sha)
end
def restore_target_branch(pull_request)
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
index add7236e339..150afa31432 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -1,8 +1,8 @@
module Gitlab
module GithubImport
class PullRequestFormatter < IssuableFormatter
- delegate :exists?, :project, :ref, :repo, :sha, to: :source_branch, prefix: true
- delegate :exists?, :project, :ref, :repo, :sha, to: :target_branch, prefix: true
+ delegate :user, :project, :ref, :repo, :sha, to: :source_branch, prefix: true
+ delegate :user, :exists?, :project, :ref, :repo, :sha, :short_sha, to: :target_branch, prefix: true
def attributes
{
@@ -20,7 +20,8 @@ module Gitlab
author_id: author_id,
assignee_id: assignee_id,
created_at: raw_data.created_at,
- updated_at: raw_data.updated_at
+ updated_at: raw_data.updated_at,
+ imported: true
}
end
@@ -37,13 +38,20 @@ module Gitlab
end
def source_branch_name
- @source_branch_name ||= begin
- if cross_project?
- "pull/#{number}/#{source_branch_repo.full_name}/#{source_branch_ref}"
+ @source_branch_name ||=
+ if cross_project? || !source_branch_exists?
+ source_branch_name_prefixed
else
- source_branch_exists? ? source_branch_ref : "pull/#{number}/#{source_branch_ref}"
+ source_branch_ref
end
- end
+ end
+
+ def source_branch_name_prefixed
+ "gh-#{target_branch_short_sha}/#{number}/#{source_branch_user}/#{source_branch_ref}"
+ end
+
+ def source_branch_exists?
+ !cross_project? && source_branch.exists?
end
def target_branch
@@ -51,13 +59,17 @@ module Gitlab
end
def target_branch_name
- @target_branch_name ||= begin
- target_branch_exists? ? target_branch_ref : "pull/#{number}/#{target_branch_ref}"
- end
+ @target_branch_name ||= target_branch_exists? ? target_branch_ref : target_branch_name_prefixed
+ end
+
+ def target_branch_name_prefixed
+ "gl-#{target_branch_short_sha}/#{number}/#{target_branch_user}/#{target_branch_ref}"
end
def cross_project?
- source_branch.repo.id != target_branch.repo.id
+ return true if source_branch_repo.nil?
+
+ source_branch_repo.id != target_branch_repo.id
end
def opened?
diff --git a/lib/gitlab/import_export/hash_util.rb b/lib/gitlab/import_export/hash_util.rb
new file mode 100644
index 00000000000..d4adeeb3797
--- /dev/null
+++ b/lib/gitlab/import_export/hash_util.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module ImportExport
+ class HashUtil
+ def self.deep_symbolize_array!(array)
+ return if array.blank?
+
+ array.map! do |hash|
+ hash.deep_symbolize_keys!
+
+ yield(hash) if block_given?
+
+ hash
+ end
+ end
+
+ def self.deep_symbolize_array_with_date!(array)
+ self.deep_symbolize_array!(array) do |hash|
+ hash.select { |k, _v| k.to_s.end_with?('_date') }.each do |key, value|
+ hash[key] = Time.zone.parse(value)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index ab74c8782f6..f69288f7d67 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -89,3 +89,5 @@ methods:
- :type
merge_request_diff:
- :utf8_st_diffs
+ merge_requests:
+ - :diff_head_sha
diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb
index 063ce74ecad..fbdd74788bc 100644
--- a/lib/gitlab/import_export/importer.rb
+++ b/lib/gitlab/import_export/importer.rb
@@ -9,7 +9,7 @@ module Gitlab
end
def execute
- if import_file && check_version! && [project_tree, avatar_restorer, repo_restorer, wiki_restorer, uploads_restorer].all?(&:restore)
+ if import_file && check_version! && [repo_restorer, wiki_restorer, project_tree, avatar_restorer, uploads_restorer].all?(&:restore)
project_tree.restored_project
else
raise Projects::ImportService::Error.new(@shared.errors.join(', '))
diff --git a/lib/gitlab/import_export/merge_request_parser.rb b/lib/gitlab/import_export/merge_request_parser.rb
new file mode 100644
index 00000000000..c20adc20bfd
--- /dev/null
+++ b/lib/gitlab/import_export/merge_request_parser.rb
@@ -0,0 +1,41 @@
+module Gitlab
+ module ImportExport
+ class MergeRequestParser
+ FORKED_PROJECT_ID = -1
+
+ def initialize(project, diff_head_sha, merge_request, relation_hash)
+ @project = project
+ @diff_head_sha = diff_head_sha
+ @merge_request = merge_request
+ @relation_hash = relation_hash
+ end
+
+ def parse!
+ if fork_merge_request? && @diff_head_sha
+ @merge_request.source_project_id = @relation_hash['project_id']
+
+ fetch_ref unless branch_exists?(@merge_request.source_branch)
+ create_target_branch unless branch_exists?(@merge_request.target_branch)
+ end
+
+ @merge_request
+ end
+
+ def create_target_branch
+ @project.repository.create_branch(@merge_request.target_branch, @merge_request.target_branch_sha)
+ end
+
+ def fetch_ref
+ @project.repository.fetch_ref(@project.repository.path, @diff_head_sha, @merge_request.source_branch)
+ end
+
+ def branch_exists?(branch_name)
+ @project.repository.branch_exists?(branch_name)
+ end
+
+ def fork_merge_request?
+ @relation_hash['source_project_id'] == FORKED_PROJECT_ID
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index cda6ddf0443..df21ff22216 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -119,7 +119,7 @@ module Gitlab
relation_hash: parsed_relation_hash(relation_hash),
members_mapper: members_mapper,
user: @user,
- project_id: restored_project.id)
+ project: restored_project)
end.compact
relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index d44563333a5..fb43e7ccdbb 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -29,11 +29,12 @@ module Gitlab
new(*args).create
end
- def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project_id:)
+ def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project:)
@relation_name = OVERRIDES[relation_sym] || relation_sym
- @relation_hash = relation_hash.except('noteable_id').merge('project_id' => project_id)
+ @relation_hash = relation_hash.except('noteable_id').merge('project_id' => project.id)
@members_mapper = members_mapper
@user = user
+ @project = project
@imported_object_retries = 0
end
@@ -66,7 +67,7 @@ module Gitlab
remove_encrypted_attributes!
@relation_hash['data'].deep_symbolize_keys! if @relation_name == :events && @relation_hash['data']
- set_st_diffs if @relation_name == :merge_request_diff
+ set_st_diff_commits if @relation_name == :merge_request_diff
end
def update_user_references
@@ -105,6 +106,8 @@ module Gitlab
imported_object do |object|
object.commit_id = nil
end
+ elsif @relation_name == :merge_requests
+ MergeRequestParser.new(@project, @relation_hash.delete('diff_head_sha'), imported_object, @relation_hash).parse!
else
imported_object
end
@@ -115,7 +118,7 @@ module Gitlab
# If source and target are the same, populate them with the new project ID.
if @relation_hash['source_project_id']
- @relation_hash['source_project_id'] = same_source_and_target? ? project_id : -1
+ @relation_hash['source_project_id'] = same_source_and_target? ? project_id : MergeRequestParser::FORKED_PROJECT_ID
end
# project_id may not be part of the export, but we always need to populate it if required.
@@ -166,6 +169,7 @@ module Gitlab
def imported_object
yield(existing_or_new_object) if block_given?
existing_or_new_object.importing = true if existing_or_new_object.respond_to?(:importing)
+
existing_or_new_object
rescue ActiveRecord::RecordNotUnique
# as the operation is not atomic, retry in the unlikely scenario an INSERT is
@@ -188,8 +192,11 @@ module Gitlab
relation_class: relation_class)
end
- def set_st_diffs
+ def set_st_diff_commits
@relation_hash['st_diffs'] = @relation_hash.delete('utf8_st_diffs')
+
+ HashUtil.deep_symbolize_array!(@relation_hash['st_diffs'])
+ HashUtil.deep_symbolize_array_with_date!(@relation_hash['st_commits'])
end
def existing_or_new_object
diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb
index 28129198438..46deea3cc9f 100644
--- a/lib/gitlab/ldap/config.rb
+++ b/lib/gitlab/ldap/config.rb
@@ -124,9 +124,9 @@ module Gitlab
def name_proc
if allow_username_or_email_login
- Proc.new { |name| name.gsub(/@.*\z/, '') }
+ proc { |name| name.gsub(/@.*\z/, '') }
else
- Proc.new { |name| name }
+ proc { |name| name }
end
end
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index fcf51b7fc5b..f98481c6d3a 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -147,10 +147,8 @@ module Gitlab
end
def build_new_user
- user = ::User.new(user_attributes)
- user.skip_confirmation!
- user.identities.new(extern_uid: auth_hash.uid, provider: auth_hash.provider)
- user
+ user_params = user_attributes.merge(extern_uid: auth_hash.uid, provider: auth_hash.provider, skip_confirmation: true)
+ Users::CreateService.new(nil, user_params).build
end
def user_attributes
diff --git a/lib/gitlab/polling_interval.rb b/lib/gitlab/polling_interval.rb
new file mode 100644
index 00000000000..c44bb1cd14d
--- /dev/null
+++ b/lib/gitlab/polling_interval.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ class PollingInterval
+ include Gitlab::CurrentSettings
+
+ HEADER_NAME = 'Poll-Interval'.freeze
+
+ def self.set_header(response, interval:)
+ if polling_enabled?
+ multiplier = current_application_settings.polling_interval_multiplier
+ value = (interval * multiplier).to_i
+ else
+ value = -1
+ end
+
+ response.headers[HEADER_NAME] = value
+ end
+
+ def self.polling_enabled?
+ !current_application_settings.polling_interval_multiplier.zero?
+ end
+ end
+end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index db325c00705..0b8959f2fb9 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -62,7 +62,7 @@ module Gitlab
data << line.sub(ref, '').sub(filename, '').sub(/^:-\d+-/, '').sub(/^::\d+:/, '')
end
- OpenStruct.new(
+ FoundBlob.new(
filename: filename,
basename: basename,
ref: ref,
diff --git a/lib/gitlab/repo_path.rb b/lib/gitlab/repo_path.rb
new file mode 100644
index 00000000000..4b1d828c45c
--- /dev/null
+++ b/lib/gitlab/repo_path.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module RepoPath
+ NotFoundError = Class.new(StandardError)
+
+ def self.strip_storage_path(repo_path)
+ result = nil
+
+ Gitlab.config.repositories.storages.values.each do |params|
+ storage_path = params['path']
+ if repo_path.start_with?(storage_path)
+ result = repo_path.sub(storage_path, '')
+ break
+ end
+ end
+
+ if result.nil?
+ raise NotFoundError.new("No known storage path matches #{repo_path.inspect}")
+ end
+
+ result.sub(/\A\/*/, '')
+ end
+ end
+end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index ccfa517e04b..efe8095beea 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -1,5 +1,26 @@
module Gitlab
class SearchResults
+ class FoundBlob
+ attr_reader :id, :filename, :basename, :ref, :startline, :data
+
+ def initialize(opts = {})
+ @id = opts.fetch(:id, nil)
+ @filename = opts.fetch(:filename, nil)
+ @basename = opts.fetch(:basename, nil)
+ @ref = opts.fetch(:ref, nil)
+ @startline = opts.fetch(:startline, nil)
+ @data = opts.fetch(:data, nil)
+ end
+
+ def path
+ filename
+ end
+
+ def no_highlighting?
+ false
+ end
+ end
+
attr_reader :current_user, :query
# Limit search results by passed projects
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index da8d8ddb8ed..36a871e5bbc 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -35,7 +35,7 @@ module Gitlab
end
def strip_key(key)
- key.split(/ /)[0, 2].join(' ')
+ key.split(/[ ]+/)[0, 2].join(' ')
end
private
@@ -88,6 +88,26 @@ module Gitlab
true
end
+ # Fetch remote for repository
+ #
+ # name - project path with namespace
+ # remote - remote name
+ # forced - should we use --force flag?
+ # no_tags - should we use --no-tags flag?
+ #
+ # Ex.
+ # fetch_remote("gitlab/gitlab-ci", "upstream")
+ #
+ def fetch_remote(storage, name, remote, forced: false, no_tags: false)
+ args = [gitlab_shell_projects_path, 'fetch-remote', storage, "#{name}.git", remote, '800']
+ args << '--force' if forced
+ args << '--no-tags' if no_tags
+
+ output, status = Popen.popen(args)
+ raise Error, output unless status.zero?
+ true
+ end
+
# Move repository
# storage - project's storage path
# path - project path with namespace
@@ -174,7 +194,10 @@ module Gitlab
# add_namespace("/path/to/storage", "gitlab")
#
def add_namespace(storage, name)
- FileUtils.mkdir_p(full_path(storage, name), mode: 0770) unless exists?(storage, name)
+ path = full_path(storage, name)
+ FileUtils.mkdir_p(path, mode: 0770) unless exists?(storage, name)
+ rescue Errno::EEXIST => e
+ Rails.logger.warn("Directory exists as a file: #{e} at: #{path}")
end
# Remove directory from repositories storage
diff --git a/lib/gitlab/uploads_transfer.rb b/lib/gitlab/uploads_transfer.rb
index 81701831a6a..7d0c47c5361 100644
--- a/lib/gitlab/uploads_transfer.rb
+++ b/lib/gitlab/uploads_transfer.rb
@@ -1,7 +1,7 @@
module Gitlab
class UploadsTransfer < ProjectTransfer
def root_dir
- File.join(Rails.root, "public", "uploads")
+ File.join(CarrierWave.root, GitlabUploader.base_dir)
end
end
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index eae1a0abf06..08011301d3c 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -1,6 +1,7 @@
require 'base64'
require 'json'
require 'securerandom'
+require 'uri'
module Gitlab
class Workhorse
@@ -15,16 +16,42 @@ module Gitlab
SECRET_LENGTH = 32
class << self
- def git_http_ok(repository, user)
+ def git_http_ok(repository, user, action)
+ repo_path = repository.path_to_repo
params = {
GL_ID: Gitlab::GlId.gl_id(user),
- RepoPath: repository.path_to_repo,
+ RepoPath: repo_path,
}
- params.merge!(
- GitalySocketPath: Gitlab.config.gitaly.socket_path,
- GitalyResourcePath: "/projects/#{repository.project.id}/git-http/info-refs",
- ) if Gitlab.config.gitaly.socket_path.present?
+ if Gitlab.config.gitaly.enabled
+ storage = repository.project.repository_storage
+ address = Gitlab::GitalyClient.get_address(storage)
+ # TODO: use GitalyClient code to assemble the Repository message
+ params[:Repository] = Gitaly::Repository.new(
+ path: repo_path,
+ storage_name: storage,
+ relative_path: Gitlab::RepoPath.strip_storage_path(repo_path),
+ ).to_h
+
+ feature_enabled = case action.to_s
+ when 'git_receive_pack'
+ # Disabled for now, see https://gitlab.com/gitlab-org/gitaly/issues/172
+ false
+ when 'git_upload_pack'
+ Gitlab::GitalyClient.feature_enabled?(:post_upload_pack)
+ when 'info_refs'
+ true
+ else
+ raise "Unsupported action: #{action}"
+ end
+
+ if feature_enabled
+ params[:GitalyAddress] = address
+ # TODO deprecate GitalySocketPath once GITLAB_WORKHORSE_VERSION points
+ # to a version that supports GitalyAddress.
+ params[:GitalySocketPath] = URI(address).path
+ end
+ end
params
end
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index a6f8c4ced5d..a9a48f7188f 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -617,7 +617,7 @@ namespace :gitlab do
end
def sidekiq_process_count
- ps_ux, _ = Gitlab::Popen.popen(%w(ps ux))
+ ps_ux, _ = Gitlab::Popen.popen(%w(ps uxww))
ps_ux.scan(/sidekiq \d+\.\d+\.\d+/).count
end
end
@@ -751,7 +751,7 @@ namespace :gitlab do
end
def mail_room_running?
- ps_ux, _ = Gitlab::Popen.popen(%w(ps ux))
+ ps_ux, _ = Gitlab::Popen.popen(%w(ps uxww))
ps_ux.include?("mail_room")
end
end
diff --git a/lib/tasks/karma.rake b/lib/tasks/karma.rake
index 40465ea3bf0..62a12174efa 100644
--- a/lib/tasks/karma.rake
+++ b/lib/tasks/karma.rake
@@ -1,9 +1,10 @@
unless Rails.env.production?
namespace :karma do
desc 'GitLab | Karma | Generate fixtures for JavaScript tests'
- RSpec::Core::RakeTask.new(:fixtures) do |t|
+ RSpec::Core::RakeTask.new(:fixtures, [:pattern]) do |t, args|
+ args.with_defaults(pattern: 'spec/javascripts/fixtures/*.rb')
ENV['NO_KNAPSACK'] = 'true'
- t.pattern = 'spec/javascripts/fixtures/*.rb'
+ t.pattern = args[:pattern]
t.rspec_opts = '--format documentation'
end
diff --git a/scripts/merge-reports b/scripts/merge-reports
index f7b574001ac..aad76bcc327 100755
--- a/scripts/merge-reports
+++ b/scripts/merge-reports
@@ -1,7 +1,6 @@
#!/usr/bin/env ruby
require 'json'
-require 'yaml'
main_report_file = ARGV.shift
unless main_report_file
diff --git a/scripts/sync-reports b/scripts/sync-reports
new file mode 100755
index 00000000000..5ed65e78005
--- /dev/null
+++ b/scripts/sync-reports
@@ -0,0 +1,95 @@
+#!/usr/bin/env ruby
+
+require 'rubygems'
+require 'fog/aws'
+
+class SyncReports
+ ACTIONS = %w[get put].freeze
+
+ attr_reader :options
+
+ def initialize(options)
+ @options = options
+
+ perform_sync!
+ end
+
+ private
+
+ def perform_sync!
+ case options[:action]
+ when 'get'
+ get_reports!
+ when 'put'
+ put_reports!
+ end
+ end
+
+ def get_reports!
+ options[:report_paths].each { |report_path| get_report!(report_path) }
+ end
+
+ def put_reports!
+ options[:report_paths].each { |report_path| put_report!(report_path) }
+ end
+
+ def get_report!(report_path)
+ file = bucket.files.get(report_path)
+
+ if file.respond_to?(:body)
+ File.write(report_path, file.body)
+ puts "#{report_path} was retrieved from S3."
+ else
+ puts "#{report_path} does not seem to exist on S3."
+ end
+ end
+
+ def put_report!(report_path)
+ bucket.files.create(
+ key: report_path,
+ body: File.open(report_path),
+ public: true
+ )
+ puts "#{report_path} was uploaded to S3."
+ end
+
+ def bucket
+ @bucket ||= storage.directories.get(options[:bucket])
+ end
+
+ def storage
+ @storage ||=
+ Fog::Storage.new(
+ provider: 'AWS',
+ aws_access_key_id: ENV['AWS_ACCESS_KEY_ID'],
+ aws_secret_access_key: ENV['AWS_SECRET_ACCESS_KEY']
+ )
+ end
+end
+
+def usage!(error: 'action')
+ print "\n[ERROR]: "
+ case error
+ when 'action'
+ puts "Please specify an action as first argument: #{SyncReports::ACTIONS.join(', ')}\n\n"
+ when 'bucket'
+ puts "Please specify a bucket as second argument!\n\n"
+ when 'files'
+ puts "Please specify one or more file paths as third argument!\n\n"
+ end
+ puts "Usage: #{__FILE__} [get|put] bucket report_path ...\n\n"
+ puts "Note: the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment "\
+ "variables need to be set\n\n"
+ exit 1
+end
+
+if $0 == __FILE__
+ action = ARGV.shift
+ usage!(error: 'action') unless SyncReports::ACTIONS.include?(action)
+
+ bucket = ARGV.shift
+ usage!(error: 'bucket') unless bucket
+ usage!(error: 'files') unless ARGV.any?
+
+ SyncReports.new(action: action, bucket: bucket, report_paths: ARGV)
+end
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
index 84a1ce773a1..5dd8f66343f 100644
--- a/spec/controllers/admin/application_settings_controller_spec.rb
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -6,23 +6,34 @@ describe Admin::ApplicationSettingsController do
let(:admin) { create(:admin) }
before do
- sign_in(admin)
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
end
- describe 'PATCH #update' do
+ describe 'PUT #update' do
+ before do
+ sign_in(admin)
+ end
+
it 'updates the default_project_visibility for string value' do
- patch :update, application_setting: { default_project_visibility: "20" }
+ put :update, application_setting: { default_project_visibility: "20" }
+
+ expect(response).to redirect_to(admin_application_settings_path)
+ expect(ApplicationSetting.current.default_project_visibility).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'update the restricted levels for string values' do
+ put :update, application_setting: { restricted_visibility_levels: %w[10 20] }
expect(response).to redirect_to(admin_application_settings_path)
- expect(ApplicationSetting.current.default_project_visibility).to eq Gitlab::VisibilityLevel::PUBLIC
+ expect(ApplicationSetting.current.restricted_visibility_levels).to eq([10, 20])
end
- it 'falls back to default with default_project_visibility setting is omitted' do
- patch :update, application_setting: {}
+ it 'falls back to defaults when settings are omitted' do
+ put :update, application_setting: {}
expect(response).to redirect_to(admin_application_settings_path)
- expect(ApplicationSetting.current.default_project_visibility).to eq Gitlab::VisibilityLevel::PRIVATE
+ expect(ApplicationSetting.current.default_project_visibility).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ expect(ApplicationSetting.current.restricted_visibility_levels).to be_empty
end
end
end
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 51f23e4eeb9..010e3180ea4 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -200,5 +200,72 @@ describe Import::BitbucketController do
end
end
end
+
+ context 'user has chosen an existing nested namespace and name for the project' do
+ let(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
+ let(:nested_namespace) { create(:namespace, name: 'bar', parent: parent_namespace, owner: user) }
+ let(:test_name) { 'test_name' }
+
+ it 'takes the selected namespace and name' do
+ expect(Gitlab::BitbucketImport::ProjectCreator).
+ to receive(:new).with(bitbucket_repo, test_name, nested_namespace, user, access_params).
+ and_return(double(execute: true))
+
+ post :create, { target_namespace: nested_namespace.full_path, new_name: test_name, format: :js }
+ end
+ end
+
+ context 'user has chosen a non-existent nested namespaces and name for the project' do
+ let(:test_name) { 'test_name' }
+
+ it 'takes the selected namespace and name' do
+ expect(Gitlab::BitbucketImport::ProjectCreator).
+ to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params).
+ and_return(double(execute: true))
+
+ post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js }
+ end
+
+ it 'creates the namespaces' do
+ allow(Gitlab::BitbucketImport::ProjectCreator).
+ to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params).
+ and_return(double(execute: true))
+
+ expect { post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js } }
+ .to change { Namespace.count }.by(2)
+ end
+
+ it 'new namespace has the right parent' do
+ allow(Gitlab::BitbucketImport::ProjectCreator).
+ to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params).
+ and_return(double(execute: true))
+
+ post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js }
+
+ expect(Namespace.find_by_path_or_name('bar').parent.path).to eq('foo')
+ end
+ end
+
+ context 'user has chosen existent and non-existent nested namespaces and name for the project' do
+ let(:test_name) { 'test_name' }
+ let!(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
+
+ it 'takes the selected namespace and name' do
+ expect(Gitlab::BitbucketImport::ProjectCreator).
+ to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params).
+ and_return(double(execute: true))
+
+ post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :js }
+ end
+
+ it 'creates the namespaces' do
+ allow(Gitlab::BitbucketImport::ProjectCreator).
+ to receive(:new).with(bitbucket_repo, test_name, kind_of(Namespace), user, access_params).
+ and_return(double(execute: true))
+
+ expect { post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :js } }
+ .to change { Namespace.count }.by(2)
+ end
+ end
end
end
diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb
index 3f73ea000ae..2dbb89219d0 100644
--- a/spec/controllers/import/gitlab_controller_spec.rb
+++ b/spec/controllers/import/gitlab_controller_spec.rb
@@ -174,6 +174,72 @@ describe Import::GitlabController do
end
end
end
+
+ context 'user has chosen an existing nested namespace for the project' do
+ let(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
+ let(:nested_namespace) { create(:namespace, name: 'bar', parent: parent_namespace, owner: user) }
+
+ it 'takes the selected namespace and name' do
+ expect(Gitlab::GitlabImport::ProjectCreator).
+ to receive(:new).with(gitlab_repo, nested_namespace, user, access_params).
+ and_return(double(execute: true))
+
+ post :create, { target_namespace: nested_namespace.full_path, format: :js }
+ end
+ end
+
+ context 'user has chosen a non-existent nested namespaces for the project' do
+ let(:test_name) { 'test_name' }
+
+ it 'takes the selected namespace and name' do
+ expect(Gitlab::GitlabImport::ProjectCreator).
+ to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params).
+ and_return(double(execute: true))
+
+ post :create, { target_namespace: 'foo/bar', format: :js }
+ end
+
+ it 'creates the namespaces' do
+ allow(Gitlab::GitlabImport::ProjectCreator).
+ to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params).
+ and_return(double(execute: true))
+
+ expect { post :create, { target_namespace: 'foo/bar', format: :js } }
+ .to change { Namespace.count }.by(2)
+ end
+
+ it 'new namespace has the right parent' do
+ allow(Gitlab::GitlabImport::ProjectCreator).
+ to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params).
+ and_return(double(execute: true))
+
+ post :create, { target_namespace: 'foo/bar', format: :js }
+
+ expect(Namespace.find_by_path_or_name('bar').parent.path).to eq('foo')
+ end
+ end
+
+ context 'user has chosen existent and non-existent nested namespaces and name for the project' do
+ let(:test_name) { 'test_name' }
+ let!(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
+
+ it 'takes the selected namespace and name' do
+ expect(Gitlab::GitlabImport::ProjectCreator).
+ to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params).
+ and_return(double(execute: true))
+
+ post :create, { target_namespace: 'foo/foobar/bar', format: :js }
+ end
+
+ it 'creates the namespaces' do
+ allow(Gitlab::GitlabImport::ProjectCreator).
+ to receive(:new).with(gitlab_repo, kind_of(Namespace), user, access_params).
+ and_return(double(execute: true))
+
+ expect { post :create, { target_namespace: 'foo/foobar/bar', format: :js } }
+ .to change { Namespace.count }.by(2)
+ end
+ end
end
end
end
diff --git a/spec/controllers/profiles/accounts_controller_spec.rb b/spec/controllers/profiles/accounts_controller_spec.rb
index 18148acde3e..2f9d18e3a0e 100644
--- a/spec/controllers/profiles/accounts_controller_spec.rb
+++ b/spec/controllers/profiles/accounts_controller_spec.rb
@@ -1,25 +1,47 @@
require 'spec_helper'
describe Profiles::AccountsController do
- let(:user) { create(:omniauth_user, provider: 'saml') }
+ describe 'DELETE unlink' do
+ let(:user) { create(:omniauth_user) }
- before do
- sign_in(user)
- end
+ before do
+ sign_in(user)
+ end
- it 'does not allow to unlink SAML connected account' do
- identity = user.identities.last
- delete :unlink, provider: 'saml'
- updated_user = User.find(user.id)
+ it 'renders 404 if someone tries to unlink a non existent provider' do
+ delete :unlink, provider: 'github'
- expect(response).to have_http_status(302)
- expect(updated_user.identities.size).to eq(1)
- expect(updated_user.identities).to include(identity)
- end
+ expect(response).to have_http_status(404)
+ end
+
+ [:saml, :cas3].each do |provider|
+ describe "#{provider} provider" do
+ let(:user) { create(:omniauth_user, provider: provider.to_s) }
+
+ it "does not allow to unlink connected account" do
+ identity = user.identities.last
+
+ delete :unlink, provider: provider.to_s
+
+ expect(response).to have_http_status(302)
+ expect(user.reload.identities).to include(identity)
+ end
+ end
+ end
+
+ [:twitter, :facebook, :google_oauth2, :gitlab, :github, :bitbucket, :crowd, :auth0].each do |provider|
+ describe "#{provider} provider" do
+ let(:user) { create(:omniauth_user, provider: provider.to_s) }
+
+ it 'allows to unlink connected account' do
+ identity = user.identities.last
- it 'does allow to delete other linked accounts' do
- user.identities.create(provider: 'twitter', extern_uid: 'twitter_123')
+ delete :unlink, provider: provider.to_s
- expect { delete :unlink, provider: 'twitter' }.to change(Identity.all, :size).by(-1)
+ expect(response).to have_http_status(302)
+ expect(user.reload.identities).not_to include(identity)
+ end
+ end
+ end
end
end
diff --git a/spec/controllers/profiles/notifications_controller_spec.rb b/spec/controllers/profiles/notifications_controller_spec.rb
new file mode 100644
index 00000000000..b97cdd4d489
--- /dev/null
+++ b/spec/controllers/profiles/notifications_controller_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Profiles::NotificationsController do
+ let(:user) do
+ create(:user) do |user|
+ user.emails.create(email: 'original@example.com')
+ user.emails.create(email: 'new@example.com')
+ user.notification_email = 'original@example.com'
+ user.save!
+ end
+ end
+
+ describe 'GET show' do
+ it 'renders' do
+ sign_in(user)
+
+ get :show
+
+ expect(response).to render_template :show
+ end
+ end
+
+ describe 'POST update' do
+ it 'updates only permitted attributes' do
+ sign_in(user)
+
+ put :update, user: { notification_email: 'new@example.com', notified_of_own_activity: true, admin: true }
+
+ user.reload
+ expect(user.notification_email).to eq('new@example.com')
+ expect(user.notified_of_own_activity).to eq(true)
+ expect(user.admin).to eq(false)
+ expect(controller).to set_flash[:notice].to('Notification settings saved')
+ end
+
+ it 'shows an error message if the params are invalid' do
+ sign_in(user)
+
+ put :update, user: { notification_email: '' }
+
+ expect(user.reload.notification_email).to eq('original@example.com')
+ expect(controller).to set_flash[:alert].to('Failed to save new settings')
+ end
+ end
+end
diff --git a/spec/controllers/profiles/personal_access_tokens_spec.rb b/spec/controllers/profiles/personal_access_tokens_spec.rb
index dfed1de2046..98a43e278b2 100644
--- a/spec/controllers/profiles/personal_access_tokens_spec.rb
+++ b/spec/controllers/profiles/personal_access_tokens_spec.rb
@@ -12,7 +12,7 @@ describe Profiles::PersonalAccessTokensController do
end
it "allows creation of a token with scopes" do
- name = FFaker::Product.brand
+ name = 'My PAT'
scopes = %w[api read_user]
post :create, personal_access_token: token_attributes.merge(scopes: scopes, name: name)
diff --git a/spec/controllers/projects/builds_controller_spec.rb b/spec/controllers/projects/builds_controller_spec.rb
new file mode 100644
index 00000000000..683667129e5
--- /dev/null
+++ b/spec/controllers/projects/builds_controller_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Projects::BuildsController do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :public) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET status.json' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:status) { build.detailed_status(double('user')) }
+
+ before do
+ get :status, namespace_id: project.namespace,
+ project_id: project,
+ id: build.id,
+ format: :json
+ end
+
+ it 'return a detailed build status in json' do
+ expect(response).to have_http_status(:ok)
+ expect(json_response['text']).to eq status.text
+ expect(json_response['label']).to eq status.label
+ expect(json_response['icon']).to eq status.icon
+ expect(json_response['favicon']).to eq status.favicon
+ end
+ end
+end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 83d80b376fb..5525fbd8130 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -81,6 +81,39 @@ describe Projects::EnvironmentsController do
end
end
+ describe 'GET folder' do
+ before do
+ create(:environment, project: project,
+ name: 'staging-1.0/review',
+ state: :available)
+ end
+
+ context 'when using default format' do
+ it 'responds with HTML' do
+ get :folder, namespace_id: project.namespace,
+ project_id: project,
+ id: 'staging-1.0'
+
+ expect(response).to be_ok
+ expect(response).to render_template 'folder'
+ end
+ end
+
+ context 'when using JSON format' do
+ it 'responds with JSON' do
+ get :folder, namespace_id: project.namespace,
+ project_id: project,
+ id: 'staging-1.0',
+ format: :json
+
+ expect(response).to be_ok
+ expect(response).not_to render_template 'folder'
+ expect(json_response['environments'][0])
+ .to include('name' => 'staging-1.0/review')
+ end
+ end
+ end
+
describe 'GET show' do
context 'with valid id' do
it 'responds with a status code 200' do
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index c467ab9fb8a..734966d50b2 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -90,6 +90,7 @@ describe Projects::IssuesController do
it 'redirects to signin if not logged in' do
get :new, namespace_id: project.namespace, project_id: project
+ expect(flash[:notice]).to eq 'Please sign in to create the new issue.'
expect(response).to redirect_to(new_user_session_path)
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index c310d830e81..72f41f7209a 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -1178,4 +1178,42 @@ describe Projects::MergeRequestsController do
end
end
end
+
+ describe 'GET pipeline_status.json' do
+ context 'when head_pipeline exists' do
+ let!(:pipeline) do
+ create(:ci_pipeline, project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+ end
+
+ let(:status) { pipeline.detailed_status(double('user')) }
+
+ before { get_pipeline_status }
+
+ it 'return a detailed head_pipeline status in json' do
+ expect(response).to have_http_status(:ok)
+ expect(json_response['text']).to eq status.text
+ expect(json_response['label']).to eq status.label
+ expect(json_response['icon']).to eq status.icon
+ expect(json_response['favicon']).to eq status.favicon
+ end
+ end
+
+ context 'when head_pipeline does not exist' do
+ before { get_pipeline_status }
+
+ it 'return empty' do
+ expect(response).to have_http_status(:ok)
+ expect(json_response).to be_empty
+ end
+ end
+
+ def get_pipeline_status
+ get :pipeline_status, namespace_id: project.namespace,
+ project_id: project,
+ id: merge_request.iid,
+ format: :json
+ end
+ end
end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 04bb5cbbd59..d8f9bfd0d37 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -69,4 +69,24 @@ describe Projects::PipelinesController do
format: :json
end
end
+
+ describe 'GET status.json' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:status) { pipeline.detailed_status(double('user')) }
+
+ before do
+ get :status, namespace_id: project.namespace,
+ project_id: project,
+ id: pipeline.id,
+ format: :json
+ end
+
+ it 'return a detailed pipeline status in json' do
+ expect(response).to have_http_status(:ok)
+ expect(json_response['text']).to eq status.text
+ expect(json_response['label']).to eq status.label
+ expect(json_response['icon']).to eq status.icon
+ expect(json_response['favicon']).to eq status.favicon
+ end
+ end
end
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index 8cc216445eb..71dd9ef3eb4 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -30,6 +30,15 @@ describe RegistrationsController do
expect(subject.current_user).to be_nil
end
end
+
+ context 'when signup_enabled? is false' do
+ it 'redirects to sign_in' do
+ allow_any_instance_of(ApplicationSetting).to receive(:signup_enabled?).and_return(false)
+
+ expect { post(:create, user_params) }.not_to change(User, :count)
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
end
context 'when reCAPTCHA is enabled' do
@@ -59,4 +68,20 @@ describe RegistrationsController do
end
end
end
+
+ describe '#destroy' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'schedules the user for destruction' do
+ expect(DeleteUserWorker).to receive(:perform_async).with(user.id, user.id)
+
+ post(:destroy)
+
+ expect(response.status).to eq(302)
+ end
+ end
end
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index a06c29dd91a..9c16a7bc08b 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -211,4 +211,20 @@ describe SessionsController do
end
end
end
+
+ describe '#new' do
+ before do
+ @request.env['devise.mapping'] = Devise.mappings[:user]
+ end
+
+ it 'redirects correctly for referer on same host with params' do
+ search_path = '/search?search=seed_project'
+ allow(controller.request).to receive(:referer).
+ and_return('http://%{host}%{path}' % { host: Gitlab.config.gitlab.host, path: search_path })
+
+ get(:new, redirect_to_referer: :yes)
+
+ expect(controller.stored_location_for(:redirect)).to eq(search_path)
+ end
+ end
end
diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb
index a581725245a..4df9aef2846 100644
--- a/spec/factories/boards.rb
+++ b/spec/factories/boards.rb
@@ -3,7 +3,7 @@ FactoryGirl.define do
project factory: :empty_project
after(:create) do |board|
- board.lists.create(list_type: :done)
+ board.lists.create(list_type: :closed)
end
end
end
diff --git a/spec/factories/chat_names.rb b/spec/factories/chat_names.rb
index 24225468d55..9a0be1a4598 100644
--- a/spec/factories/chat_names.rb
+++ b/spec/factories/chat_names.rb
@@ -6,11 +6,7 @@ FactoryGirl.define do
team_id 'T0001'
team_domain 'Awesome Team'
- sequence :chat_id do |n|
- "U#{n}"
- end
- sequence :chat_name do |n|
- "user#{n}"
- end
+ sequence(:chat_id) { |n| "U#{n}" }
+ chat_name { generate(:username) }
end
end
diff --git a/spec/factories/chat_teams.rb b/spec/factories/chat_teams.rb
index 82f44fa3d15..ffedf69a69b 100644
--- a/spec/factories/chat_teams.rb
+++ b/spec/factories/chat_teams.rb
@@ -1,9 +1,6 @@
FactoryGirl.define do
factory :chat_team, class: ChatTeam do
- sequence :team_id do |n|
- "abcdefghijklm#{n}"
- end
-
+ sequence(:team_id) { |n| "abcdefghijklm#{n}" }
namespace factory: :group
end
end
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index b67c96bc00d..561fbc8e247 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -48,6 +48,10 @@ FactoryGirl.define do
trait :success do
status :success
end
+
+ trait :failed do
+ status :failed
+ end
end
end
end
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index c3b4aff55ba..05abf60d5ce 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -1,8 +1,6 @@
FactoryGirl.define do
factory :ci_runner, class: Ci::Runner do
- sequence :description do |n|
- "My runner#{n}"
- end
+ sequence(:description) { |n| "My runner#{n}" }
platform "darwin"
is_shared false
diff --git a/spec/factories/emails.rb b/spec/factories/emails.rb
index 9794772ac7d..8303861bcfe 100644
--- a/spec/factories/emails.rb
+++ b/spec/factories/emails.rb
@@ -1,6 +1,6 @@
FactoryGirl.define do
factory :email do
user
- email { FFaker::Internet.email('alias') }
+ email { generate(:email_alias) }
end
end
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 7e09f1ba8ea..0b6977e3b17 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -1,10 +1,6 @@
FactoryGirl.define do
- sequence :issue_created_at do |n|
- 4.hours.ago + ( 2 * n ).seconds
- end
-
factory :issue do
- title
+ title { generate(:title) }
author
project factory: :empty_project
diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb
index 5ba8443c62c..22c2a1f15e2 100644
--- a/spec/factories/labels.rb
+++ b/spec/factories/labels.rb
@@ -1,7 +1,10 @@
FactoryGirl.define do
- factory :label, class: ProjectLabel do
- sequence(:title) { |n| "label#{n}" }
+ trait :base_label do
+ title { generate(:label_title) }
color "#990000"
+ end
+
+ factory :label, traits: [:base_label], class: ProjectLabel do
project factory: :empty_project
transient do
@@ -15,9 +18,7 @@ FactoryGirl.define do
end
end
- factory :group_label, class: GroupLabel do
- sequence(:title) { |n| "label#{n}" }
- color "#990000"
+ factory :group_label, traits: [:base_label] do
group
end
end
diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb
index 2a2f3cca91c..f6a78811cbe 100644
--- a/spec/factories/lists.rb
+++ b/spec/factories/lists.rb
@@ -6,8 +6,8 @@ FactoryGirl.define do
sequence(:position)
end
- factory :done_list, parent: :list do
- list_type :done
+ factory :closed_list, parent: :list do
+ list_type :closed
label nil
position nil
end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index ae0bbbd6aeb..e36fe326e1c 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -1,6 +1,6 @@
FactoryGirl.define do
factory :merge_request do
- title
+ title { generate(:title) }
author
association :source_project, :repository, factory: :project
target_project { source_project }
diff --git a/spec/factories/oauth_applications.rb b/spec/factories/oauth_applications.rb
index 86cdc208268..c7ede40f240 100644
--- a/spec/factories/oauth_applications.rb
+++ b/spec/factories/oauth_applications.rb
@@ -1,8 +1,8 @@
FactoryGirl.define do
factory :oauth_application, class: 'Doorkeeper::Application', aliases: [:application] do
- name { FFaker::Name.name }
+ sequence(:name) { |n| "OAuth App #{n}" }
uid { Doorkeeper::OAuth::Helpers::UniqueToken.generate }
- redirect_uri { FFaker::Internet.uri('http') }
+ redirect_uri { generate(:url) }
owner
owner_type 'User'
end
diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb
index 7b15ba47de1..06acaff6cd0 100644
--- a/spec/factories/personal_access_tokens.rb
+++ b/spec/factories/personal_access_tokens.rb
@@ -2,7 +2,7 @@ FactoryGirl.define do
factory :personal_access_token do
user
token { SecureRandom.hex(50) }
- name { FFaker::Product.brand }
+ sequence(:name) { |n| "PAT #{n}" }
revoked false
expires_at { 5.days.from_now }
scopes ['api']
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
index 424ecc65759..39c2a9dd1fb 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -1,6 +1,6 @@
FactoryGirl.define do
factory :project_hook do
- url { FFaker::Internet.uri('http') }
+ url { generate(:url) }
trait :token do
token { SecureRandom.hex(10) }
diff --git a/spec/factories/sequences.rb b/spec/factories/sequences.rb
new file mode 100644
index 00000000000..c0232ba5bf6
--- /dev/null
+++ b/spec/factories/sequences.rb
@@ -0,0 +1,12 @@
+FactoryGirl.define do
+ sequence(:username) { |n| "user#{n}" }
+ sequence(:name) { |n| "John Doe#{n}" }
+ sequence(:email) { |n| "user#{n}@example.org" }
+ sequence(:email_alias) { |n| "user.alias#{n}@example.org" }
+ sequence(:title) { |n| "My title #{n}" }
+ sequence(:filename) { |n| "filename-#{n}.rb" }
+ sequence(:url) { |n| "http://example#{n}.org" }
+ sequence(:label_title) { |n| "label#{n}" }
+ sequence(:branch) { |n| "my-branch-#{n}" }
+ sequence(:past_time) { |n| 4.hours.ago + (2 * n).seconds }
+end
diff --git a/spec/factories/service_hooks.rb b/spec/factories/service_hooks.rb
index 6dd6af63f3e..e3f88ab8fcc 100644
--- a/spec/factories/service_hooks.rb
+++ b/spec/factories/service_hooks.rb
@@ -1,6 +1,6 @@
FactoryGirl.define do
factory :service_hook do
- url { FFaker::Internet.uri('http') }
+ url { generate(:url) }
service
end
end
diff --git a/spec/factories/snippets.rb b/spec/factories/snippets.rb
index 365f12a0c95..18cb0f5de26 100644
--- a/spec/factories/snippets.rb
+++ b/spec/factories/snippets.rb
@@ -1,17 +1,9 @@
FactoryGirl.define do
- sequence :title, aliases: [:content] do
- FFaker::Lorem.sentence
- end
-
- sequence :file_name do
- FFaker::Internet.user_name
- end
-
factory :snippet do
author
- title
- content
- file_name
+ title { generate(:title) }
+ content { generate(:title) }
+ file_name { generate(:filename) }
trait :public do
visibility_level Snippet::PUBLIC
diff --git a/spec/factories/spam_logs.rb b/spec/factories/spam_logs.rb
index a4f6d291269..e369f9f13e9 100644
--- a/spec/factories/spam_logs.rb
+++ b/spec/factories/spam_logs.rb
@@ -1,9 +1,9 @@
FactoryGirl.define do
factory :spam_log do
user
- source_ip { FFaker::Internet.ip_v4_address }
+ sequence(:source_ip) { |n| "42.42.42.#{n % 255}" }
noteable_type 'Issue'
- title { FFaker::Lorem.sentence }
- description { FFaker::Lorem.paragraph(5) }
+ sequence(:title) { |n| "Spam title #{n}" }
+ description { "Spam description\nwith\nmultiple\nlines" }
end
end
diff --git a/spec/factories/system_hooks.rb b/spec/factories/system_hooks.rb
index c786e9cb79b..841e1e293e8 100644
--- a/spec/factories/system_hooks.rb
+++ b/spec/factories/system_hooks.rb
@@ -1,5 +1,5 @@
FactoryGirl.define do
factory :system_hook do
- url { FFaker::Internet.uri('http') }
+ url { generate(:url) }
end
end
diff --git a/spec/factories/system_note_metadata.rb b/spec/factories/system_note_metadata.rb
new file mode 100644
index 00000000000..f487a2d7e4a
--- /dev/null
+++ b/spec/factories/system_note_metadata.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ factory :system_note_metadata do
+ note
+ action 'merge'
+ end
+end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 249dabbaae8..e1ae94a08e4 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -1,10 +1,8 @@
FactoryGirl.define do
- sequence(:name) { FFaker::Name.name }
-
factory :user, aliases: [:author, :assignee, :recipient, :owner, :creator, :resource_owner] do
- email { FFaker::Internet.email }
- name
- sequence(:username) { |n| "#{FFaker::Internet.user_name}#{n}" }
+ email { generate(:email) }
+ name { generate(:name) }
+ username { generate(:username) }
password "12345678"
confirmed_at { Time.now }
confirmation_token { nil }
diff --git a/spec/features/admin/admin_browse_spam_logs_spec.rb b/spec/features/admin/admin_browse_spam_logs_spec.rb
index 562ace92598..bee57472270 100644
--- a/spec/features/admin/admin_browse_spam_logs_spec.rb
+++ b/spec/features/admin/admin_browse_spam_logs_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'Admin browse spam logs' do
- let!(:spam_log) { create(:spam_log) }
+ let!(:spam_log) { create(:spam_log, description: 'abcde ' * 20) }
before do
login_as :admin
diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb
index f7e49a56deb..523afa2318f 100644
--- a/spec/features/admin/admin_health_check_spec.rb
+++ b/spec/features/admin/admin_health_check_spec.rb
@@ -2,7 +2,6 @@ require 'spec_helper'
feature "Admin Health Check", feature: true do
include StubENV
- include WaitForAjax
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
@@ -24,11 +23,12 @@ feature "Admin Health Check", feature: true do
expect(page).to have_selector('#health-check-token', text: token)
end
- describe 'reload access token', js: true do
+ describe 'reload access token' do
it 'changes the access token' do
orig_token = current_application_settings.health_check_access_token
click_button 'Reset health check access token'
- wait_for_ajax
+
+ expect(page).to have_content('New health check access token has been generated!')
expect(find('#health-check-token').text).not_to eq orig_token
end
end
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index f246997d5a2..570c374a89b 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -26,7 +26,7 @@ describe "Admin::Hooks", feature: true do
end
describe "New Hook" do
- let(:url) { FFaker::Internet.uri('http') }
+ let(:url) { generate(:url) }
it 'adds new hook' do
visit admin_hooks_path
diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
index 9ff5c2f9d40..ff23d486355 100644
--- a/spec/features/admin/admin_users_impersonation_tokens_spec.rb
+++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb
@@ -16,7 +16,7 @@ describe 'Admin > Users > Impersonation Tokens', feature: true, js: true do
describe "token creation" do
it "allows creation of a token" do
- name = FFaker::Product.brand
+ name = 'Hello World'
visit admin_user_impersonation_tokens_path(user_id: user.username)
fill_in "Name", with: name
diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb
index ea7a97d1d4f..009e9c6b04c 100644
--- a/spec/features/auto_deploy_spec.rb
+++ b/spec/features/auto_deploy_spec.rb
@@ -42,7 +42,7 @@ describe 'Auto deploy' do
it 'includes OpenShift as an available template', js: true do
click_link 'Set up auto deploy'
- click_button 'Choose a GitLab CI Yaml template'
+ click_button 'Apply a GitLab CI Yaml template'
within '.gitlab-ci-yml-selector' do
expect(page).to have_content('OpenShift')
@@ -51,7 +51,7 @@ describe 'Auto deploy' do
it 'creates a merge request using "auto-deploy" branch', js: true do
click_link 'Set up auto deploy'
- click_button 'Choose a GitLab CI Yaml template'
+ click_button 'Apply a GitLab CI Yaml template'
within '.gitlab-ci-yml-selector' do
click_on 'OpenShift'
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index f7e8b78b54d..e168585534d 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -42,7 +42,7 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'creates default lists' do
- lists = ['To Do', 'Doing', 'Done']
+ lists = ['To Do', 'Doing', 'Closed']
page.within(find('.board-blank-state')) do
click_button('Add default lists')
@@ -65,7 +65,7 @@ describe 'Issue Boards', feature: true, js: true do
let(:testing) { create(:label, project: project, name: 'Testing') }
let(:bug) { create(:label, project: project, name: 'Bug') }
let!(:backlog) { create(:label, project: project, name: 'Backlog') }
- let!(:done) { create(:label, project: project, name: 'Done') }
+ let!(:closed) { create(:label, project: project, name: 'Closed') }
let!(:accepting) { create(:label, project: project, name: 'Accepting Merge Requests') }
let!(:list1) { create(:list, board: board, label: planning, position: 0) }
@@ -114,7 +114,7 @@ describe 'Issue Boards', feature: true, js: true do
end
end
- it 'search done list' do
+ it 'search closed list' do
find('.filtered-search').set(issue8.title)
find('.filtered-search').native.send_keys(:enter)
@@ -186,13 +186,13 @@ describe 'Issue Boards', feature: true, js: true do
end
end
- context 'done' do
- it 'shows list of done issues' do
+ context 'closed' do
+ it 'shows list of closed issues' do
wait_for_board_cards(3, 1)
wait_for_ajax
end
- it 'moves issue to done' do
+ it 'moves issue to closed' do
drag(list_from_index: 0, list_to_index: 2)
wait_for_board_cards(1, 7)
@@ -205,7 +205,7 @@ describe 'Issue Boards', feature: true, js: true do
expect(find('.board:nth-child(3)')).not_to have_content(planning.title)
end
- it 'removes all of the same issue to done' do
+ it 'removes all of the same issue to closed' do
drag(list_from_index: 0, list_to_index: 2)
wait_for_board_cards(1, 7)
@@ -252,7 +252,7 @@ describe 'Issue Boards', feature: true, js: true do
expect(find('.board:nth-child(1)').all('.card').first).not_to have_content(planning.title)
end
- it 'issue moves from done' do
+ it 'issue moves from closed' do
drag(list_from_index: 2, list_to_index: 1)
expect(find('.board:nth-child(2)')).to have_content(issue8.title)
@@ -308,12 +308,12 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_selector('.board', count: 4)
end
- it 'creates new list for Done label' do
+ it 'creates new list for Closed label' do
click_button 'Add list'
wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do
- click_link done.title
+ click_link closed.title
end
wait_for_vue_resource
@@ -326,7 +326,7 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_ajax
page.within('.dropdown-menu-issues-board-new') do
- click_link done.title
+ click_link closed.title
end
wait_for_vue_resource
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index 6d14a8cf483..e6d7cf106d4 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -25,7 +25,7 @@ describe 'Issue Boards new issue', feature: true, js: true do
expect(page).to have_selector('.board-issue-count-holder .btn', count: 1)
end
- it 'does not display new issue button in done list' do
+ it 'does not display new issue button in closed list' do
page.within('.board:nth-child(2)') do
expect(page).not_to have_selector('.board-issue-count-holder .btn')
end
diff --git a/spec/features/groups/group_name_toggle_spec.rb b/spec/features/groups/group_name_toggle_spec.rb
index 8528718a2f7..8a1d415c4f1 100644
--- a/spec/features/groups/group_name_toggle_spec.rb
+++ b/spec/features/groups/group_name_toggle_spec.rb
@@ -6,39 +6,46 @@ feature 'Group name toggle', feature: true, js: true do
let(:nested_group_2) { create(:group, parent: nested_group_1) }
let(:nested_group_3) { create(:group, parent: nested_group_2) }
+ SMALL_SCREEN = 300
+
before do
login_as :user
end
- it 'is not present for less than 3 groups' do
- visit group_path(group)
- expect(page).not_to have_css('.group-name-toggle')
+ it 'is not present if enough horizontal space' do
+ visit group_path(nested_group_3)
- visit group_path(nested_group_1)
+ container_width = page.evaluate_script("$('.title-container')[0].offsetWidth")
+ title_width = page.evaluate_script("$('.title')[0].offsetWidth")
+
+ expect(container_width).to be > title_width
expect(page).not_to have_css('.group-name-toggle')
end
- it 'is present for nested group of 3 or more in the namespace' do
- visit group_path(nested_group_2)
- expect(page).to have_css('.group-name-toggle')
-
+ it 'is present if the title is longer than the container' do
visit group_path(nested_group_3)
- expect(page).to have_css('.group-name-toggle')
+ title_width = page.evaluate_script("$('.title')[0].offsetWidth")
+
+ page_height = page.current_window.size[1]
+ page.current_window.resize_to(SMALL_SCREEN, page_height)
+
+ find('.group-name-toggle')
+ container_width = page.evaluate_script("$('.title-container')[0].offsetWidth")
+
+ expect(title_width).to be > container_width
end
- context 'for group with at least 3 groups' do
- before do
- visit group_path(nested_group_2)
- end
+ it 'should show the full group namespace when toggled' do
+ page_height = page.current_window.size[1]
+ page.current_window.resize_to(SMALL_SCREEN, page_height)
+ visit group_path(nested_group_3)
- it 'should show the full group namespace when toggled' do
- expect(page).not_to have_content(group.name)
- expect(page).to have_css('.group-path.hidable', visible: false)
+ expect(page).not_to have_content(group.name)
+ expect(page).to have_css('.group-path.hidable', visible: false)
- click_button '...'
+ click_button '...'
- expect(page).to have_content(group.name)
- expect(page).to have_css('.group-path.hidable', visible: true)
- end
+ expect(page).to have_content(group.name)
+ expect(page).to have_css('.group-path.hidable', visible: true)
end
end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index d243f9478bb..c90cc06a8f5 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -46,7 +46,7 @@ feature 'Group', feature: true do
describe 'Mattermost team creation' do
before do
- allow(Settings.mattermost).to receive_messages(enabled: mattermost_enabled)
+ stub_mattermost_setting(enabled: mattermost_enabled)
visit new_group_path
end
@@ -100,6 +100,16 @@ feature 'Group', feature: true do
end
end
+ it 'checks permissions to avoid exposing groups by parent_id' do
+ group = create(:group, :private, path: 'secret-group')
+
+ logout
+ login_as(:user)
+ visit new_group_path(parent_id: group.id)
+
+ expect(page).not_to have_content('secret-group')
+ end
+
describe 'group edit' do
let(:group) { create(:group) }
let(:path) { edit_group_path(group) }
diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb
index b90bf6268fd..3dc872ae520 100644
--- a/spec/features/issuables/issuable_list_spec.rb
+++ b/spec/features/issuables/issuable_list_spec.rb
@@ -46,16 +46,16 @@ describe 'issuable list', feature: true do
end
def create_issuables(issuable_type)
- 3.times do
+ 3.times do |n|
issuable =
if issuable_type == :issue
create(:issue, project: project, author: user)
else
- create(:merge_request, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name)
+ create(:merge_request, source_project: project, source_branch: generate(:branch))
end
2.times do
- create(:note_on_issue, noteable: issuable, project: project, note: 'Test note')
+ create(:note_on_issue, noteable: issuable, project: project)
end
create(:award_emoji, :downvote, awardable: issuable)
@@ -65,9 +65,8 @@ describe 'issuable list', feature: true do
if issuable_type == :issue
issue = Issue.reorder(:iid).first
merge_request = create(:merge_request,
- title: FFaker::Lorem.sentence,
source_project: project,
- source_branch: FFaker::Name.name)
+ source_branch: generate(:branch))
MergeRequestsClosingIssues.create!(issue: issue, merge_request: merge_request)
end
diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
index a5b22247a68..cae01f37b6b 100644
--- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
@@ -39,12 +39,12 @@ describe 'Dropdown hint', :js, :feature do
end
describe 'filtering' do
- it 'does not filter `Keep typing and press Enter`' do
+ it 'does not filter `Press Enter or click to search`' do
filtered_search.set('randomtext')
hint_dropdown = find(js_dropdown_hint)
- expect(hint_dropdown).to have_content('Keep typing and press Enter')
+ expect(hint_dropdown).to have_content('Press Enter or click to search')
expect(hint_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: 0)
end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index f463312bf57..2f880c926e7 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -1,13 +1,14 @@
require 'spec_helper'
describe 'Filter issues', js: true, feature: true do
+ include Devise::Test::IntegrationHelpers
include FilteredSearchHelpers
include WaitForAjax
let!(:group) { create(:group) }
let!(:project) { create(:project, group: group) }
- let!(:user) { create(:user) }
- let!(:user2) { create(:user) }
+ let!(:user) { create(:user, username: 'joe') }
+ let!(:user2) { create(:user, username: 'jane') }
let!(:label) { create(:label, project: project) }
let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
@@ -42,16 +43,17 @@ describe 'Filter issues', js: true, feature: true do
project.team << [user2, :master]
group.add_developer(user)
group.add_developer(user2)
- login_as(user)
- create(:issue, project: project)
- create(:issue, title: "Bug report 1", project: project)
- create(:issue, title: "Bug report 2", project: project)
- create(:issue, title: "issue with 'single quotes'", project: project)
- create(:issue, title: "issue with \"double quotes\"", project: project)
- create(:issue, title: "issue with !@\#{$%^&*()-+", project: project)
- create(:issue, title: "issue by assignee", project: project, milestone: milestone, author: user, assignee: user)
- create(:issue, title: "issue by assignee with searchTerm", project: project, milestone: milestone, author: user, assignee: user)
+ sign_in(user)
+
+ create(:issue, project: project)
+ create(:issue, project: project, title: "Bug report 1")
+ create(:issue, project: project, title: "Bug report 2")
+ create(:issue, project: project, title: "issue with 'single quotes'")
+ create(:issue, project: project, title: "issue with \"double quotes\"")
+ create(:issue, project: project, title: "issue with !@\#{$%^&*()-+")
+ create(:issue, project: project, title: "issue by assignee", milestone: milestone, author: user, assignee: user)
+ create(:issue, project: project, title: "issue by assignee with searchTerm", milestone: milestone, author: user, assignee: user)
issue = create(:issue,
title: "Bug 2",
@@ -70,7 +72,7 @@ describe 'Filter issues', js: true, feature: true do
issue_with_caps_label.labels << caps_sensitive_label
issue_with_everything = create(:issue,
- title: "Bug report with everything you thought was possible",
+ title: "Bug report foo was possible",
project: project,
milestone: milestone,
author: user,
@@ -687,10 +689,10 @@ describe 'Filter issues', js: true, feature: true do
end
it 'filters issues by searched text, author, more text, assignee and even more text' do
- input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with")
+ input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} foo")
expect_issues_list_count(1)
- expect_filtered_search_input('bug report with')
+ expect_filtered_search_input('bug report foo')
end
it 'filters issues by searched text, author, assignee and label' do
@@ -701,10 +703,10 @@ describe 'Filter issues', js: true, feature: true do
end
it 'filters issues by searched text, author, text, assignee, text, label and text' do
- input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything")
+ input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} report label:~#{bug_label.title} foo")
expect_issues_list_count(1)
- expect_filtered_search_input('bug report with everything')
+ expect_filtered_search_input('bug report foo')
end
it 'filters issues by searched text, author, assignee, label and milestone' do
@@ -715,10 +717,10 @@ describe 'Filter issues', js: true, feature: true do
end
it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do
- input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you")
+ input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} report label:~#{bug_label.title} milestone:%#{milestone.title} foo")
expect_issues_list_count(1)
- expect_filtered_search_input('bug report with everything you')
+ expect_filtered_search_input('bug report foo')
end
it 'filters issues by searched text, author, assignee, multiple labels and milestone' do
@@ -729,10 +731,10 @@ describe 'Filter issues', js: true, feature: true do
end
it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do
- input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought")
+ input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} report label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} foo")
expect_issues_list_count(1)
- expect_filtered_search_input('bug report with everything you thought')
+ expect_filtered_search_input('bug report foo')
end
end
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
index 41ff31d2b99..3fde85b0a5c 100644
--- a/spec/features/issues/todo_spec.rb
+++ b/spec/features/issues/todo_spec.rb
@@ -17,13 +17,13 @@ feature 'Manually create a todo item from issue', feature: true, js: true do
expect(page).to have_content 'Mark done'
end
- page.within '.header-content .todos-pending-count' do
+ page.within '.header-content .todos-count' do
expect(page).to have_content '1'
end
visit namespace_project_issue_path(project.namespace, project, issue)
- page.within '.header-content .todos-pending-count' do
+ page.within '.header-content .todos-count' do
expect(page).to have_content '1'
end
end
@@ -34,10 +34,10 @@ feature 'Manually create a todo item from issue', feature: true, js: true do
click_button 'Mark done'
end
- expect(page).to have_selector('.todos-pending-count', visible: false)
+ expect(page).to have_selector('.todos-count', visible: false)
visit namespace_project_issue_path(project.namespace, project, issue)
- expect(page).to have_selector('.todos-pending-count', visible: false)
+ expect(page).to have_selector('.todos-count', visible: false)
end
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index a58aedc924e..7afceb88cf9 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -13,6 +13,13 @@ describe 'Issues', feature: true do
user2 = create(:user)
project.team << [[@user, user2], :developer]
+
+ project.repository.create_file(
+ @user,
+ '.gitlab/issue_templates/bug.md',
+ 'this is a test "bug" template',
+ message: 'added issue template',
+ branch_name: 'master')
end
describe 'Edit issue' do
@@ -600,6 +607,16 @@ describe 'Issues', feature: true do
expect(page.find_field("issue_description").value).to match /\n\n$/
end
end
+
+ context 'form filled by URL parameters' do
+ before do
+ visit new_namespace_project_issue_path(project.namespace, project, issuable_template: 'bug')
+ end
+
+ it 'fills in template' do
+ expect(find('.js-issuable-selector .dropdown-toggle-text')).to have_content('bug')
+ end
+ end
end
describe 'new issue by email' do
diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb
index d5e3d8e7eff..69164aabdb2 100644
--- a/spec/features/merge_requests/diff_notes_resolve_spec.rb
+++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb
@@ -296,7 +296,7 @@ feature 'Diff notes resolve', feature: true, js: true do
it 'displays next discussion even if hidden' do
page.all('.note-discussion').each do |discussion|
page.within discussion do
- click_link 'Toggle discussion'
+ click_button 'Toggle discussion'
end
end
@@ -477,13 +477,13 @@ feature 'Diff notes resolve', feature: true, js: true do
it 'shows resolved icon' do
expect(page).to have_content '1/1 discussion resolved'
- click_link 'Toggle discussion'
+ click_button 'Toggle discussion'
expect(page).to have_selector('.line-resolve-btn.is-active')
end
it 'does not allow user to click resolve button' do
expect(page).to have_selector('.line-resolve-btn.is-disabled')
- click_link 'Toggle discussion'
+ click_button 'Toggle discussion'
expect(page).to have_selector('.line-resolve-btn.is-disabled')
end
diff --git a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
index 3dbe26cddb0..1bc2a5548dd 100644
--- a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
+++ b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
@@ -41,7 +41,7 @@ feature 'Clicking toggle commit message link', feature: true, js: true do
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
expect(textbox).not_to be_visible
- click_link "Modify commit message"
+ click_button "Modify commit message"
expect(textbox).to be_visible
end
diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb
index a2cf9b18bf2..3acd3f6a8b3 100644
--- a/spec/features/merge_requests/toggler_behavior_spec.rb
+++ b/spec/features/merge_requests/toggler_behavior_spec.rb
@@ -18,7 +18,7 @@ feature 'toggler_behavior', js: true, feature: true do
it 'should be scrolled down to fragment' do
page_height = page.current_window.size[1]
page_scroll_y = page.evaluate_script("window.scrollY")
- fragment_position_top = page.evaluate_script("$('#{fragment_id}').offset().top")
+ fragment_position_top = page.evaluate_script("Math.round($('#{fragment_id}').offset().top)")
expect(find('.js-toggle-content').visible?).to eq true
expect(find(fragment_id).visible?).to eq true
expect(fragment_position_top).to be >= page_scroll_y
diff --git a/spec/features/milestones/milestones_spec.rb b/spec/features/milestones/milestones_spec.rb
index 8de9942c54e..2fa3e72ab08 100644
--- a/spec/features/milestones/milestones_spec.rb
+++ b/spec/features/milestones/milestones_spec.rb
@@ -76,6 +76,7 @@ describe 'Milestone draggable', feature: true, js: true do
create(:issue, params.merge(title: 'Foo', project: project, milestone: milestone))
visit namespace_project_milestone_path(project.namespace, project, milestone)
+ scroll_into_view('.milestone-content')
drag_to(selector: '.issues-sortable-list', list_to_index: 1)
wait_for_ajax
@@ -86,8 +87,13 @@ describe 'Milestone draggable', feature: true, js: true do
visit namespace_project_milestone_path(project.namespace, project, milestone)
page.find("a[href='#tab-merge-requests']").click
+ scroll_into_view('.milestone-content')
drag_to(selector: '.merge_requests-sortable-list', list_to_index: 1)
wait_for_ajax
end
+
+ def scroll_into_view(selector)
+ page.evaluate_script("document.querySelector('#{selector}').scrollIntoView();")
+ end
end
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index 0917d4dc3ef..99fba594651 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -27,7 +27,7 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do
describe "token creation" do
it "allows creation of a personal access token" do
- name = FFaker::Product.brand
+ name = 'My PAT'
visit profile_personal_access_tokens_path
fill_in "Name", with: name
@@ -52,7 +52,7 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do
it "displays an error message" do
disallow_personal_access_token_saves!
visit profile_personal_access_tokens_path
- fill_in "Name", with: FFaker::Product.brand
+ fill_in "Name", with: 'My PAT'
expect { click_on "Create Personal Access Token" }.not_to change { PersonalAccessToken.count }
expect(page).to have_content("Name cannot be nil")
diff --git a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
new file mode 100644
index 00000000000..e05fbb3715c
--- /dev/null
+++ b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+feature 'Profile > Notifications > User changes notified_of_own_activity setting', feature: true, js: true do
+ let(:user) { create(:user) }
+
+ before do
+ login_as(user)
+ end
+
+ scenario 'User opts into receiving notifications about their own activity' do
+ visit profile_notifications_path
+
+ expect(page).not_to have_checked_field('user[notified_of_own_activity]')
+
+ check 'user[notified_of_own_activity]'
+
+ expect(page).to have_content('Notification settings saved')
+ expect(page).to have_checked_field('user[notified_of_own_activity]')
+ end
+
+ scenario 'User opts out of receiving notifications about their own activity' do
+ user.update!(notified_of_own_activity: true)
+ visit profile_notifications_path
+
+ expect(page).to have_checked_field('user[notified_of_own_activity]')
+
+ uncheck 'user[notified_of_own_activity]'
+
+ expect(page).to have_content('Notification settings saved')
+ expect(page).not_to have_checked_field('user[notified_of_own_activity]')
+ end
+end
diff --git a/spec/features/projects/blobs/user_create_spec.rb b/spec/features/projects/blobs/user_create_spec.rb
index 5686868a0c4..d214a531138 100644
--- a/spec/features/projects/blobs/user_create_spec.rb
+++ b/spec/features/projects/blobs/user_create_spec.rb
@@ -88,7 +88,7 @@ feature 'New blob creation', feature: true, js: true do
scenario 'shows error message' do
expect(page).to have_content('Your changes could not be committed because a file with the same name already exists')
- expect(page).to have_content('New File')
+ expect(page).to have_content('New file')
expect(page).to have_content('NextFeature')
end
end
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index e2d16e0830a..acc3efe04e6 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -166,6 +166,25 @@ feature 'Environment', :feature do
end
end
+ feature 'environment folders', :js do
+ context 'when folder name contains special charaters' do
+ before do
+ create(:environment, project: project,
+ name: 'staging-1.0/review',
+ state: :available)
+
+ visit folder_namespace_project_environments_path(project.namespace,
+ project,
+ id: 'staging-1.0')
+ end
+
+ it 'renders a correct environment folder' do
+ expect(page).to have_http_status(:ok)
+ expect(page).to have_content('Environments / staging-1.0')
+ end
+ end
+ end
+
feature 'auto-close environment when branch is deleted' do
given(:project) { create(:project) }
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 641e2cf7402..cf393afccbb 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -23,6 +23,46 @@ feature 'Environments page', :feature, :js do
expect(page).to have_link('Available')
expect(page).to have_link('Stopped')
end
+
+ describe 'with one available environment' do
+ given(:environment) { create(:environment, project: project, state: :available) }
+
+ describe 'in available tab page' do
+ it 'should show one environment' do
+ visit namespace_project_environments_path(project.namespace, project, scope: 'available')
+ expect(page).to have_css('.environments-container')
+ expect(page.all('tbody > tr').length).to eq(1)
+ end
+ end
+
+ describe 'in stopped tab page' do
+ it 'should show no environments' do
+ visit namespace_project_environments_path(project.namespace, project, scope: 'stopped')
+ expect(page).to have_css('.environments-container')
+ expect(page).to have_content('You don\'t have any environments right now')
+ end
+ end
+ end
+
+ describe 'with one stopped environment' do
+ given(:environment) { create(:environment, project: project, state: :stopped) }
+
+ describe 'in available tab page' do
+ it 'should show no environments' do
+ visit namespace_project_environments_path(project.namespace, project, scope: 'available')
+ expect(page).to have_css('.environments-container')
+ expect(page).to have_content('You don\'t have any environments right now')
+ end
+ end
+
+ describe 'in stopped tab page' do
+ it 'should show one environment' do
+ visit namespace_project_environments_path(project.namespace, project, scope: 'stopped')
+ expect(page).to have_css('.environments-container')
+ expect(page.all('tbody > tr').length).to eq(1)
+ end
+ end
+ end
end
context 'without environments' do
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index ccadc936567..6b281e6d21d 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -40,7 +40,7 @@ feature 'project owner creates a license file', feature: true, js: true do
scenario 'project master creates a license file from the "Add license" link' do
click_link 'Add License'
- expect(page).to have_content('New File')
+ expect(page).to have_content('New file')
expect(current_path).to eq(
namespace_project_new_blob_path(project.namespace, project, 'master'))
expect(find('#file_name').value).to eq('LICENSE')
@@ -63,7 +63,7 @@ feature 'project owner creates a license file', feature: true, js: true do
def select_template(template)
page.within('.js-license-selector-wrap') do
- click_button 'Choose a License template'
+ click_button 'Apply a License template'
click_link template
wait_for_ajax
end
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 420db962318..87322ac2584 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -14,7 +14,7 @@ feature 'project owner sees a link to create a license file in empty project', f
visit namespace_project_path(project.namespace, project)
click_link 'Create empty bare repository'
click_on 'LICENSE'
- expect(page).to have_content('New File')
+ expect(page).to have_content('New file')
expect(current_path).to eq(
namespace_project_new_blob_path(project.namespace, project, 'master'))
@@ -40,7 +40,7 @@ feature 'project owner sees a link to create a license file in empty project', f
def select_template(template)
page.within('.js-license-selector-wrap') do
- click_button 'Choose a License template'
+ click_button 'Apply a License template'
click_link template
wait_for_ajax
end
diff --git a/spec/features/projects/files/template_type_dropdown_spec.rb b/spec/features/projects/files/template_type_dropdown_spec.rb
new file mode 100644
index 00000000000..5ee5e5b4c4e
--- /dev/null
+++ b/spec/features/projects/files/template_type_dropdown_spec.rb
@@ -0,0 +1,135 @@
+require 'spec_helper'
+
+feature 'Template type dropdown selector', js: true do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ end
+
+ context 'editing a non-matching file' do
+ before do
+ create_and_edit_file('.random-file.js')
+ end
+
+ scenario 'not displayed' do
+ check_type_selector_display(false)
+ end
+
+ scenario 'selects every template type correctly' do
+ fill_in 'file_path', with: '.gitignore'
+ try_selecting_all_types
+ end
+
+ scenario 'updates toggle value when input matches' do
+ fill_in 'file_path', with: '.gitignore'
+ check_type_selector_toggle_text('.gitignore')
+ end
+ end
+
+ context 'editing a matching file' do
+ before do
+ visit namespace_project_edit_blob_path(project.namespace, project, File.join(project.default_branch, 'LICENSE'))
+ end
+
+ scenario 'displayed' do
+ check_type_selector_display(true)
+ end
+
+ scenario 'is displayed when input matches' do
+ check_type_selector_display(true)
+ end
+
+ scenario 'selects every template type correctly' do
+ try_selecting_all_types
+ end
+
+ context 'user previews changes' do
+ before do
+ click_link 'Preview Changes'
+ end
+
+ scenario 'type selector is hidden and shown correctly' do
+ check_type_selector_display(false)
+ click_link 'Write'
+ check_type_selector_display(true)
+ end
+ end
+ end
+
+ context 'creating a matching file' do
+ before do
+ visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: '.gitignore')
+ end
+
+ scenario 'is displayed' do
+ check_type_selector_display(true)
+ end
+
+ scenario 'toggle is set to the correct value' do
+ check_type_selector_toggle_text('.gitignore')
+ end
+
+ scenario 'selects every template type correctly' do
+ try_selecting_all_types
+ end
+ end
+
+ context 'creating a file' do
+ before do
+ visit namespace_project_new_blob_path(project.namespace, project, project.default_branch)
+ end
+
+ scenario 'type selector is shown' do
+ check_type_selector_display(true)
+ end
+
+ scenario 'toggle is set to the proper value' do
+ check_type_selector_toggle_text('Choose type')
+ end
+
+ scenario 'selects every template type correctly' do
+ try_selecting_all_types
+ end
+ end
+end
+
+def check_type_selector_display(is_visible)
+ count = is_visible ? 1 : 0
+ expect(page).to have_css('.js-template-type-selector', count: count)
+end
+
+def try_selecting_all_types
+ try_selecting_template_type('LICENSE', 'Apply a License template')
+ try_selecting_template_type('Dockerfile', 'Apply a Dockerfile template')
+ try_selecting_template_type('.gitlab-ci.yml', 'Apply a GitLab CI Yaml template')
+ try_selecting_template_type('.gitignore', 'Apply a .gitignore template')
+end
+
+def try_selecting_template_type(template_type, selector_label)
+ select_template_type(template_type)
+ check_template_selector_display(selector_label)
+ check_type_selector_toggle_text(template_type)
+end
+
+def select_template_type(template_type)
+ find('.js-template-type-selector').click
+ find('.dropdown-content li', text: template_type).click
+end
+
+def check_template_selector_display(content)
+ expect(page).to have_content(content)
+end
+
+def check_type_selector_toggle_text(template_type)
+ dropdown_toggle_button = find('.template-type-selector .dropdown-toggle-text')
+ expect(dropdown_toggle_button).to have_content(template_type)
+end
+
+def create_and_edit_file(file_name)
+ visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: file_name)
+ click_button "Commit Changes"
+ visit namespace_project_edit_blob_path(project.namespace, project, File.join(project.default_branch, file_name))
+end
diff --git a/spec/features/projects/files/undo_template_spec.rb b/spec/features/projects/files/undo_template_spec.rb
new file mode 100644
index 00000000000..5479ea34610
--- /dev/null
+++ b/spec/features/projects/files/undo_template_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+include WaitForAjax
+
+feature 'Template Undo Button', js: true do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ end
+
+ context 'editing a matching file and applying a template' do
+ before do
+ visit namespace_project_edit_blob_path(project.namespace, project, File.join(project.default_branch, "LICENSE"))
+ select_file_template('.js-license-selector', 'Apache License 2.0')
+ end
+
+ scenario 'reverts template application' do
+ try_template_undo('http://www.apache.org/licenses/', 'Apply a License template')
+ end
+ end
+
+ context 'creating a non-matching file' do
+ before do
+ visit namespace_project_new_blob_path(project.namespace, project, 'master')
+ select_file_template_type('LICENSE')
+ select_file_template('.js-license-selector', 'Apache License 2.0')
+ end
+
+ scenario 'reverts template application' do
+ try_template_undo('http://www.apache.org/licenses/', 'Apply a License template')
+ end
+ end
+end
+
+def try_template_undo(template_content, toggle_text)
+ check_undo_button_display
+ check_content_reverted(template_content)
+ check_toggle_text_set(toggle_text)
+end
+
+def check_toggle_text_set(neutral_toggle_text)
+ expect(page).to have_content(neutral_toggle_text)
+end
+
+def check_undo_button_display
+ expect(page).to have_content('Template applied')
+ expect(page).to have_css('.template-selectors-undo-menu .btn-info')
+end
+
+def check_content_reverted(template_content)
+ find('.template-selectors-undo-menu .btn-info').click
+ expect(page).not_to have_content(template_content)
+ expect(find('.template-type-selector .dropdown-toggle-text')).to have_content()
+end
+
+def select_file_template(template_selector_selector, template_name)
+ find(template_selector_selector).click
+ find('.dropdown-content li', text: template_name).click
+ wait_for_ajax
+end
+
+def select_file_template_type(template_type)
+ find('.js-template-type-selector').click
+ find('.dropdown-content li', text: template_type).click
+end
diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb
index 4c28205da9b..c969acc9140 100644
--- a/spec/features/projects/group_links_spec.rb
+++ b/spec/features/projects/group_links_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Project group links', feature: true, js: true do
+feature 'Project group links', :feature, :js do
include Select2Helper
let(:master) { create(:user) }
@@ -51,4 +51,24 @@ feature 'Project group links', feature: true, js: true do
end
end
end
+
+ describe 'the groups dropdown' do
+ before do
+ group_two = create(:group)
+ group.add_owner(master)
+ group_two.add_owner(master)
+
+ visit namespace_project_settings_members_path(project.namespace, project)
+ execute_script 'GroupsSelect.PER_PAGE = 1;'
+ open_select2 '#link_group_id'
+ end
+
+ it 'should infinitely scroll' do
+ expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1)
+
+ scroll_select2_to_bottom('.select2-drop .select2-results:visible')
+
+ expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 2)
+ end
+ end
end
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index 2d1106ea3e8..583f479ec18 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -69,12 +69,8 @@ feature 'Import/Export - project import integration test', feature: true, js: tr
select2(namespace.id, from: '#project_namespace_id')
- # click on disabled element
- find(:link, 'GitLab export').trigger('click')
-
- page.within('.flash-container') do
- expect(page).to have_content('Please enter path and name')
- end
+ # Check for tooltip disabled import button
+ expect(find('.import_gitlab_project')['title']).to eq('Please enter a valid project name.')
end
end
diff --git a/spec/features/projects/milestones/milestone_spec.rb b/spec/features/projects/milestones/milestone_spec.rb
index df229d0aa78..dab78fd3571 100644
--- a/spec/features/projects/milestones/milestone_spec.rb
+++ b/spec/features/projects/milestones/milestone_spec.rb
@@ -23,12 +23,14 @@ feature 'Project milestone', :feature do
end
it 'shows issues stats' do
- expect(page).to have_content 'issues:'
+ expect(find('.milestone-sidebar')).to have_content 'Issues 0'
end
- it 'shows Browse Issues button' do
- within('#content-body') do
- expect(page).to have_link 'Browse Issues'
+ it 'shows link to browse and add issues' do
+ within('.milestone-sidebar') do
+ expect(page).to have_link 'New issue'
+ expect(page).to have_link 'Open: 0'
+ expect(page).to have_link 'Closed: 0'
end
end
end
@@ -48,12 +50,12 @@ feature 'Project milestone', :feature do
end
it 'hides issues stats' do
- expect(page).to have_no_content 'issues:'
+ expect(find('.milestone-sidebar')).not_to have_content 'Issues 0'
end
- it 'hides Browse Issues button' do
- within('#content-body') do
- expect(page).not_to have_link 'Browse Issues'
+ it 'hides new issue button' do
+ within('.milestone-sidebar') do
+ expect(page).not_to have_link 'New issue'
end
end
diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb
index 24d22a092d4..dc3854262e7 100644
--- a/spec/features/projects/services/mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/mattermost_slash_command_spec.rb
@@ -7,7 +7,7 @@ feature 'Setup Mattermost slash commands', :feature, :js do
let(:mattermost_enabled) { true }
before do
- Settings.mattermost['enabled'] = mattermost_enabled
+ stub_mattermost_setting(enabled: mattermost_enabled)
project.team << [user, :master]
login_as(user)
visit edit_namespace_project_service_path(project.namespace, project, service)
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index fff8b9f3447..7bdaafd1beb 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -15,6 +15,10 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
let(:project) { create(:project, namespace: user.namespace) }
context 'when wiki is empty' do
+ scenario 'commit message field has value "Create home"' do
+ expect(page).to have_field('wiki[message]', with: 'Create home')
+ end
+
scenario 'directly from the wiki home page' do
fill_in :wiki_content, with: 'My awesome wiki!'
click_button 'Create page'
@@ -37,6 +41,9 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
fill_in :new_wiki_path, with: 'foo'
click_button 'Create Page'
+ # Commit message field should have correct value.
+ expect(page).to have_field('wiki[message]', with: 'Create foo')
+
fill_in :wiki_content, with: 'My awesome wiki!'
click_button 'Create page'
@@ -51,6 +58,9 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
fill_in :new_wiki_path, with: 'Spaces in the name'
click_button 'Create Page'
+ # Commit message field should have correct value.
+ expect(page).to have_field('wiki[message]', with: 'Create spaces in the name')
+
fill_in :wiki_content, with: 'My awesome wiki!'
click_button 'Create page'
@@ -65,6 +75,9 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
fill_in :new_wiki_path, with: 'hyphens-in-the-name'
click_button 'Create Page'
+ # Commit message field should have correct value.
+ expect(page).to have_field('wiki[message]', with: 'Create hyphens in the name')
+
fill_in :wiki_content, with: 'My awesome wiki!'
click_button 'Create page'
@@ -80,6 +93,10 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
let(:project) { create(:project, namespace: create(:group, :public)) }
context 'when wiki is empty' do
+ scenario 'commit message field has value "Create home"' do
+ expect(page).to have_field('wiki[message]', with: 'Create home')
+ end
+
scenario 'directly from the wiki home page' do
fill_in :wiki_content, with: 'My awesome wiki!'
click_button 'Create page'
@@ -101,6 +118,9 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do
fill_in :new_wiki_path, with: 'foo'
click_button 'Create Page'
+ # Commit message field should have correct value.
+ expect(page).to have_field('wiki[message]', with: 'Create foo')
+
fill_in :wiki_content, with: 'My awesome wiki!'
click_button 'Create page'
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index aedc0333cb9..86cf520ea80 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -19,6 +19,9 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do
scenario 'success when the wiki content is not empty' do
click_link 'Edit'
+ # Commit message field should have correct value.
+ expect(page).to have_field('wiki[message]', with: 'Update home')
+
fill_in :wiki_content, with: 'My awesome wiki!'
click_button 'Save changes'
@@ -48,6 +51,9 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do
scenario 'the home page' do
click_link 'Edit'
+ # Commit message field should have correct value.
+ expect(page).to have_field('wiki[message]', with: 'Update home')
+
fill_in :wiki_content, with: 'My awesome wiki!'
click_button 'Save changes'
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index a6560a81096..40ef4c098b9 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -119,13 +119,15 @@ describe "Search", feature: true do
visit namespace_project_path(project.namespace, project)
page.within '.search' do
- fill_in 'search', with: 'def'
+ fill_in 'search', with: 'application.js'
click_button 'Go'
end
click_link "Code"
expect(page).to have_selector('.file-content .code')
+
+ expect(page).to have_selector("span.line[lang='javascript']")
end
end
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 850020109d4..c270511c903 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -251,7 +251,7 @@ describe 'Dashboard Todos', feature: true do
end
it 'shows "All done" message' do
- within('.todos-pending-count') { expect(page).to have_content '0' }
+ within('.todos-count') { expect(page).to have_content '0' }
expect(page).to have_content 'To do 0'
expect(page).to have_content 'Done 0'
expect(page).to have_selector('.todos-all-done', count: 1)
@@ -267,7 +267,7 @@ describe 'Dashboard Todos', feature: true do
end
it 'shows 99+ for count >= 100 in notification' do
- expect(page).to have_selector('.todos-pending-count', text: '99+')
+ expect(page).to have_selector('.todos-count', text: '99+')
end
it 'shows exact number in To do tab' do
@@ -277,7 +277,7 @@ describe 'Dashboard Todos', feature: true do
it 'shows exact number for count < 100' do
3.times { first('.js-done-todo').click }
- expect(page).to have_selector('.todos-pending-count', text: '98')
+ expect(page).to have_selector('.todos-count', text: '98')
end
end
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index a8d00bb8e5a..28373098123 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: true, js: true do
+feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
include WaitForAjax
before { allow_any_instance_of(U2fHelper).to receive(:inject_u2f_api?).and_return(true) }
@@ -11,8 +11,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
wait_for_ajax
end
- def register_u2f_device(u2f_device = nil)
- name = FFaker::Name.first_name
+ def register_u2f_device(u2f_device = nil, name: 'My device')
u2f_device ||= FakeU2fDevice.new(page, name)
u2f_device.respond_to_u2f_registration
click_on 'Setup New U2F Device'
@@ -62,7 +61,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
expect(page).to have_content('Your U2F device was registered')
# Second device
- second_device = register_u2f_device
+ second_device = register_u2f_device(name: 'My other device')
expect(page).to have_content('Your U2F device was registered')
expect(page).to have_content(first_device.name)
@@ -76,7 +75,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
expect(page).to have_content("You've already enabled two-factor authentication using mobile")
first_u2f_device = register_u2f_device
- second_u2f_device = register_u2f_device
+ second_u2f_device = register_u2f_device(name: 'My other device')
click_on "Delete", match: :first
@@ -99,7 +98,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
user.update_attribute(:otp_required_for_login, true)
visit profile_account_path
manage_two_factor_authentication
- register_u2f_device(u2f_device)
+ register_u2f_device(u2f_device, name: 'My other device')
expect(page).to have_content('Your U2F device was registered')
expect(U2fRegistration.count).to eq(2)
@@ -198,7 +197,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
current_user.update_attribute(:otp_required_for_login, true)
visit profile_account_path
manage_two_factor_authentication
- register_u2f_device
+ register_u2f_device(name: 'My other device')
logout
# Try authenticating user with the old U2F device
@@ -231,7 +230,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
describe "when a given U2F device has not been registered" do
it "does not allow logging in with that particular device" do
- unregistered_device = FakeU2fDevice.new(page, FFaker::Name.first_name)
+ unregistered_device = FakeU2fDevice.new(page, 'My device')
login_as(user)
unregistered_device.respond_to_u2f_authentication
expect(page).to have_content('We heard back from your U2F device')
@@ -252,7 +251,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
# Register second device
visit profile_two_factor_auth_path
expect(page).to have_content("Your U2F device needs to be set up.")
- second_device = register_u2f_device
+ second_device = register_u2f_device(name: 'My other device')
logout
# Authenticate as both devices
diff --git a/spec/features/user_callout_spec.rb b/spec/features/user_callout_spec.rb
index 659cd7c7af7..848af5e3a4d 100644
--- a/spec/features/user_callout_spec.rb
+++ b/spec/features/user_callout_spec.rb
@@ -7,15 +7,27 @@ describe 'User Callouts', js: true do
before do
login_as(user)
- project.team << [user, :master]
+ project.team << [user, :master]
end
- it 'takes you to the profile preferences when the link is clicked' do
+ it 'takes you to the profile preferences when the link is clicked' do
visit dashboard_projects_path
click_link 'Check it out'
expect(current_path).to eq profile_preferences_path
end
+ it 'does not show when cookie is set' do
+ visit dashboard_projects_path
+
+ within('.user-callout') do
+ find('.close').click
+ end
+
+ visit dashboard_projects_path
+
+ expect(page).not_to have_selector('.user-callout')
+ end
+
describe 'user callout should appear in two routes' do
it 'shows up on the user profile' do
visit user_path(user)
@@ -31,7 +43,7 @@ describe 'User Callouts', js: true do
it 'hides the user callout when click on the dismiss icon' do
visit user_path(user)
within('.user-callout') do
- find('.close-user-callout').click
+ find('.close').click
end
expect(page).not_to have_selector('.user-callout')
end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index ee52dc65175..231fd85c464 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -9,7 +9,7 @@ describe IssuesFinder do
let(:label) { create(:label, project: project2) }
let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') }
let(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') }
- let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) }
+ let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2, title: 'tanuki', description: 'tanuki') }
describe '#execute' do
let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') }
diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json
index 819287bf919..11a4caf6628 100644
--- a/spec/fixtures/api/schemas/list.json
+++ b/spec/fixtures/api/schemas/list.json
@@ -10,7 +10,7 @@
"id": { "type": "integer" },
"list_type": {
"type": "string",
- "enum": ["label", "done"]
+ "enum": ["label", "closed"]
},
"label": {
"type": ["object", "null"],
diff --git a/spec/fixtures/metrics.json b/spec/fixtures/metrics.json
new file mode 100644
index 00000000000..06427adce57
--- /dev/null
+++ b/spec/fixtures/metrics.json
@@ -0,0 +1 @@
+{"success":true,"metrics":{"memory_values":[{"metric":{},"values":[[1490935421.33,"9.832775297619047"],[1490935481.33,"9.8359375"],[1490935541.33,"9.837983630952381"],[1490935601.33,"9.840401785714286"],[1490935661.33,"9.84375"],[1490935721.33,"9.846168154761905"],[1490935781.33,"9.849516369047619"],[1490935841.33,"9.85249255952381"],[1490935901.33,"9.855096726190476"],[1490935961.33,"9.845796130952381"],[1490936021.33,"9.847284226190476"],[1490936081.33,"9.84468005952381"],[1490936141.33,"9.847470238095237"],[1490936201.33,"9.850818452380953"],[1490936261.33,"9.852864583333334"],[1490936321.33,"9.854910714285714"],[1490936381.33,"9.857700892857142"],[1490936441.33,"9.865513392857142"],[1490936501.33,"9.874813988095237"],[1490936561.33,"9.866071428571429"],[1490936621.33,"9.849330357142858"],[1490936681.33,"9.841331845238095"],[1490936741.33,"9.853236607142858"],[1490936801.33,"9.839657738095237"],[1490936861.33,"9.841517857142858"],[1490936921.33,"9.852864583333334"],[1490936981.33,"9.851376488095237"],[1490937041.33,"9.837611607142858"],[1490937101.33,"9.840401785714286"],[1490937161.33,"9.843377976190476"],[1490937221.33,"9.845796130952381"],[1490937281.33,"9.84858630952381"],[1490937341.33,"9.866071428571429"],[1490937401.33,"9.852864583333334"],[1490937461.33,"9.855840773809524"],[1490937521.33,"9.837797619047619"],[1490937581.33,"9.840959821428571"],[1490937641.33,"9.848958333333334"],[1490937701.33,"9.844308035714286"],[1490937761.33,"9.845982142857142"],[1490937821.33,"9.83984375"],[1490937881.33,"9.830171130952381"],[1490937941.33,"9.83686755952381"],[1490938001.33,"9.834263392857142"],[1490938061.33,"9.836309523809524"],[1490938121.33,"9.83984375"],[1490938181.33,"9.832775297619047"],[1490938241.33,"9.818266369047619"],[1490938301.33,"9.820126488095237"],[1490938361.33,"9.824032738095237"],[1490938421.33,"9.826078869047619"],[1490938481.33,"9.817708333333334"],[1490938541.33,"9.811755952380953"],[1490938601.33,"9.811197916666666"],[1490938661.33,"9.81156994047619"],[1490938721.33,"9.812313988095237"],[1490938781.33,"9.813058035714286"],[1490938841.33,"9.81343005952381"],[1490938901.33,"9.81547619047619"],[1490938961.33,"9.818824404761905"],[1490939021.33,"9.819754464285714"],[1490939081.33,"9.820684523809524"],[1490939141.33,"9.824776785714286"],[1490939201.33,"9.826078869047619"],[1490939261.33,"9.828311011904763"],[1490939321.33,"9.820870535714286"],[1490939381.33,"9.823846726190476"],[1490939441.33,"9.824404761904763"],[1490939501.33,"9.82905505952381"],[1490939561.33,"9.832775297619047"],[1490939621.33,"9.835565476190476"],[1490939681.33,"9.833333333333334"],[1490939741.33,"9.835379464285714"],[1490939801.33,"9.837239583333334"],[1490939861.33,"9.839285714285714"],[1490939921.33,"9.829613095238095"],[1490939981.33,"9.832403273809524"],[1490940041.33,"9.835751488095237"],[1490940101.33,"9.837797619047619"],[1490940161.33,"9.840959821428571"],[1490940221.33,"9.84375"],[1490940281.33,"9.846354166666666"],[1490940341.33,"9.853980654761905"],[1490940401.33,"9.852678571428571"],[1490940461.33,"9.861979166666666"],[1490940521.33,"9.857700892857142"],[1490940581.33,"9.861793154761905"],[1490940641.33,"9.86421130952381"],[1490940701.33,"9.867001488095237"],[1490940761.33,"9.867931547619047"],[1490940821.33,"9.859933035714286"],[1490940881.33,"9.86235119047619"],[1490940941.33,"9.865141369047619"],[1490941001.33,"9.866443452380953"],[1490941061.33,"9.868861607142858"],[1490941121.33,"9.871465773809524"],[1490941181.33,"9.873511904761905"],[1490941241.33,"9.875558035714286"],[1490941301.33,"9.87797619047619"],[1490941361.33,"9.881324404761905"],[1490941421.33,"9.888392857142858"],[1490941481.33,"9.888392857142858"],[1490941541.33,"9.89546130952381"],[1490941601.33,"9.898065476190476"],[1490941661.33,"9.885044642857142"],[1490941721.33,"9.872395833333334"],[1490941781.33,"9.870349702380953"],[1490941841.33,"9.873325892857142"],[1490941901.33,"9.875558035714286"],[1490941961.33,"9.878534226190476"],[1490942021.33,"9.87983630952381"],[1490942081.33,"9.884300595238095"],[1490942141.33,"9.891927083333334"],[1490942201.33,"9.890252976190476"],[1490942261.33,"9.891927083333334"],[1490942321.33,"9.893787202380953"],[1490942381.33,"9.892113095238095"],[1490942441.33,"9.900111607142858"],[1490942501.33,"9.893415178571429"],[1490942561.33,"9.895647321428571"],[1490942621.33,"9.889322916666666"],[1490942681.33,"9.883556547619047"],[1490942741.33,"9.885602678571429"],[1490942801.33,"9.88764880952381"],[1490942861.33,"9.898623511904763"],[1490942921.33,"9.89453125"],[1490942981.33,"9.885044642857142"],[1490943041.33,"9.874813988095237"],[1490943101.33,"9.880766369047619"],[1490943161.33,"9.868675595238095"],[1490943221.33,"9.864769345238095"],[1490943281.33,"9.852864583333334"],[1490943341.33,"9.855096726190476"],[1490943401.33,"9.857514880952381"],[1490943461.33,"9.859747023809524"],[1490943521.33,"9.861793154761905"],[1490943581.33,"9.864025297619047"],[1490943641.33,"9.857514880952381"],[1490943701.33,"9.859002976190476"],[1490943761.33,"9.860677083333334"],[1490943821.33,"9.864025297619047"],[1490943881.33,"9.86625744047619"],[1490943941.33,"9.873325892857142"],[1490944001.33,"9.876674107142858"],[1490944061.33,"9.888950892857142"],[1490944121.33,"9.878534226190476"],[1490944181.33,"9.880766369047619"],[1490944241.33,"9.884858630952381"],[1490944301.33,"9.870535714285714"],[1490944361.33,"9.864769345238095"],[1490944421.33,"9.851190476190476"],[1490944481.33,"9.85249255952381"],[1490944541.33,"9.85844494047619"],[1490944601.33,"9.855840773809524"],[1490944661.33,"9.868303571428571"],[1490944721.33,"9.859188988095237"],[1490944781.33,"9.860491071428571"],[1490944841.33,"9.863467261904763"],[1490944901.33,"9.864025297619047"],[1490944961.33,"9.857514880952381"],[1490945021.33,"9.843377976190476"],[1490945081.33,"9.836123511904763"],[1490945141.33,"9.837983630952381"],[1490945201.33,"9.84077380952381"],[1490945261.33,"9.847284226190476"],[1490945321.33,"9.849702380952381"],[1490945381.33,"9.827380952380953"],[1490945441.33,"9.82124255952381"],[1490945501.33,"9.822916666666666"],[1490945561.33,"9.824962797619047"],[1490945621.33,"9.814546130952381"],[1490945681.33,"9.805989583333334"],[1490945741.33,"9.791294642857142"],[1490945801.33,"9.786458333333334"],[1490945861.33,"9.77641369047619"],[1490945921.33,"9.76655505952381"],[1490945981.33,"9.76953125"],[1490946041.33,"9.742745535714286"],[1490946101.33,"9.753162202380953"],[1490946161.33,"9.739583333333334"],[1490946221.33,"9.742931547619047"],[1490946281.33,"9.743489583333334"],[1490946341.33,"9.746837797619047"],[1490946401.33,"9.749255952380953"],[1490946461.33,"9.737165178571429"],[1490946521.33,"9.739583333333334"],[1490946581.33,"9.74311755952381"],[1490946641.33,"9.751302083333334"],[1490946701.33,"9.761346726190476"],[1490946761.33,"9.747953869047619"],[1490946821.33,"9.75093005952381"],[1490946881.33,"9.755580357142858"],[1490946941.33,"9.759858630952381"],[1490947001.33,"9.761904761904763"],[1490947061.33,"9.77641369047619"],[1490947121.33,"9.768787202380953"],[1490947181.33,"9.772879464285714"],[1490947241.33,"9.777715773809524"],[1490947301.33,"9.779947916666666"],[1490947361.33,"9.772135416666666"],[1490947421.33,"9.77641369047619"],[1490947481.33,"9.783668154761905"],[1490947541.33,"9.780505952380953"],[1490947601.33,"9.777157738095237"],[1490947661.33,"9.759114583333334"],[1490947721.33,"9.761532738095237"],[1490947781.33,"9.763392857142858"],[1490947841.33,"9.765252976190476"],[1490947901.33,"9.760602678571429"],[1490947961.33,"9.751488095238095"],[1490948021.33,"9.757998511904763"],[1490948081.33,"9.759486607142858"],[1490948141.33,"9.754650297619047"],[1490948201.33,"9.728050595238095"],[1490948261.33,"9.73530505952381"],[1490948321.33,"9.718005952380953"],[1490948381.33,"9.732142857142858"],[1490948441.33,"9.725260416666666"],[1490948501.33,"9.728422619047619"],[1490948561.33,"9.72953869047619"],[1490948621.33,"9.733072916666666"],[1490948681.33,"9.736421130952381"],[1490948741.33,"9.749627976190476"],[1490948801.33,"9.740141369047619"],[1490948861.33,"9.74311755952381"],[1490948921.33,"9.736607142857142"],[1490948981.33,"9.744233630952381"],[1490949041.33,"9.723772321428571"],[1490949101.33,"9.731956845238095"],[1490949161.33,"9.732514880952381"],[1490949221.33,"9.734747023809524"],[1490949281.33,"9.737723214285714"],[1490949341.33,"9.737909226190476"],[1490949401.33,"9.742373511904763"],[1490949461.33,"9.744977678571429"],[1490949521.33,"9.748139880952381"],[1490949581.33,"9.751302083333334"],[1490949641.33,"9.757440476190476"],[1490949701.33,"9.756324404761905"],[1490949761.33,"9.749813988095237"],[1490949821.33,"9.739025297619047"],[1490949881.33,"9.726004464285714"],[1490949941.33,"9.728236607142858"],[1490950001.33,"9.732514880952381"],[1490950061.33,"9.735119047619047"],[1490950121.33,"9.737165178571429"],[1490950181.33,"9.739025297619047"],[1490950241.33,"9.740513392857142"],[1490950301.33,"9.749441964285714"],[1490950361.33,"9.736979166666666"],[1490950421.33,"9.741629464285714"],[1490950481.33,"9.743303571428571"],[1490950541.33,"9.74609375"],[1490950601.33,"9.75093005952381"],[1490950661.33,"9.724330357142858"],[1490950721.33,"9.726748511904763"],[1490950781.33,"9.733258928571429"],[1490950841.33,"9.744233630952381"],[1490950901.33,"9.734375"],[1490950961.33,"9.737537202380953"],[1490951021.33,"9.741071428571429"],[1490951081.33,"9.757254464285714"],[1490951141.33,"9.760044642857142"],[1490951201.33,"9.755952380952381"],[1490951261.33,"9.745349702380953"],[1490951321.33,"9.746651785714286"],[1490951381.33,"9.749441964285714"],[1490951441.33,"9.751674107142858"],[1490951501.33,"9.757998511904763"],[1490951561.33,"9.756510416666666"],[1490951621.33,"9.76264880952381"],[1490951681.33,"9.765625"],[1490951741.33,"9.757254464285714"],[1490951801.33,"9.751674107142858"],[1490951861.33,"9.754278273809524"],[1490951921.33,"9.744233630952381"],[1490951981.33,"9.745349702380953"],[1490952041.33,"9.748883928571429"],[1490952101.33,"9.753162202380953"],[1490952161.33,"9.747953869047619"],[1490952221.33,"9.750186011904763"],[1490952281.33,"9.751116071428571"],[1490952341.33,"9.753162202380953"],[1490952401.33,"9.758928571428571"],[1490952461.33,"9.758928571428571"],[1490952521.33,"9.755394345238095"],[1490952581.33,"9.758928571428571"],[1490952641.33,"9.761160714285714"],[1490952701.33,"9.763206845238095"],[1490952761.33,"9.767857142857142"],[1490952821.33,"9.765438988095237"],[1490952881.33,"9.768229166666666"],[1490952941.33,"9.780877976190476"],[1490953001.33,"9.77250744047619"],[1490953061.33,"9.784412202380953"],[1490953121.33,"9.77827380952381"],[1490953181.33,"9.781063988095237"],[1490953241.33,"9.783668154761905"],[1490953301.33,"9.787016369047619"],[1490953361.33,"9.784970238095237"],[1490953421.33,"9.787946428571429"],[1490953481.33,"9.788690476190476"],[1490953541.33,"9.790922619047619"],[1490953601.33,"9.792596726190476"],[1490953661.33,"9.79594494047619"],[1490953721.33,"9.79780505952381"],[1490953781.33,"9.800223214285714"],[1490953841.33,"9.794828869047619"],[1490953901.33,"9.799293154761905"],[1490953961.33,"9.801525297619047"],[1490954021.33,"9.786458333333334"],[1490954081.33,"9.773809523809524"],[1490954141.33,"9.767485119047619"],[1490954201.33,"9.760044642857142"],[1490954261.33,"9.751116071428571"],[1490954321.33,"9.752790178571429"],[1490954381.33,"9.753162202380953"],[1490954441.33,"9.744419642857142"],[1490954501.33,"9.73921130952381"],[1490954561.33,"9.74125744047619"],[1490954621.33,"9.743303571428571"],[1490954681.33,"9.745535714285714"],[1490954741.33,"9.746837797619047"],[1490954801.33,"9.749255952380953"],[1490954861.33,"9.744419642857142"],[1490954921.33,"9.745349702380953"],[1490954981.33,"9.74702380952381"],[1490955041.33,"9.738467261904763"],[1490955101.33,"9.740141369047619"],[1490955161.33,"9.747767857142858"],[1490955221.33,"9.750372023809524"],[1490955281.33,"9.747767857142858"],[1490955341.33,"9.739025297619047"],[1490955401.33,"9.745349702380953"],[1490955461.33,"9.730282738095237"],[1490955521.33,"9.73139880952381"],[1490955581.33,"9.722842261904763"],[1490955641.33,"9.725818452380953"],[1490955701.33,"9.72749255952381"],[1490955761.33,"9.72953869047619"],[1490955821.33,"9.731956845238095"],[1490955881.33,"9.735677083333334"],[1490955941.33,"9.738467261904763"],[1490956001.33,"9.735863095238095"],[1490956061.33,"9.743675595238095"],[1490956121.33,"9.730840773809524"],[1490956181.33,"9.734747023809524"],[1490956241.33,"9.736235119047619"],[1490956301.33,"9.736607142857142"],[1490956361.33,"9.73921130952381"],[1490956421.33,"9.742001488095237"],[1490956481.33,"9.743675595238095"],[1490956541.33,"9.744977678571429"],[1490956601.33,"9.748697916666666"],[1490956661.33,"9.760602678571429"],[1490956721.33,"9.751302083333334"],[1490956781.33,"9.754278273809524"],[1490956841.33,"9.756324404761905"],[1490956901.33,"9.758370535714286"],[1490956961.33,"9.760416666666666"],[1490957021.33,"9.763020833333334"],[1490957081.33,"9.766183035714286"],[1490957141.33,"9.764508928571429"],[1490957201.33,"9.767299107142858"],[1490957261.33,"9.768787202380953"],[1490957321.33,"9.771019345238095"],[1490957381.33,"9.773623511904763"],[1490957441.33,"9.775111607142858"],[1490957501.33,"9.779389880952381"],[1490957561.33,"9.780691964285714"],[1490957621.33,"9.788690476190476"],[1490957681.33,"9.794828869047619"],[1490957741.33,"9.779203869047619"],[1490957801.33,"9.787016369047619"],[1490957861.33,"9.783854166666666"],[1490957921.33,"9.78515625"],[1490957981.33,"9.786644345238095"],[1490958041.33,"9.787946428571429"],[1490958101.33,"9.800409226190476"],[1490958161.33,"9.787202380952381"],[1490958221.33,"9.789806547619047"],[1490958281.33,"9.791852678571429"],[1490958341.33,"9.788876488095237"],[1490958401.33,"9.78515625"],[1490958461.33,"9.7890625"],[1490958521.33,"9.791108630952381"],[1490958581.33,"9.792596726190476"],[1490958641.33,"9.794828869047619"],[1490958701.33,"9.793154761904763"],[1490958761.33,"9.799293154761905"],[1490958821.33,"9.797247023809524"],[1490958881.33,"9.794084821428571"],[1490958941.33,"9.796875"],[1490959001.33,"9.763950892857142"],[1490959061.33,"9.765997023809524"],[1490959121.33,"9.767671130952381"],[1490959181.33,"9.77046130952381"],[1490959241.33,"9.773809523809524"],[1490959301.33,"9.765252976190476"],[1490959361.33,"9.767485119047619"],[1490959421.33,"9.76953125"],[1490959481.33,"9.774553571428571"],[1490959541.33,"9.77734375"],[1490959601.33,"9.778459821428571"],[1490959661.33,"9.780877976190476"],[1490959721.33,"9.783296130952381"],[1490959781.33,"9.794828869047619"],[1490959841.33,"9.787016369047619"],[1490959901.33,"9.798735119047619"],[1490959961.33,"9.803013392857142"],[1490960021.33,"9.801525297619047"],[1490960081.33,"9.804873511904763"],[1490960141.33,"9.80078125"],[1490960201.33,"9.80375744047619"],[1490960261.33,"9.805059523809524"],[1490960321.33,"9.807849702380953"],[1490960381.33,"9.810825892857142"],[1490960441.33,"9.813058035714286"],[1490960501.33,"9.813616071428571"],[1490960561.33,"9.815104166666666"],[1490960621.33,"9.81733630952381"],[1490960681.33,"9.812872023809524"],[1490960741.33,"9.814546130952381"],[1490960801.33,"9.808035714285714"],[1490960861.33,"9.810081845238095"],[1490960921.33,"9.813058035714286"],[1490960981.33,"9.825892857142858"],[1490961041.33,"9.816964285714286"],[1490961101.33,"9.82421875"],[1490961161.33,"9.80952380952381"],[1490961221.33,"9.804315476190476"],[1490961281.33,"9.797619047619047"],[1490961341.33,"9.80078125"],[1490961401.33,"9.802827380952381"],[1490961461.33,"9.803199404761905"],[1490961521.33,"9.80952380952381"],[1490961581.33,"9.806919642857142"],[1490961641.33,"9.808779761904763"],[1490961701.33,"9.811197916666666"],[1490961761.33,"9.813244047619047"],[1490961821.33,"9.815662202380953"],[1490961881.33,"9.819940476190476"],[1490961941.33,"9.822172619047619"],[1490962001.33,"9.82328869047619"],[1490962061.33,"9.826822916666666"],[1490962121.33,"9.829241071428571"],[1490962181.33,"9.832589285714286"],[1490962241.33,"9.835565476190476"],[1490962301.33,"9.839471726190476"],[1490962361.33,"9.825520833333334"],[1490962421.33,"9.829427083333334"],[1490962481.33,"9.832217261904763"],[1490962541.33,"9.839285714285714"],[1490962601.33,"9.837611607142858"],[1490962661.33,"9.841145833333334"],[1490962721.33,"9.834077380952381"],[1490962781.33,"9.837239583333334"],[1490962841.33,"9.841703869047619"],[1490962901.33,"9.844308035714286"],[1490962961.33,"9.838727678571429"],[1490963021.33,"9.840587797619047"],[1490963081.33,"9.849516369047619"],[1490963141.33,"9.845238095238095"],[1490963201.33,"9.84375"],[1490963261.33,"9.838541666666666"],[1490963321.33,"9.841889880952381"],[1490963381.33,"9.846354166666666"],[1490963441.33,"9.832403273809524"],[1490963501.33,"9.833891369047619"],[1490963561.33,"9.808221726190476"],[1490963621.33,"9.812686011904763"],[1490963681.33,"9.814918154761905"],[1490963741.33,"9.817708333333334"],[1490963801.33,"9.80561755952381"],[1490963861.33,"9.80859375"],[1490963921.33,"9.811197916666666"],[1490963981.33,"9.802269345238095"],[1490964041.33,"9.798177083333334"],[1490964101.33,"9.80078125"],[1490964161.33,"9.815104166666666"],[1490964221.33,"9.806361607142858"]]}],"memory_current":[{"metric":{},"value":[1490964221.593,"9.806361607142858"]}],"cpu_values":[{"metric":{},"values":[[1490935421.446,"0.011520035833333402"],[1490935481.446,"0.010738020634921052"],[1490935541.446,"0.011830812658730162"],[1490935601.446,"0.011666519206349292"],[1490935661.446,"0.012397734365079505"],[1490935721.446,"0.012264678253967905"],[1490935781.446,"0.011701125396825458"],[1490935841.446,"0.011413869087301435"],[1490935901.446,"0.011355704404762157"],[1490935961.446,"0.01295611777777756"],[1490936021.446,"0.012283088253968812"],[1490936081.446,"0.011711742103174674"],[1490936141.446,"0.011066851150792879"],[1490936201.446,"0.011525933611111726"],[1490936261.446,"0.012260294246031015"],[1490936321.446,"0.011917795238095285"],[1490936381.446,"0.011402582301587626"],[1490936441.446,"0.012311798253968057"],[1490936501.446,"0.011604295476191046"],[1490936561.446,"0.012329014206349137"],[1490936621.446,"0.011401263769840977"],[1490936681.446,"0.012310593492063392"],[1490936741.446,"0.01244334305555575"],[1490936801.446,"0.01176146669320973"],[1490936861.446,"0.011186474629011792"],[1490936921.446,"0.013234800079365536"],[1490936981.446,"0.01217435722222217"],[1490937041.446,"0.011211570753967583"],[1490937101.446,"0.012066252420634934"],[1490937161.446,"0.012175381944444839"],[1490937221.446,"0.011215347936507976"],[1490937281.446,"0.012909065515873003"],[1490937341.446,"0.011718783452381023"],[1490937401.446,"0.011740557499999828"],[1490937461.446,"0.012024899960317205"],[1490937521.446,"0.011518551626984471"],[1490937581.446,"0.013295429607829826"],[1490937641.446,"0.013578758822130006"],[1490937701.446,"0.01170811908668783"],[1490937761.446,"0.011867610238095478"],[1490937821.446,"0.012601599007937034"],[1490937881.446,"0.011028959285714405"],[1490937941.446,"0.011972864523808899"],[1490938001.446,"0.012236090515873134"],[1490938061.446,"0.012468855793650629"],[1490938121.446,"0.012324049999999686"],[1490938181.446,"0.012271810317460288"],[1490938241.446,"0.013109732103174912"],[1490938301.446,"0.01201708535714284"],[1490938361.446,"0.01198280035714318"],[1490938421.446,"0.011631491547618469"],[1490938481.446,"0.012698120317460778"],[1490938541.446,"0.011908042499999686"],[1490938601.446,"0.012941332460317123"],[1490938661.446,"0.012009558055555753"],[1490938721.446,"0.011749238293651211"],[1490938781.446,"0.012597720873015857"],[1490938841.446,"0.012128174365079517"],[1490938901.446,"0.013411003452380428"],[1490938961.446,"0.012712377896825132"],[1490939021.446,"0.0126730261111118"],[1490939081.446,"0.012196438134920173"],[1490939141.446,"0.011617917341270696"],[1490939201.446,"0.012271590992062863"],[1490939261.446,"0.01196238253968261"],[1490939321.446,"0.012446522619048245"],[1490939381.446,"0.013146698134919643"],[1490939441.446,"0.013160663611111774"],[1490939501.446,"0.012921960039682278"],[1490939561.446,"0.012100972380952405"],[1490939621.446,"0.01235039095238153"],[1490939681.446,"0.013303590992062684"],[1490939741.446,"0.012064513055556225"],[1490939801.446,"0.011846763531745252"],[1490939861.446,"0.012280224007936782"],[1490939921.446,"0.012305159166666833"],[1490939981.446,"0.012107076111110887"],[1490940041.446,"0.013109447341269884"],[1490940101.446,"0.011668830198412932"],[1490940161.446,"0.011757771468254286"],[1490940221.446,"0.013607426447330252"],[1490940281.446,"0.012069082212503184"],[1490940341.446,"0.012702448174603309"],[1490940401.446,"0.012915864642857006"],[1490940461.446,"0.012882558941478554"],[1490940521.446,"0.01180430288917485"],[1490940581.446,"0.012561457142856586"],[1490940641.446,"0.013117287261905215"],[1490940701.446,"0.0119707260317455"],[1490940761.446,"0.012110876587301957"],[1490940821.446,"0.012900523174603096"],[1490940881.446,"0.012405300317460836"],[1490940941.446,"0.013397718690476127"],[1490941001.446,"0.011853019404761512"],[1490941061.446,"0.011410178968254279"],[1490941121.446,"0.01385021210317412"],[1490941181.446,"0.012158262658730703"],[1490941241.446,"0.012590782142857021"],[1490941301.446,"0.011902994444444289"],[1490941361.446,"0.012597971468253468"],[1490941421.446,"0.013460530436508394"],[1490941481.446,"0.012871132936507318"],[1490941541.446,"0.012321937023810644"],[1490941601.446,"0.012861435992063004"],[1490941661.446,"0.011904687658730493"],[1490941721.446,"0.013068603849206292"],[1490941781.446,"0.011558027420635053"],[1490941841.446,"0.011785108134920095"],[1490941901.446,"0.013018491984126938"],[1490941961.446,"0.012803318611111494"],[1490942021.446,"0.011276595873015969"],[1490942081.446,"0.012407365753968128"],[1490942141.446,"0.01261537746031769"],[1490942201.446,"0.011981626507936492"],[1490942261.446,"0.011779192579364465"],[1490942321.446,"0.012944439365080001"],[1490942381.446,"0.012563845515873258"],[1490942441.446,"0.012490993809523204"],[1490942501.446,"0.011721826547619399"],[1490942561.446,"0.012376904523809195"],[1490942621.446,"0.012627997539682608"],[1490942681.446,"0.012353236984126971"],[1490942741.446,"0.012143749162511788"],[1490942801.446,"0.01210106380777602"],[1490942861.446,"0.01323092650793727"],[1490942921.446,"0.01217811805555557"],[1490942981.446,"0.011703709655399819"],[1490943041.446,"0.01140056596399108"],[1490943101.446,"0.011589462460317477"],[1490943161.446,"0.011424534784915178"],[1490943221.446,"0.011720420858480131"],[1490943281.446,"0.011956359603174035"],[1490943341.446,"0.011627974444444375"],[1490943401.446,"0.012056417142857899"],[1490943461.446,"0.012875421865079256"],[1490943521.446,"0.011447757222222438"],[1490943581.446,"0.011686728412698438"],[1490943641.446,"0.012264428214285543"],[1490943701.446,"0.011396086150793258"],[1490943761.446,"0.012637377857143453"],[1490943821.446,"0.012229487817460189"],[1490943881.446,"0.012519327516820155"],[1490943941.446,"0.011632154440677021"],[1490944001.446,"0.0127011905614214"],[1490944061.446,"0.012041664776432408"],[1490944121.446,"0.011550796183789442"],[1490944181.446,"0.012340807579364546"],[1490944241.446,"0.012514561706348858"],[1490944301.446,"0.011591095515873378"],[1490944361.446,"0.011562522896825472"],[1490944421.446,"0.012653687499999684"],[1490944481.446,"0.012597878095237767"],[1490944541.446,"0.011373836746032411"],[1490944601.446,"0.011489111309523512"],[1490944661.446,"0.012365606547618906"],[1490944721.446,"0.011246835793650788"],[1490944781.446,"0.011556645833333596"],[1490944841.446,"0.0114839880952384"],[1490944901.446,"0.011559932103174322"],[1490944961.446,"0.011456621547618827"],[1490945021.446,"0.011137903531746323"],[1490945081.446,"0.011371503134920238"],[1490945141.446,"0.01262392527777806"],[1490945201.446,"0.011231213571428417"],[1490945261.446,"0.011834045595238011"],[1490945321.446,"0.011222574087301793"],[1490945381.446,"0.01139294579365124"],[1490945441.446,"0.011876671865079205"],[1490945501.446,"0.012003088888888104"],[1490945561.446,"0.011232171746032069"],[1490945621.446,"0.01189458067460394"],[1490945681.446,"0.011593709801586787"],[1490945741.446,"0.01179023611111146"],[1490945801.446,"0.012056340952381187"],[1490945861.446,"0.011755026706348978"],[1490945921.446,"0.011906753412698057"],[1490945981.446,"0.011362850850868408"],[1490946041.446,"0.011567284784873766"],[1490946101.446,"0.01159940924603172"],[1490946161.446,"0.01169248444646143"],[1490946221.446,"0.011294826570231075"],[1490946281.446,"0.011797972936507535"],[1490946341.446,"0.011732454126984091"],[1490946401.446,"0.011992103412699077"],[1490946461.446,"0.011787900634920185"],[1490946521.446,"0.01170581265873045"],[1490946581.446,"0.011391009603175007"],[1490946641.446,"0.01205839841269773"],[1490946701.446,"0.01188169805555573"],[1490946761.446,"0.011459351746031153"],[1490946821.446,"0.012089251071429255"],[1490946881.446,"0.011159798611111122"],[1490946941.446,"0.012261993650793439"],[1490947001.446,"0.011150941865079526"],[1490947061.446,"0.011784560238095428"],[1490947121.446,"0.01146369333333352"],[1490947181.446,"0.011946112341269969"],[1490947241.446,"0.012244168452380742"],[1490947301.446,"0.01108276087301507"],[1490947361.446,"0.011391418571428976"],[1490947421.446,"0.012042411525379642"],[1490947481.446,"0.012082919141039653"],[1490947541.446,"0.011615924682540189"],[1490947601.446,"0.01218819496031727"],[1490947661.446,"0.011292488293650517"],[1490947721.446,"0.011232974365079479"],[1490947781.446,"0.011638264880952223"],[1490947841.446,"0.0115353722619047"],[1490947901.446,"0.011426710952381045"],[1490947961.446,"0.0121381246428574"],[1490948021.446,"0.011812514087301832"],[1490948081.446,"0.012050580317459442"],[1490948141.446,"0.011855329166666742"],[1490948201.446,"0.011649919960317898"],[1490948261.446,"0.01163187396825391"],[1490948321.446,"0.011266725634920935"],[1490948381.446,"0.011934722460317146"],[1490948441.446,"0.011368148333333088"],[1490948501.446,"0.011662377698413048"],[1490948561.446,"0.011039417341269188"],[1490948621.446,"0.012176113174603589"],[1490948681.446,"0.011265313531746158"],[1490948741.446,"0.01158711781746033"],[1490948801.446,"0.011557390912698215"],[1490948861.446,"0.012131684804188454"],[1490948921.446,"0.011474324082027133"],[1490948981.446,"0.011376334484127639"],[1490949041.446,"0.011627233571428175"],[1490949101.446,"0.012499916785714077"],[1490949161.446,"0.011920621706348947"],[1490949221.446,"0.011574053410790661"],[1490949281.446,"0.011837460242165967"],[1490949341.446,"0.011227153174603937"],[1490949401.446,"0.011635896944444115"],[1490949461.446,"0.011701339047618983"],[1490949521.446,"0.011847283650793895"],[1490949581.446,"0.0116057894841271"],[1490949641.446,"0.011789695753968094"],[1490949701.446,"0.011279284841269992"],[1490949761.446,"0.011470807460317041"],[1490949821.446,"0.012172255515873568"],[1490949881.446,"0.011721892103174175"],[1490949941.446,"0.010727560317460336"],[1490950001.446,"0.011509186269841303"],[1490950061.446,"0.01188623087301566"],[1490950121.446,"0.011476948452380968"],[1490950181.446,"0.01211593166666722"],[1490950241.446,"0.011757469444444444"],[1490950301.446,"0.011519936865079109"],[1490950361.446,"0.01165834781746044"],[1490950421.446,"0.010831068928571068"],[1490950481.446,"0.011977692023809912"],[1490950541.446,"0.011828264880952136"],[1490950601.446,"0.01191921916666625"],[1490950661.446,"0.011901336547619379"],[1490950721.446,"0.011776620238095158"],[1490950781.446,"0.011911536031746153"],[1490950841.446,"0.011467936309523809"],[1490950901.446,"0.012163667023809579"],[1490950961.446,"0.0116551746825399"],[1490951021.446,"0.011799408095237739"],[1490951081.446,"0.011845631309524084"],[1490951141.446,"0.011289116626983809"],[1490951201.446,"0.012258327777777984"],[1490951261.446,"0.012265819682539036"],[1490951321.446,"0.011346034166667811"],[1490951381.446,"0.011996446111110597"],[1490951441.446,"0.011511485714285046"],[1490951501.446,"0.011980616349206635"],[1490951561.446,"0.011565376031746316"],[1490951621.446,"0.010918043373016443"],[1490951681.446,"0.011479107380951632"],[1490951741.446,"0.012467024051997748"],[1490951801.446,"0.01235313125400671"],[1490951861.446,"0.012167793061507889"],[1490951921.446,"0.01249734373015914"],[1490951981.446,"0.011414617499999877"],[1490952041.446,"0.012559693849205949"],[1490952101.446,"0.012135384801587835"],[1490952161.446,"0.01195310698412663"],[1490952221.446,"0.011996730515873409"],[1490952281.446,"0.012245181626984071"],[1490952341.446,"0.01172794166666644"],[1490952401.446,"0.012153839325397124"],[1490952461.446,"0.01287662682539674"],[1490952521.446,"0.011412833611110576"],[1490952581.446,"0.0115385753968256"],[1490952641.446,"0.011953797142857927"],[1490952701.446,"0.012210606230158325"],[1490952761.446,"0.012193429836568915"],[1490952821.446,"0.01175164000191546"],[1490952881.446,"0.011686968928571266"],[1490952941.446,"0.01204885615079335"],[1490953001.446,"0.010858237182540066"],[1490953061.446,"0.012570554523809901"],[1490953121.446,"0.011606933412697877"],[1490953181.446,"0.011895175039682713"],[1490953241.446,"0.011877423888888992"],[1490953301.446,"0.01134354857142876"],[1490953361.446,"0.011999752857142089"],[1490953421.446,"0.011927079960317739"],[1490953481.446,"0.01172722273809559"],[1490953541.446,"0.0114388174999997"],[1490953601.446,"0.012584772738095138"],[1490953661.446,"0.011858990837323214"],[1490953721.446,"0.011489406427467985"],[1490953781.446,"0.011673106071428765"],[1490953841.446,"0.012389803452380168"],[1490953901.446,"0.010877735714285755"],[1490953961.446,"0.012098601984127518"],[1490954021.446,"0.011876002539682478"],[1490954081.446,"0.0119792138492057"],[1490954141.446,"0.01116768142857198"],[1490954201.446,"0.011819058452381173"],[1490954261.446,"0.011543723055555002"],[1490954321.446,"0.011877097777778114"],[1490954381.446,"0.011255818690476465"],[1490954441.446,"0.011544411269840424"],[1490954501.446,"0.011844739246031948"],[1490954561.446,"0.012498686626984624"],[1490954621.446,"0.011012790753967753"],[1490954681.446,"0.011763483769841236"],[1490954741.446,"0.011742064880952764"],[1490954801.446,"0.011329697023809454"],[1490954861.446,"0.011616721150793869"],[1490954921.446,"0.011935843650793056"],[1490954981.446,"0.012041806150794254"],[1490955041.446,"0.011776362817460298"],[1490955101.446,"0.011507964920634838"],[1490955161.446,"0.012249892380951723"],[1490955221.446,"0.011680689451964254"],[1490955281.446,"0.011966289381797203"],[1490955341.446,"0.011113054447726804"],[1490955401.446,"0.012155607703748966"],[1490955461.446,"0.011851554722222412"],[1490955521.446,"0.011899298531746077"],[1490955581.446,"0.01202313674603201"],[1490955641.446,"0.011739823253968055"],[1490955701.446,"0.011866135595237215"],[1490955761.446,"0.012171682563083994"],[1490955821.446,"0.01125473955952014"],[1490955881.446,"0.011791852817460289"],[1490955941.446,"0.011389896547619342"],[1490956001.446,"0.011801524404761971"],[1490956061.446,"0.011788201388888577"],[1490956121.446,"0.011472721388889214"],[1490956181.446,"0.012352298174603236"],[1490956241.446,"0.011831984404761721"],[1490956301.446,"0.0114478640476188"],[1490956361.446,"0.012315896944444986"],[1490956421.446,"0.01184387992063444"],[1490956481.446,"0.0108170579365078"],[1490956541.446,"0.012441825119047971"],[1490956601.446,"0.011650502579365023"],[1490956661.446,"0.011244622936507553"],[1490956721.446,"0.01138462460317496"],[1490956781.446,"0.012361013348424437"],[1490956841.446,"0.011687763677888905"],[1490956901.446,"0.011387440952381297"],[1490956961.446,"0.012246620039682158"],[1490957021.446,"0.010769535198412467"],[1490957081.446,"0.012311013690477024"],[1490957141.446,"0.011455958968253554"],[1490957201.446,"0.012126715198413286"],[1490957261.446,"0.011078292499999627"],[1490957321.446,"0.012041933253967746"],[1490957381.446,"0.01147051317460329"],[1490957441.446,"0.01173451460317538"],[1490957501.446,"0.011660740317459825"],[1490957561.446,"0.011851131269840753"],[1490957621.446,"0.012117949444444812"],[1490957681.446,"0.011214277301587397"],[1490957741.446,"0.011935565277777841"],[1490957801.446,"0.011180848809523986"],[1490957861.446,"0.011540955039682404"],[1490957921.446,"0.011678924523809829"],[1490957981.446,"0.01175049698412655"],[1490958041.446,"0.01179233821428546"],[1490958101.446,"0.011217207341269743"],[1490958161.446,"0.011623496111110998"],[1490958221.446,"0.011751017182540137"],[1490958281.446,"0.011548055515872839"],[1490958341.446,"0.01157145297619062"],[1490958401.446,"0.011809365079364814"],[1490958461.446,"0.011367088134920926"],[1490958521.446,"0.011220626785714515"],[1490958581.446,"0.012502413531745657"],[1490958641.446,"0.011674712222222085"],[1490958701.446,"0.010840117777778147"],[1490958761.446,"0.01169669242063464"],[1490958821.446,"0.01206404448412709"],[1490958881.446,"0.011476003253967956"],[1490958941.446,"0.011927363650794281"],[1490959001.446,"0.011834540039682623"],[1490959061.446,"0.011952310396106811"],[1490959121.446,"0.011641002569963536"],[1490959181.446,"0.011215335912698408"],[1490959241.446,"0.011801235515873079"],[1490959301.446,"0.012109150079365269"],[1490959361.446,"0.011696530238095701"],[1490959421.446,"0.01188721699431308"],[1490959481.446,"0.011013023946272025"],[1490959541.446,"0.011927455988174854"],[1490959601.446,"0.011773952156046168"],[1490959661.446,"0.011311449525742057"],[1490959721.446,"0.011926485873016056"],[1490959781.446,"0.012208613174603443"],[1490959841.446,"0.011077256706349554"],[1490959901.446,"0.012141572896825473"],[1490959961.446,"0.011884196547619123"],[1490960021.446,"0.01182910611111061"],[1490960081.446,"0.011089906190476237"],[1490960141.446,"0.011485851349206303"],[1490960201.446,"0.011621675079365073"],[1490960261.446,"0.011420984246031282"],[1490960321.446,"0.011702707664224543"],[1490960381.446,"0.011122996101531552"],[1490960441.446,"0.011923133293650747"],[1490960501.446,"0.012209551587301823"],[1490960561.446,"0.011541768293650705"],[1490960621.446,"0.01133343007936486"],[1490960681.446,"0.011718844880952742"],[1490960741.446,"0.01170618126984048"],[1490960801.446,"0.01158023575396868"],[1490960861.446,"0.012154581865079351"],[1490960921.446,"0.011287024246031918"],[1490960981.446,"0.012035483412697787"],[1490961041.446,"0.01206407186508005"],[1490961101.446,"0.011742228333332922"],[1490961161.446,"0.011460450952381294"],[1490961221.446,"0.011752177539682223"],[1490961281.446,"0.012416623373015778"],[1490961341.446,"0.01134374146825419"],[1490961401.446,"0.011742214642857577"],[1490961461.446,"0.01157076337301528"],[1490961521.446,"0.011251291190475883"],[1490961581.446,"0.010835279404761772"],[1490961641.446,"0.012082314722223412"],[1490961701.446,"0.011244282817460054"],[1490961761.446,"0.012600352738094536"],[1490961821.446,"0.011595374841270692"],[1490961881.446,"0.012047435158729298"],[1490961941.446,"0.012117879285714984"],[1490962001.446,"0.011105805912698236"],[1490962061.446,"0.011228379365079935"],[1490962121.446,"0.012051188888888457"],[1490962181.446,"0.011811605198411965"],[1490962241.446,"0.011438638690477312"],[1490962301.446,"0.011535638928571016"],[1490962361.446,"0.011846252277212543"],[1490962421.446,"0.011137096779830425"],[1490962481.446,"0.011301488807399701"],[1490962541.446,"0.011706436349206364"],[1490962601.446,"0.011607870952381014"],[1490962661.446,"0.01165941666666676"],[1490962721.446,"0.011457761706349363"],[1490962781.446,"0.012004376428571304"],[1490962841.446,"0.012380191230158676"],[1490962901.446,"0.011650816111111262"],[1490962961.446,"0.011339834484126858"],[1490963021.446,"0.011815001031746352"],[1490963081.446,"0.01215702742063424"],[1490963141.446,"0.011112767612387399"],[1490963201.446,"0.011991515394890143"],[1490963261.446,"0.011573327579365182"],[1490963321.446,"0.011559778809523533"],[1490963381.446,"0.012400119444444207"],[1490963441.446,"0.011127036507936056"],[1490963501.446,"0.012095518055556944"],[1490963561.446,"0.011203742460316668"],[1490963621.446,"0.012493672738095584"],[1490963681.446,"0.012086427023809085"],[1490963741.446,"0.01073350408730215"],[1490963801.446,"0.011784052619047683"],[1490963861.446,"0.011817165277777068"],[1490963921.446,"0.01162805619047661"],[1490963981.446,"0.01141054027777739"],[1490964041.446,"0.012398790952381392"],[1490964101.446,"0.011081906428571691"],[1490964161.446,"0.012049610714285322"],[1490964221.446,"0.011764468492063805"]]}],"cpu_current":[{"metric":{},"value":[1490964221.765,"0.011764468492063801"]}]},"last_update":"2017-03-31T12:43:41.618Z"}
diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb
index cd3281d6f51..a0e1265efff 100644
--- a/spec/helpers/auth_helper_spec.rb
+++ b/spec/helpers/auth_helper_spec.rb
@@ -62,4 +62,18 @@ describe AuthHelper do
end
end
end
+
+ describe 'unlink_allowed?' do
+ [:saml, :cas3].each do |provider|
+ it "returns true if the provider is #{provider}" do
+ expect(helper.unlink_allowed?(provider)).to be false
+ end
+ end
+
+ [:twitter, :facebook, :google_oauth2, :gitlab, :github, :bitbucket, :crowd, :auth0].each do |provider|
+ it "returns false if the provider is #{provider}" do
+ expect(helper.unlink_allowed?(provider)).to be true
+ end
+ end
+ end
end
diff --git a/spec/helpers/sidekiq_helper_spec.rb b/spec/helpers/sidekiq_helper_spec.rb
index f86e496740a..117abc9c556 100644
--- a/spec/helpers/sidekiq_helper_spec.rb
+++ b/spec/helpers/sidekiq_helper_spec.rb
@@ -53,6 +53,14 @@ describe SidekiqHelper do
expect(parts).to eq(['17725', '1.0', '12.1', 'Ssl', '19:20:15', 'sidekiq 4.2.1 gitlab-rails [0 of 25 busy]'])
end
+ it 'parses OpenBSD output' do
+ # OpenBSD 6.1
+ line = '49258 0.5 2.3 R/0 Fri10PM ruby23: sidekiq 4.2.7 gitlab [0 of 25 busy] (ruby23)'
+ parts = helper.parse_sidekiq_ps(line)
+
+ expect(parts).to eq(['49258', '0.5', '2.3', 'R/0', 'Fri10PM', 'ruby23: sidekiq 4.2.7 gitlab [0 of 25 busy] (ruby23)'])
+ end
+
it 'does fail gracefully on line not matching the format' do
line = '55137 10.0 2.1 S+ 2:30pm something'
parts = helper.parse_sidekiq_ps(line)
diff --git a/spec/initializers/trusted_proxies_spec.rb b/spec/initializers/trusted_proxies_spec.rb
index ff8b8daa347..70a18f31744 100644
--- a/spec/initializers/trusted_proxies_spec.rb
+++ b/spec/initializers/trusted_proxies_spec.rb
@@ -56,7 +56,7 @@ describe 'trusted_proxies', lib: true do
end
def stub_request(headers = {})
- ActionDispatch::RemoteIp.new(Proc.new { }, false, Rails.application.config.action_dispatch.trusted_proxies).call(headers)
+ ActionDispatch::RemoteIp.new(proc { }, false, Rails.application.config.action_dispatch.trusted_proxies).call(headers)
ActionDispatch::Request.new(headers)
end
diff --git a/spec/javascripts/blob/notebook/index_spec.js b/spec/javascripts/blob/notebook/index_spec.js
new file mode 100644
index 00000000000..11f2a950678
--- /dev/null
+++ b/spec/javascripts/blob/notebook/index_spec.js
@@ -0,0 +1,159 @@
+import Vue from 'vue';
+import renderNotebook from '~/blob/notebook';
+
+describe('iPython notebook renderer', () => {
+ preloadFixtures('static/notebook_viewer.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('static/notebook_viewer.html.raw');
+ });
+
+ it('shows loading icon', () => {
+ renderNotebook();
+
+ expect(
+ document.querySelector('.loading'),
+ ).not.toBeNull();
+ });
+
+ describe('successful response', () => {
+ const response = (request, next) => {
+ next(request.respondWith(JSON.stringify({
+ cells: [{
+ cell_type: 'markdown',
+ source: ['# test'],
+ }, {
+ cell_type: 'code',
+ execution_count: 1,
+ source: [
+ 'def test(str)',
+ ' return str',
+ ],
+ outputs: [],
+ }],
+ }), {
+ status: 200,
+ }));
+ };
+
+ beforeEach((done) => {
+ Vue.http.interceptors.push(response);
+
+ renderNotebook();
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, response,
+ );
+ });
+
+ it('does not show loading icon', () => {
+ expect(
+ document.querySelector('.loading'),
+ ).toBeNull();
+ });
+
+ it('renders the notebook', () => {
+ expect(
+ document.querySelector('.md'),
+ ).not.toBeNull();
+ });
+
+ it('renders the markdown cell', () => {
+ expect(
+ document.querySelector('h1'),
+ ).not.toBeNull();
+
+ expect(
+ document.querySelector('h1').textContent.trim(),
+ ).toBe('test');
+ });
+
+ it('highlights code', () => {
+ expect(
+ document.querySelector('.token'),
+ ).not.toBeNull();
+
+ expect(
+ document.querySelector('.language-python'),
+ ).not.toBeNull();
+ });
+ });
+
+ describe('error in JSON response', () => {
+ const response = (request, next) => {
+ next(request.respondWith('{ "cells": [{"cell_type": "markdown"} }', {
+ status: 200,
+ }));
+ };
+
+ beforeEach((done) => {
+ Vue.http.interceptors.push(response);
+
+ renderNotebook();
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, response,
+ );
+ });
+
+ it('does not show loading icon', () => {
+ expect(
+ document.querySelector('.loading'),
+ ).toBeNull();
+ });
+
+ it('shows error message', () => {
+ expect(
+ document.querySelector('.md').textContent.trim(),
+ ).toBe('An error occured whilst parsing the file.');
+ });
+ });
+
+ describe('error getting file', () => {
+ const response = (request, next) => {
+ next(request.respondWith('', {
+ status: 500,
+ }));
+ };
+
+ beforeEach((done) => {
+ Vue.http.interceptors.push(response);
+
+ renderNotebook();
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, response,
+ );
+ });
+
+ it('does not show loading icon', () => {
+ expect(
+ document.querySelector('.loading'),
+ ).toBeNull();
+ });
+
+ it('shows error message', () => {
+ expect(
+ document.querySelector('.md').textContent.trim(),
+ ).toBe('An error occured whilst loading the file. Please try again later.');
+ });
+ });
+});
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
index 73d18458366..de072e7e470 100644
--- a/spec/javascripts/boards/board_card_spec.js
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -1,10 +1,12 @@
/* global List */
+/* global ListUser */
/* global ListLabel */
/* global listObj */
/* global boardsMockInterceptor */
/* global BoardService */
import Vue from 'vue';
+import '~/boards/models/user';
require('~/boards/models/list');
require('~/boards/models/label');
@@ -130,6 +132,23 @@ describe('Issue card', () => {
expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
});
+ it('does not set detail issue if img is clicked', (done) => {
+ vm.issue.assignee = new ListUser({
+ id: 1,
+ name: 'testing 123',
+ username: 'test',
+ avatar: 'test_image',
+ });
+
+ Vue.nextTick(() => {
+ triggerEvent('mouseup', vm.$el.querySelector('img'));
+
+ expect(gl.issueBoards.BoardsStore.detail.issue).toEqual({});
+
+ done();
+ });
+ });
+
it('does not set detail issue if showDetail is false after mouseup', () => {
triggerEvent('mouseup');
diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js
new file mode 100644
index 00000000000..3f598887603
--- /dev/null
+++ b/spec/javascripts/boards/board_list_spec.js
@@ -0,0 +1,201 @@
+/* global BoardService */
+/* global boardsMockInterceptor */
+/* global List */
+/* global listObj */
+/* global ListIssue */
+import Vue from 'vue';
+import _ from 'underscore';
+import Sortable from 'vendor/Sortable';
+import BoardList from '~/boards/components/board_list';
+import eventHub from '~/boards/eventhub';
+import '~/boards/mixins/sortable_default_options';
+import '~/boards/models/issue';
+import '~/boards/models/list';
+import '~/boards/stores/boards_store';
+import './mock_data';
+
+window.Sortable = Sortable;
+
+describe('Board list component', () => {
+ let component;
+
+ beforeEach((done) => {
+ const el = document.createElement('div');
+
+ document.body.appendChild(el);
+ Vue.http.interceptors.push(boardsMockInterceptor);
+ gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
+ gl.issueBoards.BoardsStore.create();
+ gl.IssueBoardsApp = new Vue();
+
+ const BoardListComp = Vue.extend(BoardList);
+ const list = new List(listObj);
+ const issue = new ListIssue({
+ title: 'Testing',
+ iid: 1,
+ confidential: false,
+ labels: [],
+ });
+ list.issuesSize = 1;
+ list.issues.push(issue);
+
+ component = new BoardListComp({
+ el,
+ propsData: {
+ disabled: false,
+ list,
+ issues: list.issues,
+ loading: false,
+ issueLinkBase: '/issues',
+ rootPath: '/',
+ },
+ }).$mount();
+
+ Vue.nextTick(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptors, boardsMockInterceptor);
+ });
+
+ it('renders component', () => {
+ expect(
+ component.$el.classList.contains('board-list-component'),
+ ).toBe(true);
+ });
+
+ it('renders loading icon', (done) => {
+ component.loading = true;
+
+ Vue.nextTick(() => {
+ expect(
+ component.$el.querySelector('.board-list-loading'),
+ ).not.toBeNull();
+
+ done();
+ });
+ });
+
+ it('renders issues', () => {
+ expect(
+ component.$el.querySelectorAll('.card').length,
+ ).toBe(1);
+ });
+
+ it('sets data attribute with issue id', () => {
+ expect(
+ component.$el.querySelector('.card').getAttribute('data-issue-id'),
+ ).toBe('1');
+ });
+
+ it('shows new issue form', (done) => {
+ component.toggleForm();
+
+ Vue.nextTick(() => {
+ expect(
+ component.$el.querySelector('.board-new-issue-form'),
+ ).not.toBeNull();
+
+ expect(
+ component.$el.querySelector('.is-smaller'),
+ ).not.toBeNull();
+
+ done();
+ });
+ });
+
+ it('shows new issue form after eventhub event', (done) => {
+ eventHub.$emit(`hide-issue-form-${component.list.id}`);
+
+ Vue.nextTick(() => {
+ expect(
+ component.$el.querySelector('.board-new-issue-form'),
+ ).not.toBeNull();
+
+ expect(
+ component.$el.querySelector('.is-smaller'),
+ ).not.toBeNull();
+
+ done();
+ });
+ });
+
+ it('does not show new issue form for closed list', (done) => {
+ component.list.type = 'closed';
+ component.toggleForm();
+
+ Vue.nextTick(() => {
+ expect(
+ component.$el.querySelector('.board-new-issue-form'),
+ ).toBeNull();
+
+ done();
+ });
+ });
+
+ it('shows count list item', (done) => {
+ component.showCount = true;
+
+ Vue.nextTick(() => {
+ expect(
+ component.$el.querySelector('.board-list-count'),
+ ).not.toBeNull();
+
+ expect(
+ component.$el.querySelector('.board-list-count').textContent.trim(),
+ ).toBe('Showing all issues');
+
+ done();
+ });
+ });
+
+ it('shows how many more issues to load', (done) => {
+ component.showCount = true;
+ component.list.issuesSize = 20;
+
+ Vue.nextTick(() => {
+ expect(
+ component.$el.querySelector('.board-list-count').textContent.trim(),
+ ).toBe('Showing 1 of 20 issues');
+
+ done();
+ });
+ });
+
+ it('loads more issues after scrolling', (done) => {
+ spyOn(component.list, 'nextPage');
+ component.$refs.list.style.height = '100px';
+ component.$refs.list.style.overflow = 'scroll';
+
+ for (let i = 0; i < 19; i += 1) {
+ const issue = component.list.issues[0];
+ issue.id += 1;
+ component.list.issues.push(issue);
+ }
+
+ Vue.nextTick(() => {
+ component.$refs.list.scrollTop = 20000;
+
+ setTimeout(() => {
+ expect(component.list.nextPage).toHaveBeenCalled();
+
+ done();
+ });
+ });
+ });
+
+ it('shows loading more spinner', (done) => {
+ component.showCount = true;
+ component.list.loadingMore = true;
+
+ Vue.nextTick(() => {
+ expect(
+ component.$el.querySelector('.board-list-count .fa-spinner'),
+ ).not.toBeNull();
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
index e21f4ca2bc0..b55ff2f473a 100644
--- a/spec/javascripts/boards/boards_store_spec.js
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -50,9 +50,9 @@ describe('Store', () => {
it('finds list by ID', () => {
gl.issueBoards.BoardsStore.addList(listObj);
- const list = gl.issueBoards.BoardsStore.findList('id', 1);
+ const list = gl.issueBoards.BoardsStore.findList('id', listObj.id);
- expect(list.id).toBe(1);
+ expect(list.id).toBe(listObj.id);
});
it('finds list by type', () => {
@@ -64,7 +64,7 @@ describe('Store', () => {
it('gets issue when new list added', (done) => {
gl.issueBoards.BoardsStore.addList(listObj);
- const list = gl.issueBoards.BoardsStore.findList('id', 1);
+ const list = gl.issueBoards.BoardsStore.findList('id', listObj.id);
expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
@@ -89,9 +89,9 @@ describe('Store', () => {
expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
setTimeout(() => {
- const list = gl.issueBoards.BoardsStore.findList('id', 1);
+ const list = gl.issueBoards.BoardsStore.findList('id', listObj.id);
expect(list).toBeDefined();
- expect(list.id).toBe(1);
+ expect(list.id).toBe(listObj.id);
expect(list.position).toBe(0);
done();
}, 0);
@@ -106,9 +106,9 @@ describe('Store', () => {
expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false);
});
- it('check for blank state adding when done list exist', () => {
+ it('check for blank state adding when closed list exist', () => {
gl.issueBoards.BoardsStore.addList({
- list_type: 'done'
+ list_type: 'closed'
});
expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(true);
@@ -126,7 +126,7 @@ describe('Store', () => {
expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
- gl.issueBoards.BoardsStore.removeList(1, 'label');
+ gl.issueBoards.BoardsStore.removeList(listObj.id, 'label');
expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(0);
});
@@ -137,7 +137,7 @@ describe('Store', () => {
expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
- gl.issueBoards.BoardsStore.moveList(listOne, ['2', '1']);
+ gl.issueBoards.BoardsStore.moveList(listOne, [listObjDuplicate.id, listObj.id]);
expect(listOne.position).toBe(1);
});
diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js
index 66fc01fa1e5..a9d4c6ef76f 100644
--- a/spec/javascripts/boards/list_spec.js
+++ b/spec/javascripts/boards/list_spec.js
@@ -43,7 +43,7 @@ describe('List model', () => {
list = new List({
title: 'test',
label: {
- id: 1,
+ id: _.random(10000),
title: 'test',
color: 'red'
}
@@ -51,7 +51,7 @@ describe('List model', () => {
list.save();
setTimeout(() => {
- expect(list.id).toBe(1);
+ expect(list.id).toBe(listObj.id);
expect(list.type).toBe('label');
expect(list.position).toBe(0);
done();
@@ -60,7 +60,7 @@ describe('List model', () => {
it('destroys the list', (done) => {
gl.issueBoards.BoardsStore.addList(listObj);
- list = gl.issueBoards.BoardsStore.findList('id', 1);
+ list = gl.issueBoards.BoardsStore.findList('id', listObj.id);
expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
list.destroy();
@@ -92,7 +92,7 @@ describe('List model', () => {
const listDup = new List(listObjDuplicate);
const issue = new ListIssue({
title: 'Testing',
- iid: 1,
+ iid: _.random(10000),
confidential: false,
labels: [list.label, listDup.label]
});
@@ -102,7 +102,7 @@ describe('List model', () => {
spyOn(gl.boardService, 'moveIssue').and.callThrough();
- listDup.updateIssueLabel(list, issue);
+ listDup.updateIssueLabel(issue, list);
expect(gl.boardService.moveIssue)
.toHaveBeenCalledWith(issue.id, list.id, listDup.id, undefined, undefined);
diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js
index 7a399b307ad..a4fa694eebe 100644
--- a/spec/javascripts/boards/mock_data.js
+++ b/spec/javascripts/boards/mock_data.js
@@ -1,12 +1,12 @@
/* eslint-disable comma-dangle, no-unused-vars, quote-props */
const listObj = {
- id: 1,
+ id: _.random(10000),
position: 0,
title: 'Test',
list_type: 'label',
label: {
- id: 1,
+ id: _.random(10000),
title: 'Testing',
color: 'red',
description: 'testing;'
@@ -14,12 +14,12 @@ const listObj = {
};
const listObjDuplicate = {
- id: 2,
+ id: listObj.id,
position: 1,
title: 'Test',
list_type: 'label',
label: {
- id: 2,
+ id: listObj.label.id,
title: 'Testing',
color: 'red',
description: 'testing;'
diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js
new file mode 100644
index 00000000000..974815fe939
--- /dev/null
+++ b/spec/javascripts/collapsed_sidebar_todo_spec.js
@@ -0,0 +1,123 @@
+/* global Sidebar */
+/* eslint-disable no-new */
+import _ from 'underscore';
+import '~/right_sidebar';
+
+describe('Issuable right sidebar collapsed todo toggle', () => {
+ const fixtureName = 'issues/open-issue.html.raw';
+ const jsonFixtureName = 'todos/todos.json';
+
+ preloadFixtures(fixtureName);
+ preloadFixtures(jsonFixtureName);
+
+ beforeEach(() => {
+ const todoData = getJSONFixture(jsonFixtureName);
+ new Sidebar();
+ loadFixtures(fixtureName);
+
+ document.querySelector('.js-right-sidebar')
+ .classList.toggle('right-sidebar-expanded');
+ document.querySelector('.js-right-sidebar')
+ .classList.toggle('right-sidebar-collapsed');
+
+ spyOn(jQuery, 'ajax').and.callFake((res) => {
+ const d = $.Deferred();
+ const response = _.clone(todoData);
+
+ if (res.type === 'DELETE') {
+ delete response.delete_path;
+ }
+
+ d.resolve(response);
+ return d.promise();
+ });
+ });
+
+ it('shows add todo button', () => {
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon'),
+ ).not.toBeNull();
+
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .fa-plus-square'),
+ ).not.toBeNull();
+
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
+ ).toBeNull();
+ });
+
+ it('sets default tooltip title', () => {
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('title'),
+ ).toBe('Add todo');
+ });
+
+ it('toggle todo state', () => {
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
+
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
+ ).not.toBeNull();
+
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .fa-check-square'),
+ ).not.toBeNull();
+ });
+
+ it('toggle todo state of expanded todo toggle', () => {
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
+
+ expect(
+ document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
+ ).toBe('Mark done');
+ });
+
+ it('toggles todo button tooltip', () => {
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
+
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('data-original-title'),
+ ).toBe('Mark done');
+ });
+
+ it('marks todo as done', () => {
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
+
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
+ ).not.toBeNull();
+
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
+
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
+ ).toBeNull();
+
+ expect(
+ document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
+ ).toBe('Add todo');
+ });
+
+ it('updates aria-label to mark done', () => {
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
+
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
+ ).toBe('Mark done');
+ });
+
+ it('updates aria-label to add todo', () => {
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
+
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
+ ).toBe('Mark done');
+
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
+
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
+ ).toBe('Add todo');
+ });
+});
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index bc2e092db65..8cac3cad232 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -9,7 +9,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
loadFixtures('static/pipelines_table.html.raw');
});
- describe('successfull request', () => {
+ describe('successful request', () => {
describe('without pipelines', () => {
const pipelinesEmptyResponse = (request, next) => {
next(request.respondWith(JSON.stringify([]), {
@@ -17,24 +17,25 @@ describe('Pipelines table in Commits and Merge requests', () => {
}));
};
- beforeEach(() => {
+ beforeEach(function () {
Vue.http.interceptors.push(pipelinesEmptyResponse);
+
+ this.component = new PipelinesTable({
+ el: document.querySelector('#commit-pipeline-table-view'),
+ });
});
- afterEach(() => {
+ afterEach(function () {
Vue.http.interceptors = _.without(
Vue.http.interceptors, pipelinesEmptyResponse,
);
+ this.component.$destroy();
});
- it('should render the empty state', (done) => {
- const component = new PipelinesTable({
- el: document.querySelector('#commit-pipeline-table-view'),
- });
-
+ it('should render the empty state', function (done) {
setTimeout(() => {
- expect(component.$el.querySelector('.empty-state')).toBeDefined();
- expect(component.$el.querySelector('.realtime-loading')).toBe(null);
+ expect(this.component.$el.querySelector('.empty-state')).toBeDefined();
+ expect(this.component.$el.querySelector('.realtime-loading')).toBe(null);
done();
}, 1);
});
@@ -49,22 +50,23 @@ describe('Pipelines table in Commits and Merge requests', () => {
beforeEach(() => {
Vue.http.interceptors.push(pipelinesResponse);
+
+ this.component = new PipelinesTable({
+ el: document.querySelector('#commit-pipeline-table-view'),
+ });
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, pipelinesResponse,
);
+ this.component.$destroy();
});
it('should render a table with the received pipelines', (done) => {
- const component = new PipelinesTable({
- el: document.querySelector('#commit-pipeline-table-view'),
- });
-
setTimeout(() => {
- expect(component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1);
- expect(component.$el.querySelector('.realtime-loading')).toBe(null);
+ expect(this.component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1);
+ expect(this.component.$el.querySelector('.realtime-loading')).toBe(null);
done();
}, 0);
});
@@ -78,24 +80,25 @@ describe('Pipelines table in Commits and Merge requests', () => {
}));
};
- beforeEach(() => {
+ beforeEach(function () {
Vue.http.interceptors.push(pipelinesErrorResponse);
+
+ this.component = new PipelinesTable({
+ el: document.querySelector('#commit-pipeline-table-view'),
+ });
});
- afterEach(() => {
+ afterEach(function () {
Vue.http.interceptors = _.without(
Vue.http.interceptors, pipelinesErrorResponse,
);
+ this.component.$destroy();
});
- it('should render empty state', (done) => {
- const component = new PipelinesTable({
- el: document.querySelector('#commit-pipeline-table-view'),
- });
-
+ it('should render empty state', function (done) {
setTimeout(() => {
- expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
- expect(component.$el.querySelector('.realtime-loading')).toBe(null);
+ expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
+ expect(this.component.$el.querySelector('.realtime-loading')).toBe(null);
done();
}, 0);
});
diff --git a/spec/javascripts/cycle_analytics/limit_warning_component_spec.js b/spec/javascripts/cycle_analytics/limit_warning_component_spec.js
new file mode 100644
index 00000000000..50000c5a5f5
--- /dev/null
+++ b/spec/javascripts/cycle_analytics/limit_warning_component_spec.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import limitWarningComp from '~/cycle_analytics/components/limit_warning_component';
+
+describe('Limit warning component', () => {
+ let component;
+ let LimitWarningComponent;
+
+ beforeEach(() => {
+ LimitWarningComponent = Vue.extend(limitWarningComp);
+ });
+
+ it('should not render if count is not exactly than 50', () => {
+ component = new LimitWarningComponent({
+ propsData: {
+ count: 5,
+ },
+ }).$mount();
+
+ expect(component.$el.textContent.trim()).toBe('');
+
+ component = new LimitWarningComponent({
+ propsData: {
+ count: 55,
+ },
+ }).$mount();
+
+ expect(component.$el.textContent.trim()).toBe('');
+ });
+
+ it('should render if count is exactly 50', () => {
+ component = new LimitWarningComponent({
+ propsData: {
+ count: 50,
+ },
+ }).$mount();
+
+ expect(component.$el.textContent.trim()).toBe('Showing 50 events');
+ });
+});
diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js
index 85b73f1d4e2..13840b42bd6 100644
--- a/spec/javascripts/environments/environment_actions_spec.js
+++ b/spec/javascripts/environments/environment_actions_spec.js
@@ -32,7 +32,12 @@ describe('Actions Component', () => {
}).$mount();
});
- it('should render a dropdown with the provided actions', () => {
+ it('should render a dropdown button with icon and title attribute', () => {
+ expect(component.$el.querySelector('.fa-caret-down')).toBeDefined();
+ expect(component.$el.querySelector('.dropdown-new').getAttribute('title')).toEqual('Deploy to...');
+ });
+
+ it('should render a dropdown with the provided list of actions', () => {
expect(
component.$el.querySelectorAll('.dropdown-menu li').length,
).toEqual(actionsMock.length);
diff --git a/spec/javascripts/environments/environment_monitoring_spec.js b/spec/javascripts/environments/environment_monitoring_spec.js
new file mode 100644
index 00000000000..fc451cce641
--- /dev/null
+++ b/spec/javascripts/environments/environment_monitoring_spec.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import monitoringComp from '~/environments/components/environment_monitoring';
+
+describe('Monitoring Component', () => {
+ let MonitoringComponent;
+
+ beforeEach(() => {
+ MonitoringComponent = Vue.extend(monitoringComp);
+ });
+
+ it('should render a link to environment monitoring page', () => {
+ const monitoringUrl = 'https://gitlab.com';
+ const component = new MonitoringComponent({
+ propsData: {
+ monitoringUrl,
+ },
+ }).$mount();
+
+ expect(component.$el.getAttribute('href')).toEqual(monitoringUrl);
+ expect(component.$el.querySelector('.fa-area-chart')).toBeDefined();
+ expect(component.$el.getAttribute('title')).toEqual('Monitoring');
+ });
+});
diff --git a/spec/javascripts/environments/environment_spec.js b/spec/javascripts/environments/environment_spec.js
index 9601575577e..4431baa4b96 100644
--- a/spec/javascripts/environments/environment_spec.js
+++ b/spec/javascripts/environments/environment_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import '~/flash';
import EnvironmentsComponent from '~/environments/components/environment';
-import { environment } from './mock_data';
+import { environment, folder } from './mock_data';
describe('Environment', () => {
preloadFixtures('static/environments/environments.html.raw');
@@ -91,6 +91,10 @@ describe('Environment', () => {
});
describe('pagination', () => {
+ afterEach(() => {
+ window.history.pushState({}, null, '');
+ });
+
it('should render pagination', (done) => {
setTimeout(() => {
expect(
@@ -175,4 +179,101 @@ describe('Environment', () => {
}, 0);
});
});
+
+ describe('expandable folders', () => {
+ const environmentsResponseInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify({
+ environments: [folder],
+ stopped_count: 0,
+ available_count: 1,
+ }), {
+ status: 200,
+ headers: {
+ 'X-nExt-pAge': '2',
+ 'x-page': '1',
+ 'X-Per-Page': '1',
+ 'X-Prev-Page': '',
+ 'X-TOTAL': '37',
+ 'X-Total-Pages': '2',
+ },
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(environmentsResponseInterceptor);
+ component = new EnvironmentsComponent({
+ el: document.querySelector('#environments-list-view'),
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, environmentsResponseInterceptor,
+ );
+ });
+
+ it('should open a closed folder', (done) => {
+ setTimeout(() => {
+ component.$el.querySelector('.folder-name').click();
+
+ Vue.nextTick(() => {
+ expect(
+ component.$el.querySelector('.folder-icon i.fa-caret-right').getAttribute('style'),
+ ).toContain('display: none');
+ expect(
+ component.$el.querySelector('.folder-icon i.fa-caret-down').getAttribute('style'),
+ ).not.toContain('display: none');
+ done();
+ });
+ });
+ });
+
+ it('should close an opened folder', (done) => {
+ setTimeout(() => {
+ // open folder
+ component.$el.querySelector('.folder-name').click();
+
+ Vue.nextTick(() => {
+ // close folder
+ component.$el.querySelector('.folder-name').click();
+
+ Vue.nextTick(() => {
+ expect(
+ component.$el.querySelector('.folder-icon i.fa-caret-down').getAttribute('style'),
+ ).toContain('display: none');
+ expect(
+ component.$el.querySelector('.folder-icon i.fa-caret-right').getAttribute('style'),
+ ).not.toContain('display: none');
+ done();
+ });
+ });
+ });
+ });
+
+ it('should show children environments and a button to show all environments', (done) => {
+ setTimeout(() => {
+ // open folder
+ component.$el.querySelector('.folder-name').click();
+
+ Vue.nextTick(() => {
+ const folderInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify({
+ environments: [environment],
+ }), { status: 200 }));
+ };
+
+ Vue.http.interceptors.push(folderInterceptor);
+
+ // wait for next async request
+ setTimeout(() => {
+ expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1);
+ expect(component.$el.querySelector('td.text-center > a.btn').textContent).toContain('Show all');
+
+ Vue.http.interceptors = _.without(Vue.http.interceptors, folderInterceptor);
+ done();
+ });
+ });
+ });
+ });
+ });
});
diff --git a/spec/javascripts/environments/environment_stop_spec.js b/spec/javascripts/environments/environment_stop_spec.js
index 8f79b88f3df..01055e3f255 100644
--- a/spec/javascripts/environments/environment_stop_spec.js
+++ b/spec/javascripts/environments/environment_stop_spec.js
@@ -24,7 +24,7 @@ describe('Stop Component', () => {
it('should render a button to stop the environment', () => {
expect(component.$el.tagName).toEqual('BUTTON');
- expect(component.$el.getAttribute('title')).toEqual('Stop Environment');
+ expect(component.$el.getAttribute('title')).toEqual('Stop');
});
it('should call the service when an action is clicked', () => {
diff --git a/spec/javascripts/environments/environment_terminal_button_spec.js b/spec/javascripts/environments/environment_terminal_button_spec.js
index b07aa4e1745..be2289edc2b 100644
--- a/spec/javascripts/environments/environment_terminal_button_spec.js
+++ b/spec/javascripts/environments/environment_terminal_button_spec.js
@@ -18,7 +18,7 @@ describe('Stop Component', () => {
it('should render a link to open a web terminal with the provided path', () => {
expect(component.$el.tagName).toEqual('A');
- expect(component.$el.getAttribute('title')).toEqual('Open web terminal');
+ expect(component.$el.getAttribute('title')).toEqual('Terminal');
expect(component.$el.getAttribute('href')).toEqual(terminalPath);
});
});
diff --git a/spec/javascripts/environments/environments_store_spec.js b/spec/javascripts/environments/environments_store_spec.js
index 115d84b50f5..f617c4bdffe 100644
--- a/spec/javascripts/environments/environments_store_spec.js
+++ b/spec/javascripts/environments/environments_store_spec.js
@@ -1,38 +1,106 @@
import Store from '~/environments/stores/environments_store';
import { environmentsList, serverData } from './mock_data';
-(() => {
- describe('Store', () => {
- let store;
+describe('Store', () => {
+ let store;
- beforeEach(() => {
- store = new Store();
- });
+ beforeEach(() => {
+ store = new Store();
+ });
- it('should start with a blank state', () => {
- expect(store.state.environments.length).toEqual(0);
- expect(store.state.stoppedCounter).toEqual(0);
- expect(store.state.availableCounter).toEqual(0);
- expect(store.state.paginationInformation).toEqual({});
- });
+ it('should start with a blank state', () => {
+ expect(store.state.environments.length).toEqual(0);
+ expect(store.state.stoppedCounter).toEqual(0);
+ expect(store.state.availableCounter).toEqual(0);
+ expect(store.state.paginationInformation).toEqual({});
+ });
+ it('should store environments', () => {
+ store.storeEnvironments(serverData);
+ expect(store.state.environments.length).toEqual(serverData.length);
+ expect(store.state.environments[0]).toEqual(environmentsList[0]);
+ });
+
+ it('should store available count', () => {
+ store.storeAvailableCount(2);
+ expect(store.state.availableCounter).toEqual(2);
+ });
+
+ it('should store stopped count', () => {
+ store.storeStoppedCount(2);
+ expect(store.state.stoppedCounter).toEqual(2);
+ });
+
+ describe('store environments', () => {
it('should store environments', () => {
store.storeEnvironments(serverData);
expect(store.state.environments.length).toEqual(serverData.length);
- expect(store.state.environments[0]).toEqual(environmentsList[0]);
});
- it('should store available count', () => {
- store.storeAvailableCount(2);
- expect(store.state.availableCounter).toEqual(2);
+ it('should add folder keys when environment is a folder', () => {
+ const environment = {
+ name: 'bar',
+ size: 3,
+ id: 2,
+ };
+
+ store.storeEnvironments([environment]);
+ expect(store.state.environments[0].isFolder).toEqual(true);
+ expect(store.state.environments[0].folderName).toEqual('bar');
+ });
+
+ it('should extract content of `latest` key when provided', () => {
+ const environment = {
+ name: 'bar',
+ size: 3,
+ id: 2,
+ latest: {
+ last_deployment: {},
+ isStoppable: true,
+ },
+ };
+
+ store.storeEnvironments([environment]);
+ expect(store.state.environments[0].last_deployment).toEqual({});
+ expect(store.state.environments[0].isStoppable).toEqual(true);
});
- it('should store stopped count', () => {
- store.storeStoppedCount(2);
- expect(store.state.stoppedCounter).toEqual(2);
+ it('should store latest.name when the environment is not a folder', () => {
+ store.storeEnvironments(serverData);
+ expect(store.state.environments[0].name).toEqual(serverData[0].latest.name);
});
- it('should store pagination information', () => {
+ it('should store root level name when environment is a folder', () => {
+ store.storeEnvironments(serverData);
+ expect(store.state.environments[1].folderName).toEqual(serverData[1].name);
+ });
+ });
+
+ describe('toggleFolder', () => {
+ it('should toggle folder', () => {
+ store.storeEnvironments(serverData);
+
+ store.toggleFolder(store.state.environments[1]);
+ expect(store.state.environments[1].isOpen).toEqual(true);
+
+ store.toggleFolder(store.state.environments[1]);
+ expect(store.state.environments[1].isOpen).toEqual(false);
+ });
+ });
+
+ describe('setfolderContent', () => {
+ it('should store folder content', () => {
+ store.storeEnvironments(serverData);
+
+ store.setfolderContent(store.state.environments[1], serverData);
+
+ expect(store.state.environments[1].children.length).toEqual(serverData.length);
+ expect(store.state.environments[1].children[0].isChildren).toEqual(true);
+ });
+ });
+
+ describe('store pagination', () => {
+ it('should store normalized and integer pagination information', () => {
const pagination = {
'X-nExt-pAge': '2',
'X-page': '1',
@@ -55,4 +123,4 @@ import { environmentsList, serverData } from './mock_data';
expect(store.state.paginationInformation).toEqual(expectedResult);
});
});
-})();
+});
diff --git a/spec/javascripts/environments/mock_data.js b/spec/javascripts/environments/mock_data.js
index 30861481cc5..15e11aa686b 100644
--- a/spec/javascripts/environments/mock_data.js
+++ b/spec/javascripts/environments/mock_data.js
@@ -84,3 +84,19 @@ export const environment = {
updated_at: '2017-01-31T10:53:46.894Z',
},
};
+
+export const folder = {
+ folderName: 'build',
+ size: 5,
+ id: 12,
+ name: 'build/update-README',
+ state: 'available',
+ external_url: null,
+ environment_type: 'build',
+ last_deployment: null,
+ 'stop_action?': false,
+ environment_path: '/root/review-app/environments/12',
+ stop_path: '/root/review-app/environments/12/stop',
+ created_at: '2017-02-01T19:42:18.400Z',
+ updated_at: '2017-02-01T19:42:18.400Z',
+};
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
index 848c7656a8d..5f7c05e9014 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -92,6 +92,20 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
manager.search();
});
+
+ it('removes duplicated tokens', (done) => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ `);
+
+ spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
+ expect(url).toEqual(`${defaultParams}&label_name[]=bug`);
+ done();
+ });
+
+ manager.search();
+ });
});
describe('handleInputPlaceholder', () => {
diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js
index a91801cfc89..cabbc694ec4 100644
--- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js
@@ -122,6 +122,14 @@ require('~/filtered_search/filtered_search_tokenizer');
expect(results.lastToken).toBe('std::includes');
expect(results.searchToken).toBe('std::includes');
});
+
+ it('removes duplicated values', () => {
+ const results = gl.FilteredSearchTokenizer.processTokens('label:~foo label:~foo');
+ expect(results.tokens.length).toBe(1);
+ expect(results.tokens[0].key).toBe('label');
+ expect(results.tokens[0].value).toBe('foo');
+ expect(results.tokens[0].symbol).toBe('~');
+ });
});
});
})();
diff --git a/spec/javascripts/fixtures/dashboard.rb b/spec/javascripts/fixtures/dashboard.rb
new file mode 100644
index 00000000000..e83db8daaf2
--- /dev/null
+++ b/spec/javascripts/fixtures/dashboard.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Dashboard::ProjectsController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, namespace: namespace, path: 'builds-project') }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('dashboard/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'dashboard/user-callout.html.raw' do |example|
+ rendered = render_template('shared/_user_callout')
+ store_frontend_fixture(rendered, example.description)
+ end
+
+ private
+
+ def render_template(template_file_name)
+ controller.prepend_view_path(JavaScriptFixturesHelpers::FIXTURE_PATH)
+ controller.render_to_string(template_file_name, layout: false)
+ end
+end
diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb
index ee893b76c84..fddeaaf504d 100644
--- a/spec/javascripts/fixtures/merge_requests.rb
+++ b/spec/javascripts/fixtures/merge_requests.rb
@@ -6,6 +6,15 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
let(:admin) { create(:admin) }
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, namespace: namespace, path: 'merge-requests-project') }
+ let(:merge_request) { create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') }
+ let(:pipeline) do
+ create(
+ :ci_pipeline,
+ project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha
+ )
+ end
render_views
@@ -18,7 +27,8 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
end
it 'merge_requests/merge_request_with_task_list.html.raw' do |example|
- merge_request = create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item')
+ create(:ci_build, :pending, pipeline: pipeline)
+
render_merge_request(example.description, merge_request)
end
diff --git a/spec/javascripts/fixtures/notebook_viewer.html.haml b/spec/javascripts/fixtures/notebook_viewer.html.haml
new file mode 100644
index 00000000000..17a7a9d8f31
--- /dev/null
+++ b/spec/javascripts/fixtures/notebook_viewer.html.haml
@@ -0,0 +1 @@
+.file-content#js-notebook-viewer{ data: { endpoint: '/test' } }
diff --git a/spec/javascripts/fixtures/user_callout.html.haml b/spec/javascripts/fixtures/user_callout.html.haml
deleted file mode 100644
index 275359bde0a..00000000000
--- a/spec/javascripts/fixtures/user_callout.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-.user-callout{ 'callout-svg' => custom_icon('icon_customization') }
-
diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js
index 46a27b8c98f..b5dde5525e5 100644
--- a/spec/javascripts/header_spec.js
+++ b/spec/javascripts/header_spec.js
@@ -5,7 +5,7 @@ require('~/lib/utils/text_utility');
(function() {
describe('Header', function() {
- var todosPendingCount = '.todos-pending-count';
+ var todosPendingCount = '.todos-count';
var fixtureTemplate = 'issues/open-issue.html.raw';
function isTodosCountHidden() {
@@ -21,31 +21,31 @@ require('~/lib/utils/text_utility');
loadFixtures(fixtureTemplate);
});
- it('should update todos-pending-count after receiving the todo:toggle event', function() {
+ it('should update todos-count after receiving the todo:toggle event', function() {
triggerToggle(5);
expect($(todosPendingCount).text()).toEqual('5');
});
- it('should hide todos-pending-count when it is 0', function() {
+ it('should hide todos-count when it is 0', function() {
triggerToggle(0);
expect(isTodosCountHidden()).toEqual(true);
});
- it('should show todos-pending-count when it is more than 0', function() {
+ it('should show todos-count when it is more than 0', function() {
triggerToggle(10);
expect(isTodosCountHidden()).toEqual(false);
});
- describe('when todos-pending-count is 1000', function() {
+ describe('when todos-count is 1000', function() {
beforeEach(function() {
triggerToggle(1000);
});
- it('should show todos-pending-count', function() {
+ it('should show todos-count', function() {
expect(isTodosCountHidden()).toEqual(false);
});
- it('should show 99+ for todos-pending-count', function() {
+ it('should show 99+ for todos-count', function() {
expect($(todosPendingCount).text()).toEqual('99+');
});
});
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index d2e24eb7eb2..5a93d479c1f 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -46,6 +46,10 @@ require('~/lib/utils/common_utils');
spyOn(window.document, 'getElementById').and.callThrough();
});
+ afterEach(() => {
+ window.history.pushState({}, null, '');
+ });
+
function expectGetElementIdToHaveBeenCalledWith(elementId) {
expect(window.document.getElementById).toHaveBeenCalledWith(elementId);
}
@@ -75,11 +79,56 @@ require('~/lib/utils/common_utils');
});
});
+ describe('gl.utils.setParamInURL', () => {
+ afterEach(() => {
+ window.history.pushState({}, null, '');
+ });
+
+ it('should return the parameter', () => {
+ window.history.replaceState({}, null, '');
+
+ expect(gl.utils.setParamInURL('page', 156)).toBe('?page=156');
+ expect(gl.utils.setParamInURL('page', '156')).toBe('?page=156');
+ });
+
+ it('should update the existing parameter when its a number', () => {
+ window.history.pushState({}, null, '?page=15');
+
+ expect(gl.utils.setParamInURL('page', 16)).toBe('?page=16');
+ expect(gl.utils.setParamInURL('page', '16')).toBe('?page=16');
+ expect(gl.utils.setParamInURL('page', true)).toBe('?page=true');
+ });
+
+ it('should update the existing parameter when its a string', () => {
+ window.history.pushState({}, null, '?scope=all');
+
+ expect(gl.utils.setParamInURL('scope', 'finished')).toBe('?scope=finished');
+ });
+
+ it('should update the existing parameter when more than one parameter exists', () => {
+ window.history.pushState({}, null, '?scope=all&page=15');
+
+ expect(gl.utils.setParamInURL('scope', 'finished')).toBe('?scope=finished&page=15');
+ });
+
+ it('should add a new parameter to the end of the existing ones', () => {
+ window.history.pushState({}, null, '?scope=all');
+
+ expect(gl.utils.setParamInURL('page', 16)).toBe('?scope=all&page=16');
+ expect(gl.utils.setParamInURL('page', '16')).toBe('?scope=all&page=16');
+ expect(gl.utils.setParamInURL('page', true)).toBe('?scope=all&page=true');
+ });
+ });
+
describe('gl.utils.getParameterByName', () => {
beforeEach(() => {
window.history.pushState({}, null, '?scope=all&p=2');
});
+ afterEach(() => {
+ window.history.replaceState({}, null, null);
+ });
+
it('should return valid parameter', () => {
const value = gl.utils.getParameterByName('scope');
expect(value).toBe('all');
@@ -108,6 +157,37 @@ require('~/lib/utils/common_utils');
});
});
+ describe('gl.utils.normalizeCRLFHeaders', () => {
+ beforeEach(function () {
+ this.CLRFHeaders = 'a-header: a-value\nAnother-Header: ANOTHER-VALUE\nLaSt-HeAdEr: last-VALUE';
+
+ spyOn(String.prototype, 'split').and.callThrough();
+ spyOn(gl.utils, 'normalizeHeaders').and.callThrough();
+
+ this.normalizeCRLFHeaders = gl.utils.normalizeCRLFHeaders(this.CLRFHeaders);
+ });
+
+ it('should split by newline', function () {
+ expect(String.prototype.split).toHaveBeenCalledWith('\n');
+ });
+
+ it('should split by colon+space for each header', function () {
+ expect(String.prototype.split.calls.allArgs().filter(args => args[0] === ': ').length).toBe(3);
+ });
+
+ it('should call gl.utils.normalizeHeaders with a parsed headers object', function () {
+ expect(gl.utils.normalizeHeaders).toHaveBeenCalledWith(jasmine.any(Object));
+ });
+
+ it('should return a normalized headers object', function () {
+ expect(this.normalizeCRLFHeaders).toEqual({
+ 'A-HEADER': 'a-value',
+ 'ANOTHER-HEADER': 'ANOTHER-VALUE',
+ 'LAST-HEADER': 'last-VALUE',
+ });
+ });
+ });
+
describe('gl.utils.parseIntPagination', () => {
it('should parse to integers all string values and return pagination object', () => {
const pagination = {
diff --git a/spec/javascripts/lib/utils/number_utility_spec.js b/spec/javascripts/lib/utils/number_utility_spec.js
new file mode 100644
index 00000000000..5fde8be9123
--- /dev/null
+++ b/spec/javascripts/lib/utils/number_utility_spec.js
@@ -0,0 +1,41 @@
+import { formatRelevantDigits } from '~/lib/utils/number_utils';
+
+describe('Number Utils', () => {
+ describe('formatRelevantDigits', () => {
+ it('returns an empty string when the number is NaN', () => {
+ expect(formatRelevantDigits('fail')).toBe('');
+ });
+
+ it('returns 4 decimals when there is 4 plus digits to the left', () => {
+ const formattedNumber = formatRelevantDigits('1000.1234567');
+ const rightFromDecimal = formattedNumber.split('.')[1];
+ const leftFromDecimal = formattedNumber.split('.')[0];
+ expect(rightFromDecimal.length).toBe(4);
+ expect(leftFromDecimal.length).toBe(4);
+ });
+
+ it('returns 3 decimals when there is 1 digit to the left', () => {
+ const formattedNumber = formatRelevantDigits('0.1234567');
+ const rightFromDecimal = formattedNumber.split('.')[1];
+ const leftFromDecimal = formattedNumber.split('.')[0];
+ expect(rightFromDecimal.length).toBe(3);
+ expect(leftFromDecimal.length).toBe(1);
+ });
+
+ it('returns 2 decimals when there is 2 digits to the left', () => {
+ const formattedNumber = formatRelevantDigits('10.1234567');
+ const rightFromDecimal = formattedNumber.split('.')[1];
+ const leftFromDecimal = formattedNumber.split('.')[0];
+ expect(rightFromDecimal.length).toBe(2);
+ expect(leftFromDecimal.length).toBe(2);
+ });
+
+ it('returns 1 decimal when there is 3 digits to the left', () => {
+ const formattedNumber = formatRelevantDigits('100.1234567');
+ const rightFromDecimal = formattedNumber.split('.')[1];
+ const leftFromDecimal = formattedNumber.split('.')[0];
+ expect(rightFromDecimal.length).toBe(1);
+ expect(leftFromDecimal.length).toBe(3);
+ });
+ });
+});
diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js
index c794a632417..e3429c2a1cb 100644
--- a/spec/javascripts/lib/utils/poll_spec.js
+++ b/spec/javascripts/lib/utils/poll_spec.js
@@ -160,4 +160,44 @@ describe('Poll', () => {
Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
});
});
+
+ describe('restart', () => {
+ it('should restart polling when its called', (done) => {
+ const pollInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': 2 } }));
+ };
+
+ Vue.http.interceptors.push(pollInterceptor);
+
+ const service = new ServiceMock('endpoint');
+
+ spyOn(service, 'fetch').and.callThrough();
+
+ const Polling = new Poll({
+ resource: service,
+ method: 'fetch',
+ data: { page: 1 },
+ successCallback: () => {
+ Polling.stop();
+ setTimeout(() => {
+ Polling.restart();
+ }, 0);
+ },
+ errorCallback: callbacks.error,
+ });
+
+ spyOn(Polling, 'stop').and.callThrough();
+
+ Polling.makeRequest();
+
+ setTimeout(() => {
+ expect(service.fetch.calls.count()).toEqual(2);
+ expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
+ expect(Polling.stop).toHaveBeenCalled();
+ done();
+ }, 10);
+
+ Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
+ });
+ });
});
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 7506e6ab49e..7b9632be84e 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -38,6 +38,10 @@ require('vendor/jquery.scrollTo');
}
});
+ afterEach(function () {
+ this.class.destroy();
+ });
+
describe('#activateTab', function () {
beforeEach(function () {
spyOn($, 'ajax').and.callFake(function () {});
@@ -200,6 +204,42 @@ require('vendor/jquery.scrollTo');
expect(this.subject('show')).toBe('/foo/bar/merge_requests/1');
});
});
+
+ describe('#tabShown', () => {
+ beforeEach(function () {
+ loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
+ });
+
+ describe('with "Side-by-side"/parallel diff view', () => {
+ beforeEach(function () {
+ this.class.diffViewType = () => 'parallel';
+ });
+
+ it('maintains `container-limited` for pipelines tab', function (done) {
+ const asyncClick = function (selector) {
+ return new Promise((resolve) => {
+ setTimeout(() => {
+ document.querySelector(selector).click();
+ resolve();
+ });
+ });
+ };
+
+ asyncClick('.merge-request-tabs .pipelines-tab a')
+ .then(() => asyncClick('.merge-request-tabs .diffs-tab a'))
+ .then(() => asyncClick('.merge-request-tabs .pipelines-tab a'))
+ .then(() => {
+ const hasContainerLimitedClass = document.querySelector('.content-wrapper .container-fluid').classList.contains('container-limited');
+ expect(hasContainerLimitedClass).toBe(true);
+ })
+ .then(done)
+ .catch((err) => {
+ done.fail(`Something went wrong clicking MR tabs: ${err.message}\n${err.stack}`);
+ });
+ });
+ });
+ });
+
describe('#loadDiff', function () {
it('requires an absolute pathname', function () {
spyOn($, 'ajax').and.callFake(function (options) {
diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js
index a3c1c5e1b7c..c2bcd9c0f7c 100644
--- a/spec/javascripts/monitoring/prometheus_graph_spec.js
+++ b/spec/javascripts/monitoring/prometheus_graph_spec.js
@@ -37,9 +37,11 @@ describe('PrometheusGraph', () => {
it('transforms the data', () => {
this.prometheusGraph.init(prometheusMockData.metrics);
- expect(this.prometheusGraph.data).toBeDefined();
- expect(this.prometheusGraph.data.cpu_values.length).toBe(121);
- expect(this.prometheusGraph.data.memory_values.length).toBe(121);
+ Object.keys(this.prometheusGraph.graphSpecificProperties, (key) => {
+ const graphProps = this.prometheusGraph.graphSpecificProperties[key];
+ expect(graphProps.data).toBeDefined();
+ expect(graphProps.data.length).toBe(121);
+ });
});
it('creates two graphs', () => {
@@ -68,7 +70,7 @@ describe('PrometheusGraph', () => {
expect($prometheusGraphContents.find('.label-y-axis-line')).toBeDefined();
expect($prometheusGraphContents.find('.label-axis-text')).toBeDefined();
expect($prometheusGraphContents.find('.rect-axis-text')).toBeDefined();
- expect($axisLabelContainer.find('rect').length).toBe(2);
+ expect($axisLabelContainer.find('rect').length).toBe(3);
expect($axisLabelContainer.find('text').length).toBe(4);
});
});
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 285b7940174..f2072a6f350 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -74,9 +74,15 @@ import '~/right_sidebar';
var todoToggleSpy = spyOnEvent(document, 'todo:toggle');
- $('.js-issuable-todo').click();
+ $('.issuable-sidebar-header .js-issuable-todo').click();
expect(todoToggleSpy.calls.count()).toEqual(1);
});
+
+ it('should not hide collapsed icons', () => {
+ [].forEach.call(document.querySelectorAll('.sidebar-collapsed-icon'), (el) => {
+ expect(el.querySelector('.fa, svg').classList.contains('hidden')).toBeFalsy();
+ });
+ });
});
}).call(window);
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index d658f680f97..b30c5da8822 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -37,14 +37,33 @@ if (process.env.BABEL_ENV === 'coverage') {
const troubleMakers = [
'./blob_edit/blob_bundle.js',
'./boards/boards_bundle.js',
+ './cycle_analytics/cycle_analytics_bundle.js',
'./cycle_analytics/components/stage_plan_component.js',
'./cycle_analytics/components/stage_staging_component.js',
'./cycle_analytics/components/stage_test_component.js',
+ './commit/pipelines/pipelines_bundle.js',
+ './diff_notes/diff_notes_bundle.js',
'./diff_notes/components/jump_to_discussion.js',
'./diff_notes/components/resolve_count.js',
+ './dispatcher.js',
+ './environments/environments_bundle.js',
+ './filtered_search/filtered_search_bundle.js',
+ './graphs/graphs_bundle.js',
+ './issuable/issuable_bundle.js',
+ './issuable/time_tracking/time_tracking_bundle.js',
+ './main.js',
+ './merge_conflicts/merge_conflicts_bundle.js',
'./merge_conflicts/components/inline_conflict_lines.js',
'./merge_conflicts/components/parallel_conflict_lines.js',
+ './merge_request_widget/ci_bundle.js',
+ './monitoring/monitoring_bundle.js',
+ './network/network_bundle.js',
'./network/branch_graph.js',
+ './profile/profile_bundle.js',
+ './protected_branches/protected_branches_bundle.js',
+ './snippet/snippet_bundle.js',
+ './terminal/terminal_bundle.js',
+ './users/users_bundle.js',
];
describe('Uncovered files', function () {
diff --git a/spec/javascripts/user_callout_spec.js b/spec/javascripts/user_callout_spec.js
index 2398149d3ad..c0375ebc61c 100644
--- a/spec/javascripts/user_callout_spec.js
+++ b/spec/javascripts/user_callout_spec.js
@@ -4,7 +4,7 @@ import UserCallout from '~/user_callout';
const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
describe('UserCallout', function () {
- const fixtureName = 'static/user_callout.html.raw';
+ const fixtureName = 'dashboard/user-callout.html.raw';
preloadFixtures(fixtureName);
beforeEach(() => {
@@ -12,26 +12,22 @@ describe('UserCallout', function () {
Cookies.remove(USER_CALLOUT_COOKIE);
this.userCallout = new UserCallout();
- this.closeButton = $('.close-user-callout');
- this.userCalloutBtn = $('.user-callout-btn');
+ this.closeButton = $('.js-close-callout.close');
+ this.userCalloutBtn = $('.js-close-callout:not(.close)');
this.userCalloutContainer = $('.user-callout');
});
- it('does not show when cookie is set not defined', () => {
- expect(Cookies.get(USER_CALLOUT_COOKIE)).toBeUndefined();
- expect(this.userCalloutContainer.is(':visible')).toBe(true);
- });
-
- it('shows when cookie is set to false', () => {
- Cookies.set(USER_CALLOUT_COOKIE, 'false');
-
- expect(Cookies.get(USER_CALLOUT_COOKIE)).toBeDefined();
- expect(this.userCalloutContainer.is(':visible')).toBe(true);
- });
-
- it('hides when user clicks on the dismiss-icon', () => {
+ it('hides when user clicks on the dismiss-icon', (done) => {
this.closeButton.click();
expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true');
+
+ setTimeout(() => {
+ expect(
+ document.querySelector('.user-callout'),
+ ).toBeNull();
+
+ done();
+ });
});
it('hides when user clicks on the "check it out" button', () => {
@@ -39,19 +35,3 @@ describe('UserCallout', function () {
expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true');
});
});
-
-describe('UserCallout when cookie is present', function () {
- const fixtureName = 'static/user_callout.html.raw';
- preloadFixtures(fixtureName);
-
- beforeEach(() => {
- loadFixtures(fixtureName);
- Cookies.set(USER_CALLOUT_COOKIE, 'true');
- this.userCallout = new UserCallout();
- this.userCalloutContainer = $('.user-callout');
- });
-
- it('removes the DOM element', () => {
- expect(this.userCalloutContainer.length).toBe(0);
- });
-});
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_spec.js
index b0b1df5a753..4d3ced944d7 100644
--- a/spec/javascripts/vue_shared/components/pipelines_table_spec.js
+++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js
@@ -21,6 +21,10 @@ describe('Pipelines Table', () => {
}).$mount();
});
+ afterEach(() => {
+ component.$destroy();
+ });
+
it('should render a table', () => {
expect(component.$el).toEqual('TABLE');
});
diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/javascripts/vue_shared/components/table_pagination_spec.js
index a5c3870b3ac..96038718191 100644
--- a/spec/javascripts/vue_shared/components/table_pagination_spec.js
+++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js
@@ -83,7 +83,7 @@ describe('Pagination component', () => {
},
}).$mount();
- component.changePage({ target: { innerText: 'Last >>' } });
+ component.changePage({ target: { innerText: 'Last »' } });
expect(changeChanges.one).toEqual(10);
});
@@ -100,7 +100,7 @@ describe('Pagination component', () => {
},
}).$mount();
- component.changePage({ target: { innerText: '<< First' } });
+ component.changePage({ target: { innerText: '« First' } });
expect(changeChanges.one).toEqual(1);
});
@@ -124,6 +124,10 @@ describe('Pagination component', () => {
});
describe('paramHelper', () => {
+ afterEach(() => {
+ window.history.pushState({}, null, '');
+ });
+
it('can parse url parameters correctly', () => {
window.history.pushState({}, null, '?scope=all&p=2');
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index 11607d4fb26..f1082495fcc 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -21,6 +21,19 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
end
end
+ describe 'performance' do
+ let(:another_issue) { create(:issue, project: project) }
+
+ it 'does not have a N+1 query problem' do
+ single_reference = "Issue #{issue.to_reference}"
+ multiple_references = "Issues #{issue.to_reference} and #{another_issue.to_reference}"
+
+ control_count = ActiveRecord::QueryRecorder.new { reference_filter(single_reference).to_html }.count
+
+ expect { reference_filter(multiple_references).to_html }.not_to exceed_query_limit(control_count)
+ end
+ end
+
context 'internal reference' do
it_behaves_like 'a reference containing an element node'
diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
index 3d3d36061f4..40232f6e426 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -17,6 +17,19 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
end
end
+ describe 'performance' do
+ let(:another_merge) { create(:merge_request, source_project: project, source_branch: 'fix') }
+
+ it 'does not have a N+1 query problem' do
+ single_reference = "Merge request #{merge.to_reference}"
+ multiple_references = "Merge requests #{merge.to_reference} and #{another_merge.to_reference}"
+
+ control_count = ActiveRecord::QueryRecorder.new { reference_filter(single_reference).to_html }.count
+
+ expect { reference_filter(multiple_references).to_html }.not_to exceed_query_limit(control_count)
+ end
+ end
+
context 'internal reference' do
let(:reference) { merge.to_reference }
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index 9873774909e..63b23dac7ed 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -83,6 +83,14 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
expect(doc.css('a').length).to eq 1
end
+ it 'links to a User with different case-sensitivity' do
+ user = create(:user, username: 'RescueRanger')
+
+ doc = reference_filter("Hey #{user.to_reference.upcase}")
+ expect(doc.css('a').length).to eq 1
+ expect(doc.css('a').text).to eq(user.to_reference)
+ end
+
it 'includes a data-user attribute' do
doc = reference_filter("Hey #{reference}")
link = doc.css('a').first
diff --git a/spec/lib/gitlab/backend/shell_spec.rb b/spec/lib/gitlab/backend/shell_spec.rb
index 4b08a02ec73..6675d26734e 100644
--- a/spec/lib/gitlab/backend/shell_spec.rb
+++ b/spec/lib/gitlab/backend/shell_spec.rb
@@ -69,6 +69,15 @@ describe Gitlab::Shell, lib: true do
expect(io).to have_received(:puts).with("key-42\tssh-rsa foo")
end
+ it 'handles multiple spaces in the key' do
+ io = spy(:io)
+ adder = described_class.new(io)
+
+ adder.add_key('key-42', "ssh-rsa foo")
+
+ expect(io).to have_received(:puts).with("key-42\tssh-rsa foo")
+ end
+
it 'raises an exception if the key contains a tab' do
expect do
described_class.new(StringIO.new).add_key('key-42', "ssh-rsa\tfoobar")
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index 8b3bd08cf13..e648a3ac3a2 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -27,6 +27,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'passed'
expect(status.icon).to eq 'icon_status_success'
+ expect(status.favicon).to eq 'favicon_status_success'
expect(status.label).to eq 'passed'
expect(status).to have_details
expect(status).to have_action
@@ -53,6 +54,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'failed'
expect(status.icon).to eq 'icon_status_failed'
+ expect(status.favicon).to eq 'favicon_status_failed'
expect(status.label).to eq 'failed'
expect(status).to have_details
expect(status).to have_action
@@ -79,6 +81,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'failed'
expect(status.icon).to eq 'icon_status_warning'
+ expect(status.favicon).to eq 'favicon_status_failed'
expect(status.label).to eq 'failed (allowed to fail)'
expect(status).to have_details
expect(status).to have_action
@@ -107,6 +110,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'canceled'
expect(status.icon).to eq 'icon_status_canceled'
+ expect(status.favicon).to eq 'favicon_status_canceled'
expect(status.label).to eq 'canceled'
expect(status).to have_details
expect(status).to have_action
@@ -132,6 +136,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'running'
expect(status.icon).to eq 'icon_status_running'
+ expect(status.favicon).to eq 'favicon_status_running'
expect(status.label).to eq 'running'
expect(status).to have_details
expect(status).to have_action
@@ -157,6 +162,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'pending'
expect(status.icon).to eq 'icon_status_pending'
+ expect(status.favicon).to eq 'favicon_status_pending'
expect(status.label).to eq 'pending'
expect(status).to have_details
expect(status).to have_action
@@ -181,6 +187,7 @@ describe Gitlab::Ci::Status::Build::Factory do
it 'fabricates status with correct details' do
expect(status.text).to eq 'skipped'
expect(status.icon).to eq 'icon_status_skipped'
+ expect(status.favicon).to eq 'favicon_status_skipped'
expect(status.label).to eq 'skipped'
expect(status).to have_details
expect(status).not_to have_action
@@ -208,6 +215,7 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(status.text).to eq 'manual'
expect(status.group).to eq 'manual'
expect(status.icon).to eq 'icon_status_manual'
+ expect(status.favicon).to eq 'favicon_status_manual'
expect(status.label).to eq 'manual play action'
expect(status).to have_details
expect(status).to have_action
@@ -235,6 +243,7 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(status.text).to eq 'manual'
expect(status.group).to eq 'manual'
expect(status.icon).to eq 'icon_status_manual'
+ expect(status.favicon).to eq 'favicon_status_manual'
expect(status.label).to eq 'manual stop action'
expect(status).to have_details
expect(status).to have_action
diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb
index 768f8926f1d..530639a5897 100644
--- a/spec/lib/gitlab/ci/status/canceled_spec.rb
+++ b/spec/lib/gitlab/ci/status/canceled_spec.rb
@@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Canceled do
it { expect(subject.icon).to eq 'icon_status_canceled' }
end
+ describe '#favicon' do
+ it { expect(subject.favicon).to eq 'favicon_status_canceled' }
+ end
+
describe '#group' do
it { expect(subject.group).to eq 'canceled' }
end
diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb
index e96c13aede3..aef982e17f1 100644
--- a/spec/lib/gitlab/ci/status/created_spec.rb
+++ b/spec/lib/gitlab/ci/status/created_spec.rb
@@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Created do
it { expect(subject.icon).to eq 'icon_status_created' }
end
+ describe '#favicon' do
+ it { expect(subject.favicon).to eq 'favicon_status_created' }
+ end
+
describe '#group' do
it { expect(subject.group).to eq 'created' }
end
diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb
index e5da0a91159..9a25743885c 100644
--- a/spec/lib/gitlab/ci/status/failed_spec.rb
+++ b/spec/lib/gitlab/ci/status/failed_spec.rb
@@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Failed do
it { expect(subject.icon).to eq 'icon_status_failed' }
end
+ describe '#favicon' do
+ it { expect(subject.favicon).to eq 'favicon_status_failed' }
+ end
+
describe '#group' do
it { expect(subject.group).to eq 'failed' }
end
diff --git a/spec/lib/gitlab/ci/status/manual_spec.rb b/spec/lib/gitlab/ci/status/manual_spec.rb
index 3fd3727b92d..6fdc3801d71 100644
--- a/spec/lib/gitlab/ci/status/manual_spec.rb
+++ b/spec/lib/gitlab/ci/status/manual_spec.rb
@@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Manual do
it { expect(subject.icon).to eq 'icon_status_manual' }
end
+ describe '#favicon' do
+ it { expect(subject.favicon).to eq 'favicon_status_manual' }
+ end
+
describe '#group' do
it { expect(subject.group).to eq 'manual' }
end
diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb
index 8d09cf2a05a..ffc53f0506b 100644
--- a/spec/lib/gitlab/ci/status/pending_spec.rb
+++ b/spec/lib/gitlab/ci/status/pending_spec.rb
@@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Pending do
it { expect(subject.icon).to eq 'icon_status_pending' }
end
+ describe '#favicon' do
+ it { expect(subject.favicon).to eq 'favicon_status_pending' }
+ end
+
describe '#group' do
it { expect(subject.group).to eq 'pending' }
end
diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb
index 10d3bf749c1..0babf1fb54e 100644
--- a/spec/lib/gitlab/ci/status/running_spec.rb
+++ b/spec/lib/gitlab/ci/status/running_spec.rb
@@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Running do
it { expect(subject.icon).to eq 'icon_status_running' }
end
+ describe '#favicon' do
+ it { expect(subject.favicon).to eq 'favicon_status_running' }
+ end
+
describe '#group' do
it { expect(subject.group).to eq 'running' }
end
diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb
index 10db93d3802..670747c9f0b 100644
--- a/spec/lib/gitlab/ci/status/skipped_spec.rb
+++ b/spec/lib/gitlab/ci/status/skipped_spec.rb
@@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Skipped do
it { expect(subject.icon).to eq 'icon_status_skipped' }
end
+ describe '#favicon' do
+ it { expect(subject.favicon).to eq 'favicon_status_skipped' }
+ end
+
describe '#group' do
it { expect(subject.group).to eq 'skipped' }
end
diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb
index 230f24b94a4..ff65b074808 100644
--- a/spec/lib/gitlab/ci/status/success_spec.rb
+++ b/spec/lib/gitlab/ci/status/success_spec.rb
@@ -17,6 +17,10 @@ describe Gitlab::Ci::Status::Success do
it { expect(subject.icon).to eq 'icon_status_success' }
end
+ describe '#favicon' do
+ it { expect(subject.favicon).to eq 'favicon_status_success' }
+ end
+
describe '#group' do
it { expect(subject.group).to eq 'success' }
end
diff --git a/spec/lib/gitlab/etag_caching/middleware_spec.rb b/spec/lib/gitlab/etag_caching/middleware_spec.rb
index 8b5bfc4dbb0..6ec4360adc2 100644
--- a/spec/lib/gitlab/etag_caching/middleware_spec.rb
+++ b/spec/lib/gitlab/etag_caching/middleware_spec.rb
@@ -99,6 +99,19 @@ describe Gitlab::EtagCaching::Middleware do
middleware.call(build_env(path, if_none_match))
end
+
+ context 'when polling is disabled' do
+ before do
+ allow(Gitlab::PollingInterval).to receive(:polling_enabled?).
+ and_return(false)
+ end
+
+ it 'returns status code 429' do
+ status, _, _ = middleware.call(build_env(path, if_none_match))
+
+ expect(status).to eq 429
+ end
+ end
end
context 'when If-None-Match header does not match ETag in store' do
diff --git a/spec/lib/gitlab/git/blob_snippet_spec.rb b/spec/lib/gitlab/git/blob_snippet_spec.rb
index 17d6be470ac..d6d365f6492 100644
--- a/spec/lib/gitlab/git/blob_snippet_spec.rb
+++ b/spec/lib/gitlab/git/blob_snippet_spec.rb
@@ -3,7 +3,7 @@
require "spec_helper"
describe Gitlab::Git::BlobSnippet, seed_helper: true do
- describe :data do
+ describe '#data' do
context 'empty lines' do
let(:snippet) { Gitlab::Git::BlobSnippet.new('master', nil, nil, nil) }
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 8049e2c120d..b883526151e 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -5,7 +5,7 @@ require "spec_helper"
describe Gitlab::Git::Blob, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
- describe :initialize do
+ describe 'initialize' do
let(:blob) { Gitlab::Git::Blob.new(name: 'test') }
it 'handles nil data' do
@@ -15,7 +15,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
- describe :find do
+ describe '.find' do
context 'file in subdir' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") }
@@ -101,7 +101,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
- describe :raw do
+ describe '.raw' do
let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) }
it { expect(raw_blob.id).to eq(SeedRepo::RubyBlob::ID) }
it { expect(raw_blob.data[0..10]).to eq("require \'fi") }
@@ -222,7 +222,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
- describe :lfs_pointers do
+ describe 'lfs_pointers' do
context 'file a valid lfs pointer' do
let(:blob) do
Gitlab::Git::Blob.find(
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index e1be6784c20..5cf4631fbfc 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -65,7 +65,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
context 'Class methods' do
- describe :find do
+ describe '.find' do
it "should return first head commit if without params" do
expect(Gitlab::Git::Commit.last(repository).id).to eq(
repository.raw.head.target.oid
@@ -103,7 +103,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe :last_for_path do
+ describe '.last_for_path' do
context 'no path' do
subject { Gitlab::Git::Commit.last_for_path(repository, 'master') }
@@ -132,7 +132,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe "where" do
+ describe '.where' do
context 'path is empty string' do
subject do
commits = Gitlab::Git::Commit.where(
@@ -230,7 +230,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe :between do
+ describe '.between' do
subject do
commits = Gitlab::Git::Commit.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID)
commits.map { |c| c.id }
@@ -243,7 +243,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
it { is_expected.not_to include(SeedRepo::FirstCommit::ID) }
end
- describe :find_all do
+ describe '.find_all' do
context 'max_count' do
subject do
commits = Gitlab::Git::Commit.find_all(
@@ -304,7 +304,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe :init_from_rugged do
+ describe '#init_from_rugged' do
let(:gitlab_commit) { Gitlab::Git::Commit.new(rugged_commit) }
subject { gitlab_commit }
@@ -314,7 +314,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe :init_from_hash do
+ describe '#init_from_hash' do
let(:commit) { Gitlab::Git::Commit.new(sample_commit_hash) }
subject { commit }
@@ -329,7 +329,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe :stats do
+ describe '#stats' do
subject { commit.stats }
describe '#additions' do
@@ -343,25 +343,25 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe :to_diff do
+ describe '#to_diff' do
subject { commit.to_diff }
it { is_expected.not_to include "From #{SeedRepo::Commit::ID}" }
it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'}
end
- describe :has_zero_stats? do
+ describe '#has_zero_stats?' do
it { expect(commit.has_zero_stats?).to eq(false) }
end
- describe :to_patch do
+ describe '#to_patch' do
subject { commit.to_patch }
it { is_expected.to include "From #{SeedRepo::Commit::ID}" }
it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'}
end
- describe :to_hash do
+ describe '#to_hash' do
let(:hash) { commit.to_hash }
subject { hash }
@@ -373,7 +373,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe :diffs do
+ describe '#diffs' do
subject { commit.diffs }
it { is_expected.to be_kind_of Gitlab::Git::DiffCollection }
@@ -381,7 +381,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
it { expect(subject.first).to be_kind_of Gitlab::Git::Diff }
end
- describe :ref_names do
+ describe '#ref_names' do
let(:commit) { Gitlab::Git::Commit.find(repository, 'master') }
subject { commit.ref_names(repository) }
diff --git a/spec/lib/gitlab/git/compare_spec.rb b/spec/lib/gitlab/git/compare_spec.rb
index f66b68e4218..e28debe1494 100644
--- a/spec/lib/gitlab/git/compare_spec.rb
+++ b/spec/lib/gitlab/git/compare_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Git::Compare, seed_helper: true do
let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, false) }
let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, true) }
- describe :commits do
+ describe '#commits' do
subject do
compare.commits.map(&:id)
end
@@ -42,7 +42,7 @@ describe Gitlab::Git::Compare, seed_helper: true do
end
end
- describe :diffs do
+ describe '#diffs' do
subject do
compare.diffs.map(&:new_path)
end
@@ -67,7 +67,7 @@ describe Gitlab::Git::Compare, seed_helper: true do
end
end
- describe :same do
+ describe '#same' do
subject do
compare.same
end
@@ -81,7 +81,7 @@ describe Gitlab::Git::Compare, seed_helper: true do
end
end
- describe :commits_straight do
+ describe '#commits', 'straight compare' do
subject do
compare_straight.commits.map(&:id)
end
@@ -94,7 +94,7 @@ describe Gitlab::Git::Compare, seed_helper: true do
it { is_expected.not_to include(SeedRepo::BigCommit::PARENT_ID) }
end
- describe :diffs_straight do
+ describe '#diffs', 'straight compare' do
subject do
compare_straight.diffs.map(&:new_path)
end
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
index 47bdd7310d5..122c93dcd69 100644
--- a/spec/lib/gitlab/git/diff_collection_spec.rb
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -24,7 +24,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
it { is_expected.to be_kind_of ::Array }
end
- describe :decorate! do
+ describe '#decorate!' do
let(:file_count) { 3 }
it 'modifies the array in place' do
@@ -302,7 +302,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
end
end
- describe :each do
+ describe '#each' do
context 'when diff are too large' do
let(:collection) do
Gitlab::Git::DiffCollection.new([{ diff: 'a' * 204800 }])
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 9ed43da1116..d4b7684adfd 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -771,8 +771,8 @@ describe Gitlab::Git::Repository, seed_helper: true do
commits = repository.log(options)
expect(commits.size).to be > 0
- satisfy do
- commits.all? { |commit| commit.created_at >= options[:after] }
+ expect(commits).to satisfy do |commits|
+ commits.all? { |commit| commit.time >= options[:after] }
end
end
end
@@ -784,8 +784,27 @@ describe Gitlab::Git::Repository, seed_helper: true do
commits = repository.log(options)
expect(commits.size).to be > 0
- satisfy do
- commits.all? { |commit| commit.created_at <= options[:before] }
+ expect(commits).to satisfy do |commits|
+ commits.all? { |commit| commit.time <= options[:before] }
+ end
+ end
+ end
+
+ context 'when multiple paths are provided' do
+ let(:options) { { ref: 'master', path: ['PROCESS.md', 'README.md'] } }
+
+ def commit_files(commit)
+ commit.diff(commit.parent_ids.first).deltas.flat_map do |delta|
+ [delta.old_file[:path], delta.new_file[:path]].uniq.compact
+ end
+ end
+
+ it 'only returns commits matching at least one path' do
+ commits = repository.log(options)
+
+ expect(commits.size).to be > 0
+ expect(commits).to satisfy do |commits|
+ commits.none? { |commit| (commit_files(commit) & options[:path]).empty? }
end
end
end
diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb
index 688e2a75373..82685712b5b 100644
--- a/spec/lib/gitlab/git/tree_spec.rb
+++ b/spec/lib/gitlab/git/tree_spec.rb
@@ -11,7 +11,7 @@ describe Gitlab::Git::Tree, seed_helper: true do
it { expect(tree.select(&:file?).size).to eq(10) }
it { expect(tree.select(&:submodule?).size).to eq(2) }
- describe :dir do
+ describe '#dir?' do
let(:dir) { tree.select(&:dir?).first }
it { expect(dir).to be_kind_of Gitlab::Git::Tree }
@@ -19,6 +19,7 @@ describe Gitlab::Git::Tree, seed_helper: true do
it { expect(dir.commit_id).to eq(SeedRepo::Commit::ID) }
it { expect(dir.name).to eq('encoding') }
it { expect(dir.path).to eq('encoding') }
+ it { expect(dir.mode).to eq('40000') }
context :subdir do
let(:subdir) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files').first }
@@ -41,7 +42,7 @@ describe Gitlab::Git::Tree, seed_helper: true do
end
end
- describe :file do
+ describe '#file?' do
let(:file) { tree.select(&:file?).first }
it { expect(file).to be_kind_of Gitlab::Git::Tree }
@@ -50,21 +51,21 @@ describe Gitlab::Git::Tree, seed_helper: true do
it { expect(file.name).to eq('.gitignore') }
end
- describe :readme do
+ describe '#readme?' do
let(:file) { tree.select(&:readme?).first }
it { expect(file).to be_kind_of Gitlab::Git::Tree }
it { expect(file.name).to eq('README.md') }
end
- describe :contributing do
+ describe '#contributing?' do
let(:file) { tree.select(&:contributing?).first }
it { expect(file).to be_kind_of Gitlab::Git::Tree }
it { expect(file.name).to eq('CONTRIBUTING.md') }
end
- describe :submodule do
+ describe '#submodule?' do
let(:submodule) { tree.select(&:submodule?).first }
it { expect(submodule).to be_kind_of Gitlab::Git::Tree }
diff --git a/spec/lib/gitlab/git/util_spec.rb b/spec/lib/gitlab/git/util_spec.rb
index 8d43b570e98..bcca4d4c746 100644
--- a/spec/lib/gitlab/git/util_spec.rb
+++ b/spec/lib/gitlab/git/util_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Git::Util do
- describe :count_lines do
+ describe '#count_lines' do
[
["", 0],
["foo", 1],
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 48f7754bed8..703b41f95ac 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -183,7 +183,7 @@ describe Gitlab::GitAccess, lib: true do
describe '#check_push_access!' do
before { merge_into_protected_branch }
- let(:unprotected_branch) { FFaker::Internet.user_name }
+ let(:unprotected_branch) { 'unprotected_branch' }
let(:changes) do
{ push_new_branch: "#{Gitlab::Git::BLANK_SHA} 570e7b2ab refs/heads/wow",
@@ -211,9 +211,9 @@ describe Gitlab::GitAccess, lib: true do
target_branch = project.repository.lookup('feature')
source_branch = project.repository.create_file(
user,
- FFaker::InternetSE.login_user_name,
- FFaker::HipsterIpsum.paragraph,
- message: FFaker::HipsterIpsum.sentence,
+ 'John Doe',
+ 'This is the file content',
+ message: 'This is a good commit message',
branch_name: unprotected_branch)
rugged = project.repository.rugged
author = { email: "email@example.com", time: Time.now, name: "Example Git User" }
diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb
index 8eaf7aac264..36f0e6507c8 100644
--- a/spec/lib/gitlab/git_spec.rb
+++ b/spec/lib/gitlab/git_spec.rb
@@ -1,21 +1,8 @@
require 'spec_helper'
describe Gitlab::Git, lib: true do
- let(:committer_email) { FFaker::Internet.email }
-
- # I have to remove periods from the end of the name
- # This happened when the user's name had a suffix (i.e. "Sr.")
- # This seems to be what git does under the hood. For example, this commit:
- #
- # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?'
- #
- # results in this:
- #
- # $ git show --pretty
- # ...
- # Author: Foo Sr <foo@example.com>
- # ...
- let(:committer_name) { FFaker::Name.name.chomp("\.") }
+ let(:committer_email) { 'user@example.org' }
+ let(:committer_name) { 'John Doe' }
describe 'committer_hash' do
it "returns a hash containing the given email and name" do
diff --git a/spec/lib/gitlab/gitaly_client/notifications_spec.rb b/spec/lib/gitlab/gitaly_client/notifications_spec.rb
index a6252c99aa1..bb5d93994ad 100644
--- a/spec/lib/gitlab/gitaly_client/notifications_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/notifications_spec.rb
@@ -1,20 +1,13 @@
require 'spec_helper'
describe Gitlab::GitalyClient::Notifications do
- let(:client) { Gitlab::GitalyClient::Notifications.new }
-
- before do
- allow(Gitlab.config.gitaly).to receive(:socket_path).and_return('path/to/gitaly.socket')
- end
-
describe '#post_receive' do
- let(:repo_path) { '/path/to/my_repo.git' }
-
it 'sends a post_receive message' do
+ repo_path = create(:empty_project).repository.path_to_repo
expect_any_instance_of(Gitaly::Notifications::Stub).
to receive(:post_receive).with(post_receive_request_with_repo_path(repo_path))
- client.post_receive(repo_path)
+ described_class.new(repo_path).post_receive
end
end
end
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
new file mode 100644
index 00000000000..55fcf91fb6e
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient, lib: true do
+ describe '.new_channel' do
+ context 'when passed a UNIX socket address' do
+ it 'passes the address as-is to GRPC::Core::Channel initializer' do
+ address = 'unix:/tmp/gitaly.sock'
+
+ expect(GRPC::Core::Channel).to receive(:new).with(address, any_args)
+
+ described_class.new_channel(address)
+ end
+ end
+
+ context 'when passed a TCP address' do
+ it 'strips tcp:// prefix before passing it to GRPC::Core::Channel initializer' do
+ address = 'localhost:9876'
+ prefixed_address = "tcp://#{address}"
+
+ expect(GRPC::Core::Channel).to receive(:new).with(address, any_args)
+
+ described_class.new_channel(prefixed_address)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
index 8b867fbe322..9d5e20841b5 100644
--- a/spec/lib/gitlab/github_import/importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -215,9 +215,9 @@ describe Gitlab::GithubImport::Importer, lib: true do
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
let(:repository) { double(id: 1, fork: false) }
let(:source_sha) { create(:commit, project: project).id }
- let(:source_branch) { double(ref: 'branch-merged', repo: repository, sha: source_sha) }
+ let(:source_branch) { double(ref: 'branch-merged', repo: repository, sha: source_sha, user: octocat) }
let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
- let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) }
+ let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha, user: octocat) }
let(:pull_request) do
double(
number: 1347,
diff --git a/spec/lib/gitlab/github_import/label_formatter_spec.rb b/spec/lib/gitlab/github_import/label_formatter_spec.rb
index 10449ef5fcb..565435824fd 100644
--- a/spec/lib/gitlab/github_import/label_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/label_formatter_spec.rb
@@ -25,7 +25,7 @@ describe Gitlab::GithubImport::LabelFormatter, lib: true do
context 'when label exists' do
it 'does not create a new label' do
- project.labels.create(name: raw.name)
+ Labels::CreateService.new(name: raw.name).execute(project: project)
expect { subject.create! }.not_to change(Label, :count)
end
diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
index 44423917944..b7c59918a76 100644
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -4,15 +4,18 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
let(:client) { double }
let(:project) { create(:project, :repository) }
let(:source_sha) { create(:commit, project: project).id }
- let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
+ let(:target_commit) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit) }
+ let(:target_sha) { target_commit.id }
+ let(:target_short_sha) { target_commit.id.to_s[0..7] }
let(:repository) { double(id: 1, fork: false) }
let(:source_repo) { repository }
let(:source_branch) { double(ref: 'branch-merged', repo: source_repo, sha: source_sha) }
let(:forked_source_repo) { double(id: 2, fork: true, name: 'otherproject', full_name: 'company/otherproject') }
let(:target_repo) { repository }
- let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) }
- let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') }
- let(:forked_branch) { double(ref: 'master', repo: forked_source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') }
+ let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha, user: octocat) }
+ let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', user: octocat) }
+ let(:forked_branch) { double(ref: 'master', repo: forked_source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', user: octocat) }
+ let(:branch_deleted_repo) { double(ref: 'master', repo: nil, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b', user: octocat) }
let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
@@ -61,7 +64,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
author_id: project.creator_id,
assignee_id: nil,
created_at: created_at,
- updated_at: updated_at
+ updated_at: updated_at,
+ imported: true
}
expect(pull_request.attributes).to eq(expected)
@@ -87,7 +91,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
author_id: project.creator_id,
assignee_id: nil,
created_at: created_at,
- updated_at: updated_at
+ updated_at: updated_at,
+ imported: true
}
expect(pull_request.attributes).to eq(expected)
@@ -114,7 +119,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
author_id: project.creator_id,
assignee_id: nil,
created_at: created_at,
- updated_at: updated_at
+ updated_at: updated_at,
+ imported: true
}
expect(pull_request.attributes).to eq(expected)
@@ -203,16 +209,24 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
context 'when source branch does not exist' do
let(:raw_data) { double(base_data.merge(head: removed_branch)) }
- it 'prefixes branch name with pull request number' do
- expect(pull_request.source_branch_name).to eq 'pull/1347/removed-branch'
+ it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do
+ expect(pull_request.source_branch_name).to eq "gh-#{target_short_sha}/1347/octocat/removed-branch"
end
end
context 'when source branch is from a fork' do
let(:raw_data) { double(base_data.merge(head: forked_branch)) }
- it 'prefixes branch name with pull request number and project with namespace to avoid collision' do
- expect(pull_request.source_branch_name).to eq 'pull/1347/company/otherproject/master'
+ it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do
+ expect(pull_request.source_branch_name).to eq "gh-#{target_short_sha}/1347/octocat/master"
+ end
+ end
+
+ context 'when source branch is from a deleted fork' do
+ let(:raw_data) { double(base_data.merge(head: branch_deleted_repo)) }
+
+ it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do
+ expect(pull_request.source_branch_name).to eq "gh-#{target_short_sha}/1347/octocat/master"
end
end
end
@@ -229,8 +243,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
context 'when target branch does not exist' do
let(:raw_data) { double(base_data.merge(base: removed_branch)) }
- it 'prefixes branch name with pull request number' do
- expect(pull_request.target_branch_name).to eq 'pull/1347/removed-branch'
+ it 'prefixes branch name with gh-:short_sha/:number/:user pattern to avoid collision' do
+ expect(pull_request.target_branch_name).to eq 'gl-2e5d3239/1347/octocat/removed-branch'
end
end
end
@@ -290,6 +304,14 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
end
+ context 'when source repository does not exist anymore' do
+ let(:raw_data) { double(base_data.merge(head: branch_deleted_repo)) }
+
+ it 'returns true' do
+ expect(pull_request.cross_project?).to eq true
+ end
+ end
+
context 'when source and target repositories are the same' do
let(:raw_data) { double(base_data.merge(head: source_branch)) }
@@ -299,6 +321,14 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
end
+ describe '#source_branch_exists?' do
+ let(:raw_data) { double(base_data.merge(head: forked_branch)) }
+
+ it 'returns false when is a cross_project' do
+ expect(pull_request.source_branch_exists?).to eq false
+ end
+ end
+
describe '#url' do
let(:raw_data) { double(base_data) }
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index ddeb71730e7..24654bf6afd 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -29,6 +29,7 @@ notes:
- resolved_by
- todos
- events
+- system_note_metadata
label_links:
- target
- label
@@ -155,6 +156,8 @@ project:
- external_wiki_service
- kubernetes_service
- mock_ci_service
+- mock_deployment_service
+- mock_monitoring_service
- forked_project_link
- forked_from_project
- forked_project_links
diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb
new file mode 100644
index 00000000000..c5ce06afd73
--- /dev/null
+++ b/spec/lib/gitlab/import_export/fork_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe 'forked project import', services: true do
+ let(:user) { create(:user) }
+ let!(:project_with_repo) { create(:project, :test_repo, name: 'test-repo-restorer', path: 'test-repo-restorer') }
+ let!(:project) { create(:empty_project) }
+ let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
+ let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
+ let(:forked_from_project) { create(:project) }
+ let(:fork_link) { create(:forked_project_link, forked_from_project: project_with_repo) }
+ let(:repo_saver) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) }
+ let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
+
+ let(:repo_restorer) do
+ Gitlab::ImportExport::RepoRestorer.new(path_to_bundle: bundle_path, shared: shared, project: project)
+ end
+
+ let!(:merge_request) do
+ create(:merge_request, source_project: fork_link.forked_to_project, target_project: project_with_repo)
+ end
+
+ let(:saver) do
+ Gitlab::ImportExport::ProjectTreeSaver.new(project: project_with_repo, current_user: user, shared: shared)
+ end
+
+ let(:restorer) do
+ Gitlab::ImportExport::ProjectTreeRestorer.new(user: user, shared: shared, project: project)
+ end
+
+ before do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+
+ saver.save
+ repo_saver.save
+
+ repo_restorer.restore
+ restorer.restore
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ FileUtils.rm_rf(project_with_repo.repository.path_to_repo)
+ FileUtils.rm_rf(project.repository.path_to_repo)
+ end
+
+ it 'can access the MR' do
+ expect(project.merge_requests.first.ensure_ref_fetched.first).to include('refs/merge-requests/1/head')
+ end
+end
diff --git a/spec/lib/gitlab/import_export/hash_util_spec.rb b/spec/lib/gitlab/import_export/hash_util_spec.rb
new file mode 100644
index 00000000000..1c3a0b23ece
--- /dev/null
+++ b/spec/lib/gitlab/import_export/hash_util_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::HashUtil, lib: true do
+ let(:stringified_array) { [{ 'test' => 1 }] }
+ let(:stringified_array_with_date) { [{ 'test_date' => '2016-04-06 06:17:44 +0200' }] }
+
+ describe '.deep_symbolize_array!' do
+ it 'symbolizes keys' do
+ expect { described_class.deep_symbolize_array!(stringified_array) }.to change {
+ stringified_array.first.keys.first
+ }.from('test').to(:test)
+ end
+ end
+
+ describe '.deep_symbolize_array_with_date!' do
+ it 'symbolizes keys' do
+ expect { described_class.deep_symbolize_array_with_date!(stringified_array_with_date) }.to change {
+ stringified_array_with_date.first.keys.first
+ }.from('test_date').to(:test_date)
+ end
+
+ it 'transforms date strings into Time objects' do
+ expect { described_class.deep_symbolize_array_with_date!(stringified_array_with_date) }.to change {
+ stringified_array_with_date.first.values.first.class
+ }.from(String).to(ActiveSupport::TimeWithZone)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
new file mode 100644
index 00000000000..349be4596b6
--- /dev/null
+++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::MergeRequestParser do
+ let(:user) { create(:user) }
+ let!(:project) { create(:project, :test_repo, name: 'test-repo-restorer', path: 'test-repo-restorer') }
+ let(:forked_from_project) { create(:project) }
+ let(:fork_link) { create(:forked_project_link, forked_from_project: project) }
+
+ let!(:merge_request) do
+ create(:merge_request, source_project: fork_link.forked_to_project, target_project: project)
+ end
+
+ let(:parsed_merge_request) do
+ described_class.new(project,
+ merge_request.diff_head_sha,
+ merge_request,
+ merge_request.as_json).parse!
+ end
+
+ after do
+ FileUtils.rm_rf(project.repository.path_to_repo)
+ end
+
+ it 'has a source branch' do
+ expect(project.repository.branch_exists?(parsed_merge_request.source_branch)).to be true
+ end
+
+ it 'has a target branch' do
+ expect(project.repository.branch_exists?(parsed_merge_request.target_branch)).to be true
+ end
+end
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index c36f12dbd82..af9c25acb02 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -82,6 +82,12 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(MergeRequestDiff.where.not(st_diffs: nil).count).to eq(9)
end
+ it 'has the correct time for merge request st_commits' do
+ st_commits = MergeRequestDiff.where.not(st_commits: nil).first.st_commits
+
+ expect(st_commits.first[:committed_date]).to be_kind_of(Time)
+ end
+
it 'has labels associated to label links, associated to issues' do
expect(Label.first.label_links.first.target).not_to be_nil
end
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index 012c22ec5ad..d2d89e3b019 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -79,6 +79,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
expect(saved_project_json['merge_requests'].first['merge_request_diff']).not_to be_empty
end
+ it 'has merge requests diff st_diffs' do
+ expect(saved_project_json['merge_requests'].first['merge_request_diff']['utf8_st_diffs']).not_to be_nil
+ end
+
it 'has merge requests comments' do
expect(saved_project_json['merge_requests'].first['notes']).not_to be_empty
end
diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb
index 57e412b0cef..fcc23a75ca1 100644
--- a/spec/lib/gitlab/import_export/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb
@@ -9,7 +9,7 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do
relation_hash: relation_hash,
members_mapper: members_mapper,
user: user,
- project_id: project.id)
+ project: project)
end
context 'hook object' do
diff --git a/spec/lib/gitlab/import_export/repo_bundler_spec.rb b/spec/lib/gitlab/import_export/repo_saver_spec.rb
index a7f4e11271e..a7f4e11271e 100644
--- a/spec/lib/gitlab/import_export/repo_bundler_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_saver_spec.rb
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 2f3bd4393b7..346cf0d117c 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -57,7 +57,7 @@ describe Gitlab::LDAP::User, lib: true do
end
end
- describe :find_or_create do
+ describe 'find or create' do
it "finds the user if already existing" do
create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain')
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index 6c84a4c8b73..8f09266c3b3 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -40,6 +40,15 @@ describe Gitlab::OAuth::User, lib: true do
let(:provider) { 'twitter' }
describe 'signup' do
+ it 'marks user as having password_automatically_set' do
+ stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['twitter'])
+
+ oauth_user.save
+
+ expect(gl_user).to be_persisted
+ expect(gl_user).to be_password_automatically_set
+ end
+
shared_examples 'to verify compliance with allow_single_sign_on' do
context 'provider is marked as external' do
it 'marks user as external' do
diff --git a/spec/lib/gitlab/polling_interval_spec.rb b/spec/lib/gitlab/polling_interval_spec.rb
new file mode 100644
index 00000000000..56c2847e26a
--- /dev/null
+++ b/spec/lib/gitlab/polling_interval_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Gitlab::PollingInterval, lib: true do
+ let(:polling_interval) { described_class }
+
+ describe '.set_header' do
+ let(:headers) { {} }
+ let(:response) { double(headers: headers) }
+
+ context 'when polling is disabled' do
+ before do
+ stub_application_setting(polling_interval_multiplier: 0)
+ end
+
+ it 'sets value to -1' do
+ polling_interval.set_header(response, interval: 10_000)
+
+ expect(headers['Poll-Interval']).to eq(-1)
+ end
+ end
+
+ context 'when polling is enabled' do
+ before do
+ stub_application_setting(polling_interval_multiplier: 0.33333)
+ end
+
+ it 'applies modifier to base interval' do
+ polling_interval.set_header(response, interval: 10_000)
+
+ expect(headers['Poll-Interval']).to eq(3333)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 9a8096208db..e0ebea63eb4 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -41,8 +41,10 @@ describe Gitlab::ProjectSearchResults, lib: true do
subject { described_class.parse_search_result(search_result) }
- it "returns a valid OpenStruct object" do
- is_expected.to be_an OpenStruct
+ it "returns a valid FoundBlob" do
+ is_expected.to be_an Gitlab::SearchResults::FoundBlob
+ expect(subject.id).to be_nil
+ expect(subject.path).to eq('CHANGELOG')
expect(subject.filename).to eq('CHANGELOG')
expect(subject.basename).to eq('CHANGELOG')
expect(subject.ref).to eq('master')
@@ -53,6 +55,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
context "when filename has extension" do
let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" }
+ it { expect(subject.path).to eq('CONTRIBUTE.md') }
it { expect(subject.filename).to eq('CONTRIBUTE.md') }
it { expect(subject.basename).to eq('CONTRIBUTE') }
end
@@ -60,6 +63,7 @@ describe Gitlab::ProjectSearchResults, lib: true do
context "when file under directory" do
let(:search_result) { "master:a/b/c.md:5:a b c\n" }
+ it { expect(subject.path).to eq('a/b/c.md') }
it { expect(subject.filename).to eq('a/b/c.md') }
it { expect(subject.basename).to eq('a/b/c') }
end
diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb
new file mode 100644
index 00000000000..0fb5d7646f2
--- /dev/null
+++ b/spec/lib/gitlab/repo_path_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe ::Gitlab::RepoPath do
+ describe '.strip_storage_path' do
+ before do
+ allow(Gitlab.config.repositories).to receive(:storages).and_return({
+ 'storage1' => { 'path' => '/foo' },
+ 'storage2' => { 'path' => '/bar' },
+ })
+ end
+
+ it 'strips the storage path' do
+ expect(described_class.strip_storage_path('/bar/foo/qux/baz.git')).to eq('foo/qux/baz.git')
+ end
+
+ it 'raises NotFoundError if no storage matches the path' do
+ expect { described_class.strip_storage_path('/doesnotexist/foo.git') }.to raise_error(
+ described_class::NotFoundError
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 8e5e8288c49..9fbcb1fee69 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -179,23 +179,73 @@ describe Gitlab::Workhorse, lib: true do
describe '.git_http_ok' do
let(:user) { create(:user) }
+ let(:repo_path) { repository.path_to_repo }
+ let(:action) { 'info_refs' }
- subject { described_class.git_http_ok(repository, user) }
+ subject { described_class.git_http_ok(repository, user, action) }
- it { expect(subject).to eq({ GL_ID: "user-#{user.id}", RepoPath: repository.path_to_repo }) }
+ it { expect(subject).to include({ GL_ID: "user-#{user.id}", RepoPath: repo_path }) }
- context 'when Gitaly socket path is present' do
- let(:gitaly_socket_path) { '/tmp/gitaly.sock' }
+ context 'when Gitaly is enabled' do
+ let(:gitaly_params) do
+ address = Gitlab::GitalyClient.get_address('default')
+ {
+ GitalySocketPath: URI(address).path,
+ GitalyAddress: address,
+ }
+ end
before do
- allow(Gitlab.config.gitaly).to receive(:socket_path).and_return(gitaly_socket_path)
+ allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true)
+ end
+
+ it 'includes a Repository param' do
+ repo_param = { Repository: {
+ path: repo_path,
+ storage_name: 'default',
+ relative_path: project.full_path + '.git',
+ } }
+
+ expect(subject).to include(repo_param)
+ end
+
+ context "when git_upload_pack action is passed" do
+ let(:action) { 'git_upload_pack' }
+ let(:feature_flag) { :post_upload_pack }
+
+ context 'when action is enabled by feature flag' do
+ it 'includes Gitaly params in the returned value' do
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(feature_flag).and_return(true)
+
+ expect(subject).to include(gitaly_params)
+ end
+ end
+
+ context 'when action is not enabled by feature flag' do
+ it 'does not include Gitaly params in the returned value' do
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(feature_flag).and_return(false)
+
+ expect(subject).not_to include(gitaly_params)
+ end
+ end
+ end
+
+ context "when git_receive_pack action is passed" do
+ let(:action) { 'git_receive_pack' }
+
+ it { expect(subject).not_to include(gitaly_params) }
end
- it 'includes Gitaly params in the returned value' do
- expect(subject).to include({
- GitalyResourcePath: "/projects/#{repository.project.id}/git-http/info-refs",
- GitalySocketPath: gitaly_socket_path,
- })
+ context "when info_refs action is passed" do
+ let(:action) { 'info_refs' }
+
+ it { expect(subject).to include(gitaly_params) }
+ end
+
+ context 'when action passed is not supported by Gitaly' do
+ let(:action) { 'download' }
+
+ it { expect { subject }.to raise_exception('Unsupported action: download') }
end
end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 4b72eb2eaa3..6a89b007f96 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -37,7 +37,7 @@ describe Notify do
context 'for issues' do
let(:issue) { create(:issue, author: current_user, assignee: assignee, project: project) }
- let(:issue_with_description) { create(:issue, author: current_user, assignee: assignee, project: project, description: FFaker::Lorem.sentence) }
+ let(:issue_with_description) { create(:issue, author: current_user, assignee: assignee, project: project, description: 'My awesome description') }
describe 'that are new' do
subject { Notify.new_issue_email(issue.assignee_id, issue.id) }
@@ -184,9 +184,10 @@ describe Notify do
end
context 'for merge requests' do
+ let(:project) { create(:project, :repository) }
let(:merge_author) { create(:user) }
let(:merge_request) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project) }
- let(:merge_request_with_description) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project, description: FFaker::Lorem.sentence) }
+ let(:merge_request_with_description) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project, description: 'My awesome description') }
describe 'that are new' do
subject { Notify.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
@@ -334,7 +335,7 @@ describe Notify do
end
describe 'project was moved' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:user) { create(:user) }
subject { Notify.project_was_moved_email(project.id, user.id, "gitlab/gitlab") }
@@ -460,7 +461,7 @@ describe Notify do
end
describe 'project invitation' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:project_member) { invite_to_project(project, inviter: master) }
@@ -480,7 +481,7 @@ describe Notify do
end
describe 'project invitation accepted' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:invited_user) { create(:user, name: 'invited user') }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:project_member) do
@@ -505,7 +506,7 @@ describe Notify do
end
describe 'project invitation declined' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:project_member) do
invitee = invite_to_project(project, inviter: master)
@@ -569,6 +570,7 @@ describe Notify do
end
describe 'on a commit' do
+ let(:project) { create(:project, :repository) }
let(:commit) { project.commit }
before(:each) { allow(note).to receive(:noteable).and_return(commit) }
@@ -636,6 +638,7 @@ describe Notify do
end
context 'items that are noteable, emails for a note on a diff' do
+ let(:project) { create(:project, :repository) }
let(:note_author) { create(:user, name: 'author_name') }
before :each do
@@ -977,6 +980,7 @@ describe Notify do
end
describe 'email on push with multiple commits' do
+ let(:project) { create(:project, :repository) }
let(:example_site_path) { root_path }
let(:user) { create(:user) }
let(:raw_compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_image_commit.id, sample_commit.id) }
@@ -1070,6 +1074,7 @@ describe Notify do
end
describe 'email on push with a single commit' do
+ let(:project) { create(:project, :repository) }
let(:example_site_path) { root_path }
let(:user) { create(:user) }
let(:raw_compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) }
@@ -1102,7 +1107,7 @@ describe Notify do
end
describe 'HTML emails setting' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:user) { create(:user) }
let(:multipart_mail) { Notify.project_was_moved_email(project.id, user.id, "gitlab/gitlab") }
diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
index b6d678bac18..3db57595fa6 100644
--- a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
+++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require Rails.root.join('db', 'migrate', '20161124141322_migrate_process_commit_worker_jobs.rb')
describe MigrateProcessCommitWorkerJobs do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:commit) { project.commit.raw.raw_commit }
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index 94c25a454aa..552229e9b07 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -53,6 +53,20 @@ describe Blob do
end
end
+ describe '#ipython_notebook?' do
+ it 'is falsey when language is not Jupyter Notebook' do
+ git_blob = double(text?: true, language: double(name: 'JSON'))
+
+ expect(described_class.decorate(git_blob)).not_to be_ipython_notebook
+ end
+
+ it 'is truthy when language is Jupyter Notebook' do
+ git_blob = double(text?: true, language: double(name: 'Jupyter Notebook'))
+
+ expect(described_class.decorate(git_blob)).to be_ipython_notebook
+ end
+ end
+
describe '#video?' do
it 'is falsey with image extension' do
git_blob = Gitlab::Git::Blob.new(name: 'image.png')
@@ -116,6 +130,11 @@ describe Blob do
blob = stubbed_blob
expect(blob.to_partial_path(project)).to eq 'download'
end
+
+ it 'handles iPython notebooks' do
+ blob = stubbed_blob(text?: true, ipython_notebook?: true)
+ expect(blob.to_partial_path(project)).to eq 'notebook'
+ end
end
describe '#size_within_svg_limits?' do
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 53282b999dc..e4a24fd63c2 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1055,10 +1055,13 @@ describe Ci::Pipeline, models: true do
end
before do
- reset_delivered_emails!
-
project.team << [pipeline.user, Gitlab::Access::DEVELOPER]
+ pipeline.user.global_notification_setting.
+ update(level: 'custom', failed_pipeline: true, success_pipeline: true)
+
+ reset_delivered_emails!
+
perform_enqueued_jobs do
pipeline.enqueue
pipeline.run
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index ea5e4e21039..7343b735a74 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -297,4 +297,40 @@ describe CommitStatus, :models do
end
end
end
+
+ describe '#locking_enabled?' do
+ before do
+ commit_status.lock_version = 100
+ end
+
+ subject { commit_status.locking_enabled? }
+
+ context "when changing status" do
+ before do
+ commit_status.status = "running"
+ end
+
+ it "lock" do
+ is_expected.to be true
+ end
+
+ it "raise exception when trying to update" do
+ expect{ commit_status.save }.to raise_error(ActiveRecord::StaleObjectError)
+ end
+ end
+
+ context "when changing description" do
+ before do
+ commit_status.description = "test"
+ end
+
+ it "do not lock" do
+ is_expected.to be false
+ end
+
+ it "save correctly" do
+ expect(commit_status.save).to be true
+ end
+ end
+ end
end
diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb
index 55483fc876a..4f33f3c6d69 100644
--- a/spec/models/cycle_analytics/plan_spec.rb
+++ b/spec/models/cycle_analytics/plan_spec.rb
@@ -13,7 +13,7 @@ describe 'CycleAnalytics#plan', feature: true do
data_fn: -> (context) do
{
issue: context.create(:issue, project: context.project),
- branch_name: context.random_git_name
+ branch_name: context.generate(:branch)
}
end,
start_time_conditions: [["issue associated with a milestone",
@@ -35,7 +35,7 @@ describe 'CycleAnalytics#plan', feature: true do
context "when a regular label (instead of a list label) is added to the issue" do
it "returns nil" do
- branch_name = random_git_name
+ branch_name = generate(:branch)
label = create(:label)
issue = create(:issue, project: project)
issue.update(label_ids: [label.id])
diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb
index e6a826a9418..4744b9e05ea 100644
--- a/spec/models/cycle_analytics/production_spec.rb
+++ b/spec/models/cycle_analytics/production_spec.rb
@@ -23,7 +23,7 @@ describe 'CycleAnalytics#production', feature: true do
# Make other changes on master
sha = context.project.repository.create_file(
context.user,
- context.random_git_name,
+ context.generate(:branch),
'content',
message: 'commit message',
branch_name: 'master')
diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb
index 3a02ed81adb..f78d7a23105 100644
--- a/spec/models/cycle_analytics/staging_spec.rb
+++ b/spec/models/cycle_analytics/staging_spec.rb
@@ -28,7 +28,7 @@ describe 'CycleAnalytics#staging', feature: true do
# Make other changes on master
sha = context.project.repository.create_file(
context.user,
- context.random_git_name,
+ context.generate(:branch),
'content',
message: 'commit message',
branch_name: 'master')
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index e8caad00c44..8acec805584 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -6,6 +6,9 @@ describe SystemHook, models: true do
let(:user) { create(:user) }
let(:project) { create(:empty_project, namespace: user.namespace) }
let(:group) { create(:group) }
+ let(:params) do
+ { name: 'John Doe', username: 'jduser', email: 'jg@example.com', password: 'mydummypass' }
+ end
before do
WebMock.stub_request(:post, system_hook.url)
@@ -29,7 +32,7 @@ describe SystemHook, models: true do
end
it "user_create hook" do
- create(:user)
+ Users::CreateService.new(nil, params).execute
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_create/,
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index b8584301baa..4bdd46a581d 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -51,14 +51,6 @@ describe Issue, models: true do
expect(issue.closed_at).to eq(now)
end
-
- it 'sets closed_at to nil when issue is reopened' do
- issue = create(:issue, state: 'closed')
-
- issue.reopen
-
- expect(issue.closed_at).to be_nil
- end
end
describe '#to_reference' do
diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb
index e6ca4853873..db2c2619968 100644
--- a/spec/models/list_spec.rb
+++ b/spec/models/list_spec.rb
@@ -19,8 +19,8 @@ describe List do
expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:board_id)
end
- context 'when list_type is set to done' do
- subject { described_class.new(list_type: :done) }
+ context 'when list_type is set to closed' do
+ subject { described_class.new(list_type: :closed) }
it { is_expected.not_to validate_presence_of(:label) }
it { is_expected.not_to validate_presence_of(:position) }
@@ -34,8 +34,8 @@ describe List do
expect(subject.destroy).to be_truthy
end
- it 'can not be destroyed when when list_type is set to done' do
- subject = create(:done_list)
+ it 'can not be destroyed when when list_type is set to closed' do
+ subject = create(:closed_list)
expect(subject.destroy).to be_falsey
end
@@ -48,8 +48,8 @@ describe List do
expect(subject).to be_destroyable
end
- it 'returns false when list_type is set to done' do
- subject.list_type = :done
+ it 'returns false when list_type is set to closed' do
+ subject.list_type = :closed
expect(subject).not_to be_destroyable
end
@@ -62,8 +62,8 @@ describe List do
expect(subject).to be_movable
end
- it 'returns false when list_type is set to done' do
- subject.list_type = :done
+ it 'returns false when list_type is set to closed' do
+ subject.list_type = :closed
expect(subject).not_to be_movable
end
@@ -77,10 +77,10 @@ describe List do
expect(subject.title).to eq 'Development'
end
- it 'returns Done when list_type is set to done' do
- subject.list_type = :done
+ it 'returns Closed when list_type is set to closed' do
+ subject.list_type = :closed
- expect(subject.title).to eq 'Done'
+ expect(subject.title).to eq 'Closed'
end
end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 3cee2b7714f..f3f48f951a8 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -109,7 +109,7 @@ describe Milestone, models: true do
it { expect(milestone.percent_complete(user)).to eq(75) }
end
- describe :items_count do
+ describe '#is_empty?' do
before do
milestone.issues << create(:issue, project: project)
milestone.issues << create(:closed_issue, project: project)
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 67d48557184..ccaf0d7abc7 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -129,10 +129,10 @@ describe Namespace, models: true do
end
end
- describe '#move_dir' do
+ describe '#move_dir', repository: true do
before do
@namespace = create :namespace
- @project = create(:empty_project, namespace: @namespace)
+ @project = create(:project_empty_repo, namespace: @namespace)
allow(@namespace).to receive(:path_changed?).and_return(true)
end
@@ -141,9 +141,9 @@ describe Namespace, models: true do
end
it "moves dir if path changed" do
- new_path = @namespace.path + "_new"
- allow(@namespace).to receive(:path_was).and_return(@namespace.path)
- allow(@namespace).to receive(:path).and_return(new_path)
+ new_path = @namespace.full_path + "_new"
+ allow(@namespace).to receive(:full_path_was).and_return(@namespace.full_path)
+ allow(@namespace).to receive(:full_path).and_return(new_path)
expect(@namespace).to receive(:remove_exports!)
expect(@namespace.move_dir).to be_truthy
end
@@ -161,16 +161,75 @@ describe Namespace, models: true do
it { expect { @namespace.move_dir }.to raise_error('Namespace cannot be moved, because at least one project has tags in container registry') }
end
+
+ context 'renaming a sub-group' do
+ let(:parent) { create(:group, name: 'parent', path: 'parent') }
+ let(:child) { create(:group, name: 'child', path: 'child', parent: parent) }
+ let!(:project) { create(:project_empty_repo, path: 'the-project', namespace: child) }
+ let(:uploads_dir) { File.join(CarrierWave.root, 'uploads', 'parent') }
+ let(:pages_dir) { File.join(TestEnv.pages_path, 'parent') }
+
+ before do
+ FileUtils.mkdir_p(File.join(uploads_dir, 'child', 'the-project'))
+ FileUtils.mkdir_p(File.join(pages_dir, 'child', 'the-project'))
+ end
+
+ it 'correctly moves the repository, uploads and pages' do
+ expected_repository_path = File.join(TestEnv.repos_path, 'parent', 'renamed', 'the-project.git')
+ expected_upload_path = File.join(uploads_dir, 'renamed', 'the-project')
+ expected_pages_path = File.join(pages_dir, 'renamed', 'the-project')
+
+ child.update_attributes!(path: 'renamed')
+
+ expect(File.directory?(expected_repository_path)).to be(true)
+ expect(File.directory?(expected_upload_path)).to be(true)
+ expect(File.directory?(expected_pages_path)).to be(true)
+ end
+ end
end
- describe :rm_dir do
- let!(:project) { create(:empty_project, namespace: namespace) }
- let!(:path) { File.join(Gitlab.config.repositories.storages.default['path'], namespace.full_path) }
+ describe '#rm_dir', 'callback', repository: true do
+ let!(:project) { create(:project_empty_repo, namespace: namespace) }
+ let(:repository_storage_path) { Gitlab.config.repositories.storages.default['path'] }
+ let(:path_in_dir) { File.join(repository_storage_path, namespace.full_path) }
+ let(:deleted_path) { namespace.full_path.gsub(namespace.path, "#{namespace.full_path}+#{namespace.id}+deleted") }
+ let(:deleted_path_in_dir) { File.join(repository_storage_path, deleted_path) }
+
+ it 'renames its dirs when deleted' do
+ allow(GitlabShellWorker).to receive(:perform_in)
- it "removes its dirs when deleted" do
namespace.destroy
- expect(File.exist?(path)).to be(false)
+ expect(File.exist?(deleted_path_in_dir)).to be(true)
+ end
+
+ it 'schedules the namespace for deletion' do
+ expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage_path, deleted_path)
+
+ namespace.destroy
+ end
+
+ context 'in sub-groups' do
+ let(:parent) { create(:namespace, path: 'parent') }
+ let(:child) { create(:namespace, parent: parent, path: 'child') }
+ let!(:project) { create(:project_empty_repo, namespace: child) }
+ let(:path_in_dir) { File.join(repository_storage_path, 'parent', 'child') }
+ let(:deleted_path) { File.join('parent', "child+#{child.id}+deleted") }
+ let(:deleted_path_in_dir) { File.join(repository_storage_path, deleted_path) }
+
+ it 'renames its dirs when deleted' do
+ allow(GitlabShellWorker).to receive(:perform_in)
+
+ child.destroy
+
+ expect(File.exist?(deleted_path_in_dir)).to be(true)
+ end
+
+ it 'schedules the namespace for deletion' do
+ expect(GitlabShellWorker).to receive(:perform_in).with(5.minutes, :rm_namespace, repository_storage_path, deleted_path)
+
+ child.destroy
+ end
end
it 'removes the exports folder' do
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index e6a4583a8fb..c6c45d78990 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -5,7 +5,7 @@ describe PagesDomain, models: true do
it { is_expected.to belong_to(:project) }
end
- describe :validate_domain do
+ describe 'validate domain' do
subject { build(:pages_domain, domain: domain) }
context 'is unique' do
@@ -75,7 +75,7 @@ describe PagesDomain, models: true do
end
end
- describe :url do
+ describe '#url' do
subject { domain.url }
context 'without the certificate' do
@@ -91,7 +91,7 @@ describe PagesDomain, models: true do
end
end
- describe :has_matching_key? do
+ describe '#has_matching_key?' do
subject { domain.has_matching_key? }
context 'for matching key' do
@@ -107,7 +107,7 @@ describe PagesDomain, models: true do
end
end
- describe :has_intermediates? do
+ describe '#has_intermediates?' do
subject { domain.has_intermediates? }
context 'for self signed' do
@@ -133,7 +133,7 @@ describe PagesDomain, models: true do
end
end
- describe :expired? do
+ describe '#expired?' do
subject { domain.expired? }
context 'for valid' do
@@ -149,7 +149,7 @@ describe PagesDomain, models: true do
end
end
- describe :subject do
+ describe '#subject' do
let(:domain) { build(:pages_domain, :with_certificate) }
subject { domain.subject }
@@ -157,7 +157,7 @@ describe PagesDomain, models: true do
it { is_expected.to eq('/CN=test-certificate') }
end
- describe :certificate_text do
+ describe '#certificate_text' do
let(:domain) { build(:pages_domain, :with_certificate) }
subject { domain.certificate_text }
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index bf7950ef1c9..e69eb0098dd 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -4,7 +4,7 @@ describe KubernetesService, models: true, caching: true do
include KubernetesHelpers
include ReactiveCachingHelpers
- let(:project) { create(:kubernetes_project) }
+ let(:project) { build_stubbed(:kubernetes_project) }
let(:service) { project.kubernetes_service }
# We use Kubeclient to interactive with the Kubernetes API. It will
@@ -32,7 +32,8 @@ describe KubernetesService, models: true, caching: true do
describe 'Validations' do
context 'when service is active' do
before { subject.active = true }
- it { is_expected.to validate_presence_of(:namespace) }
+
+ it { is_expected.not_to validate_presence_of(:namespace) }
it { is_expected.to validate_presence_of(:api_url) }
it { is_expected.to validate_presence_of(:token) }
@@ -55,7 +56,7 @@ describe KubernetesService, models: true, caching: true do
'a.b' => false,
'a*b' => false,
}.each do |namespace, validity|
- it "should validate #{namespace} as #{validity ? 'valid' : 'invalid'}" do
+ it "validates #{namespace} as #{validity ? 'valid' : 'invalid'}" do
subject.namespace = namespace
expect(subject.valid?).to eq(validity)
@@ -66,24 +67,40 @@ describe KubernetesService, models: true, caching: true do
context 'when service is inactive' do
before { subject.active = false }
- it { is_expected.not_to validate_presence_of(:namespace) }
+
it { is_expected.not_to validate_presence_of(:api_url) }
it { is_expected.not_to validate_presence_of(:token) }
end
end
describe '#initialize_properties' do
- context 'with a project' do
- let(:namespace_name) { "#{project.path}-#{project.id}" }
+ context 'without a project' do
+ it 'leaves the namespace unset' do
+ expect(described_class.new.namespace).to be_nil
+ end
+ end
+ end
+
+ describe '#fields' do
+ let(:kube_namespace) do
+ subject.fields.find { |h| h[:name] == 'namespace' }
+ end
+
+ context 'as template' do
+ before { subject.template = true }
- it 'defaults to the project name with ID' do
- expect(described_class.new(project: project).namespace).to eq(namespace_name)
+ it 'sets the namespace to the default' do
+ expect(kube_namespace).not_to be_nil
+ expect(kube_namespace[:placeholder]).to eq(subject.class::TEMPLATE_PLACEHOLDER)
end
end
- context 'without a project' do
- it 'leaves the namespace unset' do
- expect(described_class.new.namespace).to be_nil
+ context 'with associated project' do
+ before { subject.project = project }
+
+ it 'sets the namespace to the default' do
+ expect(kube_namespace).not_to be_nil
+ expect(kube_namespace[:placeholder]).to match(/\A#{Gitlab::Regex::PATH_REGEX_STR}-\d+\z/)
end
end
end
@@ -138,38 +155,40 @@ describe KubernetesService, models: true, caching: true do
before do
subject.api_url = 'https://kube.domain.com'
subject.token = 'token'
- subject.namespace = 'my-project'
subject.ca_pem = 'CA PEM DATA'
+ subject.project = project
end
- it 'sets KUBE_URL' do
- expect(subject.predefined_variables).to include(
- { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true }
- )
- end
+ context 'namespace is provided' do
+ before { subject.namespace = 'my-project' }
- it 'sets KUBE_TOKEN' do
- expect(subject.predefined_variables).to include(
- { key: 'KUBE_TOKEN', value: 'token', public: false }
- )
+ it 'sets the variables' do
+ expect(subject.predefined_variables).to include(
+ { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true },
+ { key: 'KUBE_TOKEN', value: 'token', public: false },
+ { key: 'KUBE_NAMESPACE', value: 'my-project', public: true },
+ { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true },
+ { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true },
+ )
+ end
end
- it 'sets KUBE_NAMESPACE' do
- expect(subject.predefined_variables).to include(
- { key: 'KUBE_NAMESPACE', value: 'my-project', public: true }
- )
- end
+ context 'no namespace provided' do
+ it 'sets the variables' do
+ expect(subject.predefined_variables).to include(
+ { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true },
+ { key: 'KUBE_TOKEN', value: 'token', public: false },
+ { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true },
+ { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true },
+ )
+ end
- it 'sets KUBE_CA_PEM' do
- expect(subject.predefined_variables).to include(
- { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true }
- )
- end
+ it 'sets the KUBE_NAMESPACE' do
+ kube_namespace = subject.predefined_variables.find { |h| h[:key] == 'KUBE_NAMESPACE' }
- it 'sets KUBE_CA_PEM_FILE' do
- expect(subject.predefined_variables).to include(
- { key: 'KUBE_CA_PEM_FILE', value: 'CA PEM DATA', public: true, file: true }
- )
+ expect(kube_namespace).not_to be_nil
+ expect(kube_namespace[:value]).to match(/\A#{Gitlab::Regex::PATH_REGEX_STR}-\d+\z/)
+ end
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 274e4f00a0a..d805e65b3c6 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -24,21 +24,8 @@ describe Repository, models: true do
repository.commit(merge_commit_id)
end
- let(:author_email) { FFaker::Internet.email }
-
- # I have to remove periods from the end of the name
- # This happened when the user's name had a suffix (i.e. "Sr.")
- # This seems to be what git does under the hood. For example, this commit:
- #
- # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?'
- #
- # results in this:
- #
- # $ git show --pretty
- # ...
- # Author: Foo Sr <foo@example.com>
- # ...
- let(:author_name) { FFaker::Name.name.chomp("\.") }
+ let(:author_email) { 'user@example.org' }
+ let(:author_name) { 'John Doe' }
describe '#branch_names_contains' do
subject { repository.branch_names_contains(sample_commit.id) }
@@ -1083,7 +1070,7 @@ describe Repository, models: true do
end
end
- describe :skip_merged_commit do
+ describe 'skip_merges option' do
subject { repository.commits(Gitlab::Git::BRANCH_REF_PREFIX + "'test'", limit: 100, skip_merges: true).map{ |k| k.id } }
it { is_expected.not_to include('e56497bb5f03a90a51293fc6d516788730953899') }
@@ -1293,14 +1280,6 @@ describe Repository, models: true do
end
end
- describe '#before_import' do
- it 'flushes the repository caches' do
- expect(repository).to receive(:expire_content_cache)
-
- repository.before_import
- end
- end
-
describe '#after_import' do
it 'flushes and builds the cache' do
expect(repository).to receive(:expire_content_cache)
@@ -1851,4 +1830,17 @@ describe Repository, models: true do
end
end
end
+
+ describe '#is_ancestor?' do
+ context 'Gitaly is_ancestor feature enabled' do
+ it 'asks Gitaly server if it\'s an ancestor' do
+ commit = repository.commit
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(true)
+ expect(Gitlab::GitalyClient::Commit).to receive(:is_ancestor).
+ with(repository.raw_repository, commit.id, commit.id).and_return(true)
+
+ expect(repository.is_ancestor?(commit.id, commit.id)).to be true
+ end
+ end
+ end
end
diff --git a/spec/models/system_note_metadata_spec.rb b/spec/models/system_note_metadata_spec.rb
new file mode 100644
index 00000000000..d238e28209a
--- /dev/null
+++ b/spec/models/system_note_metadata_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe SystemNoteMetadata, models: true do
+ describe 'associations' do
+ it { is_expected.to belong_to(:note) }
+ end
+
+ describe 'validation' do
+ it { is_expected.to validate_presence_of(:note) }
+
+ context 'when action type is invalid' do
+ subject do
+ build(:system_note_metadata, note: build(:note), action: 'invalid_type' )
+ end
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'when action type is valid' do
+ subject do
+ build(:system_note_metadata, note: build(:note), action: 'merge' )
+ end
+
+ it { is_expected.to be_valid }
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 90378179e32..a9e37be1157 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -81,6 +81,7 @@ describe User, models: true do
it { is_expected.to validate_numericality_of(:projects_limit) }
it { is_expected.to allow_value(0).for(:projects_limit) }
it { is_expected.not_to allow_value(-1).for(:projects_limit) }
+ it { is_expected.not_to allow_value(Gitlab::Database::MAX_INT_VALUE + 1).for(:projects_limit) }
it { is_expected.to validate_length_of(:bio).is_at_most(255) }
@@ -360,22 +361,10 @@ describe User, models: true do
end
describe '#generate_password' do
- it "executes callback when force_random_password specified" do
- user = build(:user, force_random_password: true)
- expect(user).to receive(:generate_password)
- user.save
- end
-
it "does not generate password by default" do
user = create(:user, password: 'abcdefghe')
expect(user.password).to eq('abcdefghe')
end
-
- it "generates password when forcing random password" do
- allow(Devise).to receive(:friendly_token).and_return('123456789')
- user = create(:user, password: 'abcdefg', force_random_password: true)
- expect(user.password).to eq('12345678')
- end
end
describe 'authentication token' do
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index 7591bfd1471..2905d5b26a5 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -5,7 +5,7 @@ describe IssuePolicy, models: true do
describe '#rules' do
context 'using a regular issue' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:issue) { create(:issue, project: project) }
let(:policies) { described_class.abilities(user, issue).to_set }
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 0a5edf35f59..064847ee3dc 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -74,7 +74,7 @@ describe ProjectPolicy, models: true do
end
it 'does not include the read_issue permission when the issue author is not a member of the private project' do
- project = create(:project, :private)
+ project = create(:empty_project, :private)
issue = create(:issue, project: project)
user = issue.author
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index a7fad7f0bdb..8012530f139 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -11,21 +11,8 @@ describe API::Files, api: true do
ref: 'master'
}
end
- let(:author_email) { FFaker::Internet.email }
-
- # I have to remove periods from the end of the name
- # This happened when the user's name had a suffix (i.e. "Sr.")
- # This seems to be what git does under the hood. For example, this commit:
- #
- # $ git commit --author='Foo Sr. <foo@example.com>' -m 'Where's my trailing period?'
- #
- # results in this:
- #
- # $ git show --pretty
- # ...
- # Author: Foo Sr <foo@example.com>
- # ...
- let(:author_name) { FFaker::Name.name.chomp("\.") }
+ let(:author_email) { 'user@example.org' }
+ let(:author_name) { 'John Doe' }
before { project.team << [user, :developer] }
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 63ec00cdf04..eed45d37444 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -424,12 +424,12 @@ describe API::Internal, api: true do
end
before do
- allow(Gitlab.config.gitaly).to receive(:socket_path).and_return('path/to/gitaly.socket')
+ allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true)
end
it "calls the Gitaly client if it's enabled" do
expect_any_instance_of(Gitlab::GitalyClient::Notifications).
- to receive(:post_receive).with(project.repository.path)
+ to receive(:post_receive)
post api("/internal/notify_post_receive"), valid_params
@@ -438,7 +438,7 @@ describe API::Internal, api: true do
it "returns 500 if the gitaly call fails" do
expect_any_instance_of(Gitlab::GitalyClient::Notifications).
- to receive(:post_receive).with(project.repository.path).and_raise(GRPC::Unavailable)
+ to receive(:post_receive).and_raise(GRPC::Unavailable)
post api("/internal/notify_post_receive"), valid_params
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 52f68fed2cc..4729adba11c 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -19,7 +19,7 @@ describe API::Issues, api: true do
project: project,
state: :closed,
milestone: milestone,
- created_at: generate(:issue_created_at),
+ created_at: generate(:past_time),
updated_at: 3.hours.ago
end
let!(:confidential_issue) do
@@ -28,7 +28,7 @@ describe API::Issues, api: true do
project: project,
author: author,
assignee: assignee,
- created_at: generate(:issue_created_at),
+ created_at: generate(:past_time),
updated_at: 2.hours.ago
end
let!(:issue) do
@@ -37,7 +37,7 @@ describe API::Issues, api: true do
assignee: user,
project: project,
milestone: milestone,
- created_at: generate(:issue_created_at),
+ created_at: generate(:past_time),
updated_at: 1.hour.ago
end
let!(:label) do
@@ -333,8 +333,16 @@ describe API::Issues, api: true do
end
let(:base_url) { "/groups/#{group.id}/issues" }
+ it 'returns all group issues (including opened and closed)' do
+ get api(base_url, admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ end
+
it 'returns group issues without confidential issues for non project members' do
- get api(base_url, non_member)
+ get api("#{base_url}?state=opened", non_member)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -344,7 +352,7 @@ describe API::Issues, api: true do
end
it 'returns group confidential issues for author' do
- get api(base_url, author)
+ get api("#{base_url}?state=opened", author)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -353,7 +361,7 @@ describe API::Issues, api: true do
end
it 'returns group confidential issues for assignee' do
- get api(base_url, assignee)
+ get api("#{base_url}?state=opened", assignee)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -362,7 +370,7 @@ describe API::Issues, api: true do
end
it 'returns group issues with confidential issues for project members' do
- get api(base_url, user)
+ get api("#{base_url}?state=opened", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -371,7 +379,7 @@ describe API::Issues, api: true do
end
it 'returns group confidential issues for admin' do
- get api(base_url, admin)
+ get api("#{base_url}?state=opened", admin)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -460,7 +468,7 @@ describe API::Issues, api: true do
end
it 'returns an array of issues in given milestone' do
- get api("#{base_url}?milestone=#{group_milestone.title}", user)
+ get api("#{base_url}?state=opened&milestone=#{group_milestone.title}", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 2d37d026a39..84dca51801f 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::Members, api: true do
include ApiHelpers
- let(:master) { create(:user) }
+ let(:master) { create(:user, username: 'master_user') }
let(:developer) { create(:user) }
let(:access_requester) { create(:user) }
let(:stranger) { create(:user) }
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 9aba1d75612..61d965e8974 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -623,64 +623,6 @@ describe API::MergeRequests, api: true do
end
end
- describe "POST /projects/:id/merge_requests/:merge_request_iid/comments" do
- it "returns comment" do
- original_count = merge_request.notes.size
-
- post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user), note: "My comment"
-
- expect(response).to have_http_status(201)
- expect(json_response['note']).to eq('My comment')
- expect(json_response['author']['name']).to eq(user.name)
- expect(json_response['author']['username']).to eq(user.username)
- expect(merge_request.reload.notes.size).to eq(original_count + 1)
- end
-
- it "returns 400 if note is missing" do
- post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user)
- expect(response).to have_http_status(400)
- end
-
- it "returns 404 if merge request iid is invalid" do
- post api("/projects/#{project.id}/merge_requests/404/comments", user),
- note: 'My comment'
- expect(response).to have_http_status(404)
- end
-
- it "returns 404 if merge request id is used instead of iid" do
- post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user),
- note: 'My comment'
- expect(response).to have_http_status(404)
- end
- end
-
- describe "GET :id/merge_requests/:merge_request_iid/comments" do
- let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
- let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
-
- it "returns merge_request comments ordered by created_at" do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/comments", user)
-
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(2)
- expect(json_response.first['note']).to eq("a comment on a MR")
- expect(json_response.first['author']['id']).to eq(user.id)
- expect(json_response.last['note']).to eq("another comment on a MR")
- end
-
- it "returns a 404 error if merge_request_iid is invalid" do
- get api("/projects/#{project.id}/merge_requests/999/comments", user)
- expect(response).to have_http_status(404)
- end
-
- it "returns a 404 error if merge_request id is used instead of iid" do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user)
- expect(response).to have_http_status(404)
- end
- end
-
describe 'GET :id/merge_requests/:merge_request_iid/closes_issues' do
it 'returns the issue that will be closed on merge' do
issue = create(:issue, project: project)
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index 7fb728fed6f..598968aff70 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -306,6 +306,8 @@ describe API::Milestones, api: true do
end
it 'returns project merge_requests for a particular milestone' do
+ # eager-load another_merge_request
+ another_merge_request
get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user)
expect(response).to have_http_status(200)
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 347f8f6fa3b..d8eb8ce921e 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -34,7 +34,7 @@ describe API::Notes, api: true do
describe "GET /projects/:id/noteable/:noteable_id/notes" do
context "when noteable is an Issue" do
it "returns an array of issue notes" do
- get api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
+ get api("/projects/#{project.id}/issues/#{issue.iid}/notes", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -50,7 +50,7 @@ describe API::Notes, api: true do
context "and current user cannot view the notes" do
it "returns an empty array" do
- get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
+ get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -62,7 +62,7 @@ describe API::Notes, api: true do
before { ext_issue.update_attributes(confidential: true) }
it "returns 404" do
- get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
+ get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", user)
expect(response).to have_http_status(404)
end
@@ -70,7 +70,7 @@ describe API::Notes, api: true do
context "and current user can view the note" do
it "returns an empty array" do
- get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user)
+ get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", private_user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -106,7 +106,7 @@ describe API::Notes, api: true do
context "when noteable is a Merge Request" do
it "returns an array of merge_requests notes" do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/notes", user)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -131,21 +131,21 @@ describe API::Notes, api: true do
describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do
context "when noteable is an Issue" do
it "returns an issue note by id" do
- get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user)
+ get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{issue_note.id}", user)
expect(response).to have_http_status(200)
expect(json_response['body']).to eq(issue_note.note)
end
it "returns a 404 error if issue note not found" do
- get api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user)
+ get api("/projects/#{project.id}/issues/#{issue.iid}/notes/12345", user)
expect(response).to have_http_status(404)
end
context "and current user cannot view the note" do
it "returns a 404 error" do
- get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user)
+ get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes/#{cross_reference_note.id}", user)
expect(response).to have_http_status(404)
end
@@ -154,7 +154,7 @@ describe API::Notes, api: true do
before { issue.update_attributes(confidential: true) }
it "returns 404" do
- get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", private_user)
+ get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{issue_note.id}", private_user)
expect(response).to have_http_status(404)
end
@@ -162,7 +162,7 @@ describe API::Notes, api: true do
context "and current user can view the note" do
it "returns an issue note by id" do
- get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user)
+ get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes/#{cross_reference_note.id}", private_user)
expect(response).to have_http_status(200)
expect(json_response['body']).to eq(cross_reference_note.note)
@@ -190,7 +190,7 @@ describe API::Notes, api: true do
describe "POST /projects/:id/noteable/:noteable_id/notes" do
context "when noteable is an Issue" do
it "creates a new issue note" do
- post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
+ post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user), body: 'hi!'
expect(response).to have_http_status(201)
expect(json_response['body']).to eq('hi!')
@@ -198,13 +198,13 @@ describe API::Notes, api: true do
end
it "returns a 400 bad request error if body not given" do
- post api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
+ post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user)
expect(response).to have_http_status(400)
end
it "returns a 401 unauthorized error if user not authenticated" do
- post api("/projects/#{project.id}/issues/#{issue.id}/notes"), body: 'hi!'
+ post api("/projects/#{project.id}/issues/#{issue.iid}/notes"), body: 'hi!'
expect(response).to have_http_status(401)
end
@@ -212,7 +212,7 @@ describe API::Notes, api: true do
context 'when an admin or owner makes the request' do
it 'accepts the creation date to be set' do
creation_time = 2.weeks.ago
- post api("/projects/#{project.id}/issues/#{issue.id}/notes", user),
+ post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user),
body: 'hi!', created_at: creation_time
expect(response).to have_http_status(201)
@@ -226,7 +226,7 @@ describe API::Notes, api: true do
let(:issue2) { create(:issue, project: project) }
it 'creates a new issue note' do
- post api("/projects/#{project.id}/issues/#{issue2.id}/notes", user), body: ':+1:'
+ post api("/projects/#{project.id}/issues/#{issue2.iid}/notes", user), body: ':+1:'
expect(response).to have_http_status(201)
expect(json_response['body']).to eq(':+1:')
@@ -235,7 +235,7 @@ describe API::Notes, api: true do
context 'when the user is posting an award emoji on his/her own issue' do
it 'creates a new issue note' do
- post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: ':+1:'
+ post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user), body: ':+1:'
expect(response).to have_http_status(201)
expect(json_response['body']).to eq(':+1:')
@@ -270,7 +270,7 @@ describe API::Notes, api: true do
project = create(:empty_project, :private) { |p| p.add_guest(user) }
issue = create(:issue, :confidential, project: project)
- post api("/projects/#{project.id}/issues/#{issue.id}/notes", user),
+ post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user),
body: 'Foo'
expect(response).to have_http_status(404)
@@ -285,7 +285,7 @@ describe API::Notes, api: true do
# from a different project, see #15577
#
before do
- post api("/projects/#{project.id}/issues/#{private_issue.id}/notes", user),
+ post api("/projects/#{private_issue.project.id}/issues/#{private_issue.iid}/notes", user),
body: 'Hi!'
end
@@ -303,14 +303,14 @@ describe API::Notes, api: true do
it "creates an activity event when an issue note is created" do
expect(Event).to receive(:create)
- post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!'
+ post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user), body: 'hi!'
end
end
describe 'PUT /projects/:id/noteable/:noteable_id/notes/:note_id' do
context 'when noteable is an Issue' do
it 'returns modified note' do
- put api("/projects/#{project.id}/issues/#{issue.id}/"\
+ put api("/projects/#{project.id}/issues/#{issue.iid}/"\
"notes/#{issue_note.id}", user), body: 'Hello!'
expect(response).to have_http_status(200)
@@ -318,14 +318,14 @@ describe API::Notes, api: true do
end
it 'returns a 404 error when note id not found' do
- put api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user),
+ put api("/projects/#{project.id}/issues/#{issue.iid}/notes/12345", user),
body: 'Hello!'
expect(response).to have_http_status(404)
end
it 'returns a 400 bad request error if body not given' do
- put api("/projects/#{project.id}/issues/#{issue.id}/"\
+ put api("/projects/#{project.id}/issues/#{issue.iid}/"\
"notes/#{issue_note.id}", user)
expect(response).to have_http_status(400)
@@ -351,7 +351,7 @@ describe API::Notes, api: true do
context 'when noteable is a Merge Request' do
it 'returns modified note' do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/"\
"notes/#{merge_request_note.id}", user), body: 'Hello!'
expect(response).to have_http_status(200)
@@ -359,7 +359,7 @@ describe API::Notes, api: true do
end
it 'returns a 404 error when note id not found' do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/"\
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/"\
"notes/12345", user), body: "Hello!"
expect(response).to have_http_status(404)
@@ -370,18 +370,18 @@ describe API::Notes, api: true do
describe 'DELETE /projects/:id/noteable/:noteable_id/notes/:note_id' do
context 'when noteable is an Issue' do
it 'deletes a note' do
- delete api("/projects/#{project.id}/issues/#{issue.id}/"\
+ delete api("/projects/#{project.id}/issues/#{issue.iid}/"\
"notes/#{issue_note.id}", user)
expect(response).to have_http_status(204)
# Check if note is really deleted
- delete api("/projects/#{project.id}/issues/#{issue.id}/"\
+ delete api("/projects/#{project.id}/issues/#{issue.iid}/"\
"notes/#{issue_note.id}", user)
expect(response).to have_http_status(404)
end
it 'returns a 404 error when note id not found' do
- delete api("/projects/#{project.id}/issues/#{issue.id}/notes/12345", user)
+ delete api("/projects/#{project.id}/issues/#{issue.iid}/notes/12345", user)
expect(response).to have_http_status(404)
end
@@ -410,18 +410,18 @@ describe API::Notes, api: true do
context 'when noteable is a Merge Request' do
it 'deletes a note' do
delete api("/projects/#{project.id}/merge_requests/"\
- "#{merge_request.id}/notes/#{merge_request_note.id}", user)
+ "#{merge_request.iid}/notes/#{merge_request_note.id}", user)
expect(response).to have_http_status(204)
# Check if note is really deleted
delete api("/projects/#{project.id}/merge_requests/"\
- "#{merge_request.id}/notes/#{merge_request_note.id}", user)
+ "#{merge_request.iid}/notes/#{merge_request_note.id}", user)
expect(response).to have_http_status(404)
end
it 'returns a 404 error when note id not found' do
delete api("/projects/#{project.id}/merge_requests/"\
- "#{merge_request.id}/notes/12345", user)
+ "#{merge_request.iid}/notes/12345", user)
expect(response).to have_http_status(404)
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index c481b7e72b1..2e291eb3cea 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -341,7 +341,6 @@ describe API::Projects, :api do
it "assigns attributes to project" do
project = attributes_for(:project, {
path: 'camelCasePath',
- description: FFaker::Lorem.sentence,
issues_enabled: false,
merge_requests_enabled: false,
wiki_enabled: false,
@@ -475,7 +474,6 @@ describe API::Projects, :api do
it 'assigns attributes to project' do
project = attributes_for(:project, {
- description: FFaker::Lorem.sentence,
issues_enabled: false,
merge_requests_enabled: false,
wiki_enabled: false,
@@ -902,7 +900,7 @@ describe API::Projects, :api do
end
end
- describe :fork_admin do
+ describe 'fork management' do
let(:project_fork_target) { create(:empty_project) }
let(:project_fork_source) { create(:empty_project, :public) }
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 044b989e5ba..1cfac7353d4 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -461,6 +461,29 @@ describe API::Runner do
end
end
+ context 'when dependencies is an empty array' do
+ let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
+ let!(:empty_dependencies_job) do
+ create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'empty_dependencies_job',
+ stage: 'deploy', stage_idx: 1,
+ options: { dependencies: [] })
+ end
+
+ before do
+ job.success
+ job2.success
+ end
+
+ it 'returns an empty array' do
+ request_job
+
+ expect(response).to have_http_status(201)
+ expect(json_response['id']).to eq(empty_dependencies_job.id)
+ expect(json_response['dependencies'].count).to eq(0)
+ end
+ end
+
context 'when job has no tags' do
before { job.update(tags: []) }
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 04e7837fd7a..f793c0db2f3 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -676,7 +676,7 @@ describe API::Users, api: true do
before { admin }
it "deletes user" do
- delete api("/users/#{user.id}", admin)
+ Sidekiq::Testing.inline! { delete api("/users/#{user.id}", admin) }
expect(response).to have_http_status(204)
expect { User.find(user.id) }.to raise_error ActiveRecord::RecordNotFound
@@ -684,23 +684,23 @@ describe API::Users, api: true do
end
it "does not delete for unauthenticated user" do
- delete api("/users/#{user.id}")
+ Sidekiq::Testing.inline! { delete api("/users/#{user.id}") }
expect(response).to have_http_status(401)
end
it "is not available for non admin users" do
- delete api("/users/#{user.id}", user)
+ Sidekiq::Testing.inline! { delete api("/users/#{user.id}", user) }
expect(response).to have_http_status(403)
end
it "returns 404 for non-existing user" do
- delete api("/users/999999", admin)
+ Sidekiq::Testing.inline! { delete api("/users/999999", admin) }
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
it "returns a 404 for invalid ID" do
- delete api("/users/ASDF", admin)
+ Sidekiq::Testing.inline! { delete api("/users/ASDF", admin) }
expect(response).to have_http_status(404)
end
diff --git a/spec/requests/api/v3/files_spec.rb b/spec/requests/api/v3/files_spec.rb
index 3b61139a2cd..349fd6b3415 100644
--- a/spec/requests/api/v3/files_spec.rb
+++ b/spec/requests/api/v3/files_spec.rb
@@ -26,8 +26,8 @@ describe API::V3::Files, api: true do
ref: 'master'
}
end
- let(:author_email) { FFaker::Internet.email }
- let(:author_name) { FFaker::Name.name.chomp("\.") }
+ let(:author_email) { 'user@example.org' }
+ let(:author_name) { 'John Doe' }
before { project.team << [user, :developer] }
diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb
index 51021eec63c..b1b398a897e 100644
--- a/spec/requests/api/v3/issues_spec.rb
+++ b/spec/requests/api/v3/issues_spec.rb
@@ -19,7 +19,7 @@ describe API::V3::Issues, api: true do
project: project,
state: :closed,
milestone: milestone,
- created_at: generate(:issue_created_at),
+ created_at: generate(:past_time),
updated_at: 3.hours.ago
end
let!(:confidential_issue) do
@@ -28,7 +28,7 @@ describe API::V3::Issues, api: true do
project: project,
author: author,
assignee: assignee,
- created_at: generate(:issue_created_at),
+ created_at: generate(:past_time),
updated_at: 2.hours.ago
end
let!(:issue) do
@@ -37,7 +37,7 @@ describe API::V3::Issues, api: true do
assignee: user,
project: project,
milestone: milestone,
- created_at: generate(:issue_created_at),
+ created_at: generate(:past_time),
updated_at: 1.hour.ago
end
let!(:label) do
@@ -285,8 +285,16 @@ describe API::V3::Issues, api: true do
end
let(:base_url) { "/groups/#{group.id}/issues" }
+ it 'returns all group issues (including opened and closed)' do
+ get v3_api(base_url, admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ end
+
it 'returns group issues without confidential issues for non project members' do
- get v3_api(base_url, non_member)
+ get v3_api("#{base_url}?state=opened", non_member)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -295,7 +303,7 @@ describe API::V3::Issues, api: true do
end
it 'returns group confidential issues for author' do
- get v3_api(base_url, author)
+ get v3_api("#{base_url}?state=opened", author)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -303,7 +311,7 @@ describe API::V3::Issues, api: true do
end
it 'returns group confidential issues for assignee' do
- get v3_api(base_url, assignee)
+ get v3_api("#{base_url}?state=opened", assignee)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -311,7 +319,7 @@ describe API::V3::Issues, api: true do
end
it 'returns group issues with confidential issues for project members' do
- get v3_api(base_url, user)
+ get v3_api("#{base_url}?state=opened", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -319,7 +327,7 @@ describe API::V3::Issues, api: true do
end
it 'returns group confidential issues for admin' do
- get v3_api(base_url, admin)
+ get v3_api("#{base_url}?state=opened", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -368,7 +376,7 @@ describe API::V3::Issues, api: true do
end
it 'returns an array of issues in given milestone' do
- get v3_api("#{base_url}?milestone=#{group_milestone.title}", user)
+ get v3_api("#{base_url}?state=opened&milestone=#{group_milestone.title}", user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
diff --git a/spec/requests/api/v3/members_spec.rb b/spec/requests/api/v3/members_spec.rb
index 13814ed10c3..af1c5cff67f 100644
--- a/spec/requests/api/v3/members_spec.rb
+++ b/spec/requests/api/v3/members_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::V3::Members, api: true do
include ApiHelpers
- let(:master) { create(:user) }
+ let(:master) { create(:user, username: 'master_user') }
let(:developer) { create(:user) }
let(:access_requester) { create(:user) }
let(:stranger) { create(:user) }
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
index d8bb562587d..40531fe7545 100644
--- a/spec/requests/api/v3/projects_spec.rb
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -356,7 +356,6 @@ describe API::V3::Projects, api: true do
it "assigns attributes to project" do
project = attributes_for(:project, {
path: 'camelCasePath',
- description: FFaker::Lorem.sentence,
issues_enabled: false,
merge_requests_enabled: false,
wiki_enabled: false,
@@ -501,7 +500,6 @@ describe API::V3::Projects, api: true do
it 'assigns attributes to project' do
project = attributes_for(:project, {
- description: FFaker::Lorem.sentence,
issues_enabled: false,
merge_requests_enabled: false,
wiki_enabled: false,
@@ -949,7 +947,7 @@ describe API::V3::Projects, api: true do
end
end
- describe :fork_admin do
+ describe 'fork management' do
let(:project_fork_target) { create(:empty_project) }
let(:project_fork_source) { create(:empty_project, :public) }
diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb
index 17bbb0b53c1..b38cbe74b85 100644
--- a/spec/requests/api/v3/users_spec.rb
+++ b/spec/requests/api/v3/users_spec.rb
@@ -263,4 +263,18 @@ describe API::V3::Users, api: true do
expect(json_response['message']).to eq('404 User Not Found')
end
end
+
+ describe 'POST /users' do
+ it 'creates confirmed user when confirm parameter is false' do
+ optional_attributes = { confirm: false }
+ attributes = attributes_for(:user).merge(optional_attributes)
+
+ post v3_api('/users', admin), attributes
+
+ user_id = json_response['id']
+ new_user = User.find(user_id)
+
+ expect(new_user).to be_confirmed
+ end
+ end
end
diff --git a/spec/routing/environments_spec.rb b/spec/routing/environments_spec.rb
new file mode 100644
index 00000000000..ba124de70bb
--- /dev/null
+++ b/spec/routing/environments_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Projects::EnvironmentsController, :routing do
+ let(:project) { create(:empty_project) }
+
+ let(:environment) do
+ create(:environment, project: project,
+ name: 'staging-1.0/review')
+ end
+
+ let(:environments_route) do
+ "#{project.namespace.name}/#{project.name}/environments/"
+ end
+
+ describe 'routing environment folders' do
+ context 'when using JSON format' do
+ it 'correctly matches environment name and JSON format' do
+ expect(get_folder('staging-1.0.json'))
+ .to route_to(*folder_action(id: 'staging-1.0', format: 'json'))
+ end
+ end
+
+ context 'when using HTML format' do
+ it 'correctly matches environment name and HTML format' do
+ expect(get_folder('staging-1.0.html'))
+ .to route_to(*folder_action(id: 'staging-1.0', format: 'html'))
+ end
+ end
+
+ context 'when using implicit format' do
+ it 'correctly matches environment name' do
+ expect(get_folder('staging-1.0'))
+ .to route_to(*folder_action(id: 'staging-1.0'))
+ end
+ end
+ end
+
+ def get_folder(folder)
+ get("#{project.namespace.name}/#{project.name}/" \
+ "environments/folders/#{folder}")
+ end
+
+ def folder_action(**opts)
+ options = { namespace_id: project.namespace.name,
+ project_id: project.name }
+
+ ['projects/environments#folder', options.merge(opts)]
+ end
+end
diff --git a/spec/serializers/analytics_issue_serializer_spec.rb b/spec/serializers/analytics_issue_serializer_spec.rb
index 2f08958a783..ba24cf8e481 100644
--- a/spec/serializers/analytics_issue_serializer_spec.rb
+++ b/spec/serializers/analytics_issue_serializer_spec.rb
@@ -8,7 +8,7 @@ describe AnalyticsIssueSerializer do
end
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:resource) do
{
total_time: "172802.724419",
diff --git a/spec/serializers/analytics_merge_request_serializer_spec.rb b/spec/serializers/analytics_merge_request_serializer_spec.rb
index 62067cc0ef2..56cb08acfc6 100644
--- a/spec/serializers/analytics_merge_request_serializer_spec.rb
+++ b/spec/serializers/analytics_merge_request_serializer_spec.rb
@@ -8,7 +8,7 @@ describe AnalyticsMergeRequestSerializer do
end
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:resource) do
{
total_time: "172802.724419",
diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/build_entity_spec.rb
index 60c9642ee2c..7dcdf54fd93 100644
--- a/spec/serializers/build_entity_spec.rb
+++ b/spec/serializers/build_entity_spec.rb
@@ -1,10 +1,16 @@
require 'spec_helper'
describe BuildEntity do
+ let(:user) { create(:user) }
let(:build) { create(:ci_build) }
+ let(:request) { double('request') }
+
+ before do
+ allow(request).to receive(:user).and_return(user)
+ end
let(:entity) do
- described_class.new(build, request: double)
+ described_class.new(build, request: request)
end
subject { entity.as_json }
@@ -22,6 +28,11 @@ describe BuildEntity do
expect(subject).to include(:created_at, :updated_at)
end
+ it 'contains details' do
+ expect(subject).to include :status
+ expect(subject[:status]).to include :icon, :favicon, :text, :label
+ end
+
context 'when build is a regular job' do
it 'does not contain path to play action' do
expect(subject).not_to include(:play_path)
diff --git a/spec/serializers/build_serializer_spec.rb b/spec/serializers/build_serializer_spec.rb
new file mode 100644
index 00000000000..3cc791bca50
--- /dev/null
+++ b/spec/serializers/build_serializer_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe BuildSerializer do
+ let(:user) { create(:user) }
+
+ let(:serializer) do
+ described_class.new(user: user)
+ end
+
+ subject { serializer.represent(resource) }
+
+ describe '#represent' do
+ context 'when a single object is being serialized' do
+ let(:resource) { create(:ci_build) }
+
+ it 'serializers the pipeline object' do
+ expect(subject[:id]).to eq resource.id
+ end
+ end
+
+ context 'when multiple objects are being serialized' do
+ let(:resource) { create_list(:ci_build, 2) }
+
+ it 'serializers the array of pipelines' do
+ expect(subject).not_to be_empty
+ end
+ end
+ end
+
+ describe '#represent_status' do
+ context 'when represents only status' do
+ let(:resource) { create(:ci_build) }
+ let(:status) { resource.detailed_status(double('user')) }
+
+ subject { serializer.represent_status(resource) }
+
+ it 'serializes only status' do
+ expect(subject[:text]).to eq(status.text)
+ expect(subject[:label]).to eq(status.label)
+ expect(subject[:icon]).to eq(status.icon)
+ expect(subject[:favicon]).to eq(status.favicon)
+ end
+ end
+ end
+end
diff --git a/spec/serializers/commit_entity_spec.rb b/spec/serializers/commit_entity_spec.rb
index 0333d73b5b5..04247c78549 100644
--- a/spec/serializers/commit_entity_spec.rb
+++ b/spec/serializers/commit_entity_spec.rb
@@ -6,7 +6,7 @@ describe CommitEntity do
end
let(:request) { double('request') }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:commit) { project.commit }
subject { entity.as_json }
diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb
index ea87771e2a2..95eca5463eb 100644
--- a/spec/serializers/deployment_entity_spec.rb
+++ b/spec/serializers/deployment_entity_spec.rb
@@ -1,8 +1,15 @@
require 'spec_helper'
describe DeploymentEntity do
+ let(:user) { create(:user) }
+ let(:request) { double('request') }
+
+ before do
+ allow(request).to receive(:user).and_return(user)
+ end
+
let(:entity) do
- described_class.new(deployment, request: double)
+ described_class.new(deployment, request: request)
end
let(:deployment) { create(:deployment) }
diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb
index 57728ce3181..979d9921941 100644
--- a/spec/serializers/environment_entity_spec.rb
+++ b/spec/serializers/environment_entity_spec.rb
@@ -15,4 +15,24 @@ describe EnvironmentEntity do
it 'exposes core elements of environment' do
expect(subject).to include(:id, :name, :state, :environment_path)
end
+
+ context 'metrics disabled' do
+ before do
+ allow(environment).to receive(:has_metrics?).and_return(false)
+ end
+
+ it "doesn't expose metrics path" do
+ expect(subject).not_to include(:metrics_path)
+ end
+ end
+
+ context 'metrics enabled' do
+ before do
+ allow(environment).to receive(:has_metrics?).and_return(true)
+ end
+
+ it 'exposes metrics path' do
+ expect(subject).to include(:metrics_path)
+ end
+ end
end
diff --git a/spec/serializers/environment_serializer_spec.rb b/spec/serializers/environment_serializer_spec.rb
index 6a6df377b35..1909e6385b5 100644
--- a/spec/serializers/environment_serializer_spec.rb
+++ b/spec/serializers/environment_serializer_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe EnvironmentSerializer do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:json) do
described_class
@@ -11,21 +11,20 @@ describe EnvironmentSerializer do
end
context 'when there is a single object provided' do
- before do
- create(:ci_build, :manual, name: 'manual1',
- pipeline: deployable.pipeline)
- end
-
+ let(:project) { create(:project, :repository) }
+ let(:deployable) { create(:ci_build) }
let(:deployment) do
create(:deployment, deployable: deployable,
user: user,
project: project,
sha: project.commit.id)
end
-
- let(:deployable) { create(:ci_build) }
let(:resource) { deployment.environment }
+ before do
+ create(:ci_build, :manual, name: 'manual1', pipeline: deployable.pipeline)
+ end
+
it 'contains important elements of environment' do
expect(json)
.to include(:name, :external_url, :environment_path, :last_deployment)
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index ccb72973f9c..93d5a21419d 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -30,7 +30,7 @@ describe PipelineEntity do
.to include :duration, :finished_at
expect(subject[:details])
.to include :stages, :artifacts, :manual_actions
- expect(subject[:details][:status]).to include :icon, :text, :label
+ expect(subject[:details][:status]).to include :icon, :favicon, :text, :label
end
it 'contains flags' do
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index 2aaef03cb93..8642b803844 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -94,4 +94,20 @@ describe PipelineSerializer do
end
end
end
+
+ describe '#represent_status' do
+ context 'when represents only status' do
+ let(:resource) { create(:ci_pipeline) }
+ let(:status) { resource.detailed_status(double('user')) }
+
+ subject { serializer.represent_status(resource) }
+
+ it 'serializes only status' do
+ expect(subject[:text]).to eq(status.text)
+ expect(subject[:label]).to eq(status.label)
+ expect(subject[:icon]).to eq(status.icon)
+ expect(subject[:favicon]).to eq(status.favicon)
+ end
+ end
+ end
end
diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb
index 89428b4216e..c94902dbab8 100644
--- a/spec/serializers/status_entity_spec.rb
+++ b/spec/serializers/status_entity_spec.rb
@@ -16,7 +16,7 @@ describe StatusEntity do
subject { entity.as_json }
it 'contains status details' do
- expect(subject).to include :text, :icon, :label, :group
+ expect(subject).to include :text, :icon, :favicon, :label, :group
expect(subject).to include :has_details, :details_path
end
end
diff --git a/spec/services/after_branch_delete_service_spec.rb b/spec/services/after_branch_delete_service_spec.rb
index d29e0addb53..77ca17bc82c 100644
--- a/spec/services/after_branch_delete_service_spec.rb
+++ b/spec/services/after_branch_delete_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe AfterBranchDeleteService, services: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:service) { described_class.new(project, user) }
diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb
index 7b29b043296..a8555f5b4a0 100644
--- a/spec/services/boards/create_service_spec.rb
+++ b/spec/services/boards/create_service_spec.rb
@@ -15,7 +15,7 @@ describe Boards::CreateService, services: true do
board = service.execute
expect(board.lists.size).to eq 1
- expect(board.lists.first).to be_done
+ expect(board.lists.first).to be_closed
end
end
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index d841bdaa292..c982031c791 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -15,7 +15,7 @@ describe Boards::Issues::ListService, services: true do
let!(:list1) { create(:list, board: board, label: development, position: 0) }
let!(:list2) { create(:list, board: board, label: testing, position: 1) }
- let!(:done) { create(:done_list, board: board) }
+ let!(:closed) { create(:closed_list, board: board) }
let!(:opened_issue1) { create(:labeled_issue, project: project, labels: [bug]) }
let!(:opened_issue2) { create(:labeled_issue, project: project, labels: [p2]) }
@@ -53,8 +53,8 @@ describe Boards::Issues::ListService, services: true do
expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1]
end
- it 'returns closed issues when listing issues from Done' do
- params = { board_id: board.id, id: done.id }
+ it 'returns closed issues when listing issues from Closed' do
+ params = { board_id: board.id, id: closed.id }
issues = described_class.new(project, user, params).execute
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
index 727ea04ea5c..4ff7ac6bb2f 100644
--- a/spec/services/boards/issues/move_service_spec.rb
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -12,7 +12,7 @@ describe Boards::Issues::MoveService, services: true do
let!(:list1) { create(:list, board: board1, label: development, position: 0) }
let!(:list2) { create(:list, board: board1, label: testing, position: 1) }
- let!(:done) { create(:done_list, board: board1) }
+ let!(:closed) { create(:closed_list, board: board1) }
before do
project.team << [user, :developer]
@@ -35,13 +35,13 @@ describe Boards::Issues::MoveService, services: true do
end
end
- context 'when moving to done' do
+ context 'when moving to closed' do
let(:board2) { create(:board, project: project) }
let(:regression) { create(:label, project: project, name: 'Regression') }
let!(:list3) { create(:list, board: board2, label: regression, position: 1) }
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development, testing, regression]) }
- let(:params) { { board_id: board1.id, from_list_id: list2.id, to_list_id: done.id } }
+ let(:params) { { board_id: board1.id, from_list_id: list2.id, to_list_id: closed.id } }
it 'delegates the close proceedings to Issues::CloseService' do
expect_any_instance_of(Issues::CloseService).to receive(:execute).with(issue).once
@@ -58,9 +58,9 @@ describe Boards::Issues::MoveService, services: true do
end
end
- context 'when moving from done' do
+ context 'when moving from closed' do
let(:issue) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
- let(:params) { { board_id: board1.id, from_list_id: done.id, to_list_id: list2.id } }
+ let(:params) { { board_id: board1.id, from_list_id: closed.id, to_list_id: list2.id } }
it 'delegates the re-open proceedings to Issues::ReopenService' do
expect_any_instance_of(Issues::ReopenService).to receive(:execute).with(issue).once
diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb
index a30860f828a..af2d7c784bb 100644
--- a/spec/services/boards/lists/destroy_service_spec.rb
+++ b/spec/services/boards/lists/destroy_service_spec.rb
@@ -18,18 +18,18 @@ describe Boards::Lists::DestroyService, services: true do
development = create(:list, board: board, position: 0)
review = create(:list, board: board, position: 1)
staging = create(:list, board: board, position: 2)
- done = board.done_list
+ closed = board.closed_list
described_class.new(project, user).execute(development)
expect(review.reload.position).to eq 0
expect(staging.reload.position).to eq 1
- expect(done.reload.position).to be_nil
+ expect(closed.reload.position).to be_nil
end
end
- it 'does not remove list from board when list type is done' do
- list = board.done_list
+ it 'does not remove list from board when list type is closed' do
+ list = board.closed_list
service = described_class.new(project, user)
expect { service.execute(list) }.not_to change(board.lists, :count)
diff --git a/spec/services/boards/lists/list_service_spec.rb b/spec/services/boards/lists/list_service_spec.rb
index 2dffc62b215..ab9fb1bc914 100644
--- a/spec/services/boards/lists/list_service_spec.rb
+++ b/spec/services/boards/lists/list_service_spec.rb
@@ -10,7 +10,7 @@ describe Boards::Lists::ListService, services: true do
service = described_class.new(project, double)
- expect(service.execute(board)).to eq [list, board.done_list]
+ expect(service.execute(board)).to eq [list, board.closed_list]
end
end
end
diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb
index 3786dc82bf0..4b3bdd133f2 100644
--- a/spec/services/boards/lists/move_service_spec.rb
+++ b/spec/services/boards/lists/move_service_spec.rb
@@ -10,7 +10,7 @@ describe Boards::Lists::MoveService, services: true do
let!(:development) { create(:list, board: board, position: 1) }
let!(:review) { create(:list, board: board, position: 2) }
let!(:staging) { create(:list, board: board, position: 3) }
- let!(:done) { create(:done_list, board: board) }
+ let!(:closed) { create(:closed_list, board: board) }
context 'when list type is set to label' do
it 'keeps position of lists when new position is nil' do
@@ -86,10 +86,10 @@ describe Boards::Lists::MoveService, services: true do
end
end
- it 'keeps position of lists when list type is done' do
+ it 'keeps position of lists when list type is closed' do
service = described_class.new(project, user, position: 2)
- service.execute(done)
+ service.execute(closed)
expect(current_list_positions).to eq [0, 1, 2, 3]
end
diff --git a/spec/services/chat_names/find_user_service_spec.rb b/spec/services/chat_names/find_user_service_spec.rb
index 51441e8f3be..0dc96521fa8 100644
--- a/spec/services/chat_names/find_user_service_spec.rb
+++ b/spec/services/chat_names/find_user_service_spec.rb
@@ -18,9 +18,16 @@ describe ChatNames::FindUserService, services: true do
end
it 'updates when last time chat name was used' do
+ expect(chat_name.last_used_at).to be_nil
+
subject
- expect(chat_name.reload.last_used_at).to be_like_time(Time.now)
+ initial_last_used = chat_name.reload.last_used_at
+ expect(initial_last_used).to be_present
+
+ Timecop.travel(2.days.from_now) { described_class.new(service, params).execute }
+
+ expect(chat_name.reload.last_used_at).to be > initial_last_used
end
end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index a969829a63e..d2f0337c260 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Ci::CreatePipelineService, services: true do
- let(:project) { FactoryGirl.create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:admin) }
before do
diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb
index 5e68343784d..5a20102872a 100644
--- a/spec/services/ci/create_trigger_request_service_spec.rb
+++ b/spec/services/ci/create_trigger_request_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Ci::CreateTriggerRequestService, services: true do
let(:service) { described_class.new }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:trigger) { create(:ci_trigger, project: project) }
before do
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index d93616c4f50..bb98fb37a90 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -418,65 +418,6 @@ describe Ci::ProcessPipelineService, '#execute', :services do
end
end
- context 'when there are builds that are not created yet' do
- let(:pipeline) do
- create(:ci_pipeline, config: config)
- end
-
- let(:config) do
- { rspec: { stage: 'test', script: 'rspec' },
- deploy: { stage: 'deploy', script: 'rsync' } }
- end
-
- before do
- create_build('linux', stage: 'build', stage_idx: 0)
- create_build('mac', stage: 'build', stage_idx: 0)
- end
-
- it 'processes the pipeline' do
- # Currently we have five builds with state created
- #
- expect(builds.count).to eq(0)
- expect(all_builds.count).to eq(2)
-
- # Process builds service will enqueue builds from the first stage.
- #
- process_pipeline
-
- expect(builds.count).to eq(2)
- expect(all_builds.count).to eq(2)
-
- # When builds succeed we will enqueue remaining builds.
- #
- # We will have 2 succeeded, 1 pending (from stage test), total 4 (two
- # additional build from `.gitlab-ci.yml`).
- #
- succeed_pending
- process_pipeline
-
- expect(builds.success.count).to eq(2)
- expect(builds.pending.count).to eq(1)
- expect(all_builds.count).to eq(4)
-
- # When pending merge_when_pipeline_succeeds in stage test, we enqueue deploy stage.
- #
- succeed_pending
- process_pipeline
-
- expect(builds.pending.count).to eq(1)
- expect(builds.success.count).to eq(3)
- expect(all_builds.count).to eq(4)
-
- # When the last one succeeds we have 4 successful builds.
- #
- succeed_pending
- process_pipeline
-
- expect(builds.success.count).to eq(4)
- expect(all_builds.count).to eq(4)
- end
- end
-
def process_pipeline
described_class.new(pipeline.project, user).execute(pipeline)
end
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index 5445b65f4e8..f1b2d3a4798 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -9,6 +9,19 @@ describe Ci::RetryPipelineService, '#execute', :services do
context 'when user has ability to modify pipeline' do
let(:user) { create(:admin) }
+ context 'when there are already retried jobs present' do
+ before do
+ create_build('rspec', :canceled, 0)
+ create_build('rspec', :failed, 0)
+ end
+
+ it 'does not retry jobs that has already been retried' do
+ expect(statuses.first).to be_retried
+ expect { service.execute(pipeline) }
+ .to change { CommitStatus.count }.by(1)
+ end
+ end
+
context 'when there are failed builds in the last stage' do
before do
create_build('rspec 1', :success, 0)
diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb
index 560f83d94f7..32c72a9cf5e 100644
--- a/spec/services/ci/stop_environments_service_spec.rb
+++ b/spec/services/ci/stop_environments_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Ci::StopEnvironmentsService, services: true do
- let(:project) { create(:project, :private) }
+ let(:project) { create(:project, :private, :repository) }
let(:user) { create(:user) }
let(:service) { described_class.new(project, user) }
diff --git a/spec/services/ci/update_build_queue_service_spec.rb b/spec/services/ci/update_build_queue_service_spec.rb
index f01a388b895..c44e6b2a48b 100644
--- a/spec/services/ci/update_build_queue_service_spec.rb
+++ b/spec/services/ci/update_build_queue_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Ci::UpdateBuildQueueService, :services do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:pipeline) { create(:ci_pipeline, project: project) }
diff --git a/spec/services/compare_service_spec.rb b/spec/services/compare_service_spec.rb
index 0a7fc58523f..bea7c965233 100644
--- a/spec/services/compare_service_spec.rb
+++ b/spec/services/compare_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe CompareService, services: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:service) { described_class.new(project, 'feature') }
diff --git a/spec/services/create_release_service_spec.rb b/spec/services/create_release_service_spec.rb
index 61e5ae72f51..271ccfe7968 100644
--- a/spec/services/create_release_service_spec.rb
+++ b/spec/services/create_release_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe CreateReleaseService, services: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:tag_name) { project.repository.tag_names.first }
let(:description) { 'Awesome release!' }
diff --git a/spec/services/delete_branch_service_spec.rb b/spec/services/delete_branch_service_spec.rb
index 336f5dafb5b..c4685c9aa31 100644
--- a/spec/services/delete_branch_service_spec.rb
+++ b/spec/services/delete_branch_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe DeleteBranchService, services: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:user) { create(:user) }
let(:service) { described_class.new(project, user) }
diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/delete_merged_branches_service_spec.rb
index 181488e89c7..a41a421fa6e 100644
--- a/spec/services/delete_merged_branches_service_spec.rb
+++ b/spec/services/delete_merged_branches_service_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe DeleteMergedBranchesService, services: true do
subject(:service) { described_class.new(project, project.owner) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
context '#execute' do
context 'unprotected branches' do
diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb
index 35e6e139238..26aa5b432d4 100644
--- a/spec/services/files/update_service_spec.rb
+++ b/spec/services/files/update_service_spec.rb
@@ -3,7 +3,7 @@ require "spec_helper"
describe Files::UpdateService do
subject { described_class.new(project, user, commit_params) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:file_path) { 'files/ruby/popen.rb' }
let(:new_contents) { 'New Content' }
diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb
index 3318dfb22b6..ac7ccfbaab0 100644
--- a/spec/services/git_hooks_service_spec.rb
+++ b/spec/services/git_hooks_service_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
describe GitHooksService, services: true do
include RepoHelpers
- let(:user) { create :user }
- let(:project) { create :project }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
let(:service) { GitHooksService.new }
before do
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index bd71618e6f4..0477cac6677 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
describe GitPushService, services: true do
include RepoHelpers
- let(:user) { create :user }
- let(:project) { create :project }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
before do
project.team << [user, :master]
diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb
index bd074b9bd71..b73beb3f6fc 100644
--- a/spec/services/git_tag_push_service_spec.rb
+++ b/spec/services/git_tag_push_service_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
describe GitTagPushService, services: true do
include RepoHelpers
- let(:user) { create :user }
- let(:project) { create :project }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
let(:service) { GitTagPushService.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) }
let(:oldrev) { Gitlab::Git::BLANK_SHA }
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index ec89b540e6a..bcb62429275 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -44,7 +44,7 @@ describe Groups::CreateService, '#execute', services: true do
let!(:service) { described_class.new(user, params) }
before do
- Settings.mattermost['enabled'] = true
+ stub_mattermost_setting(enabled: true)
end
it 'create the chat team with the group' do
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index 98c560ffb26..2ee11fc8b4c 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -6,7 +6,7 @@ describe Groups::DestroyService, services: true do
let!(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
- let!(:project) { create(:project, namespace: group) }
+ let!(:project) { create(:empty_project, namespace: group) }
let!(:gitlab_shell) { Gitlab::Shell.new }
let!(:remove_path) { group.path + "+#{group.id}+deleted" }
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index 7c0fccb9d41..f6ad5cebd2c 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -13,7 +13,7 @@ describe Groups::UpdateService, services: true do
before do
public_group.add_user(user, Gitlab::Access::MASTER)
- create(:project, :public, group: public_group)
+ create(:empty_project, :public, group: public_group)
end
it "does not change permission level" do
@@ -27,7 +27,7 @@ describe Groups::UpdateService, services: true do
before do
internal_group.add_user(user, Gitlab::Access::MASTER)
- create(:project, :internal, group: internal_group)
+ create(:empty_project, :internal, group: internal_group)
end
it "does not change permission level" do
@@ -36,6 +36,20 @@ describe Groups::UpdateService, services: true do
end
end
end
+
+ context "with parent_id user doesn't have permissions for" do
+ let(:service) { described_class.new(public_group, user, parent_id: private_group.id) }
+
+ before do
+ service.execute
+ end
+
+ it 'does not update parent_id' do
+ updated_group = public_group.reload
+
+ expect(updated_group.parent_id).to be_nil
+ end
+ end
end
context "unauthorized visibility_level validation" do
@@ -55,7 +69,7 @@ describe Groups::UpdateService, services: true do
before do
internal_group.add_user(user, Gitlab::Access::MASTER)
- create(:project, :internal, group: internal_group)
+ create(:empty_project, :internal, group: internal_group)
end
it 'returns true' do
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index 1dd53236fbd..17990f41b3b 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper.rb'
describe Issues::BuildService, services: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
before do
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index db196ed5751..9f8346d52bb 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -5,8 +5,8 @@ describe Issues::MoveService, services: true do
let(:author) { create(:user) }
let(:title) { 'Some issue' }
let(:description) { 'Some issue description' }
- let(:old_project) { create(:project) }
- let(:new_project) { create(:project) }
+ let(:old_project) { create(:empty_project) }
+ let(:new_project) { create(:empty_project) }
let(:milestone1) { create(:milestone, project_id: old_project.id, title: 'v9.0') }
let(:old_issue) do
diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb
index 6cc738aec08..3a72f92383c 100644
--- a/spec/services/issues/resolve_discussions_spec.rb
+++ b/spec/services/issues/resolve_discussions_spec.rb
@@ -10,7 +10,7 @@ class DummyService < Issues::BaseService
end
describe DummyService, services: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
before do
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index fa472f3e2c3..5b324f3c706 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -13,6 +13,7 @@ describe Issues::UpdateService, services: true do
let(:issue) do
create(:issue, title: 'Old title',
+ description: "for #{user2.to_reference}",
assignee_id: user3.id,
project: project)
end
@@ -182,16 +183,24 @@ describe Issues::UpdateService, services: true do
it 'marks pending todos as done' do
expect(todo.reload.done?).to eq true
end
+
+ it 'does not create any new todos' do
+ expect(Todo.count).to eq(1)
+ end
end
context 'when the description change' do
before do
- update_issue(description: 'Also please fix')
+ update_issue(description: "Also please fix #{user2.to_reference} #{user3.to_reference}")
end
it 'marks todos as done' do
expect(todo.reload.done?).to eq true
end
+
+ it 'creates only 1 new todo' do
+ expect(Todo.count).to eq(2)
+ end
end
context 'when is reassigned' do
diff --git a/spec/services/labels/create_service_spec.rb b/spec/services/labels/create_service_spec.rb
new file mode 100644
index 00000000000..0ecab0c3475
--- /dev/null
+++ b/spec/services/labels/create_service_spec.rb
@@ -0,0 +1,186 @@
+require 'spec_helper'
+
+describe Labels::CreateService, services: true do
+ describe '#execute' do
+ let(:project) { create(:project) }
+ let(:group) { create(:group) }
+
+ let(:hex_color) { '#FF0000' }
+ let(:named_color) { 'red' }
+ let(:upcase_color) { 'RED' }
+ let(:spaced_color) { ' red ' }
+ let(:unknown_color) { 'unknown' }
+ let(:no_color) { '' }
+
+ let(:expected_saved_color) { hex_color }
+
+ context 'in a project' do
+ context 'with color in hex-code' do
+ it 'creates a label' do
+ label = Labels::CreateService.new(params_with(hex_color)).execute(project: project)
+
+ expect(label).to be_persisted
+ expect(label.color).to eq expected_saved_color
+ end
+ end
+
+ context 'with color in allowed name' do
+ it 'creates a label' do
+ label = Labels::CreateService.new(params_with(named_color)).execute(project: project)
+
+ expect(label).to be_persisted
+ expect(label.color).to eq expected_saved_color
+ end
+ end
+
+ context 'with color in up-case allowed name' do
+ it 'creates a label' do
+ label = Labels::CreateService.new(params_with(upcase_color)).execute(project: project)
+
+ expect(label).to be_persisted
+ expect(label.color).to eq expected_saved_color
+ end
+ end
+
+ context 'with color surrounded by spaces' do
+ it 'creates a label' do
+ label = Labels::CreateService.new(params_with(spaced_color)).execute(project: project)
+
+ expect(label).to be_persisted
+ expect(label.color).to eq expected_saved_color
+ end
+ end
+
+ context 'with unknown color' do
+ it 'doesn\'t create a label' do
+ label = Labels::CreateService.new(params_with(unknown_color)).execute(project: project)
+
+ expect(label).not_to be_persisted
+ end
+ end
+
+ context 'with no color' do
+ it 'doesn\'t create a label' do
+ label = Labels::CreateService.new(params_with(no_color)).execute(project: project)
+
+ expect(label).not_to be_persisted
+ end
+ end
+ end
+
+ context 'in a group' do
+ context 'with color in hex-code' do
+ it 'creates a label' do
+ label = Labels::CreateService.new(params_with(hex_color)).execute(group: group)
+
+ expect(label).to be_persisted
+ expect(label.color).to eq expected_saved_color
+ end
+ end
+
+ context 'with color in allowed name' do
+ it 'creates a label' do
+ label = Labels::CreateService.new(params_with(named_color)).execute(group: group)
+
+ expect(label).to be_persisted
+ expect(label.color).to eq expected_saved_color
+ end
+ end
+
+ context 'with color in up-case allowed name' do
+ it 'creates a label' do
+ label = Labels::CreateService.new(params_with(upcase_color)).execute(group: group)
+
+ expect(label).to be_persisted
+ expect(label.color).to eq expected_saved_color
+ end
+ end
+
+ context 'with color surrounded by spaces' do
+ it 'creates a label' do
+ label = Labels::CreateService.new(params_with(spaced_color)).execute(group: group)
+
+ expect(label).to be_persisted
+ expect(label.color).to eq expected_saved_color
+ end
+ end
+
+ context 'with unknown color' do
+ it 'doesn\'t create a label' do
+ label = Labels::CreateService.new(params_with(unknown_color)).execute(group: group)
+
+ expect(label).not_to be_persisted
+ end
+ end
+
+ context 'with no color' do
+ it 'doesn\'t create a label' do
+ label = Labels::CreateService.new(params_with(no_color)).execute(group: group)
+
+ expect(label).not_to be_persisted
+ end
+ end
+ end
+
+ context 'in admin area' do
+ context 'with color in hex-code' do
+ it 'creates a label' do
+ label = Labels::CreateService.new(params_with(hex_color)).execute(template: true)
+
+ expect(label).to be_persisted
+ expect(label.color).to eq expected_saved_color
+ end
+ end
+
+ context 'with color in allowed name' do
+ it 'creates a label' do
+ label = Labels::CreateService.new(params_with(named_color)).execute(template: true)
+
+ expect(label).to be_persisted
+ expect(label.color).to eq expected_saved_color
+ end
+ end
+
+ context 'with color in up-case allowed name' do
+ it 'creates a label' do
+ label = Labels::CreateService.new(params_with(upcase_color)).execute(template: true)
+
+ expect(label).to be_persisted
+ expect(label.color).to eq expected_saved_color
+ end
+ end
+
+ context 'with color surrounded by spaces' do
+ it 'creates a label' do
+ label = Labels::CreateService.new(params_with(spaced_color)).execute(template: true)
+
+ expect(label).to be_persisted
+ expect(label.color).to eq expected_saved_color
+ end
+ end
+
+ context 'with unknown color' do
+ it 'doesn\'t create a label' do
+ label = Labels::CreateService.new(params_with(unknown_color)).execute(template: true)
+
+ expect(label).not_to be_persisted
+ end
+ end
+
+ context 'with no color' do
+ it 'doesn\'t create a label' do
+ label = Labels::CreateService.new(params_with(no_color)).execute(template: true)
+
+ expect(label).not_to be_persisted
+ end
+ end
+ end
+ end
+
+ def params_with(color)
+ {
+ title: 'A Label',
+ color: color
+ }
+ end
+end
diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb
index 7a9b34f9f96..de8f1827cce 100644
--- a/spec/services/labels/find_or_create_service_spec.rb
+++ b/spec/services/labels/find_or_create_service_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Labels::FindOrCreateService, services: true do
describe '#execute' do
let(:group) { create(:group) }
- let(:project) { create(:project, namespace: group) }
+ let(:project) { create(:empty_project, namespace: group) }
let(:params) do
{
diff --git a/spec/services/labels/transfer_service_spec.rb b/spec/services/labels/transfer_service_spec.rb
index 13654a0881c..11d5f1cad5e 100644
--- a/spec/services/labels/transfer_service_spec.rb
+++ b/spec/services/labels/transfer_service_spec.rb
@@ -6,8 +6,8 @@ describe Labels::TransferService, services: true do
let(:group_1) { create(:group) }
let(:group_2) { create(:group) }
let(:group_3) { create(:group) }
- let(:project_1) { create(:project, namespace: group_2) }
- let(:project_2) { create(:project, namespace: group_3) }
+ let(:project_1) { create(:empty_project, namespace: group_2) }
+ let(:project_2) { create(:empty_project, namespace: group_3) }
let(:group_label_1) { create(:group_label, group: group_1, name: 'Group Label 1') }
let(:group_label_2) { create(:group_label, group: group_1, name: 'Group Label 2') }
diff --git a/spec/services/labels/update_service_spec.rb b/spec/services/labels/update_service_spec.rb
new file mode 100644
index 00000000000..f2498ea6990
--- /dev/null
+++ b/spec/services/labels/update_service_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+describe Labels::UpdateService, services: true do
+ describe '#execute' do
+ let(:project) { create(:project) }
+
+ let(:hex_color) { '#FF0000' }
+ let(:named_color) { 'red' }
+ let(:upcase_color) { 'RED' }
+ let(:spaced_color) { ' red ' }
+ let(:unknown_color) { 'unknown' }
+ let(:no_color) { '' }
+
+ let(:expected_saved_color) { hex_color }
+
+ before(:each) do
+ @label = Labels::CreateService.new(title: 'Initial', color: '#000000').execute(project: project)
+ expect(@label).to be_persisted
+ end
+
+ context 'with color in hex-code' do
+ it 'updates the label' do
+ label = Labels::UpdateService.new(params_with(hex_color)).execute(@label)
+
+ expect(label).to be_valid
+ expect(label.reload.color).to eq expected_saved_color
+ end
+ end
+
+ context 'with color in allowed name' do
+ it 'updates the label' do
+ label = Labels::UpdateService.new(params_with(named_color)).execute(@label)
+
+ expect(label).to be_valid
+ expect(label.reload.color).to eq expected_saved_color
+ end
+ end
+
+ context 'with color in up-case allowed name' do
+ it 'updates the label' do
+ label = Labels::UpdateService.new(params_with(upcase_color)).execute(@label)
+
+ expect(label).to be_valid
+ expect(label.reload.color).to eq expected_saved_color
+ end
+ end
+
+ context 'with color surrounded by spaces' do
+ it 'updates the label' do
+ label = Labels::UpdateService.new(params_with(spaced_color)).execute(@label)
+
+ expect(label).to be_valid
+ expect(label.reload.color).to eq expected_saved_color
+ end
+ end
+
+ context 'with unknown color' do
+ it 'doesn\'t update the label' do
+ label = Labels::UpdateService.new(params_with(unknown_color)).execute(@label)
+
+ expect(label).not_to be_valid
+ end
+ end
+
+ context 'with no color' do
+ it 'doesn\'t update the label' do
+ label = Labels::UpdateService.new(params_with(no_color)).execute(@label)
+
+ expect(label).not_to be_valid
+ end
+ end
+ end
+
+ def params_with(color)
+ {
+ title: 'A Label',
+ color: color
+ }
+ end
+end
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index 574df6e0f42..41450c67d7e 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Members::DestroyService, services: true do
let(:user) { create(:user) }
let(:member_user) { create(:user) }
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:group) { create(:group, :public) }
shared_examples 'a service raising ActiveRecord::RecordNotFound' do
diff --git a/spec/services/members/request_access_service_spec.rb b/spec/services/members/request_access_service_spec.rb
index 853c125dadb..814c17b9ad0 100644
--- a/spec/services/members/request_access_service_spec.rb
+++ b/spec/services/members/request_access_service_spec.rb
@@ -29,7 +29,7 @@ describe Members::RequestAccessService, services: true do
end
context 'when current user cannot request access to the project' do
- %i[project group].each do |source_type|
+ %i[empty_project group].each do |source_type|
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { create(source_type, :private) }
end
@@ -37,7 +37,7 @@ describe Members::RequestAccessService, services: true do
end
context 'when access requests are disabled' do
- %i[project group].each do |source_type|
+ %i[empty_project group].each do |source_type|
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { create(source_type, :public) }
end
@@ -45,7 +45,7 @@ describe Members::RequestAccessService, services: true do
end
context 'when current user can request access to the project' do
- %i[project group].each do |source_type|
+ %i[empty_project group].each do |source_type|
it_behaves_like 'a service creating a access request' do
let(:source) { create(source_type, :public, :access_requestable) }
end
diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
index d80fb8a1af1..af0a214c00f 100644
--- a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
+++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe MergeRequests::AddTodoWhenBuildFailsService do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:sha) { '1234567890abcdef1234567890abcdef12345678' }
let(:ref) { merge_request.source_branch }
diff --git a/spec/services/merge_requests/assign_issues_service_spec.rb b/spec/services/merge_requests/assign_issues_service_spec.rb
index 5034b6ef33f..fe75757dd29 100644
--- a/spec/services/merge_requests/assign_issues_service_spec.rb
+++ b/spec/services/merge_requests/assign_issues_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe MergeRequests::AssignIssuesService, services: true do
let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
let(:issue) { create(:issue, project: project) }
let(:merge_request) { create(:merge_request, :simple, source_project: project, author: user, description: "fixes #{issue.to_reference}") }
let(:service) { described_class.new(project, user, merge_request: merge_request) }
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index adfa75a524f..c8bd4d4601a 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe MergeRequests::BuildService, services: true do
include RepoHelpers
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:issue_confidential) { false }
let(:issue) { create(:issue, project: project, title: 'A bug', confidential: issue_confidential) }
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index 673c0bd6c9c..0e16c7cc94b 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe MergeRequests::CreateService, services: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:assignee) { create(:user) }
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
index b7a05907208..290e00ea1ba 100644
--- a/spec/services/merge_requests/get_urls_service_spec.rb
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe MergeRequests::GetUrlsService do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
let(:service) { MergeRequests::GetUrlsService.new(project) }
let(:source_branch) { "my_branch" }
let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" }
diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
index c2f205c389d..769b3193275 100644
--- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
+++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe MergeRequests::MergeWhenPipelineSucceedsService do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:mr_merge_if_green_enabled) do
create(:merge_request, merge_when_pipeline_succeeds: true, merge_user: user,
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 92729f68e5f..c22d145ca5d 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe MergeRequests::RefreshService, services: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:service) { MergeRequests::RefreshService }
@@ -11,7 +11,7 @@ describe MergeRequests::RefreshService, services: true do
group = create(:group)
group.add_owner(@user)
- @project = create(:project, namespace: group)
+ @project = create(:project, :repository, namespace: group)
@fork_project = Projects::ForkService.new(@project, @user).execute
@merge_request = create(:merge_request,
source_project: @project,
@@ -252,7 +252,7 @@ describe MergeRequests::RefreshService, services: true do
context 'when the merge request is sourced from a different project' do
it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do
- forked_project = create(:project)
+ forked_project = create(:project, :repository)
create(:forked_project_link, forked_to_project: forked_project, forked_from_project: @project)
merge_request = create(:merge_request,
diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb
index d33535d22af..eaf7785e549 100644
--- a/spec/services/merge_requests/resolve_service_spec.rb
+++ b/spec/services/merge_requests/resolve_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe MergeRequests::ResolveService do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:fork_project) do
create(:forked_project_with_submodules) do |fork_project|
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 7d73c0ea5d0..f2ca1e6fcbd 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe MergeRequests::UpdateService, services: true do
include EmailHelpers
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
@@ -12,6 +12,7 @@ describe MergeRequests::UpdateService, services: true do
let(:merge_request) do
create(:merge_request, :simple, title: 'Old title',
+ description: "FYI #{user2.to_reference}",
assignee_id: user3.id,
source_project: project)
end
@@ -225,16 +226,24 @@ describe MergeRequests::UpdateService, services: true do
it 'marks pending todos as done' do
expect(pending_todo.reload).to be_done
end
+
+ it 'does not create any new todos' do
+ expect(Todo.count).to eq(1)
+ end
end
context 'when the description change' do
before do
- update_merge_request({ description: 'Also please fix' })
+ update_merge_request({ description: "Also please fix #{user2.to_reference} #{user3.to_reference}" })
end
it 'marks pending todos as done' do
expect(pending_todo.reload).to be_done
end
+
+ it 'creates only 1 new todo' do
+ expect(Todo.count).to eq(2)
+ end
end
context 'when is reassigned' do
diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb
index 92b84308f73..d581b94ff43 100644
--- a/spec/services/milestones/close_service_spec.rb
+++ b/spec/services/milestones/close_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Milestones::CloseService, services: true do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:milestone) { create(:milestone, title: "Milestone v1.2", project: project) }
before do
@@ -17,7 +17,7 @@ describe Milestones::CloseService, services: true do
it { expect(milestone).to be_valid }
it { expect(milestone).to be_closed }
- describe :event do
+ describe 'event' do
let(:event) { Event.recent.first }
it { expect(event.milestone).to be_truthy }
diff --git a/spec/services/note_summary_spec.rb b/spec/services/note_summary_spec.rb
new file mode 100644
index 00000000000..39f06f8f8e7
--- /dev/null
+++ b/spec/services/note_summary_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe NoteSummary, services: true do
+ let(:project) { build(:empty_project) }
+ let(:noteable) { build(:issue) }
+ let(:user) { build(:user) }
+
+ def create_note_summary
+ described_class.new(noteable, project, user, 'note', action: 'icon', commit_count: 5)
+ end
+
+ describe '#metadata?' do
+ it 'returns true when metadata present' do
+ expect(create_note_summary.metadata?).to be_truthy
+ end
+
+ it 'returns false when metadata not present' do
+ expect(described_class.new(noteable, project, user, 'note').metadata?).to be_falsey
+ end
+ end
+
+ describe '#note' do
+ it 'returns note hash' do
+ expect(create_note_summary.note).to eq(noteable: noteable, project: project, author: user, note: 'note')
+ end
+
+ context 'when noteable is a commit' do
+ let(:noteable) { build(:commit) }
+
+ it 'returns note hash specific to commit' do
+ expect(create_note_summary.note).to eq(
+ noteable: nil, project: project, author: user, note: 'note',
+ noteable_type: 'Commit', commit_id: noteable.id
+ )
+ end
+ end
+ end
+
+ describe '#metadata' do
+ it 'returns metadata hash' do
+ expect(create_note_summary.metadata).to eq(action: 'icon', commit_count: 5)
+ end
+ end
+end
diff --git a/spec/services/notes/diff_position_update_service_spec.rb b/spec/services/notes/diff_position_update_service_spec.rb
index 110efb54fa0..d73ae51fbc3 100644
--- a/spec/services/notes/diff_position_update_service_spec.rb
+++ b/spec/services/notes/diff_position_update_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Notes::DiffPositionUpdateService, services: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:create_commit) { project.commit("913c66a37b4a45b9769037c55c2d238bd0942d2e") }
let(:modify_commit) { project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e") }
let(:edit_commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") }
diff --git a/spec/services/notes/update_service_spec.rb b/spec/services/notes/update_service_spec.rb
index dde4bde7dc2..905e2f46bde 100644
--- a/spec/services/notes/update_service_spec.rb
+++ b/spec/services/notes/update_service_spec.rb
@@ -4,12 +4,14 @@ describe Notes::UpdateService, services: true do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
let(:user2) { create(:user) }
+ let(:user3) { create(:user) }
let(:issue) { create(:issue, project: project) }
- let(:note) { create(:note, project: project, noteable: issue, author: user, note: 'Old note') }
+ let(:note) { create(:note, project: project, noteable: issue, author: user, note: "Old note #{user2.to_reference}") }
before do
project.team << [user, :master]
project.team << [user2, :developer]
+ project.team << [user3, :developer]
end
describe '#execute' do
@@ -23,22 +25,30 @@ describe Notes::UpdateService, services: true do
context 'when the note change' do
before do
- update_note({ note: 'New note' })
+ update_note({ note: "New note #{user2.to_reference} #{user3.to_reference}" })
end
it 'marks todos as done' do
expect(todo.reload).to be_done
end
+
+ it 'creates only 1 new todo' do
+ expect(Todo.count).to eq(2)
+ end
end
context 'when the note does not change' do
before do
- update_note({ note: 'Old note' })
+ update_note({ note: "Old note #{user2.to_reference}" })
end
it 'keep todos' do
expect(todo.reload).to be_pending
end
+
+ it 'does not create any new todos' do
+ expect(Todo.count).to eq(1)
+ end
end
end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index f7240969588..e3146a56495 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -113,7 +113,7 @@ describe NotificationService, services: true do
project.add_master(issue.assignee)
project.add_master(note.author)
create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@subscribed_participant cc this guy')
- update_custom_notification(:new_note, @u_guest_custom, project)
+ update_custom_notification(:new_note, @u_guest_custom, resource: project)
update_custom_notification(:new_note, @u_custom_global)
end
@@ -146,6 +146,16 @@ describe NotificationService, services: true do
should_not_email(@u_lazy_participant)
end
+ it "emails the note author if they've opted into notifications about their activity" do
+ add_users_with_subscription(note.project, issue)
+ note.author.notified_of_own_activity = true
+ reset_delivered_emails!
+
+ notification.new_note(note)
+
+ should_email(note.author)
+ end
+
it 'filters out "mentioned in" notes' do
mentioned_note = SystemNoteService.cross_reference(mentioned_issue, issue, issue.author)
@@ -362,14 +372,14 @@ describe NotificationService, services: true do
end
context 'commit note' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
let(:note) { create(:note_on_commit, project: project) }
before do
build_team(note.project)
reset_delivered_emails!
allow_any_instance_of(Commit).to receive(:author).and_return(@u_committer)
- update_custom_notification(:new_note, @u_guest_custom, project)
+ update_custom_notification(:new_note, @u_guest_custom, resource: project)
update_custom_notification(:new_note, @u_custom_global)
end
@@ -411,7 +421,7 @@ describe NotificationService, services: true do
end
context "merge request diff note" do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request, source_project: project, assignee: user) }
let(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
@@ -447,7 +457,7 @@ describe NotificationService, services: true do
add_users_with_subscription(issue.project, issue)
reset_delivered_emails!
- update_custom_notification(:new_issue, @u_guest_custom, project)
+ update_custom_notification(:new_issue, @u_guest_custom, resource: project)
update_custom_notification(:new_issue, @u_custom_global)
end
@@ -476,6 +486,20 @@ describe NotificationService, services: true do
should_not_email(issue.assignee)
end
+ it "emails the author if they've opted into notifications about their activity" do
+ issue.author.notified_of_own_activity = true
+
+ notification.new_issue(issue, issue.author)
+
+ should_email(issue.author)
+ end
+
+ it "doesn't email the author if they haven't opted into notifications about their activity" do
+ notification.new_issue(issue, issue.author)
+
+ should_not_email(issue.author)
+ end
+
it "emails subscribers of the issue's labels" do
user_1 = create(:user)
user_2 = create(:user)
@@ -543,7 +567,7 @@ describe NotificationService, services: true do
describe '#reassigned_issue' do
before do
- update_custom_notification(:reassign_issue, @u_guest_custom, project)
+ update_custom_notification(:reassign_issue, @u_guest_custom, resource: project)
update_custom_notification(:reassign_issue, @u_custom_global)
end
@@ -665,6 +689,19 @@ describe NotificationService, services: true do
should_email(subscriber_to_label_2)
end
+ it "emails the current user if they've opted into notifications about their activity" do
+ subscriber_to_label_2.notified_of_own_activity = true
+ notification.relabeled_issue(issue, [group_label_2, label_2], subscriber_to_label_2)
+
+ should_email(subscriber_to_label_2)
+ end
+
+ it "doesn't email the current user if they haven't opted into notifications about their activity" do
+ notification.relabeled_issue(issue, [group_label_2, label_2], subscriber_to_label_2)
+
+ should_not_email(subscriber_to_label_2)
+ end
+
it "doesn't send email to anyone but subscribers of the given labels" do
notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled)
@@ -723,7 +760,7 @@ describe NotificationService, services: true do
describe '#close_issue' do
before do
- update_custom_notification(:close_issue, @u_guest_custom, project)
+ update_custom_notification(:close_issue, @u_guest_custom, resource: project)
update_custom_notification(:close_issue, @u_custom_global)
end
@@ -754,7 +791,7 @@ describe NotificationService, services: true do
describe '#reopen_issue' do
before do
- update_custom_notification(:reopen_issue, @u_guest_custom, project)
+ update_custom_notification(:reopen_issue, @u_guest_custom, resource: project)
update_custom_notification(:reopen_issue, @u_custom_global)
end
@@ -812,21 +849,21 @@ describe NotificationService, services: true do
describe 'Merge Requests' do
let(:group) { create(:group) }
- let(:project) { create(:project, :public, namespace: group) }
+ let(:project) { create(:project, :public, :repository, namespace: group) }
let(:another_project) { create(:empty_project, :public, namespace: group) }
let(:merge_request) { create :merge_request, source_project: project, assignee: create(:user), description: 'cc @participant' }
before do
build_team(merge_request.target_project)
add_users_with_subscription(merge_request.target_project, merge_request)
- update_custom_notification(:new_merge_request, @u_guest_custom, project)
+ update_custom_notification(:new_merge_request, @u_guest_custom, resource: project)
update_custom_notification(:new_merge_request, @u_custom_global)
reset_delivered_emails!
end
describe '#new_merge_request' do
before do
- update_custom_notification(:new_merge_request, @u_guest_custom, project)
+ update_custom_notification(:new_merge_request, @u_guest_custom, resource: project)
update_custom_notification(:new_merge_request, @u_custom_global)
end
@@ -845,6 +882,20 @@ describe NotificationService, services: true do
should_not_email(@u_lazy_participant)
end
+ it "emails the author if they've opted into notifications about their activity" do
+ merge_request.author.notified_of_own_activity = true
+
+ notification.new_merge_request(merge_request, merge_request.author)
+
+ should_email(merge_request.author)
+ end
+
+ it "doesn't email the author if they haven't opted into notifications about their activity" do
+ notification.new_merge_request(merge_request, merge_request.author)
+
+ should_not_email(merge_request.author)
+ end
+
it "emails subscribers of the merge request's labels" do
user_1 = create(:user)
user_2 = create(:user)
@@ -901,7 +952,7 @@ describe NotificationService, services: true do
describe '#reassigned_merge_request' do
before do
- update_custom_notification(:reassign_merge_request, @u_guest_custom, project)
+ update_custom_notification(:reassign_merge_request, @u_guest_custom, resource: project)
update_custom_notification(:reassign_merge_request, @u_custom_global)
end
@@ -975,7 +1026,7 @@ describe NotificationService, services: true do
describe '#closed_merge_request' do
before do
- update_custom_notification(:close_merge_request, @u_guest_custom, project)
+ update_custom_notification(:close_merge_request, @u_guest_custom, resource: project)
update_custom_notification(:close_merge_request, @u_custom_global)
end
@@ -1005,7 +1056,7 @@ describe NotificationService, services: true do
describe '#merged_merge_request' do
before do
- update_custom_notification(:merge_merge_request, @u_guest_custom, project)
+ update_custom_notification(:merge_merge_request, @u_guest_custom, resource: project)
update_custom_notification(:merge_merge_request, @u_custom_global)
end
@@ -1040,6 +1091,14 @@ describe NotificationService, services: true do
should_not_email(@u_watcher)
end
+ it "notifies the merger when the pipeline succeeds is false but they've opted into notifications about their activity" do
+ merge_request.merge_when_pipeline_succeeds = false
+ @u_watcher.notified_of_own_activity = true
+ notification.merge_mr(merge_request, @u_watcher)
+
+ should_email(@u_watcher)
+ end
+
it_behaves_like 'participating notifications' do
let(:participant) { create(:user, username: 'user-participant') }
let(:issuable) { merge_request }
@@ -1049,7 +1108,7 @@ describe NotificationService, services: true do
describe '#reopen_merge_request' do
before do
- update_custom_notification(:reopen_merge_request, @u_guest_custom, project)
+ update_custom_notification(:reopen_merge_request, @u_guest_custom, resource: project)
update_custom_notification(:reopen_merge_request, @u_custom_global)
end
@@ -1102,7 +1161,7 @@ describe NotificationService, services: true do
end
describe 'Projects' do
- let(:project) { create :project }
+ let(:project) { create(:empty_project) }
before do
build_team(project)
@@ -1147,7 +1206,7 @@ describe NotificationService, services: true do
describe 'ProjectMember' do
describe '#decline_group_invite' do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:member) { create(:user) }
before(:each) do
@@ -1221,41 +1280,173 @@ describe NotificationService, services: true do
describe 'Pipelines' do
describe '#pipeline_finished' do
- let(:project) { create(:project, :public) }
- let(:current_user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
let(:u_member) { create(:user) }
- let(:u_other) { create(:user) }
+ let(:u_watcher) { create_user_with_notification(:watch, 'watcher') }
+
+ let(:u_custom_notification_unset) do
+ create_user_with_notification(:custom, 'custom_unset')
+ end
+
+ let(:u_custom_notification_enabled) do
+ user = create_user_with_notification(:custom, 'custom_enabled')
+ update_custom_notification(:success_pipeline, user, resource: project)
+ update_custom_notification(:failed_pipeline, user, resource: project)
+ user
+ end
+
+ let(:u_custom_notification_disabled) do
+ user = create_user_with_notification(:custom, 'custom_disabled')
+ update_custom_notification(:success_pipeline, user, resource: project, value: false)
+ update_custom_notification(:failed_pipeline, user, resource: project, value: false)
+ user
+ end
let(:commit) { project.commit }
- let(:pipeline) do
- create(:ci_pipeline, :success,
+
+ def create_pipeline(user, status)
+ create(:ci_pipeline, status,
project: project,
- user: current_user,
+ user: user,
ref: 'refs/heads/master',
sha: commit.id,
before_sha: '00000000')
end
before do
- project.add_master(current_user)
project.add_master(u_member)
+ project.add_master(u_watcher)
+ project.add_master(u_custom_notification_unset)
+ project.add_master(u_custom_notification_enabled)
+ project.add_master(u_custom_notification_disabled)
+
reset_delivered_emails!
end
- context 'without custom recipients' do
- it 'notifies the pipeline user' do
- notification.pipeline_finished(pipeline)
+ context 'with a successful pipeline' do
+ context 'when the creator has default settings' do
+ before do
+ pipeline = create_pipeline(u_member, :success)
+ notification.pipeline_finished(pipeline)
+ end
- should_only_email(current_user, kind: :bcc)
+ it 'notifies nobody' do
+ should_not_email_anyone
+ end
+ end
+
+ context 'when the creator has watch set' do
+ before do
+ pipeline = create_pipeline(u_watcher, :success)
+ notification.pipeline_finished(pipeline)
+ end
+
+ it 'notifies nobody' do
+ should_not_email_anyone
+ end
+ end
+
+ context 'when the creator has custom notifications, but without any set' do
+ before do
+ pipeline = create_pipeline(u_custom_notification_unset, :success)
+ notification.pipeline_finished(pipeline)
+ end
+
+ it 'notifies nobody' do
+ should_not_email_anyone
+ end
+ end
+
+ context 'when the creator has custom notifications disabled' do
+ before do
+ pipeline = create_pipeline(u_custom_notification_disabled, :success)
+ notification.pipeline_finished(pipeline)
+ end
+
+ it 'notifies nobody' do
+ should_not_email_anyone
+ end
+ end
+
+ context 'when the creator has custom notifications enabled' do
+ before do
+ pipeline = create_pipeline(u_custom_notification_enabled, :success)
+ notification.pipeline_finished(pipeline)
+ end
+
+ it 'emails only the creator' do
+ should_only_email(u_custom_notification_enabled, kind: :bcc)
+ end
end
end
- context 'with custom recipients' do
- it 'notifies the custom recipients' do
- users = [u_member, u_other]
- notification.pipeline_finished(pipeline, users.map(&:notification_email))
+ context 'with a failed pipeline' do
+ context 'when the creator has no custom notification set' do
+ before do
+ pipeline = create_pipeline(u_member, :failed)
+ notification.pipeline_finished(pipeline)
+ end
+
+ it 'emails only the creator' do
+ should_only_email(u_member, kind: :bcc)
+ end
+ end
+
+ context 'when the creator has watch set' do
+ before do
+ pipeline = create_pipeline(u_watcher, :failed)
+ notification.pipeline_finished(pipeline)
+ end
+
+ it 'emails only the creator' do
+ should_only_email(u_watcher, kind: :bcc)
+ end
+ end
+
+ context 'when the creator has custom notifications, but without any set' do
+ before do
+ pipeline = create_pipeline(u_custom_notification_unset, :failed)
+ notification.pipeline_finished(pipeline)
+ end
+
+ it 'emails only the creator' do
+ should_only_email(u_custom_notification_unset, kind: :bcc)
+ end
+ end
+
+ context 'when the creator has custom notifications disabled' do
+ before do
+ pipeline = create_pipeline(u_custom_notification_disabled, :failed)
+ notification.pipeline_finished(pipeline)
+ end
+
+ it 'notifies nobody' do
+ should_not_email_anyone
+ end
+ end
- should_only_email(*users, kind: :bcc)
+ context 'when the creator has custom notifications set' do
+ before do
+ pipeline = create_pipeline(u_custom_notification_enabled, :failed)
+ notification.pipeline_finished(pipeline)
+ end
+
+ it 'emails only the creator' do
+ should_only_email(u_custom_notification_enabled, kind: :bcc)
+ end
+ end
+
+ context 'when the creator has no read_build access' do
+ before do
+ pipeline = create_pipeline(u_member, :failed)
+ project.update(public_builds: false)
+ project.team.truncate
+ notification.pipeline_finished(pipeline)
+ end
+
+ it 'does not send emails' do
+ should_not_email_anyone
+ end
end
end
end
@@ -1326,9 +1517,9 @@ describe NotificationService, services: true do
# Create custom notifications
# When resource is nil it means global notification
- def update_custom_notification(event, user, resource = nil)
+ def update_custom_notification(event, user, resource: nil, value: true)
setting = user.notification_settings_for(resource)
- setting.events[event] = true
+ setting.events[event] = value
setting.save
end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 74bfba44dfd..b1e10f4562e 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Projects::DestroyService, services: true do
let!(:user) { create(:user) }
- let!(:project) { create(:project, namespace: user.namespace) }
+ let!(:project) { create(:project, :repository, namespace: user.namespace) }
let!(:path) { project.repository.path_to_repo }
let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") }
let!(:async) { false } # execute or async_execute
diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb
index 122a7cea2a1..33b267c069c 100644
--- a/spec/services/projects/download_service_spec.rb
+++ b/spec/services/projects/download_service_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
describe Projects::DownloadService, services: true do
describe 'File service' do
before do
- @user = create :user
- @project = create :project, creator_id: @user.id, namespace: @user.namespace
+ @user = create(:user)
+ @project = create(:empty_project, creator_id: @user.id, namespace: @user.namespace)
end
context 'for a URL that is not on whitelist' do
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 8e614211116..f8eb34f2ef4 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -1,12 +1,13 @@
require 'spec_helper'
describe Projects::ForkService, services: true do
- describe :fork_by_user do
+ describe 'fork by user' do
before do
@from_namespace = create(:namespace)
@from_user = create(:user, namespace: @from_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,
@@ -54,7 +55,7 @@ describe Projects::ForkService, services: true do
context 'project already exists' do
it "fails due to validation, not transaction failure" do
- @existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
+ @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)
expect(@existing_project).to be_persisted
@@ -100,13 +101,14 @@ describe Projects::ForkService, services: true do
end
end
- describe :fork_to_namespace do
+ describe 'fork to namespace' do
before do
@group_owner = create(:user)
@developer = create(:user)
- @project = create(:project, creator_id: @group_owner.id,
- star_count: 777,
- description: 'Wow, such a cool project!')
+ @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)
@@ -139,8 +141,9 @@ describe Projects::ForkService, services: true do
context 'project already exists in group' do
it 'fails due to validation, not transaction failure' do
- existing_project = create(:project, name: @project.name,
- namespace: @group)
+ 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'])
diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
index 57a5aa5cedc..eaf63457b32 100644
--- a/spec/services/projects/housekeeping_service_spec.rb
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Projects::HousekeepingService do
subject { Projects::HousekeepingService.new(project) }
- let(:project) { create :project }
+ let(:project) { create(:project, :repository) }
before do
project.reset_pushes_since_gc
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index e5917bb0b7a..09cfa36b3b9 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -26,30 +26,59 @@ describe Projects::ImportService, services: true do
result = subject.execute
expect(result[:status]).to eq :error
- expect(result[:message]).to eq 'The repository could not be created.'
+ expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - The repository could not be created."
end
end
context 'with known url' do
before do
project.import_url = 'https://github.com/vim/vim.git'
+ project.import_type = 'github'
end
- it 'succeeds if repository import is successfully' do
- expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.repository_storage_path, project.path_with_namespace, project.import_url).and_return(true)
+ context 'with a Github repository' do
+ it 'succeeds if repository import is successfully' do
+ expect_any_instance_of(Repository).to receive(:fetch_remote).and_return(true)
+ expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(true)
- result = subject.execute
+ result = subject.execute
- expect(result[:status]).to eq :success
+ expect(result[:status]).to eq :success
+ end
+
+ it 'fails if repository import fails' do
+ expect_any_instance_of(Repository).to receive(:fetch_remote).and_raise(Gitlab::Shell::Error.new('Failed to import the repository'))
+
+ result = subject.execute
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - Failed to import the repository"
+ end
end
- it 'fails if repository import fails' do
- expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.repository_storage_path, project.path_with_namespace, project.import_url).and_raise(Gitlab::Shell::Error.new('Failed to import the repository'))
+ context 'with a non Github repository' do
+ before do
+ project.import_url = 'https://bitbucket.org/vim/vim.git'
+ project.import_type = 'bitbucket'
+ end
- result = subject.execute
+ it 'succeeds if repository import is successfully' do
+ expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true)
+ expect_any_instance_of(Gitlab::BitbucketImport::Importer).to receive(:execute).and_return(true)
- expect(result[:status]).to eq :error
- expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - Failed to import the repository"
+ result = subject.execute
+
+ expect(result[:status]).to eq :success
+ end
+
+ it 'fails if repository import fails' do
+ expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error.new('Failed to import the repository'))
+
+ result = subject.execute
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - Failed to import the repository"
+ end
end
end
@@ -64,8 +93,8 @@ describe Projects::ImportService, services: true do
end
it 'succeeds if importer succeeds' do
- expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.repository_storage_path, project.path_with_namespace, project.import_url).and_return(true)
- expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(true)
+ allow_any_instance_of(Repository).to receive(:fetch_remote).and_return(true)
+ allow_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(true)
result = subject.execute
@@ -73,48 +102,42 @@ describe Projects::ImportService, services: true do
end
it 'flushes various caches' do
- expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).
- with(project.repository_storage_path, project.path_with_namespace, project.import_url).
+ allow_any_instance_of(Repository).to receive(:fetch_remote).
and_return(true)
- expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).
+ allow_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).
and_return(true)
- expect_any_instance_of(Repository).to receive(:expire_emptiness_caches).
- and_call_original
-
- expect_any_instance_of(Repository).to receive(:expire_exists_cache).
- and_call_original
+ expect_any_instance_of(Repository).to receive(:expire_content_cache)
subject.execute
end
it 'fails if importer fails' do
- expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.repository_storage_path, project.path_with_namespace, project.import_url).and_return(true)
- expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(false)
+ allow_any_instance_of(Repository).to receive(:fetch_remote).and_return(true)
+ allow_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(false)
result = subject.execute
expect(result[:status]).to eq :error
- expect(result[:message]).to eq 'The remote data could not be imported.'
+ expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - The remote data could not be imported."
end
it 'fails if importer raise an error' do
- expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.repository_storage_path, project.path_with_namespace, project.import_url).and_return(true)
- expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_raise(Projects::ImportService::Error.new('Github: failed to connect API'))
+ allow_any_instance_of(Gitlab::Shell).to receive(:fetch_remote).and_return(true)
+ allow_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_raise(Projects::ImportService::Error.new('Github: failed to connect API'))
result = subject.execute
expect(result[:status]).to eq :error
- expect(result[:message]).to eq 'Github: failed to connect API'
+ expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - Github: failed to connect API"
end
- it 'expires existence cache after error' do
+ it 'expires content cache after error' do
allow_any_instance_of(Project).to receive(:repository_exists?).and_return(false, true)
- expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.repository_storage_path, project.path_with_namespace, project.import_url).and_raise(Gitlab::Shell::Error.new('Failed to import the repository'))
- expect_any_instance_of(Repository).to receive(:expire_emptiness_caches).and_call_original
- expect_any_instance_of(Repository).to receive(:expire_exists_cache).and_call_original
+ expect_any_instance_of(Gitlab::Shell).to receive(:fetch_remote).and_raise(Gitlab::Shell::Error.new('Failed to import the repository'))
+ expect_any_instance_of(Repository).to receive(:expire_content_cache)
subject.execute
end
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 5c6fbea8d0e..f8187fefc14 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Projects::TransferService, services: true do
let(:user) { create(:user) }
let(:group) { create(:group) }
- let(:project) { create(:project, namespace: user.namespace) }
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
context 'namespace -> namespace' do
before do
@@ -58,7 +58,7 @@ describe Projects::TransferService, services: true do
before { internal_group.add_owner(user) }
context 'when namespace visibility level < project visibility level' do
- let(:public_project) { create(:project, :public, namespace: user.namespace) }
+ let(:public_project) { create(:project, :public, :repository, namespace: user.namespace) }
before { transfer_project(public_project, user, internal_group) }
@@ -66,7 +66,7 @@ describe Projects::TransferService, services: true do
end
context 'when namespace visibility level > project visibility level' do
- let(:private_project) { create(:project, :private, namespace: user.namespace) }
+ let(:private_project) { create(:project, :private, :repository, namespace: user.namespace) }
before { transfer_project(private_project, user, internal_group) }
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index f75fdd9e03f..fc0a17296f3 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -1,9 +1,9 @@
require "spec_helper"
describe Projects::UpdatePagesService do
- let(:project) { create :project }
- let(:pipeline) { create :ci_pipeline, project: project, sha: project.commit('HEAD').sha }
- let(:build) { create :ci_build, pipeline: pipeline, ref: 'HEAD' }
+ let(:project) { create(:project, :repository) }
+ let(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit('HEAD').sha) }
+ let(:build) { create(:ci_build, pipeline: pipeline, ref: 'HEAD') }
let(:invalid_file) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png') }
subject { described_class.new(project, build) }
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index caa23962519..05b18fef061 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Projects::UpdateService, services: true do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
- let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
+ let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
describe 'update_by_user' do
context 'when visibility_level is INTERNAL' do
@@ -56,7 +56,7 @@ describe Projects::UpdateService, services: true do
end
describe 'visibility_level' do
- let(:project) { create(:project, :internal) }
+ let(:project) { create(:empty_project, :internal) }
let(:forked_project) { create(:forked_project_with_submodules, :internal) }
before do
diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/projects/upload_service_spec.rb
index 150c8ccaef7..d2cefa46bfa 100644
--- a/spec/services/projects/upload_service_spec.rb
+++ b/spec/services/projects/upload_service_spec.rb
@@ -3,8 +3,8 @@ require 'spec_helper'
describe Projects::UploadService, services: true do
describe 'File service' do
before do
- @user = create :user
- @project = create :project, creator_id: @user.id, namespace: @user.namespace
+ @user = create(:user)
+ @project = create(:empty_project, creator_id: @user.id, namespace: @user.namespace)
end
context 'for valid gif file' do
diff --git a/spec/services/search/global_service_spec.rb b/spec/services/search/global_service_spec.rb
new file mode 100644
index 00000000000..2531607acad
--- /dev/null
+++ b/spec/services/search/global_service_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Search::GlobalService, services: true do
+ let(:user) { create(:user) }
+ let(:internal_user) { create(:user) }
+
+ let!(:found_project) { create(:empty_project, :private, name: 'searchable_project') }
+ let!(:unfound_project) { create(:empty_project, :private, name: 'unfound_project') }
+ let!(:internal_project) { create(:empty_project, :internal, name: 'searchable_internal_project') }
+ let!(:public_project) { create(:empty_project, :public, name: 'searchable_public_project') }
+
+ before do
+ found_project.add_master(user)
+ end
+
+ describe '#execute' do
+ context 'unauthenticated' do
+ it 'returns public projects only' do
+ results = Search::GlobalService.new(nil, search: "searchable").execute
+
+ expect(results.objects('projects')).to match_array [public_project]
+ end
+ end
+
+ context 'authenticated' do
+ it 'returns public, internal and private projects' do
+ results = Search::GlobalService.new(user, search: "searchable").execute
+
+ expect(results.objects('projects')).to match_array [public_project, found_project, internal_project]
+ end
+
+ it 'returns only public & internal projects' do
+ results = Search::GlobalService.new(internal_user, search: "searchable").execute
+
+ expect(results.objects('projects')).to match_array [internal_project, public_project]
+ end
+
+ it 'namespace name is searchable' do
+ results = Search::GlobalService.new(user, search: found_project.namespace.path).execute
+
+ expect(results.objects('projects')).to match_array [found_project]
+ end
+
+ context 'nested group' do
+ let!(:nested_group) { create(:group, :nested) }
+ let!(:project) { create(:empty_project, namespace: nested_group) }
+
+ before do
+ project.add_master(user)
+ end
+
+ it 'returns result from nested group' do
+ results = Search::GlobalService.new(user, search: project.path).execute
+
+ expect(results.objects('projects')).to match_array [project]
+ end
+
+ it 'returns result from descendants when search inside group' do
+ results = Search::GlobalService.new(user, search: project.path, group_id: nested_group.parent).execute
+
+ expect(results.objects('projects')).to match_array [project]
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index bed1031e40a..2112f1cf9ea 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -1,65 +1,286 @@
require 'spec_helper'
-describe 'Search::GlobalService', services: true do
+describe SearchService, services: true do
let(:user) { create(:user) }
- let(:public_user) { create(:user) }
- let(:internal_user) { create(:user) }
- let!(:found_project) { create(:empty_project, :private, name: 'searchable_project') }
- let!(:unfound_project) { create(:empty_project, :private, name: 'unfound_project') }
- let!(:internal_project) { create(:empty_project, :internal, name: 'searchable_internal_project') }
- let!(:public_project) { create(:empty_project, :public, name: 'searchable_public_project') }
+ let(:accessible_group) { create(:group, :private) }
+ let(:inaccessible_group) { create(:group, :private) }
+ let!(:group_member) { create(:group_member, group: accessible_group, user: user) }
+
+ let!(:accessible_project) { create(:empty_project, :private, name: 'accessible_project') }
+ let!(:inaccessible_project) { create(:empty_project, :private, name: 'inaccessible_project') }
+ let(:note) { create(:note_on_issue, project: accessible_project) }
+
+ let(:snippet) { create(:snippet, author: user) }
+ let(:group_project) { create(:empty_project, group: accessible_group, name: 'group_project') }
+ let(:public_project) { create(:empty_project, :public, name: 'public_project') }
before do
- found_project.team << [user, :master]
+ accessible_project.add_master(user)
+ end
+
+ describe '#project' do
+ context 'when the project is accessible' do
+ it 'returns the project' do
+ project = SearchService.new(user, project_id: accessible_project.id).project
+
+ expect(project).to eq accessible_project
+ end
+ end
+
+ context 'when the project is not accessible' do
+ it 'returns nil' do
+ project = SearchService.new(user, project_id: inaccessible_project.id).project
+
+ expect(project).to be_nil
+ end
+ end
+
+ context 'when there is no project_id' do
+ it 'returns nil' do
+ project = SearchService.new(user).project
+
+ expect(project).to be_nil
+ end
+ end
end
- describe '#execute' do
- context 'unauthenticated' do
- it 'returns public projects only' do
- context = Search::GlobalService.new(nil, search: "searchable")
- results = context.execute
- expect(results.objects('projects')).to match_array [public_project]
+ describe '#group' do
+ context 'when the group is accessible' do
+ it 'returns the group' do
+ group = SearchService.new(user, group_id: accessible_group.id).group
+
+ expect(group).to eq accessible_group
end
end
- context 'authenticated' do
- it 'returns public, internal and private projects' do
- context = Search::GlobalService.new(user, search: "searchable")
- results = context.execute
- expect(results.objects('projects')).to match_array [public_project, found_project, internal_project]
+ context 'when the group is not accessible' do
+ it 'returns nil' do
+ group = SearchService.new(user, group_id: inaccessible_group.id).group
+
+ expect(group).to be_nil
end
+ end
+
+ context 'when there is no group_id' do
+ it 'returns nil' do
+ group = SearchService.new(user).group
- it 'returns only public & internal projects' do
- context = Search::GlobalService.new(internal_user, search: "searchable")
- results = context.execute
- expect(results.objects('projects')).to match_array [internal_project, public_project]
+ expect(group).to be_nil
end
+ end
+ end
+
+ describe '#show_snippets?' do
+ context 'when :snippets is \'true\'' do
+ it 'returns true' do
+ show_snippets = SearchService.new(user, snippets: 'true').show_snippets?
- it 'namespace name is searchable' do
- context = Search::GlobalService.new(user, search: found_project.namespace.path)
- results = context.execute
- expect(results.objects('projects')).to match_array [found_project]
+ expect(show_snippets).to be_truthy
end
+ end
- context 'nested group' do
- let!(:nested_group) { create(:group, :nested) }
- let!(:project) { create(:project, namespace: nested_group) }
+ context 'when :snippets is not \'true\'' do
+ it 'returns false' do
+ show_snippets = SearchService.new(user, snippets: 'tru').show_snippets?
+
+ expect(show_snippets).to be_falsey
+ end
+ end
- before { project.add_master(user) }
+ context 'when :snippets is missing' do
+ it 'returns false' do
+ show_snippets = SearchService.new(user).show_snippets?
- it 'returns result from nested group' do
- context = Search::GlobalService.new(user, search: project.path)
- results = context.execute
- expect(results.objects('projects')).to match_array [project]
+ expect(show_snippets).to be_falsey
+ end
+ end
+ end
+
+ describe '#scope' do
+ context 'with accessible project_id' do
+ context 'and allowed scope' do
+ it 'returns the specified scope' do
+ scope = SearchService.new(user, project_id: accessible_project.id, scope: 'notes').scope
+
+ expect(scope).to eq 'notes'
end
+ end
+
+ context 'and disallowed scope' do
+ it 'returns the default scope' do
+ scope = SearchService.new(user, project_id: accessible_project.id, scope: 'projects').scope
- it 'returns result from descendants when search inside group' do
- context = Search::GlobalService.new(user, search: project.path, group_id: nested_group.parent)
- results = context.execute
- expect(results.objects('projects')).to match_array [project]
+ expect(scope).to eq 'blobs'
end
end
+
+ context 'and no scope' do
+ it 'returns the default scope' do
+ scope = SearchService.new(user, project_id: accessible_project.id).scope
+
+ expect(scope).to eq 'blobs'
+ end
+ end
+ end
+
+ context 'with \'true\' snippets' do
+ context 'and allowed scope' do
+ it 'returns the specified scope' do
+ scope = SearchService.new(user, snippets: 'true', scope: 'snippet_titles').scope
+
+ expect(scope).to eq 'snippet_titles'
+ end
+ end
+
+ context 'and disallowed scope' do
+ it 'returns the default scope' do
+ scope = SearchService.new(user, snippets: 'true', scope: 'projects').scope
+
+ expect(scope).to eq 'snippet_blobs'
+ end
+ end
+
+ context 'and no scope' do
+ it 'returns the default scope' do
+ scope = SearchService.new(user, snippets: 'true').scope
+
+ expect(scope).to eq 'snippet_blobs'
+ end
+ end
+ end
+
+ context 'with no project_id, no snippets' do
+ context 'and allowed scope' do
+ it 'returns the specified scope' do
+ scope = SearchService.new(user, scope: 'issues').scope
+
+ expect(scope).to eq 'issues'
+ end
+ end
+
+ context 'and disallowed scope' do
+ it 'returns the default scope' do
+ scope = SearchService.new(user, scope: 'blobs').scope
+
+ expect(scope).to eq 'projects'
+ end
+ end
+
+ context 'and no scope' do
+ it 'returns the default scope' do
+ scope = SearchService.new(user).scope
+
+ expect(scope).to eq 'projects'
+ end
+ end
+ end
+ end
+
+ describe '#search_results' do
+ context 'with accessible project_id' do
+ it 'returns an instance of Gitlab::ProjectSearchResults' do
+ search_results = SearchService.new(
+ user,
+ project_id: accessible_project.id,
+ scope: 'notes',
+ search: note.note).search_results
+
+ expect(search_results).to be_a Gitlab::ProjectSearchResults
+ end
+ end
+
+ context 'with accessible project_id and \'true\' snippets' do
+ it 'returns an instance of Gitlab::ProjectSearchResults' do
+ search_results = SearchService.new(
+ user,
+ project_id: accessible_project.id,
+ snippets: 'true',
+ scope: 'notes',
+ search: note.note).search_results
+
+ expect(search_results).to be_a Gitlab::ProjectSearchResults
+ end
+ end
+
+ context 'with \'true\' snippets' do
+ it 'returns an instance of Gitlab::SnippetSearchResults' do
+ search_results = SearchService.new(
+ user,
+ snippets: 'true',
+ search: snippet.content).search_results
+
+ expect(search_results).to be_a Gitlab::SnippetSearchResults
+ end
+ end
+
+ context 'with no project_id and no snippets' do
+ it 'returns an instance of Gitlab::SearchResults' do
+ search_results = SearchService.new(
+ user,
+ search: public_project.name).search_results
+
+ expect(search_results).to be_a Gitlab::SearchResults
+ end
+ end
+ end
+
+ describe '#search_objects' do
+ context 'with accessible project_id' do
+ it 'returns objects in the project' do
+ search_objects = SearchService.new(
+ user,
+ project_id: accessible_project.id,
+ scope: 'notes',
+ search: note.note).search_objects
+
+ expect(search_objects.first).to eq note
+ end
+ end
+
+ context 'with accessible project_id and \'true\' snippets' do
+ it 'returns objects in the project' do
+ search_objects = SearchService.new(
+ user,
+ project_id: accessible_project.id,
+ snippets: 'true',
+ scope: 'notes',
+ search: note.note).search_objects
+
+ expect(search_objects.first).to eq note
+ end
+ end
+
+ context 'with \'true\' snippets' do
+ it 'returns objects in snippets' do
+ search_objects = SearchService.new(
+ user,
+ snippets: 'true',
+ search: snippet.content).search_objects
+
+ expect(search_objects.first).to eq snippet
+ end
+ end
+
+ context 'with accessible group_id' do
+ it 'returns objects in the group' do
+ search_objects = SearchService.new(
+ user,
+ group_id: accessible_group.id,
+ search: group_project.name).search_objects
+
+ expect(search_objects.first).to eq group_project
+ end
+ end
+
+ context 'with no project_id, group_id or snippets' do
+ it 'returns objects in global' do
+ search_objects = SearchService.new(
+ user,
+ search: public_project.name).search_objects
+
+ expect(search_objects.first).to eq public_project
+ end
end
end
end
diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb
index 52e8678cb9d..a63281f0eab 100644
--- a/spec/services/slash_commands/interpret_service_spec.rb
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe SlashCommands::InterpretService, services: true do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:developer) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:milestone) { create(:milestone, project: project, title: '9.10') }
@@ -260,6 +260,8 @@ describe SlashCommands::InterpretService, services: true do
end
shared_examples 'merge command' do
+ let(:project) { create(:project, :repository) }
+
it 'runs merge command if content contains /merge' do
_, updates = service.execute(content, issuable)
@@ -322,6 +324,7 @@ describe SlashCommands::InterpretService, services: true do
end
context 'when sha is missing' do
+ let(:project) { create(:project, :repository) }
let(:service) { described_class.new(project, developer, {}) }
it 'precheck passes and returns merge command' do
@@ -694,7 +697,7 @@ describe SlashCommands::InterpretService, services: true do
end
context '/target_branch command' do
- let(:non_empty_project) { create(:project) }
+ let(:non_empty_project) { create(:project, :repository) }
let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) }
let(:service) { described_class.new(non_empty_project, developer)}
diff --git a/spec/services/spam_service_spec.rb b/spec/services/spam_service_spec.rb
index e09c05ccf32..74cba8c014b 100644
--- a/spec/services/spam_service_spec.rb
+++ b/spec/services/spam_service_spec.rb
@@ -15,7 +15,7 @@ describe SpamService, services: true do
end
context 'when recaptcha was not verified' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:empty_project, :public) }
let(:issue) { create(:issue, project: project) }
let(:request) { double(:request, env: {}) }
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index 11037a4917b..667059f230c 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
describe SystemHooksService, services: true do
- let(:user) { create :user }
- let(:project) { create :project }
- let(:project_member) { create :project_member }
- let(:key) { create(:key, user: user) }
- let(:deploy_key) { create(:key) }
- let(:group) { create(:group) }
- let(:group_member) { create(:group_member) }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:project_member) { create(:project_member) }
+ let(:key) { create(:key, user: user) }
+ let(:deploy_key) { create(:key) }
+ let(:group) { create(:group) }
+ let(:group_member) { create(:group_member) }
context 'event data' do
it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username) }
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 36a17a3bf2e..5ec1ed8237b 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -3,17 +3,20 @@ require 'spec_helper'
describe SystemNoteService, services: true do
include Gitlab::Routing.url_helpers
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:author) { create(:user) }
let(:noteable) { create(:issue, project: project) }
shared_examples_for 'a system note' do
+ let(:expected_noteable) { noteable }
+ let(:commit_count) { nil }
+
it 'is valid' do
expect(subject).to be_valid
end
it 'sets the noteable model' do
- expect(subject.noteable).to eq noteable
+ expect(subject.noteable).to eq expected_noteable
end
it 'sets the project' do
@@ -27,17 +30,34 @@ describe SystemNoteService, services: true do
it 'is a system note' do
expect(subject).to be_system
end
+
+ context 'metadata' do
+ it 'creates a new system note metadata record' do
+ expect { subject }.to change{ SystemNoteMetadata.count }.from(0).to(1)
+ end
+
+ it 'creates a record correctly' do
+ metadata = subject.system_note_metadata
+
+ expect(metadata.commit_count).to eq(commit_count)
+ expect(metadata.action).to eq(action)
+ end
+ end
end
describe '.add_commits' do
subject { described_class.add_commits(noteable, project, author, new_commits, old_commits, oldrev) }
+ let(:project) { create(:project, :repository) }
let(:noteable) { create(:merge_request, source_project: project) }
let(:new_commits) { noteable.commits }
let(:old_commits) { [] }
let(:oldrev) { nil }
- it_behaves_like 'a system note'
+ it_behaves_like 'a system note' do
+ let(:commit_count) { new_commits.size }
+ let(:action) { 'commit' }
+ end
describe 'note body' do
let(:note_lines) { subject.note.split("\n").reject(&:blank?) }
@@ -116,7 +136,9 @@ describe SystemNoteService, services: true do
let(:assignee) { create(:user) }
- it_behaves_like 'a system note'
+ it_behaves_like 'a system note' do
+ let(:action) { 'assignee' }
+ end
context 'when assignee added' do
it 'sets the note text' do
@@ -140,7 +162,9 @@ describe SystemNoteService, services: true do
let(:added) { [] }
let(:removed) { [] }
- it_behaves_like 'a system note'
+ it_behaves_like 'a system note' do
+ let(:action) { 'label' }
+ end
context 'with added labels' do
let(:added) { labels }
@@ -175,7 +199,9 @@ describe SystemNoteService, services: true do
let(:milestone) { create(:milestone, project: project) }
- it_behaves_like 'a system note'
+ it_behaves_like 'a system note' do
+ let(:action) { 'milestone' }
+ end
context 'when milestone added' do
it 'sets the note text' do
@@ -195,27 +221,27 @@ describe SystemNoteService, services: true do
describe '.change_status' do
subject { described_class.change_status(noteable, project, author, status, source) }
- let(:status) { 'new_status' }
- let(:source) { nil }
+ context 'with status reopened' do
+ let(:status) { 'reopened' }
+ let(:source) { nil }
- it_behaves_like 'a system note'
+ it_behaves_like 'a system note' do
+ let(:action) { 'opened' }
+ end
+ end
context 'with a source' do
+ let(:status) { 'opened' }
let(:source) { double('commit', gfm_reference: 'commit 123456') }
it 'sets the note text' do
expect(subject.note).to eq "#{status} via commit 123456"
end
end
-
- context 'without a source' do
- it 'sets the note text' do
- expect(subject.note).to eq status
- end
- end
end
describe '.merge_when_pipeline_succeeds' do
+ let(:project) { create(:project, :repository) }
let(:pipeline) { build(:ci_pipeline_without_jobs )}
let(:noteable) do
create(:merge_request, source_project: project, target_project: project)
@@ -223,21 +249,26 @@ describe SystemNoteService, services: true do
subject { described_class.merge_when_pipeline_succeeds(noteable, project, author, noteable.diff_head_commit) }
- it_behaves_like 'a system note'
+ it_behaves_like 'a system note' do
+ let(:action) { 'merge' }
+ end
it "posts the 'merge when pipeline succeeds' system note" do
- expect(subject.note).to match /enabled an automatic merge when the pipeline for (\w+\/\w+@)?\h{40} succeeds/
+ expect(subject.note).to match(/enabled an automatic merge when the pipeline for (\w+\/\w+@)?\h{40} succeeds/)
end
end
describe '.cancel_merge_when_pipeline_succeeds' do
+ let(:project) { create(:project, :repository) }
let(:noteable) do
create(:merge_request, source_project: project, target_project: project)
end
subject { described_class.cancel_merge_when_pipeline_succeeds(noteable, project, author) }
- it_behaves_like 'a system note'
+ it_behaves_like 'a system note' do
+ let(:action) { 'merge' }
+ end
it "posts the 'merge when pipeline succeeds' system note" do
expect(subject.note).to eq "canceled the automatic merge"
@@ -250,7 +281,9 @@ describe SystemNoteService, services: true do
subject { described_class.change_title(noteable, project, author, 'Old title') }
context 'when noteable responds to `title`' do
- it_behaves_like 'a system note'
+ it_behaves_like 'a system note' do
+ let(:action) { 'title' }
+ end
it 'sets the note text' do
expect(subject.note).
@@ -262,8 +295,24 @@ describe SystemNoteService, services: true do
describe '.change_issue_confidentiality' do
subject { described_class.change_issue_confidentiality(noteable, project, author) }
- context 'when noteable responds to `confidential`' do
- it_behaves_like 'a system note'
+ context 'issue has been made confidential' do
+ before do
+ noteable.update_attribute(:confidential, true)
+ end
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'confidential' }
+ end
+
+ it 'sets the note text' do
+ expect(subject.note).to eq 'made the issue confidential'
+ end
+ end
+
+ context 'issue has been made visible' do
+ it_behaves_like 'a system note' do
+ let(:action) { 'visible' }
+ end
it 'sets the note text' do
expect(subject.note).to eq 'made the issue visible to everyone'
@@ -273,10 +322,14 @@ describe SystemNoteService, services: true do
describe '.change_branch' do
subject { described_class.change_branch(noteable, project, author, 'target', old_branch, new_branch) }
+
+ let(:project) { create(:project, :repository) }
let(:old_branch) { 'old_branch'}
let(:new_branch) { 'new_branch'}
- it_behaves_like 'a system note'
+ it_behaves_like 'a system note' do
+ let(:action) { 'branch' }
+ end
context 'when target branch name changed' do
it 'sets the note text' do
@@ -288,7 +341,11 @@ describe SystemNoteService, services: true do
describe '.change_branch_presence' do
subject { described_class.change_branch_presence(noteable, project, author, :source, 'feature', :delete) }
- it_behaves_like 'a system note'
+ let(:project) { create(:project, :repository) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'branch' }
+ end
context 'when source branch deleted' do
it 'sets the note text' do
@@ -300,11 +357,15 @@ describe SystemNoteService, services: true do
describe '.new_issue_branch' do
subject { described_class.new_issue_branch(noteable, project, author, "1-mepmep") }
- it_behaves_like 'a system note'
+ let(:project) { create(:project, :repository) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'branch' }
+ end
context 'when a branch is created from the new branch button' do
it 'sets the note text' do
- expect(subject.note).to match /\Acreated branch [`1-mepmep`]/
+ expect(subject.note).to start_with("created branch [`1-mepmep`]")
end
end
end
@@ -314,7 +375,9 @@ describe SystemNoteService, services: true do
let(:mentioner) { create(:issue, project: project) }
- it_behaves_like 'a system note'
+ it_behaves_like 'a system note' do
+ let(:action) { 'cross_reference' }
+ end
context 'when cross-reference disallowed' do
before do
@@ -324,6 +387,10 @@ describe SystemNoteService, services: true do
it 'returns nil' do
expect(subject).to be_nil
end
+
+ it 'does not create a system note metadata record' do
+ expect { subject }.not_to change{ SystemNoteMetadata.count }
+ end
end
context 'when cross-reference allowed' do
@@ -331,9 +398,13 @@ describe SystemNoteService, services: true do
expect(described_class).to receive(:cross_reference_disallowed?).and_return(false)
end
+ it_behaves_like 'a system note' do
+ let(:action) { 'cross_reference' }
+ end
+
describe 'note_body' do
context 'cross-project' do
- let(:project2) { create(:project) }
+ let(:project2) { create(:project, :repository) }
let(:mentioner) { create(:issue, project: project2) }
context 'from Commit' do
@@ -353,6 +424,7 @@ describe SystemNoteService, services: true do
context 'within the same project' do
context 'from Commit' do
+ let(:project) { create(:project, :repository) }
let(:mentioner) { project.repository.commit }
it 'references the mentioning commit' do
@@ -394,6 +466,7 @@ describe SystemNoteService, services: true do
end
context 'when mentioner is a MergeRequest' do
+ let(:project) { create(:project, :repository) }
let(:mentioner) { create(:merge_request, :simple, source_project: project) }
let(:noteable) { project.commit }
@@ -421,6 +494,7 @@ describe SystemNoteService, services: true do
end
describe '.cross_reference_exists?' do
+ let(:project) { create(:project, :repository) }
let(:commit0) { project.commit }
let(:commit1) { project.commit('HEAD~2') }
@@ -513,7 +587,7 @@ describe SystemNoteService, services: true do
end
describe '.noteable_moved' do
- let(:new_project) { create(:project) }
+ let(:new_project) { create(:empty_project) }
let(:new_noteable) { create(:issue, project: new_project) }
subject do
@@ -540,9 +614,12 @@ describe SystemNoteService, services: true do
let(:direction) { :to }
it_behaves_like 'cross project mentionable'
+ it_behaves_like 'a system note' do
+ let(:action) { 'moved' }
+ end
it 'notifies about noteable being moved to' do
- expect(subject.note).to match /moved to/
+ expect(subject.note).to match('moved to')
end
end
@@ -550,9 +627,12 @@ describe SystemNoteService, services: true do
let(:direction) { :from }
it_behaves_like 'cross project mentionable'
+ it_behaves_like 'a system note' do
+ let(:action) { 'moved' }
+ end
it 'notifies about noteable being moved from' do
- expect(subject.note).to match /moved from/
+ expect(subject.note).to match('moved from')
end
end
@@ -574,13 +654,13 @@ describe SystemNoteService, services: true do
end
end
- include JiraServiceHelper
-
describe 'JIRA integration' do
+ include JiraServiceHelper
+
let(:project) { create(:jira_project) }
let(:author) { create(:user) }
let(:issue) { create(:issue, project: project) }
- let(:merge_request) { create(:merge_request, :simple, target_project: project, source_project: project) }
+ let(:merge_request) { create(:merge_request, :simple, target_project: project, source_project: project) }
let(:jira_issue) { ExternalIssue.new("JIRA-1", project)}
let(:jira_tracker) { project.jira_service }
let(:commit) { project.commit }
@@ -720,33 +800,34 @@ describe SystemNoteService, services: true do
let(:merge_request) { discussion.noteable }
let(:project) { merge_request.source_project }
let(:issue) { create(:issue, project: project) }
- let(:user) { create(:user) }
def reloaded_merge_request
MergeRequest.find(merge_request.id)
end
- before do
- project.team << [user, :developer]
+ subject { described_class.discussion_continued_in_issue(discussion, project, author, issue) }
+
+ it_behaves_like 'a system note' do
+ let(:expected_noteable) { discussion.first_note.noteable }
+ let(:action) { 'discussion' }
end
it 'creates a new note in the discussion' do
# we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded.
- expect { SystemNoteService.discussion_continued_in_issue(discussion, project, user, issue) }.
- to change { reloaded_merge_request.discussions.first.notes.size }.by(1)
+ expect { subject }.to change { reloaded_merge_request.discussions.first.notes.size }.by(1)
end
it 'mentions the created issue in the system note' do
- note = SystemNoteService.discussion_continued_in_issue(discussion, project, user, issue)
-
- expect(note.note).to include(issue.to_reference)
+ expect(subject.note).to include(issue.to_reference)
end
end
describe '.change_time_estimate' do
subject { described_class.change_time_estimate(noteable, project, author) }
- it_behaves_like 'a system note'
+ it_behaves_like 'a system note' do
+ let(:action) { 'time_tracking' }
+ end
context 'with a time estimate' do
it 'sets the note text' do
@@ -776,7 +857,9 @@ describe SystemNoteService, services: true do
described_class.change_time_spent(noteable, project, author)
end
- it_behaves_like 'a system note'
+ it_behaves_like 'a system note' do
+ let(:action) { 'time_tracking' }
+ end
context 'when time was added' do
it 'sets the note text' do
@@ -808,7 +891,36 @@ describe SystemNoteService, services: true do
end
end
+ describe '.remove_merge_request_wip' do
+ let(:noteable) { create(:issue, project: project, title: 'WIP: Lorem ipsum') }
+
+ subject { described_class.remove_merge_request_wip(noteable, project, author) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'title' }
+ end
+
+ it 'sets the note text' do
+ expect(subject.note).to eq 'unmarked as a **Work In Progress**'
+ end
+ end
+
+ describe '.add_merge_request_wip' do
+ let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum') }
+
+ subject { described_class.add_merge_request_wip(noteable, project, author) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'title' }
+ end
+
+ it 'sets the note text' do
+ expect(subject.note).to eq 'marked as a **Work In Progress**'
+ end
+ end
+
describe '.add_merge_request_wip_from_commit' do
+ let(:project) { create(:project, :repository) }
let(:noteable) do
create(:merge_request, source_project: project, target_project: project)
end
@@ -822,7 +934,9 @@ describe SystemNoteService, services: true do
)
end
- it_behaves_like 'a system note'
+ it_behaves_like 'a system note' do
+ let(:action) { 'title' }
+ end
it "posts the 'marked as a Work In Progress from commit' system note" do
expect(subject.note).to match(
@@ -830,4 +944,33 @@ describe SystemNoteService, services: true do
)
end
end
+
+ describe '.change_task_status' do
+ let(:noteable) { create(:issue, project: project) }
+ let(:task) { double(:task, complete?: true, source: 'task') }
+
+ subject { described_class.change_task_status(noteable, project, author, task) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'task' }
+ end
+
+ it "posts the 'marked as a Work In Progress from commit' system note" do
+ expect(subject.note).to eq("marked the task **task** as completed")
+ end
+ end
+
+ describe '.resolve_all_discussions' do
+ let(:noteable) { create(:merge_request, source_project: project, target_project: project) }
+
+ subject { described_class.resolve_all_discussions(noteable, project, author) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'discussion' }
+ end
+
+ it 'sets the note text' do
+ expect(subject.note).to eq 'resolved all discussions'
+ end
+ end
end
diff --git a/spec/services/tags/create_service_spec.rb b/spec/services/tags/create_service_spec.rb
index 5478b8c9ec0..b9121b1de49 100644
--- a/spec/services/tags/create_service_spec.rb
+++ b/spec/services/tags/create_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Tags::CreateService, services: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:user) { create(:user) }
let(:service) { described_class.new(project, user) }
diff --git a/spec/services/tags/destroy_service_spec.rb b/spec/services/tags/destroy_service_spec.rb
index a388c93379a..28396fc3658 100644
--- a/spec/services/tags/destroy_service_spec.rb
+++ b/spec/services/tags/destroy_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Tags::DestroyService, services: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:user) { create(:user) }
let(:service) { described_class.new(project, user) }
diff --git a/spec/services/test_hook_service_spec.rb b/spec/services/test_hook_service_spec.rb
index 4f6dd8c6d3f..f99fd8434c2 100644
--- a/spec/services/test_hook_service_spec.rb
+++ b/spec/services/test_hook_service_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
describe TestHookService, services: true do
- let(:user) { create :user }
- let(:project) { create :project }
- let(:hook) { create :project_hook, project: project }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:hook) { create(:project_hook, project: project) }
describe '#execute' do
it "executes successfully" do
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 3645b73b039..89b3b6aad10 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -8,10 +8,12 @@ describe TodoService, services: true do
let(:guest) { create(:user) }
let(:admin) { create(:admin) }
let(:john_doe) { create(:user) }
- let(:project) { create(:project) }
- let(:mentions) { 'FYI: ' + [author, assignee, john_doe, member, guest, non_member, admin].map(&:to_reference).join(' ') }
- let(:directly_addressed) { [author, assignee, john_doe, member, guest, non_member, admin].map(&:to_reference).join(' ') }
- let(:directly_addressed_and_mentioned) { member.to_reference + ", what do you think? cc: " + [guest, admin].map(&:to_reference).join(' ') }
+ let(:skipped) { create(:user) }
+ let(:skip_users) { [skipped] }
+ let(:project) { create(:empty_project) }
+ let(:mentions) { 'FYI: ' + [author, assignee, john_doe, member, guest, non_member, admin, skipped].map(&:to_reference).join(' ') }
+ let(:directly_addressed) { [author, assignee, john_doe, member, guest, non_member, admin, skipped].map(&:to_reference).join(' ') }
+ let(:directly_addressed_and_mentioned) { member.to_reference + ", what do you think? cc: " + [guest, admin, skipped].map(&:to_reference).join(' ') }
let(:service) { described_class.new }
before do
@@ -19,6 +21,7 @@ describe TodoService, services: true do
project.team << [author, :developer]
project.team << [member, :developer]
project.team << [john_doe, :developer]
+ project.team << [skipped, :developer]
end
describe 'Issues' do
@@ -99,9 +102,9 @@ describe TodoService, services: true do
end
context 'when a private group is mentioned' do
- let(:group) { create :group, :private }
- let(:project) { create :project, :private, group: group }
- let(:issue) { create :issue, author: author, project: project, description: group.to_reference }
+ let(:group) { create(:group, :private) }
+ let(:project) { create(:empty_project, :private, group: group) }
+ let(:issue) { create(:issue, author: author, project: project, description: group.to_reference) }
before do
group.add_owner(author)
@@ -119,46 +122,61 @@ describe TodoService, services: true do
end
describe '#update_issue' do
- it 'creates a todo for each valid mentioned user' do
- service.update_issue(issue, author)
+ it 'creates a todo for each valid mentioned user not included in skip_users' do
+ service.update_issue(issue, author, skip_users)
should_create_todo(user: member, target: issue, action: Todo::MENTIONED)
should_create_todo(user: guest, target: issue, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED)
should_create_todo(user: author, target: issue, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: issue, action: Todo::MENTIONED)
+ should_not_create_todo(user: skipped, target: issue, action: Todo::MENTIONED)
end
- it 'creates a todo for each valid user based on the type of mention' do
+ it 'creates a todo for each valid user not included in skip_users based on the type of mention' do
issue.update(description: directly_addressed_and_mentioned)
- service.update_issue(issue, author)
+ service.update_issue(issue, author, skip_users)
should_create_todo(user: member, target: issue, action: Todo::DIRECTLY_ADDRESSED)
should_create_todo(user: guest, target: issue, action: Todo::MENTIONED)
should_create_todo(user: admin, target: issue, action: Todo::MENTIONED)
+ should_not_create_todo(user: skipped, target: issue)
end
- it 'creates a directly addressed todo for each valid addressed user' do
- service.update_issue(addressed_issue, author)
+ it 'creates a directly addressed todo for each valid addressed user not included in skip_users' do
+ service.update_issue(addressed_issue, author, skip_users)
should_create_todo(user: member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
should_create_todo(user: guest, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
should_create_todo(user: john_doe, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
should_create_todo(user: author, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
should_not_create_todo(user: non_member, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: skipped, target: addressed_issue, action: Todo::DIRECTLY_ADDRESSED)
end
- it 'does not create a todo if user was already mentioned' do
+ it 'does not create a todo if user was already mentioned and todo is pending' do
create(:todo, :mentioned, user: member, project: project, target: issue, author: author)
- expect { service.update_issue(issue, author) }.not_to change(member.todos, :count)
+ expect { service.update_issue(issue, author, skip_users) }.not_to change(member.todos, :count)
+ end
+
+ it 'does not create a todo if user was already mentioned and todo is done' do
+ create(:todo, :mentioned, :done, user: skipped, project: project, target: issue, author: author)
+
+ expect { service.update_issue(issue, author, skip_users) }.not_to change(skipped.todos, :count)
end
- it 'does not create a directly addressed todo if user was already mentioned or addressed' do
+ it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is pending' do
create(:todo, :directly_addressed, user: member, project: project, target: addressed_issue, author: author)
- expect { service.update_issue(addressed_issue, author) }.not_to change(member.todos, :count)
+ expect { service.update_issue(addressed_issue, author, skip_users) }.not_to change(member.todos, :count)
+ end
+
+ it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is done' do
+ create(:todo, :directly_addressed, :done, user: skipped, project: project, target: addressed_issue, author: author)
+
+ expect { service.update_issue(addressed_issue, author, skip_users) }.not_to change(skipped.todos, :count)
end
it 'does not create todo if user can not see the issue when issue is confidential' do
@@ -422,22 +440,26 @@ describe TodoService, services: true do
should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue)
end
- it 'creates a todo for each valid mentioned user when leaving a note on commit' do
- service.new_note(note_on_commit, john_doe)
+ context 'on commit' do
+ let(:project) { create(:project, :repository) }
- should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
- should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
- should_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
- should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
- end
+ it 'creates a todo for each valid mentioned user when leaving a note on commit' do
+ service.new_note(note_on_commit, john_doe)
+
+ should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
+ should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
+ should_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
+ should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit)
+ end
- it 'creates a directly addressed todo for each valid mentioned user when leaving a note on commit' do
- service.new_note(addressed_note_on_commit, john_doe)
+ it 'creates a directly addressed todo for each valid mentioned user when leaving a note on commit' do
+ service.new_note(addressed_note_on_commit, john_doe)
- should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit)
- should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit)
- should_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit)
- should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit)
+ should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit)
+ should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit)
+ should_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit)
+ should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit)
+ end
end
it 'does not create todo when leaving a note on snippet' do
@@ -517,47 +539,62 @@ describe TodoService, services: true do
end
describe '#update_merge_request' do
- it 'creates a todo for each valid mentioned user' do
- service.update_merge_request(mr_assigned, author)
+ it 'creates a todo for each valid mentioned user not included in skip_users' do
+ service.update_merge_request(mr_assigned, author, skip_users)
should_create_todo(user: member, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED)
should_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED)
should_not_create_todo(user: non_member, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: skipped, target: mr_assigned, action: Todo::MENTIONED)
end
- it 'creates a todo for each valid user based on the type of mention' do
+ it 'creates a todo for each valid user not included in skip_users based on the type of mention' do
mr_assigned.update(description: directly_addressed_and_mentioned)
- service.update_merge_request(mr_assigned, author)
+ service.update_merge_request(mr_assigned, author, skip_users)
should_create_todo(user: member, target: mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
should_create_todo(user: admin, target: mr_assigned, action: Todo::MENTIONED)
+ should_not_create_todo(user: skipped, target: mr_assigned)
end
- it 'creates a directly addressed todo for each valid addressed user' do
- service.update_merge_request(addressed_mr_assigned, author)
+ it 'creates a directly addressed todo for each valid addressed user not included in skip_users' do
+ service.update_merge_request(addressed_mr_assigned, author, skip_users)
should_create_todo(user: member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
should_create_todo(user: john_doe, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
should_create_todo(user: author, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
should_not_create_todo(user: non_member, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: skipped, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
end
- it 'does not create a todo if user was already mentioned' do
+ it 'does not create a todo if user was already mentioned and todo is pending' do
create(:todo, :mentioned, user: member, project: project, target: mr_assigned, author: author)
expect { service.update_merge_request(mr_assigned, author) }.not_to change(member.todos, :count)
end
- it 'does not create a directly addressed todo if user was already mentioned or addressed' do
+ it 'does not create a todo if user was already mentioned and todo is done' do
+ create(:todo, :mentioned, :done, user: skipped, project: project, target: mr_assigned, author: author)
+
+ expect { service.update_merge_request(mr_assigned, author, skip_users) }.not_to change(skipped.todos, :count)
+ end
+
+ it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is pending' do
create(:todo, :directly_addressed, user: member, project: project, target: addressed_mr_assigned, author: author)
expect{ service.update_merge_request(addressed_mr_assigned, author) }.not_to change(member.todos, :count)
end
+ it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is done' do
+ create(:todo, :directly_addressed, user: skipped, project: project, target: addressed_mr_assigned, author: author)
+
+ expect{ service.update_merge_request(addressed_mr_assigned, author, skip_users) }.not_to change(skipped.todos, :count)
+ end
+
context 'with a task list' do
it 'does not create todo when tasks are marked as completed' do
mr_assigned.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
@@ -720,6 +757,7 @@ describe TodoService, services: true do
end
describe '#new_note' do
+ let(:project) { create(:project, :repository) }
let(:mention) { john_doe.to_reference }
let(:diff_note_on_merge_request) { create(:diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "Hey #{mention}") }
let(:addressed_diff_note_on_merge_request) { create(:diff_note_on_merge_request, project: project, noteable: mr_unassigned, author: author, note: "#{mention}, hey!") }
@@ -752,6 +790,69 @@ describe TodoService, services: true do
end
end
+ describe '#update_note' do
+ let(:noteable) { create(:issue, project: project) }
+ let(:note) { create(:note, project: project, note: mentions, noteable: noteable) }
+ let(:addressed_note) { create(:note, project: project, note: "#{directly_addressed}", noteable: noteable) }
+
+ it 'creates a todo for each valid mentioned user not included in skip_users' do
+ service.update_note(note, author, skip_users)
+
+ should_create_todo(user: member, target: noteable, action: Todo::MENTIONED)
+ should_create_todo(user: guest, target: noteable, action: Todo::MENTIONED)
+ should_create_todo(user: john_doe, target: noteable, action: Todo::MENTIONED)
+ should_create_todo(user: author, target: noteable, action: Todo::MENTIONED)
+ should_not_create_todo(user: non_member, target: noteable, action: Todo::MENTIONED)
+ should_not_create_todo(user: skipped, target: noteable, action: Todo::MENTIONED)
+ end
+
+ it 'creates a todo for each valid user not included in skip_users based on the type of mention' do
+ note.update(note: directly_addressed_and_mentioned)
+
+ service.update_note(note, author, skip_users)
+
+ should_create_todo(user: member, target: noteable, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: guest, target: noteable, action: Todo::MENTIONED)
+ should_create_todo(user: admin, target: noteable, action: Todo::MENTIONED)
+ should_not_create_todo(user: skipped, target: noteable)
+ end
+
+ it 'creates a directly addressed todo for each valid addressed user not included in skip_users' do
+ service.update_note(addressed_note, author, skip_users)
+
+ should_create_todo(user: member, target: noteable, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: guest, target: noteable, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: john_doe, target: noteable, action: Todo::DIRECTLY_ADDRESSED)
+ should_create_todo(user: author, target: noteable, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: non_member, target: noteable, action: Todo::DIRECTLY_ADDRESSED)
+ should_not_create_todo(user: skipped, target: noteable, action: Todo::DIRECTLY_ADDRESSED)
+ end
+
+ it 'does not create a todo if user was already mentioned and todo is pending' do
+ create(:todo, :mentioned, user: member, project: project, target: noteable, author: author)
+
+ expect { service.update_note(note, author, skip_users) }.not_to change(member.todos, :count)
+ end
+
+ it 'does not create a todo if user was already mentioned and todo is done' do
+ create(:todo, :mentioned, :done, user: skipped, project: project, target: noteable, author: author)
+
+ expect { service.update_note(note, author, skip_users) }.not_to change(skipped.todos, :count)
+ end
+
+ it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is pending' do
+ create(:todo, :directly_addressed, user: member, project: project, target: noteable, author: author)
+
+ expect { service.update_note(addressed_note, author, skip_users) }.not_to change(member.todos, :count)
+ end
+
+ it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is done' do
+ create(:todo, :directly_addressed, :done, user: skipped, project: project, target: noteable, author: author)
+
+ expect { service.update_note(addressed_note, author, skip_users) }.not_to change(skipped.todos, :count)
+ end
+ end
+
it 'updates cached counts when a todo is created' do
issue = create(:issue, project: project, assignee: john_doe, author: author, description: mentions)
diff --git a/spec/services/update_release_service_spec.rb b/spec/services/update_release_service_spec.rb
index bba211089a8..69ed8de9c31 100644
--- a/spec/services/update_release_service_spec.rb
+++ b/spec/services/update_release_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe UpdateReleaseService, services: true do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:tag_name) { project.repository.tag_names.first }
let(:description) { 'Awesome release!' }
diff --git a/spec/services/users/create_service_spec.rb b/spec/services/users/create_service_spec.rb
new file mode 100644
index 00000000000..a111aec2f89
--- /dev/null
+++ b/spec/services/users/create_service_spec.rb
@@ -0,0 +1,225 @@
+require 'spec_helper'
+
+describe Users::CreateService, services: true do
+ describe '#build' do
+ let(:params) do
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass' }
+ end
+
+ context 'with an admin user' do
+ let(:admin_user) { create(:admin) }
+ let(:service) { described_class.new(admin_user, params) }
+
+ it 'returns a valid user' do
+ expect(service.build).to be_valid
+ end
+ end
+
+ context 'with non admin user' do
+ let(:user) { create(:user) }
+ let(:service) { described_class.new(user, params) }
+
+ it 'raises AccessDeniedError exception' do
+ expect { service.build }.to raise_error Gitlab::Access::AccessDeniedError
+ end
+ end
+
+ context 'with nil user' do
+ let(:service) { described_class.new(nil, params) }
+
+ it 'returns a valid user' do
+ expect(service.build).to be_valid
+ end
+ end
+ end
+
+ describe '#execute' do
+ let(:admin_user) { create(:admin) }
+
+ context 'with an admin user' do
+ let(:service) { described_class.new(admin_user, params) }
+
+ context 'when required parameters are provided' do
+ let(:params) do
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass' }
+ end
+
+ it 'returns a persisted user' do
+ expect(service.execute).to be_persisted
+ end
+
+ it 'persists the given attributes' do
+ user = service.execute
+ user.reload
+
+ expect(user).to have_attributes(
+ name: params[:name],
+ username: params[:username],
+ email: params[:email],
+ password: params[:password],
+ created_by_id: admin_user.id
+ )
+ end
+
+ context 'when the current_user is not persisted' do
+ let(:admin_user) { build(:admin) }
+
+ it 'persists the given attributes and sets created_by_id to nil' do
+ user = service.execute
+ user.reload
+
+ expect(user).to have_attributes(
+ name: params[:name],
+ username: params[:username],
+ email: params[:email],
+ password: params[:password],
+ created_by_id: nil
+ )
+ end
+ end
+
+ it 'user is not confirmed if skip_confirmation param is not present' do
+ expect(service.execute).not_to be_confirmed
+ end
+
+ it 'logs the user creation' do
+ expect(service).to receive(:log_info).with("User \"John Doe\" (jd@example.com) was created")
+
+ service.execute
+ end
+
+ it 'executes system hooks ' do
+ system_hook_service = spy(:system_hook_service)
+
+ expect(service).to receive(:system_hook_service).and_return(system_hook_service)
+
+ user = service.execute
+
+ expect(system_hook_service).to have_received(:execute_hooks_for).with(user, :create)
+ end
+
+ it 'does not send a notification email' do
+ notification_service = spy(:notification_service)
+
+ expect(service).not_to receive(:notification_service)
+
+ service.execute
+
+ expect(notification_service).not_to have_received(:new_user)
+ end
+ end
+
+ context 'when force_random_password parameter is true' do
+ let(:params) do
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', force_random_password: true }
+ end
+
+ it 'generates random password' do
+ user = service.execute
+
+ expect(user.password).not_to eq 'mydummypass'
+ expect(user.password).to be_present
+ end
+ end
+
+ context 'when password_automatically_set parameter is true' do
+ let(:params) do
+ {
+ name: 'John Doe',
+ username: 'jduser',
+ email: 'jd@example.com',
+ password: 'mydummypass',
+ password_automatically_set: true
+ }
+ end
+
+ it 'persists the given attributes' do
+ user = service.execute
+ user.reload
+
+ expect(user).to have_attributes(
+ name: params[:name],
+ username: params[:username],
+ email: params[:email],
+ password: params[:password],
+ created_by_id: admin_user.id,
+ password_automatically_set: params[:password_automatically_set]
+ )
+ end
+ end
+
+ context 'when skip_confirmation parameter is true' do
+ let(:params) do
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', skip_confirmation: true }
+ end
+
+ it 'confirms the user' do
+ expect(service.execute).to be_confirmed
+ end
+ end
+
+ context 'when reset_password parameter is true' do
+ let(:params) do
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', reset_password: true }
+ end
+
+ it 'resets password even if a password parameter is given' do
+ expect(service.execute).to be_recently_sent_password_reset
+ end
+
+ it 'sends a notification email' do
+ notification_service = spy(:notification_service)
+
+ expect(service).to receive(:notification_service).and_return(notification_service)
+
+ user = service.execute
+
+ expect(notification_service).to have_received(:new_user).with(user, an_instance_of(String))
+ end
+ end
+ end
+
+ context 'with nil user' do
+ let(:params) do
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', skip_confirmation: true }
+ end
+ let(:service) { described_class.new(nil, params) }
+
+ context 'when "send_user_confirmation_email" application setting is true' do
+ before do
+ current_application_settings = double(:current_application_settings, send_user_confirmation_email: true, signup_enabled?: true)
+ allow(service).to receive(:current_application_settings).and_return(current_application_settings)
+ end
+
+ it 'does not confirm the user' do
+ expect(service.execute).not_to be_confirmed
+ end
+ end
+
+ context 'when "send_user_confirmation_email" application setting is false' do
+ before do
+ current_application_settings = double(:current_application_settings, send_user_confirmation_email: false, signup_enabled?: true)
+ allow(service).to receive(:current_application_settings).and_return(current_application_settings)
+ end
+
+ it 'confirms the user' do
+ expect(service.execute).to be_confirmed
+ end
+
+ it 'persists the given attributes' do
+ user = service.execute
+ user.reload
+
+ expect(user).to have_attributes(
+ name: params[:name],
+ username: params[:username],
+ email: params[:email],
+ password: params[:password],
+ created_by_id: nil,
+ admin: false
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/users/destroy_spec.rb b/spec/services/users/destroy_spec.rb
index 922e82445d0..66c61b7f8ff 100644
--- a/spec/services/users/destroy_spec.rb
+++ b/spec/services/users/destroy_spec.rb
@@ -5,7 +5,7 @@ describe Users::DestroyService, services: true do
let!(:user) { create(:user) }
let!(:admin) { create(:admin) }
let!(:namespace) { create(:namespace, owner: user) }
- let!(:project) { create(:project, namespace: namespace) }
+ let!(:project) { create(:empty_project, namespace: namespace) }
let(:service) { described_class.new(admin) }
context 'no options are given' do
@@ -17,15 +17,30 @@ describe Users::DestroyService, services: true do
expect { Namespace.with_deleted.find(user.namespace.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
- it 'will delete the project in the near future' do
- expect_any_instance_of(Projects::DestroyService).to receive(:async_execute).once
+ it 'will delete the project' do
+ expect_any_instance_of(Projects::DestroyService).to receive(:execute).once
service.execute(user)
end
end
+ context 'projects in pending_delete' do
+ before do
+ project.pending_delete = true
+ project.save
+ end
+
+ it 'destroys a project in pending_delete' do
+ expect_any_instance_of(Projects::DestroyService).to receive(:execute).once
+
+ service.execute(user)
+
+ expect { Project.find(project.id) }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
context "a deleted user's issues" do
- let(:project) { create :project }
+ let(:project) { create(:project) }
before do
project.add_developer(user)
diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb
index 08733d6dcf1..b19374ef1a2 100644
--- a/spec/services/users/refresh_authorized_projects_service_spec.rb
+++ b/spec/services/users/refresh_authorized_projects_service_spec.rb
@@ -152,7 +152,7 @@ describe Users::RefreshAuthorizedProjectsService do
context 'projects of groups the user is a member of' do
let(:group) { create(:group) }
- let!(:other_project) { create(:project, group: group) }
+ let!(:other_project) { create(:empty_project, group: group) }
before do
group.add_owner(user)
@@ -166,7 +166,7 @@ describe Users::RefreshAuthorizedProjectsService do
context 'projects of subgroups of groups the user is a member of' do
let(:group) { create(:group) }
let(:nested_group) { create(:group, parent: group) }
- let!(:other_project) { create(:project, group: nested_group) }
+ let!(:other_project) { create(:empty_project, group: nested_group) }
before do
group.add_master(user)
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 5ab8f0d981a..4eb5b150af5 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -9,7 +9,8 @@ require 'rspec/rails'
require 'shoulda/matchers'
require 'rspec/retry'
-if ENV['RSPEC_PROFILING_POSTGRES_URL'] || ENV['RSPEC_PROFILING']
+if (ENV['RSPEC_PROFILING_POSTGRES_URL'] || ENV['RSPEC_PROFILING']) &&
+ (!ENV.has_key?('CI') || ENV['CI_COMMIT_REF_NAME'] == 'master')
require 'rspec_profiling/rspec'
end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index aa14709bc9c..b8ca8f22a3d 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -1,10 +1,11 @@
+# rubocop:disable Style/GlobalVars
require 'capybara/rails'
require 'capybara/rspec'
require 'capybara/poltergeist'
require 'capybara-screenshot/rspec'
# Give CI some extra time
-timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 30 : 10
+timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
@@ -26,7 +27,10 @@ Capybara.ignore_hidden_elements = true
Capybara::Screenshot.prune_strategy = :keep_last_run
RSpec.configure do |config|
- config.before(:suite) do
- TestEnv.warm_asset_cache
+ config.before(:context, :js) do
+ next if $capybara_server_already_started
+
+ TestEnv.eager_load_driver_server
+ $capybara_server_already_started = true
end
end
diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb
index d0fd2d52004..c59b30c772d 100644
--- a/spec/support/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb
@@ -180,7 +180,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
it "takes the new namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, provider_repo.name, an_instance_of(Group), user, access_params, type: provider).
- and_return(double(execute: true))
+ and_return(double(execute: true))
post :create, target_namespace: provider_repo.name, format: :js
end
@@ -201,7 +201,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
it "takes the current user's namespace" do
expect(Gitlab::GithubImport::ProjectCreator).
to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider).
- and_return(double(execute: true))
+ and_return(double(execute: true))
post :create, format: :js
end
@@ -228,5 +228,72 @@ shared_examples 'a GitHub-ish import controller: POST create' do
post :create, { new_name: test_name, format: :js }
end
end
+
+ context 'user has chosen an existing nested namespace and name for the project' do
+ let(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
+ let(:nested_namespace) { create(:namespace, name: 'bar', parent: parent_namespace, owner: user) }
+ let(:test_name) { 'test_name' }
+
+ it 'takes the selected namespace and name' do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, test_name, nested_namespace, user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ post :create, { target_namespace: nested_namespace.full_path, new_name: test_name, format: :js }
+ end
+ end
+
+ context 'user has chosen a non-existent nested namespaces and name for the project' do
+ let(:test_name) { 'test_name' }
+
+ it 'takes the selected namespace and name' do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js }
+ end
+
+ it 'creates the namespaces' do
+ allow(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ expect { post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js } }
+ .to change { Namespace.count }.by(2)
+ end
+
+ it 'new namespace has the right parent' do
+ allow(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ post :create, { target_namespace: 'foo/bar', new_name: test_name, format: :js }
+
+ expect(Namespace.find_by_path_or_name('bar').parent.path).to eq('foo')
+ end
+ end
+
+ context 'user has chosen existent and non-existent nested namespaces and name for the project' do
+ let(:test_name) { 'test_name' }
+ let!(:parent_namespace) { create(:namespace, name: 'foo', owner: user) }
+
+ it 'takes the selected namespace and name' do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :js }
+ end
+
+ it 'creates the namespaces' do
+ allow(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider).
+ and_return(double(execute: true))
+
+ expect { post :create, { target_namespace: 'foo/foobar/bar', new_name: test_name, format: :js } }
+ .to change { Namespace.count }.by(2)
+ end
+ end
end
end
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
index c864a705ca4..8ad042f5e3b 100644
--- a/spec/support/cycle_analytics_helpers.rb
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -1,5 +1,5 @@
module CycleAnalyticsHelpers
- def create_commit_referencing_issue(issue, branch_name: random_git_name)
+ def create_commit_referencing_issue(issue, branch_name: generate(:branch))
project.repository.add_branch(user, branch_name, 'master')
create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name)
end
@@ -7,9 +7,7 @@ module CycleAnalyticsHelpers
def create_commit(message, project, user, branch_name, count: 1)
oldrev = project.repository.commit(branch_name).sha
commit_shas = Array.new(count) do |index|
- filename = random_git_name
-
- commit_sha = project.repository.create_file(user, filename, "content", message: message, branch_name: branch_name)
+ commit_sha = project.repository.create_file(user, generate(:branch), "content", message: message, branch_name: branch_name)
project.repository.commit(commit_sha)
commit_sha
@@ -24,13 +22,13 @@ module CycleAnalyticsHelpers
def create_merge_request_closing_issue(issue, message: nil, source_branch: nil)
if !source_branch || project.repository.commit(source_branch).blank?
- source_branch = random_git_name
+ source_branch = generate(:branch)
project.repository.add_branch(user, source_branch, 'master')
end
sha = project.repository.create_file(
user,
- random_git_name,
+ generate(:branch),
'content',
message: 'commit message',
branch_name: source_branch)
diff --git a/spec/support/drag_to_helper.rb b/spec/support/drag_to_helper.rb
index 0c0659d3ecd..ae149631ed9 100644
--- a/spec/support/drag_to_helper.rb
+++ b/spec/support/drag_to_helper.rb
@@ -3,11 +3,11 @@ module DragTo
evaluate_script("simulateDrag({scrollable: $('#{scrollable}').get(0), from: {el: $('#{selector}').eq(#{list_from_index}).get(0), index: #{from_index}}, to: {el: $('#{selector}').eq(#{list_to_index}).get(0), index: #{to_index}}});")
Timeout.timeout(Capybara.default_max_wait_time) do
- loop until drag_active?
+ loop while drag_active?
end
end
def drag_active?
- page.evaluate_script('window.SIMULATE_DRAG_ACTIVE').zero?
+ page.evaluate_script('window.SIMULATE_DRAG_ACTIVE').nonzero?
end
end
diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb
index a8e454eb09e..b871b7ffc90 100644
--- a/spec/support/filter_spec_helper.rb
+++ b/spec/support/filter_spec_helper.rb
@@ -63,9 +63,9 @@ module FilterSpecHelper
#
# Returns a String
def invalidate_reference(reference)
- if reference =~ /\A(.+)?.\d+\z/
+ if reference =~ /\A(.+)?[^\d]\d+\z/
# Integer-based reference with optional project prefix
- reference.gsub(/\d+\z/) { |i| i.to_i + 1 }
+ reference.gsub(/\d+\z/) { |i| i.to_i + 10_000 }
elsif reference =~ /\A(.+@)?(\h{7,40}\z)/
# SHA-based reference with optional prefix
reference.gsub(/\h{7,40}\z/) { |v| v.reverse }
diff --git a/spec/support/git_helpers.rb b/spec/support/git_helpers.rb
deleted file mode 100644
index 93422390ef7..00000000000
--- a/spec/support/git_helpers.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-module GitHelpers
- def random_git_name
- "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}"
- end
-end
-
-RSpec.configure do |config|
- config.include GitHelpers
-end
diff --git a/spec/support/issuables_list_metadata_shared_examples.rb b/spec/support/issuables_list_metadata_shared_examples.rb
index 4c0f556e736..3406e4c3161 100644
--- a/spec/support/issuables_list_metadata_shared_examples.rb
+++ b/spec/support/issuables_list_metadata_shared_examples.rb
@@ -2,12 +2,12 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
before do
@issuable_ids = []
- 2.times do
+ 2.times do |n|
issuable =
if issuable_type == :issue
create(issuable_type, project: project)
else
- create(issuable_type, title: FFaker::Lorem.sentence, source_project: project, source_branch: FFaker::Name.name)
+ create(issuable_type, source_project: project, source_branch: "#{n}-feature")
end
@issuable_ids << issuable.id
@@ -33,4 +33,19 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
expect(meta_data[id].upvotes).to eq(id + 2)
end
end
+
+ describe "when given empty collection" do
+ let(:project2) { create(:empty_project, :public) }
+
+ it "doesn't execute any queries with false conditions" do
+ get_action =
+ if action
+ proc { get action }
+ else
+ proc { get :index, namespace_id: project2.namespace, project_id: project2 }
+ end
+
+ expect(&get_action).not_to make_queries_matching(/WHERE (?:1=0|0=1)/)
+ end
+ end
end
diff --git a/spec/support/matchers/query_matcher.rb b/spec/support/matchers/query_matcher.rb
new file mode 100644
index 00000000000..ac8c4ab91d9
--- /dev/null
+++ b/spec/support/matchers/query_matcher.rb
@@ -0,0 +1,33 @@
+RSpec::Matchers.define :make_queries_matching do |matcher, expected_count = nil|
+ supports_block_expectations
+
+ match do |block|
+ @counter = query_count(matcher, &block)
+ if expected_count
+ @counter.count == expected_count
+ else
+ @counter.count > 0
+ end
+ end
+
+ failure_message_when_negated do |_|
+ if expected_count
+ "expected #{matcher} not to match #{expected_count} queries, got #{@counter.count} matches:\n\n#{@counter.inspect}"
+ else
+ "expected #{matcher} not to match any query, got #{@counter.count} matches:\n\n#{@counter.inspect}"
+ end
+ end
+
+ failure_message do |_|
+ if expected_count
+ "expected #{matcher} to match #{expected_count} queries, got #{@counter.count} matches:\n\n#{@counter.inspect}"
+ else
+ "expected #{matcher} to match at least one query, got #{@counter.count} matches:\n\n#{@counter.inspect}"
+ end
+ end
+
+ def query_count(regex, &block)
+ @recorder = ActiveRecord::QueryRecorder.new(&block).log
+ @recorder.select{ |q| q.match(regex) }
+ end
+end
diff --git a/spec/support/notify_shared_examples.rb b/spec/support/notify_shared_examples.rb
index 16a425f2ca2..76411065265 100644
--- a/spec/support/notify_shared_examples.rb
+++ b/spec/support/notify_shared_examples.rb
@@ -3,7 +3,7 @@ shared_context 'gitlab email notification' do
let(:gitlab_sender) { Gitlab.config.gitlab.email_from }
let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to }
let(:recipient) { create(:user, email: 'recipient@example.com') }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
let(:new_user_address) { 'newguy@example.com' }
before do
diff --git a/spec/support/prometheus_helpers.rb b/spec/support/prometheus_helpers.rb
index 4afdbd68304..cc79b11616a 100644
--- a/spec/support/prometheus_helpers.rb
+++ b/spec/support/prometheus_helpers.rb
@@ -1,10 +1,10 @@
module PrometheusHelpers
def prometheus_memory_query(environment_slug)
- %{(sum(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"}) / count(container_memory_usage_bytes{container_name="app",environment="#{environment_slug}"})) /1024/1024}
+ %{(sum(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / count(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"})) /1024/1024}
end
def prometheus_cpu_query(environment_slug)
- %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}) * 100}
+ %{sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}) * 100}
end
def prometheus_query_url(prometheus_query)
diff --git a/spec/support/select2_helper.rb b/spec/support/select2_helper.rb
index 0d526045012..6b1853c2364 100644
--- a/spec/support/select2_helper.rb
+++ b/spec/support/select2_helper.rb
@@ -22,4 +22,12 @@ module Select2Helper
execute_script("$('#{selector}').select2('val', '#{value}').trigger('change');")
end
end
+
+ def open_select2(selector)
+ execute_script("$('#{selector}').select2('open');")
+ end
+
+ def scroll_select2_to_bottom(selector)
+ evaluate_script "$('#{selector}').scrollTop($('#{selector}')[0].scrollHeight); $('#{selector}');"
+ end
end
diff --git a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
index 81d06dc2a3d..9e9cdf3e48b 100644
--- a/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
+++ b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
@@ -2,12 +2,12 @@
# It can take a `default_params`.
shared_examples 'new issuable record that supports slash commands' do
- let!(:project) { create(:project) }
+ let!(:project) { create(:project, :repository) }
let(:user) { create(:user).tap { |u| project.team << [u, :master] } }
let(:assignee) { create(:user) }
let!(:milestone) { create(:milestone, project: project) }
let!(:labels) { create_list(:label, 3, project: project) }
- let(:base_params) { { title: FFaker::Lorem.sentence(3) } }
+ let(:base_params) { { title: 'My issuable title' } }
let(:params) { base_params.merge(defined?(default_params) ? default_params : {}).merge(example_params) }
let(:issuable) { described_class.new(project, user, params).execute }
diff --git a/spec/support/stored_repositories.rb b/spec/support/stored_repositories.rb
new file mode 100644
index 00000000000..df18926d58c
--- /dev/null
+++ b/spec/support/stored_repositories.rb
@@ -0,0 +1,5 @@
+RSpec.configure do |config|
+ config.before(:each, :repository) do
+ TestEnv.clean_test_path
+ end
+end
diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb
index f40ee862df8..444adcc1906 100644
--- a/spec/support/stub_configuration.rb
+++ b/spec/support/stub_configuration.rb
@@ -21,6 +21,10 @@ module StubConfiguration
allow(Gitlab.config.incoming_email).to receive_messages(messages)
end
+ def stub_mattermost_setting(messages)
+ allow(Gitlab.config.mattermost).to receive_messages(messages)
+ end
+
private
# Modifies stubbed messages to also stub possible predicate versions
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 648b0380f18..1b5cb71a6b0 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -40,7 +40,7 @@ module TestEnv
'csv' => '3dd0896',
'v1.1.0' => 'b83d6e3'
}.freeze
-
+
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
# need to keep all the branches in sync.
# We currently only need a subset of the branches
@@ -61,9 +61,6 @@ module TestEnv
clean_test_path
- FileUtils.mkdir_p(repos_path)
- FileUtils.mkdir_p(backup_path)
-
# Setup GitLab shell for test instance
setup_gitlab_shell
@@ -95,10 +92,14 @@ module TestEnv
tmp_test_path = Rails.root.join('tmp', 'tests', '**')
Dir[tmp_test_path].each do |entry|
- unless File.basename(entry) =~ /\Agitlab-(shell|test|test-fork)\z/
+ unless File.basename(entry) =~ /\Agitlab-(shell|test|test_bare|test-fork|test-fork_bare)\z/
FileUtils.rm_rf(entry)
end
end
+
+ FileUtils.mkdir_p(repos_path)
+ FileUtils.mkdir_p(backup_path)
+ FileUtils.mkdir_p(pages_path)
end
def setup_gitlab_shell
@@ -130,8 +131,10 @@ module TestEnv
set_repo_refs(repo_path, branch_sha)
- # We must copy bare repositories because we will push to them.
- system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare}))
+ unless File.directory?(repo_path_bare)
+ # We must copy bare repositories because we will push to them.
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare}))
+ end
end
def copy_repo(project)
@@ -151,6 +154,10 @@ module TestEnv
Gitlab.config.backup.path
end
+ def pages_path
+ Gitlab.config.pages.path
+ end
+
def copy_forked_repo_with_submodules(project)
base_repo_path = File.expand_path(forked_repo_path_bare)
target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.full_path}.git")
@@ -164,16 +171,11 @@ module TestEnv
#
# Otherwise they'd be created by the first test, often timing out and
# causing a transient test failure
- def warm_asset_cache
- return if warm_asset_cache?
+ def eager_load_driver_server
return unless defined?(Capybara)
- Capybara.current_session.driver.visit '/'
- end
-
- def warm_asset_cache?
- cache = Rails.root.join(*%w(tmp cache assets test))
- Dir.exist?(cache) && Dir.entries(cache).length > 2
+ puts "Starting the Capybara driver server..."
+ Capybara.current_session.visit '/'
end
private
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 10458966cb9..daea0c6bb37 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -81,6 +81,10 @@ describe 'gitlab:app namespace rake task' do
end # backup_restore task
describe 'backup' do
+ before(:all) do
+ ENV['force'] = 'yes'
+ end
+
def tars_glob
Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar'))
end
@@ -88,6 +92,9 @@ describe 'gitlab:app namespace rake task' do
def create_backup
FileUtils.rm tars_glob
+ # This reconnect makes our project fixture disappear, breaking the restore. Stub it out.
+ allow(ActiveRecord::Base.connection).to receive(:reconnect!)
+
# Redirect STDOUT and run the rake task
orig_stdout = $stdout
$stdout = StringIO.new
@@ -109,7 +116,7 @@ describe 'gitlab:app namespace rake task' do
end
describe 'backup creation and deletion using custom_hooks' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user_backup_path) { "repositories/#{project.path_with_namespace}" }
before(:each) do
@@ -119,9 +126,6 @@ describe 'gitlab:app namespace rake task' do
FileUtils.mkdir_p(path)
FileUtils.touch(File.join(path, "dummy.txt"))
- # We need to use the full path instead of the relative one
- allow(Gitlab.config.gitlab_shell).to receive(:path).and_return(File.expand_path(Gitlab.config.gitlab_shell.path, Rails.root.to_s))
-
ENV["SKIP"] = "db"
create_backup
end
@@ -220,15 +224,15 @@ describe 'gitlab:app namespace rake task' do
end
context 'multiple repository storages' do
- let(:project_a) { create(:project, repository_storage: 'default') }
- let(:project_b) { create(:project, repository_storage: 'custom') }
+ let(:project_a) { create(:project, :repository, repository_storage: 'default') }
+ let(:project_b) { create(:project, :repository, repository_storage: 'custom') }
before do
FileUtils.mkdir('tmp/tests/default_storage')
FileUtils.mkdir('tmp/tests/custom_storage')
storages = {
- 'default' => { 'path' => 'tmp/tests/default_storage' },
- 'custom' => { 'path' => 'tmp/tests/custom_storage' }
+ 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage') },
+ 'custom' => { 'path' => Settings.absolute('tmp/tests/custom_storage') }
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
diff --git a/spec/views/projects/builds/_build.html.haml_spec.rb b/spec/views/projects/builds/_build.html.haml_spec.rb
index e141a117731..751482cac42 100644
--- a/spec/views/projects/builds/_build.html.haml_spec.rb
+++ b/spec/views/projects/builds/_build.html.haml_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'projects/ci/builds/_build' do
include Devise::Test::ControllerHelpers
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_empty_pipeline, id: 1337, project: project, sha: project.commit.id) }
let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', stage_idx: 1, name: 'rspec 0:2', status: :pending) }
diff --git a/spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb b/spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb
index 49b20e5b36b..dc2ffc9dc47 100644
--- a/spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb
+++ b/spec/views/projects/builds/_generic_commit_status.html.haml_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'projects/generic_commit_statuses/_generic_commit_status.html.haml' do
include Devise::Test::ControllerHelpers
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_empty_pipeline, id: 1337, project: project, sha: project.commit.id) }
let(:generic_commit_status) { create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3) }
diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb
index ec78ac30593..55b64808fb3 100644
--- a/spec/views/projects/builds/show.html.haml_spec.rb
+++ b/spec/views/projects/builds/show.html.haml_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'projects/builds/show', :view do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:build) { create(:ci_build, pipeline: pipeline) }
let(:pipeline) do
diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
index 8bc344bfbf6..cec87dcecc8 100644
--- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb
+++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
@@ -4,7 +4,7 @@ describe 'projects/commit/_commit_box.html.haml' do
include Devise::Test::ControllerHelpers
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
before do
assign(:project, project)
diff --git a/spec/views/projects/issues/_related_branches.html.haml_spec.rb b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
index 889d9a38887..8c845251765 100644
--- a/spec/views/projects/issues/_related_branches.html.haml_spec.rb
+++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'projects/issues/_related_branches' do
include Devise::Test::ControllerHelpers
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:branch) { project.repository.find_branch('feature') }
let!(:pipeline) { create(:ci_pipeline, project: project, sha: branch.dereferenced_target.id, ref: 'feature') }
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 6f70b3daf8e..4052dbf8df3 100644
--- a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb
@@ -4,11 +4,8 @@ describe 'projects/merge_requests/show/_commits.html.haml' do
include Devise::Test::ControllerHelpers
let(:user) { create(:user) }
- let(:target_project) { create(:project) }
-
- let(:source_project) do
- create(:project, forked_from_project: target_project)
- end
+ let(:target_project) { create(:project, :repository) }
+ let(:source_project) { create(:project, :repository, forked_from_project: target_project) }
let(:merge_request) do
create(:merge_request, :simple,
diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
index 3650b22c389..69c7d0cbf28 100644
--- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
@@ -4,8 +4,8 @@ describe 'projects/merge_requests/edit.html.haml' do
include Devise::Test::ControllerHelpers
let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:project) { create(:project, :repository) }
+ let(:fork_project) { create(:project, :repository, forked_from_project: project) }
let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
let(:milestone) { create(:milestone, project: project) }
diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb
index 7f123b15194..dc2fcc3e715 100644
--- a/spec/views/projects/merge_requests/show.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb
@@ -4,8 +4,8 @@ describe 'projects/merge_requests/show.html.haml' do
include Devise::Test::ControllerHelpers
let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:project) { create(:project, :repository) }
+ let(:fork_project) { create(:project, :repository, forked_from_project: project) }
let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
let(:note) { create(:note_on_merge_request, project: project, noteable: closed_merge_request) }
diff --git a/spec/views/projects/pipelines/_stage.html.haml_spec.rb b/spec/views/projects/pipelines/_stage.html.haml_spec.rb
index 65f9d0125e6..10095ad7694 100644
--- a/spec/views/projects/pipelines/_stage.html.haml_spec.rb
+++ b/spec/views/projects/pipelines/_stage.html.haml_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'projects/pipelines/_stage', :view do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:stage) { build(:ci_stage, pipeline: pipeline) }
diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb
index e4aeaeca508..dca78dec6df 100644
--- a/spec/views/projects/pipelines/show.html.haml_spec.rb
+++ b/spec/views/projects/pipelines/show.html.haml_spec.rb
@@ -4,7 +4,7 @@ describe 'projects/pipelines/show' do
include Devise::Test::ControllerHelpers
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, user: user) }
before do
diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb
index c381b1a86df..900f8d4732f 100644
--- a/spec/views/projects/tree/show.html.haml_spec.rb
+++ b/spec/views/projects/tree/show.html.haml_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'projects/tree/show' do
include Devise::Test::ControllerHelpers
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
before do
diff --git a/spec/workers/delete_merged_branches_worker_spec.rb b/spec/workers/delete_merged_branches_worker_spec.rb
index d9497bd486c..39009d9e4b2 100644
--- a/spec/workers/delete_merged_branches_worker_spec.rb
+++ b/spec/workers/delete_merged_branches_worker_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe DeleteMergedBranchesWorker do
subject(:worker) { described_class.new }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
describe "#perform" do
it "calls DeleteMergedBranchesService" do
diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index f27e413f7b8..8cf2b888f9a 100644
--- a/spec/workers/emails_on_push_worker_spec.rb
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -5,7 +5,7 @@ describe EmailsOnPushWorker do
include EmailHelpers
include EmailSpec::Matchers
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
let(:recipients) { user.email }
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index a60af574a08..029f35512e0 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -3,7 +3,7 @@ require 'fileutils'
require 'spec_helper'
describe GitGarbageCollectWorker do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:shell) { Gitlab::Shell.new }
subject { GitGarbageCollectWorker.new }
diff --git a/spec/workers/group_destroy_worker_spec.rb b/spec/workers/group_destroy_worker_spec.rb
index 4e4eaf9b2f7..1ff5a3b9034 100644
--- a/spec/workers/group_destroy_worker_spec.rb
+++ b/spec/workers/group_destroy_worker_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe GroupDestroyWorker do
let(:group) { create(:group) }
let(:user) { create(:admin) }
- let!(:project) { create(:project, namespace: group) }
+ let!(:project) { create(:empty_project, namespace: group) }
subject { GroupDestroyWorker.new }
diff --git a/spec/workers/pipeline_metrics_worker_spec.rb b/spec/workers/pipeline_metrics_worker_spec.rb
index 2d47d93acec..5dbc0da95c2 100644
--- a/spec/workers/pipeline_metrics_worker_spec.rb
+++ b/spec/workers/pipeline_metrics_worker_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe PipelineMetricsWorker do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) }
let(:pipeline) do
diff --git a/spec/workers/pipeline_notification_worker_spec.rb b/spec/workers/pipeline_notification_worker_spec.rb
index 603ae52ed1e..139032d77bd 100644
--- a/spec/workers/pipeline_notification_worker_spec.rb
+++ b/spec/workers/pipeline_notification_worker_spec.rb
@@ -3,131 +3,19 @@ require 'spec_helper'
describe PipelineNotificationWorker do
include EmailHelpers
- let(:pipeline) do
- create(:ci_pipeline,
- project: project,
- sha: project.commit('master').sha,
- user: pusher,
- status: status)
- end
-
- let(:project) { create(:project, public_builds: false) }
- let(:user) { create(:user) }
- let(:pusher) { user }
- let(:watcher) { pusher }
+ let(:pipeline) { create(:ci_pipeline) }
describe '#execute' do
- before do
- reset_delivered_emails!
- pipeline.project.team << [pusher, Gitlab::Access::DEVELOPER]
- end
-
- context 'when watcher has developer access' do
- before do
- pipeline.project.team << [watcher, Gitlab::Access::DEVELOPER]
- end
-
- shared_examples 'sending emails' do
- it 'sends emails' do
- perform_enqueued_jobs do
- subject.perform(pipeline.id)
- end
-
- emails = ActionMailer::Base.deliveries
- actual = emails.flat_map(&:bcc).sort
- expected_receivers = receivers.map(&:email).uniq.sort
-
- expect(actual).to eq(expected_receivers)
- expect(emails.size).to eq(1)
- expect(emails.last.subject).to include(email_subject)
- end
- end
-
- context 'with success pipeline' do
- let(:status) { 'success' }
- let(:email_subject) { "Pipeline ##{pipeline.id} has succeeded" }
- let(:receivers) { [pusher, watcher] }
-
- it_behaves_like 'sending emails'
-
- context 'with pipeline from someone else' do
- let(:pusher) { create(:user) }
- let(:watcher) { user }
-
- context 'with success pipeline notification on' do
- before do
- watcher.global_notification_setting.
- update(level: 'custom', success_pipeline: true)
- end
-
- it_behaves_like 'sending emails'
- end
-
- context 'with success pipeline notification off' do
- let(:receivers) { [pusher] }
+ it 'calls NotificationService#pipeline_finished when the pipeline exists' do
+ expect(NotificationService).to receive_message_chain(:new, :pipeline_finished)
- before do
- watcher.global_notification_setting.
- update(level: 'custom', success_pipeline: false)
- end
-
- it_behaves_like 'sending emails'
- end
- end
-
- context 'with failed pipeline' do
- let(:status) { 'failed' }
- let(:email_subject) { "Pipeline ##{pipeline.id} has failed" }
-
- it_behaves_like 'sending emails'
-
- context 'with pipeline from someone else' do
- let(:pusher) { create(:user) }
- let(:watcher) { user }
-
- context 'with failed pipeline notification on' do
- before do
- watcher.global_notification_setting.
- update(level: 'custom', failed_pipeline: true)
- end
-
- it_behaves_like 'sending emails'
- end
-
- context 'with failed pipeline notification off' do
- let(:receivers) { [pusher] }
-
- before do
- watcher.global_notification_setting.
- update(level: 'custom', failed_pipeline: false)
- end
-
- it_behaves_like 'sending emails'
- end
- end
- end
- end
+ subject.perform(pipeline.id)
end
- context 'when watcher has no read_build access' do
- let(:status) { 'failed' }
- let(:email_subject) { "Pipeline ##{pipeline.id} has failed" }
- let(:watcher) { create(:user) }
-
- before do
- pipeline.project.team << [watcher, Gitlab::Access::GUEST]
-
- watcher.global_notification_setting.
- update(level: 'custom', failed_pipeline: true)
-
- perform_enqueued_jobs do
- subject.perform(pipeline.id)
- end
- end
+ it 'does nothing when the pipeline does not exist' do
+ expect(NotificationService).not_to receive(:new)
- it 'does not send emails' do
- should_only_email(pusher, kind: :bcc)
- end
+ subject.perform(Ci::Pipeline.maximum(:id).to_i.succ)
end
end
end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 7bcb5521202..a2a559a2369 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -4,7 +4,7 @@ describe PostReceive do
let(:changes) { "123456 789012 refs/heads/tést\n654321 210987 refs/tags/tag" }
let(:wrongly_encoded_changes) { changes.encode("ISO-8859-1").force_encoding("UTF-8") }
let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:key) { create(:key, user: project.owner) }
let(:key_id) { key.shell_id }
diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb
index 75c7fc1efd2..9afe2e610b9 100644
--- a/spec/workers/process_commit_worker_spec.rb
+++ b/spec/workers/process_commit_worker_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe ProcessCommitWorker do
let(:worker) { described_class.new }
let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
let(:issue) { create(:issue, project: project, author: user) }
let(:commit) { project.commit }
@@ -99,6 +99,13 @@ describe ProcessCommitWorker do
expect(metric.first_mentioned_in_commit_at).to eq(commit.committed_date)
end
+
+ it "doesn't execute any queries with false conditions" do
+ allow(commit).to receive(:safe_message).
+ and_return("Lorem Ipsum")
+
+ expect { worker.update_issue_metrics(commit, user) }.not_to make_queries_matching(/WHERE (?:1=0|0=1)/)
+ end
end
describe '#build_commit' do
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index f4f63b57a5f..c23ffdf99c0 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe ProjectCacheWorker do
let(:worker) { described_class.new }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:statistics) { project.statistics }
describe '#perform' do
diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb
index 1f4c39eb64a..0ab42f99510 100644
--- a/spec/workers/project_destroy_worker_spec.rb
+++ b/spec/workers/project_destroy_worker_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe ProjectDestroyWorker do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:path) { project.repository.path_to_repo }
subject { ProjectDestroyWorker.new }
diff --git a/spec/workers/repository_check/batch_worker_spec.rb b/spec/workers/repository_check/batch_worker_spec.rb
index 27727d6abf9..bcd97a4f6ef 100644
--- a/spec/workers/repository_check/batch_worker_spec.rb
+++ b/spec/workers/repository_check/batch_worker_spec.rb
@@ -4,7 +4,7 @@ describe RepositoryCheck::BatchWorker do
subject { described_class.new }
it 'prefers projects that have never been checked' do
- projects = create_list(:project, 3, created_at: 1.week.ago)
+ projects = create_list(:empty_project, 3, created_at: 1.week.ago)
projects[0].update_column(:last_repository_check_at, 4.months.ago)
projects[2].update_column(:last_repository_check_at, 3.months.ago)
@@ -12,7 +12,7 @@ describe RepositoryCheck::BatchWorker do
end
it 'sorts projects by last_repository_check_at' do
- projects = create_list(:project, 3, created_at: 1.week.ago)
+ projects = create_list(:empty_project, 3, created_at: 1.week.ago)
projects[0].update_column(:last_repository_check_at, 2.months.ago)
projects[1].update_column(:last_repository_check_at, 4.months.ago)
projects[2].update_column(:last_repository_check_at, 3.months.ago)
@@ -21,7 +21,7 @@ describe RepositoryCheck::BatchWorker do
end
it 'excludes projects that were checked recently' do
- projects = create_list(:project, 3, created_at: 1.week.ago)
+ projects = create_list(:empty_project, 3, created_at: 1.week.ago)
projects[0].update_column(:last_repository_check_at, 2.days.ago)
projects[1].update_column(:last_repository_check_at, 2.months.ago)
projects[2].update_column(:last_repository_check_at, 3.days.ago)
@@ -40,7 +40,7 @@ describe RepositoryCheck::BatchWorker do
it 'skips projects created less than 24 hours ago' do
project = create(:empty_project)
project.update_column(:created_at, 23.hours.ago)
-
+
expect(subject.perform).to eq([])
end
end
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index 87521ae408e..7d6a2db2972 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
describe RepositoryForkWorker do
- let(:project) { create(:project) }
- let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:project) { create(:project, :repository) }
+ let(:fork_project) { create(:project, :repository, forked_from_project: project) }
let(:shell) { Gitlab::Shell.new }
subject { RepositoryForkWorker.new }
diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
index c42f3147b7a..fbb22439f33 100644
--- a/spec/workers/repository_import_worker_spec.rb
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe RepositoryImportWorker do
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project) }
subject { described_class.new }
diff --git a/spec/workers/update_merge_requests_worker_spec.rb b/spec/workers/update_merge_requests_worker_spec.rb
index 262d6e5a9ab..558ff9109ec 100644
--- a/spec/workers/update_merge_requests_worker_spec.rb
+++ b/spec/workers/update_merge_requests_worker_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe UpdateMergeRequestsWorker do
include RepoHelpers
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
subject { described_class.new }
diff --git a/vendor/assets/javascripts/notebooklab.js b/vendor/assets/javascripts/notebooklab.js
new file mode 100644
index 00000000000..296271205d1
--- /dev/null
+++ b/vendor/assets/javascripts/notebooklab.js
@@ -0,0 +1,5887 @@
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define("NotebookLab", [], factory);
+ else if(typeof exports === 'object')
+ exports["NotebookLab"] = factory();
+ else
+ root["NotebookLab"] = factory();
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+/******/
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ i: moduleId,
+/******/ l: false,
+/******/ exports: {}
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.l = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // identity function for calling harmony imports with the correct context
+/******/ __webpack_require__.i = function(value) { return value; };
+/******/
+/******/ // define getter function for harmony exports
+/******/ __webpack_require__.d = function(exports, name, getter) {
+/******/ if(!__webpack_require__.o(exports, name)) {
+/******/ Object.defineProperty(exports, name, {
+/******/ configurable: false,
+/******/ enumerable: true,
+/******/ get: getter
+/******/ });
+/******/ }
+/******/ };
+/******/
+/******/ // getDefaultExport function for compatibility with non-harmony modules
+/******/ __webpack_require__.n = function(module) {
+/******/ var getter = module && module.__esModule ?
+/******/ function getDefault() { return module['default']; } :
+/******/ function getModuleExports() { return module; };
+/******/ __webpack_require__.d(getter, 'a', getter);
+/******/ return getter;
+/******/ };
+/******/
+/******/ // Object.prototype.hasOwnProperty.call
+/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(__webpack_require__.s = 47);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ (function(module, exports) {
+
+// this module is a runtime utility for cleaner component module output and will
+// be included in the final webpack user bundle
+
+module.exports = function normalizeComponent (
+ rawScriptExports,
+ compiledTemplate,
+ scopeId,
+ cssModules
+) {
+ var esModule
+ var scriptExports = rawScriptExports = rawScriptExports || {}
+
+ // ES6 modules interop
+ var type = typeof rawScriptExports.default
+ if (type === 'object' || type === 'function') {
+ esModule = rawScriptExports
+ scriptExports = rawScriptExports.default
+ }
+
+ // Vue.extend constructor export interop
+ var options = typeof scriptExports === 'function'
+ ? scriptExports.options
+ : scriptExports
+
+ // render functions
+ if (compiledTemplate) {
+ options.render = compiledTemplate.render
+ options.staticRenderFns = compiledTemplate.staticRenderFns
+ }
+
+ // scopedId
+ if (scopeId) {
+ options._scopeId = scopeId
+ }
+
+ // inject cssModules
+ if (cssModules) {
+ var computed = Object.create(options.computed || null)
+ Object.keys(cssModules).forEach(function (key) {
+ var module = cssModules[key]
+ computed[key] = function () { return module }
+ })
+ options.computed = computed
+ }
+
+ return {
+ esModule: esModule,
+ exports: scriptExports,
+ options: options
+ }
+}
+
+
+/***/ }),
+/* 1 */
+/***/ (function(module, exports, __webpack_require__) {
+
+/* WEBPACK VAR INJECTION */(function(Buffer) {/*
+ MIT License http://www.opensource.org/licenses/mit-license.php
+ Author Tobias Koppers @sokra
+*/
+// css base code, injected by the css-loader
+module.exports = function(useSourceMap) {
+ var list = [];
+
+ // return the list of modules as css string
+ list.toString = function toString() {
+ return this.map(function (item) {
+ var content = cssWithMappingToString(item, useSourceMap);
+ if(item[2]) {
+ return "@media " + item[2] + "{" + content + "}";
+ } else {
+ return content;
+ }
+ }).join("");
+ };
+
+ // import a list of modules into the list
+ list.i = function(modules, mediaQuery) {
+ if(typeof modules === "string")
+ modules = [[null, modules, ""]];
+ var alreadyImportedModules = {};
+ for(var i = 0; i < this.length; i++) {
+ var id = this[i][0];
+ if(typeof id === "number")
+ alreadyImportedModules[id] = true;
+ }
+ for(i = 0; i < modules.length; i++) {
+ var item = modules[i];
+ // skip already imported module
+ // this implementation is not 100% perfect for weird media query combinations
+ // when a module is imported multiple times with different media queries.
+ // I hope this will never occur (Hey this way we have smaller bundles)
+ if(typeof item[0] !== "number" || !alreadyImportedModules[item[0]]) {
+ if(mediaQuery && !item[2]) {
+ item[2] = mediaQuery;
+ } else if(mediaQuery) {
+ item[2] = "(" + item[2] + ") and (" + mediaQuery + ")";
+ }
+ list.push(item);
+ }
+ }
+ };
+ return list;
+};
+
+function cssWithMappingToString(item, useSourceMap) {
+ var content = item[1] || '';
+ var cssMapping = item[3];
+ if (!cssMapping) {
+ return content;
+ }
+
+ if (useSourceMap) {
+ var sourceMapping = toComment(cssMapping);
+ var sourceURLs = cssMapping.sources.map(function (source) {
+ return '/*# sourceURL=' + cssMapping.sourceRoot + source + ' */'
+ });
+
+ return [content].concat(sourceURLs).concat([sourceMapping]).join('\n');
+ }
+
+ return [content].join('\n');
+}
+
+// Adapted from convert-source-map (MIT)
+function toComment(sourceMap) {
+ var base64 = new Buffer(JSON.stringify(sourceMap)).toString('base64');
+ var data = 'sourceMappingURL=data:application/json;charset=utf-8;base64,' + base64;
+
+ return '/*# ' + data + ' */';
+}
+
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(18).Buffer))
+
+/***/ }),
+/* 2 */
+/***/ (function(module, exports, __webpack_require__) {
+
+
+/* styles */
+__webpack_require__(44)
+
+var Component = __webpack_require__(0)(
+ /* script */
+ __webpack_require__(13),
+ /* template */
+ __webpack_require__(39),
+ /* scopeId */
+ "data-v-4f6bf458",
+ /* cssModules */
+ null
+)
+Component.options.__file = "/Users/phil/Projects/notebooklab/src/cells/prompt.vue"
+if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")}
+if (Component.options.functional) {console.error("[vue-loader] prompt.vue: functional components are not supported with templates, they should use render functions.")}
+
+/* hot reload */
+if (false) {(function () {
+ var hotAPI = require("vue-hot-reload-api")
+ hotAPI.install(require("vue"), false)
+ if (!hotAPI.compatible) return
+ module.hot.accept()
+ if (!module.hot.data) {
+ hotAPI.createRecord("data-v-4f6bf458", Component.options)
+ } else {
+ hotAPI.reload("data-v-4f6bf458", Component.options)
+ }
+})()}
+
+module.exports = Component.exports
+
+
+/***/ }),
+/* 3 */
+/***/ (function(module, exports, __webpack_require__) {
+
+/*
+ MIT License http://www.opensource.org/licenses/mit-license.php
+ Author Tobias Koppers @sokra
+ Modified by Evan You @yyx990803
+*/
+
+var hasDocument = typeof document !== 'undefined'
+
+if (typeof DEBUG !== 'undefined' && DEBUG) {
+ if (!hasDocument) {
+ throw new Error(
+ 'vue-style-loader cannot be used in a non-browser environment. ' +
+ "Use { target: 'node' } in your Webpack config to indicate a server-rendering environment."
+ ) }
+}
+
+var listToStyles = __webpack_require__(46)
+
+/*
+type StyleObject = {
+ id: number;
+ parts: Array<StyleObjectPart>
+}
+
+type StyleObjectPart = {
+ css: string;
+ media: string;
+ sourceMap: ?string
+}
+*/
+
+var stylesInDom = {/*
+ [id: number]: {
+ id: number,
+ refs: number,
+ parts: Array<(obj?: StyleObjectPart) => void>
+ }
+*/}
+
+var head = hasDocument && (document.head || document.getElementsByTagName('head')[0])
+var singletonElement = null
+var singletonCounter = 0
+var isProduction = false
+var noop = function () {}
+
+// Force single-tag solution on IE6-9, which has a hard limit on the # of <style>
+// tags it will allow on a page
+var isOldIE = typeof navigator !== 'undefined' && /msie [6-9]\b/.test(navigator.userAgent.toLowerCase())
+
+module.exports = function (parentId, list, _isProduction) {
+ isProduction = _isProduction
+
+ var styles = listToStyles(parentId, list)
+ addStylesToDom(styles)
+
+ return function update (newList) {
+ var mayRemove = []
+ for (var i = 0; i < styles.length; i++) {
+ var item = styles[i]
+ var domStyle = stylesInDom[item.id]
+ domStyle.refs--
+ mayRemove.push(domStyle)
+ }
+ if (newList) {
+ styles = listToStyles(parentId, newList)
+ addStylesToDom(styles)
+ } else {
+ styles = []
+ }
+ for (var i = 0; i < mayRemove.length; i++) {
+ var domStyle = mayRemove[i]
+ if (domStyle.refs === 0) {
+ for (var j = 0; j < domStyle.parts.length; j++) {
+ domStyle.parts[j]()
+ }
+ delete stylesInDom[domStyle.id]
+ }
+ }
+ }
+}
+
+function addStylesToDom (styles /* Array<StyleObject> */) {
+ for (var i = 0; i < styles.length; i++) {
+ var item = styles[i]
+ var domStyle = stylesInDom[item.id]
+ if (domStyle) {
+ domStyle.refs++
+ for (var j = 0; j < domStyle.parts.length; j++) {
+ domStyle.parts[j](item.parts[j])
+ }
+ for (; j < item.parts.length; j++) {
+ domStyle.parts.push(addStyle(item.parts[j]))
+ }
+ if (domStyle.parts.length > item.parts.length) {
+ domStyle.parts.length = item.parts.length
+ }
+ } else {
+ var parts = []
+ for (var j = 0; j < item.parts.length; j++) {
+ parts.push(addStyle(item.parts[j]))
+ }
+ stylesInDom[item.id] = { id: item.id, refs: 1, parts: parts }
+ }
+ }
+}
+
+function createStyleElement () {
+ var styleElement = document.createElement('style')
+ styleElement.type = 'text/css'
+ head.appendChild(styleElement)
+ return styleElement
+}
+
+function addStyle (obj /* StyleObjectPart */) {
+ var update, remove
+ var styleElement = document.querySelector('style[data-vue-ssr-id~="' + obj.id + '"]')
+
+ if (styleElement) {
+ if (isProduction) {
+ // has SSR styles and in production mode.
+ // simply do nothing.
+ return noop
+ } else {
+ // has SSR styles but in dev mode.
+ // for some reason Chrome can't handle source map in server-rendered
+ // style tags - source maps in <style> only works if the style tag is
+ // created and inserted dynamically. So we remove the server rendered
+ // styles and inject new ones.
+ styleElement.parentNode.removeChild(styleElement)
+ }
+ }
+
+ if (isOldIE) {
+ // use singleton mode for IE9.
+ var styleIndex = singletonCounter++
+ styleElement = singletonElement || (singletonElement = createStyleElement())
+ update = applyToSingletonTag.bind(null, styleElement, styleIndex, false)
+ remove = applyToSingletonTag.bind(null, styleElement, styleIndex, true)
+ } else {
+ // use multi-style-tag mode in all other cases
+ styleElement = createStyleElement()
+ update = applyToTag.bind(null, styleElement)
+ remove = function () {
+ styleElement.parentNode.removeChild(styleElement)
+ }
+ }
+
+ update(obj)
+
+ return function updateStyle (newObj /* StyleObjectPart */) {
+ if (newObj) {
+ if (newObj.css === obj.css &&
+ newObj.media === obj.media &&
+ newObj.sourceMap === obj.sourceMap) {
+ return
+ }
+ update(obj = newObj)
+ } else {
+ remove()
+ }
+ }
+}
+
+var replaceText = (function () {
+ var textStore = []
+
+ return function (index, replacement) {
+ textStore[index] = replacement
+ return textStore.filter(Boolean).join('\n')
+ }
+})()
+
+function applyToSingletonTag (styleElement, index, remove, obj) {
+ var css = remove ? '' : obj.css
+
+ if (styleElement.styleSheet) {
+ styleElement.styleSheet.cssText = replaceText(index, css)
+ } else {
+ var cssNode = document.createTextNode(css)
+ var childNodes = styleElement.childNodes
+ if (childNodes[index]) styleElement.removeChild(childNodes[index])
+ if (childNodes.length) {
+ styleElement.insertBefore(cssNode, childNodes[index])
+ } else {
+ styleElement.appendChild(cssNode)
+ }
+ }
+}
+
+function applyToTag (styleElement, obj) {
+ var css = obj.css
+ var media = obj.media
+ var sourceMap = obj.sourceMap
+
+ if (media) {
+ styleElement.setAttribute('media', media)
+ }
+
+ if (sourceMap) {
+ // https://developer.chrome.com/devtools/docs/javascript-debugging
+ // this makes source maps inside style tags work properly in Chrome
+ css += '\n/*# sourceURL=' + sourceMap.sources[0] + ' */'
+ // http://stackoverflow.com/a/26603875
+ css += '\n/*# sourceMappingURL=data:application/json;base64,' + btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))) + ' */'
+ }
+
+ if (styleElement.styleSheet) {
+ styleElement.styleSheet.cssText = css
+ } else {
+ while (styleElement.firstChild) {
+ styleElement.removeChild(styleElement.firstChild)
+ }
+ styleElement.appendChild(document.createTextNode(css))
+ }
+}
+
+
+/***/ }),
+/* 4 */
+/***/ (function(module, exports) {
+
+var g;
+
+// This works in non-strict mode
+g = (function() {
+ return this;
+})();
+
+try {
+ // This works if eval is allowed (see CSP)
+ g = g || Function("return this")() || (1,eval)("this");
+} catch(e) {
+ // This works if the window reference is available
+ if(typeof window === "object")
+ g = window;
+}
+
+// g can still be undefined, but nothing to do about it...
+// We return undefined, instead of nothing here, so it's
+// easier to handle this case. if(!global) { ...}
+
+module.exports = g;
+
+
+/***/ }),
+/* 5 */
+/***/ (function(module, exports, __webpack_require__) {
+
+var Component = __webpack_require__(0)(
+ /* script */
+ __webpack_require__(8),
+ /* template */
+ __webpack_require__(41),
+ /* scopeId */
+ null,
+ /* cssModules */
+ null
+)
+Component.options.__file = "/Users/phil/Projects/notebooklab/src/cells/code/index.vue"
+if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")}
+if (Component.options.functional) {console.error("[vue-loader] index.vue: functional components are not supported with templates, they should use render functions.")}
+
+/* hot reload */
+if (false) {(function () {
+ var hotAPI = require("vue-hot-reload-api")
+ hotAPI.install(require("vue"), false)
+ if (!hotAPI.compatible) return
+ module.hot.accept()
+ if (!module.hot.data) {
+ hotAPI.createRecord("data-v-d42105b8", Component.options)
+ } else {
+ hotAPI.reload("data-v-d42105b8", Component.options)
+ }
+})()}
+
+module.exports = Component.exports
+
+
+/***/ }),
+/* 6 */
+/***/ (function(module, exports, __webpack_require__) {
+
+
+/* styles */
+__webpack_require__(43)
+
+var Component = __webpack_require__(0)(
+ /* script */
+ __webpack_require__(14),
+ /* template */
+ __webpack_require__(38),
+ /* scopeId */
+ null,
+ /* cssModules */
+ null
+)
+Component.options.__file = "/Users/phil/Projects/notebooklab/src/index.vue"
+if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")}
+if (Component.options.functional) {console.error("[vue-loader] index.vue: functional components are not supported with templates, they should use render functions.")}
+
+/* hot reload */
+if (false) {(function () {
+ var hotAPI = require("vue-hot-reload-api")
+ hotAPI.install(require("vue"), false)
+ if (!hotAPI.compatible) return
+ module.hot.accept()
+ if (!module.hot.data) {
+ hotAPI.createRecord("data-v-4cb2b168", Component.options)
+ } else {
+ hotAPI.reload("data-v-4cb2b168", Component.options)
+ }
+})()}
+
+module.exports = Component.exports
+
+
+/***/ }),
+/* 7 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _index = __webpack_require__(5);
+
+var _index2 = _interopRequireDefault(_index);
+
+var _index3 = __webpack_require__(33);
+
+var _index4 = _interopRequireDefault(_index3);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+
+exports.default = {
+ components: {
+ 'code-cell': _index2.default,
+ 'output-cell': _index4.default
+ },
+ props: {
+ cell: {
+ type: Object,
+ required: true
+ },
+ codeCssClass: {
+ type: String,
+ required: false,
+ default: ''
+ }
+ },
+ computed: {
+ rawInputCode: function rawInputCode() {
+ if (this.cell.source) {
+ return this.cell.source.join('');
+ } else {
+ return '';
+ }
+ },
+ hasOutput: function hasOutput() {
+ return this.cell.outputs.length;
+ },
+ output: function output() {
+ return this.cell.outputs[0];
+ }
+ }
+};
+
+/***/ }),
+/* 8 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _highlight = __webpack_require__(16);
+
+var _highlight2 = _interopRequireDefault(_highlight);
+
+var _prompt = __webpack_require__(2);
+
+var _prompt2 = _interopRequireDefault(_prompt);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+
+exports.default = {
+ components: {
+ prompt: _prompt2.default
+ },
+ props: {
+ count: {
+ type: Number,
+ required: false,
+ default: 0
+ },
+ codeCssClass: {
+ type: String,
+ required: false,
+ default: ''
+ },
+ type: {
+ type: String,
+ required: true
+ },
+ rawCode: {
+ type: String,
+ required: true
+ }
+ },
+ computed: {
+ code: function code() {
+ return this.rawCode;
+ },
+ promptType: function promptType() {
+ var type = this.type.split('put')[0];
+
+ return type.charAt(0).toUpperCase() + type.slice(1);
+ }
+ },
+ mounted: function mounted() {
+ _highlight2.default.highlightElement(this.$refs.code);
+ }
+};
+
+/***/ }),
+/* 9 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _marked = __webpack_require__(25);
+
+var _marked2 = _interopRequireDefault(_marked);
+
+var _prompt = __webpack_require__(2);
+
+var _prompt2 = _interopRequireDefault(_prompt);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+//
+//
+//
+//
+//
+//
+//
+
+exports.default = {
+ components: {
+ prompt: _prompt2.default
+ },
+ props: {
+ cell: {
+ type: Object,
+ required: true
+ }
+ },
+ computed: {
+ markdown: function markdown() {
+ var regex = new RegExp('^\\$\\$(.*)\\$\\$$', 'g');
+
+ var source = this.cell.source.map(function (line) {
+ var matches = regex.exec(line.trim());
+
+ // Only render use the Katex library if it is actually loaded
+ if (matches && matches.length > 0 && typeof katex !== 'undefined') {
+ return katex.renderToString(matches[1]);
+ }
+
+ return line;
+ });
+
+ return (0, _marked2.default)(source.join(''));
+ }
+ }
+};
+
+/***/ }),
+/* 10 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _prompt = __webpack_require__(2);
+
+var _prompt2 = _interopRequireDefault(_prompt);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = {
+ props: {
+ rawCode: {
+ type: String,
+ required: true
+ }
+ },
+ components: {
+ prompt: _prompt2.default
+ }
+}; //
+//
+//
+//
+//
+//
+//
+
+/***/ }),
+/* 11 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _prompt = __webpack_require__(2);
+
+var _prompt2 = _interopRequireDefault(_prompt);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = {
+ props: {
+ outputType: {
+ type: String,
+ required: true
+ },
+ rawCode: {
+ type: String,
+ required: true
+ }
+ },
+ components: {
+ prompt: _prompt2.default
+ }
+}; //
+//
+//
+//
+//
+//
+//
+//
+
+/***/ }),
+/* 12 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; //
+//
+//
+//
+//
+//
+//
+//
+//
+
+var _index = __webpack_require__(5);
+
+var _index2 = _interopRequireDefault(_index);
+
+var _html = __webpack_require__(31);
+
+var _html2 = _interopRequireDefault(_html);
+
+var _image = __webpack_require__(32);
+
+var _image2 = _interopRequireDefault(_image);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+exports.default = {
+ props: {
+ codeCssClass: {
+ type: String,
+ required: false,
+ default: ''
+ },
+ count: {
+ type: Number,
+ required: false,
+ default: 0
+ },
+ output: {
+ type: Object,
+ requred: true
+ }
+ },
+ components: {
+ 'code-cell': _index2.default,
+ 'html-output': _html2.default,
+ 'image-output': _image2.default
+ },
+ data: function data() {
+ return {
+ outputType: ''
+ };
+ },
+
+ computed: {
+ componentName: function componentName() {
+ if (this.output.text) {
+ return 'code-cell';
+ } else if (this.output.data['image/png']) {
+ this.outputType = 'image/png';
+
+ return 'image-output';
+ } else if (this.output.data['text/html']) {
+ this.outputType = 'text/html';
+
+ return 'html-output';
+ } else if (this.output.data['image/svg+xml']) {
+ this.outputType = 'image/svg+xml';
+
+ return 'html-output';
+ }
+
+ this.outputType = 'text/plain';
+ return 'code-cell';
+ },
+ rawCode: function rawCode() {
+ if (this.output.text) {
+ return this.output.text.join('');
+ }
+
+ return this.dataForType(this.outputType);
+ }
+ },
+ methods: {
+ dataForType: function dataForType(type) {
+ var data = this.output.data[type];
+
+ if ((typeof data === 'undefined' ? 'undefined' : _typeof(data)) === 'object') {
+ data = data.join('');
+ }
+
+ return data;
+ }
+ }
+};
+
+/***/ }),
+/* 13 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+//
+//
+//
+//
+//
+//
+//
+//
+
+exports.default = {
+ props: {
+ type: {
+ type: String,
+ required: false
+ },
+ count: {
+ type: Number,
+ required: false
+ }
+ }
+};
+
+/***/ }),
+/* 14 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _cells = __webpack_require__(15);
+
+exports.default = {
+ components: {
+ 'code-cell': _cells.CodeCell,
+ 'markdown-cell': _cells.MarkdownCell
+ },
+ props: {
+ notebook: {
+ type: Object,
+ required: true
+ },
+ codeCssClass: {
+ type: String,
+ required: false,
+ default: ''
+ }
+ },
+ methods: {
+ cellType: function cellType(type) {
+ return type + '-cell';
+ }
+ },
+ computed: {
+ cells: function cells() {
+ if (this.notebook.worksheets) {
+ var data = {
+ cells: []
+ };
+
+ return this.notebook.worksheets.reduce(function (data, sheet) {
+ data.cells = data.cells.concat(sheet.cells);
+ return data;
+ }, data).cells;
+ } else {
+ return this.notebook.cells;
+ }
+ },
+ hasNotebook: function hasNotebook() {
+ return Object.keys(this.notebook).length;
+ }
+ }
+}; //
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+
+/***/ }),
+/* 15 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _markdown = __webpack_require__(30);
+
+Object.defineProperty(exports, 'MarkdownCell', {
+ enumerable: true,
+ get: function get() {
+ return _interopRequireDefault(_markdown).default;
+ }
+});
+
+var _code = __webpack_require__(29);
+
+Object.defineProperty(exports, 'CodeCell', {
+ enumerable: true,
+ get: function get() {
+ return _interopRequireDefault(_code).default;
+ }
+});
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+/***/ }),
+/* 16 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _prismjs = __webpack_require__(28);
+
+var _prismjs2 = _interopRequireDefault(_prismjs);
+
+__webpack_require__(26);
+
+__webpack_require__(27);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+_prismjs2.default.plugins.customClass.map({
+ comment: 'c',
+ error: 'err',
+ operator: 'o',
+ constant: 'kc',
+ namespace: 'kn',
+ keyword: 'k',
+ string: 's',
+ number: 'm',
+ 'attr-name': 'na',
+ builtin: 'nb',
+ entity: 'ni',
+ function: 'nf',
+ tag: 'nt',
+ variable: 'nv'
+});
+
+exports.default = _prismjs2.default;
+
+/***/ }),
+/* 17 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+exports.byteLength = byteLength
+exports.toByteArray = toByteArray
+exports.fromByteArray = fromByteArray
+
+var lookup = []
+var revLookup = []
+var Arr = typeof Uint8Array !== 'undefined' ? Uint8Array : Array
+
+var code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
+for (var i = 0, len = code.length; i < len; ++i) {
+ lookup[i] = code[i]
+ revLookup[code.charCodeAt(i)] = i
+}
+
+revLookup['-'.charCodeAt(0)] = 62
+revLookup['_'.charCodeAt(0)] = 63
+
+function placeHoldersCount (b64) {
+ var len = b64.length
+ if (len % 4 > 0) {
+ throw new Error('Invalid string. Length must be a multiple of 4')
+ }
+
+ // the number of equal signs (place holders)
+ // if there are two placeholders, than the two characters before it
+ // represent one byte
+ // if there is only one, then the three characters before it represent 2 bytes
+ // this is just a cheap hack to not do indexOf twice
+ return b64[len - 2] === '=' ? 2 : b64[len - 1] === '=' ? 1 : 0
+}
+
+function byteLength (b64) {
+ // base64 is 4/3 + up to two characters of the original data
+ return b64.length * 3 / 4 - placeHoldersCount(b64)
+}
+
+function toByteArray (b64) {
+ var i, j, l, tmp, placeHolders, arr
+ var len = b64.length
+ placeHolders = placeHoldersCount(b64)
+
+ arr = new Arr(len * 3 / 4 - placeHolders)
+
+ // if there are placeholders, only get up to the last complete 4 chars
+ l = placeHolders > 0 ? len - 4 : len
+
+ var L = 0
+
+ for (i = 0, j = 0; i < l; i += 4, j += 3) {
+ tmp = (revLookup[b64.charCodeAt(i)] << 18) | (revLookup[b64.charCodeAt(i + 1)] << 12) | (revLookup[b64.charCodeAt(i + 2)] << 6) | revLookup[b64.charCodeAt(i + 3)]
+ arr[L++] = (tmp >> 16) & 0xFF
+ arr[L++] = (tmp >> 8) & 0xFF
+ arr[L++] = tmp & 0xFF
+ }
+
+ if (placeHolders === 2) {
+ tmp = (revLookup[b64.charCodeAt(i)] << 2) | (revLookup[b64.charCodeAt(i + 1)] >> 4)
+ arr[L++] = tmp & 0xFF
+ } else if (placeHolders === 1) {
+ tmp = (revLookup[b64.charCodeAt(i)] << 10) | (revLookup[b64.charCodeAt(i + 1)] << 4) | (revLookup[b64.charCodeAt(i + 2)] >> 2)
+ arr[L++] = (tmp >> 8) & 0xFF
+ arr[L++] = tmp & 0xFF
+ }
+
+ return arr
+}
+
+function tripletToBase64 (num) {
+ return lookup[num >> 18 & 0x3F] + lookup[num >> 12 & 0x3F] + lookup[num >> 6 & 0x3F] + lookup[num & 0x3F]
+}
+
+function encodeChunk (uint8, start, end) {
+ var tmp
+ var output = []
+ for (var i = start; i < end; i += 3) {
+ tmp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2])
+ output.push(tripletToBase64(tmp))
+ }
+ return output.join('')
+}
+
+function fromByteArray (uint8) {
+ var tmp
+ var len = uint8.length
+ var extraBytes = len % 3 // if we have 1 byte left, pad 2 bytes
+ var output = ''
+ var parts = []
+ var maxChunkLength = 16383 // must be multiple of 3
+
+ // go through the array every three bytes, we'll deal with trailing stuff later
+ for (var i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) {
+ parts.push(encodeChunk(uint8, i, (i + maxChunkLength) > len2 ? len2 : (i + maxChunkLength)))
+ }
+
+ // pad the end with zeros, but make sure to not forget the extra bytes
+ if (extraBytes === 1) {
+ tmp = uint8[len - 1]
+ output += lookup[tmp >> 2]
+ output += lookup[(tmp << 4) & 0x3F]
+ output += '=='
+ } else if (extraBytes === 2) {
+ tmp = (uint8[len - 2] << 8) + (uint8[len - 1])
+ output += lookup[tmp >> 10]
+ output += lookup[(tmp >> 4) & 0x3F]
+ output += lookup[(tmp << 2) & 0x3F]
+ output += '='
+ }
+
+ parts.push(output)
+
+ return parts.join('')
+}
+
+
+/***/ }),
+/* 18 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+/* WEBPACK VAR INJECTION */(function(global) {/*!
+ * The buffer module from node.js, for the browser.
+ *
+ * @author Feross Aboukhadijeh <feross@feross.org> <http://feross.org>
+ * @license MIT
+ */
+/* eslint-disable no-proto */
+
+
+
+var base64 = __webpack_require__(17)
+var ieee754 = __webpack_require__(23)
+var isArray = __webpack_require__(24)
+
+exports.Buffer = Buffer
+exports.SlowBuffer = SlowBuffer
+exports.INSPECT_MAX_BYTES = 50
+
+/**
+ * If `Buffer.TYPED_ARRAY_SUPPORT`:
+ * === true Use Uint8Array implementation (fastest)
+ * === false Use Object implementation (most compatible, even IE6)
+ *
+ * Browsers that support typed arrays are IE 10+, Firefox 4+, Chrome 7+, Safari 5.1+,
+ * Opera 11.6+, iOS 4.2+.
+ *
+ * Due to various browser bugs, sometimes the Object implementation will be used even
+ * when the browser supports typed arrays.
+ *
+ * Note:
+ *
+ * - Firefox 4-29 lacks support for adding new properties to `Uint8Array` instances,
+ * See: https://bugzilla.mozilla.org/show_bug.cgi?id=695438.
+ *
+ * - Chrome 9-10 is missing the `TypedArray.prototype.subarray` function.
+ *
+ * - IE10 has a broken `TypedArray.prototype.subarray` function which returns arrays of
+ * incorrect length in some situations.
+
+ * We detect these buggy browsers and set `Buffer.TYPED_ARRAY_SUPPORT` to `false` so they
+ * get the Object implementation, which is slower but behaves correctly.
+ */
+Buffer.TYPED_ARRAY_SUPPORT = global.TYPED_ARRAY_SUPPORT !== undefined
+ ? global.TYPED_ARRAY_SUPPORT
+ : typedArraySupport()
+
+/*
+ * Export kMaxLength after typed array support is determined.
+ */
+exports.kMaxLength = kMaxLength()
+
+function typedArraySupport () {
+ try {
+ var arr = new Uint8Array(1)
+ arr.__proto__ = {__proto__: Uint8Array.prototype, foo: function () { return 42 }}
+ return arr.foo() === 42 && // typed array instances can be augmented
+ typeof arr.subarray === 'function' && // chrome 9-10 lack `subarray`
+ arr.subarray(1, 1).byteLength === 0 // ie10 has broken `subarray`
+ } catch (e) {
+ return false
+ }
+}
+
+function kMaxLength () {
+ return Buffer.TYPED_ARRAY_SUPPORT
+ ? 0x7fffffff
+ : 0x3fffffff
+}
+
+function createBuffer (that, length) {
+ if (kMaxLength() < length) {
+ throw new RangeError('Invalid typed array length')
+ }
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ // Return an augmented `Uint8Array` instance, for best performance
+ that = new Uint8Array(length)
+ that.__proto__ = Buffer.prototype
+ } else {
+ // Fallback: Return an object instance of the Buffer class
+ if (that === null) {
+ that = new Buffer(length)
+ }
+ that.length = length
+ }
+
+ return that
+}
+
+/**
+ * The Buffer constructor returns instances of `Uint8Array` that have their
+ * prototype changed to `Buffer.prototype`. Furthermore, `Buffer` is a subclass of
+ * `Uint8Array`, so the returned instances will have all the node `Buffer` methods
+ * and the `Uint8Array` methods. Square bracket notation works as expected -- it
+ * returns a single octet.
+ *
+ * The `Uint8Array` prototype remains unmodified.
+ */
+
+function Buffer (arg, encodingOrOffset, length) {
+ if (!Buffer.TYPED_ARRAY_SUPPORT && !(this instanceof Buffer)) {
+ return new Buffer(arg, encodingOrOffset, length)
+ }
+
+ // Common case.
+ if (typeof arg === 'number') {
+ if (typeof encodingOrOffset === 'string') {
+ throw new Error(
+ 'If encoding is specified then the first argument must be a string'
+ )
+ }
+ return allocUnsafe(this, arg)
+ }
+ return from(this, arg, encodingOrOffset, length)
+}
+
+Buffer.poolSize = 8192 // not used by this implementation
+
+// TODO: Legacy, not needed anymore. Remove in next major version.
+Buffer._augment = function (arr) {
+ arr.__proto__ = Buffer.prototype
+ return arr
+}
+
+function from (that, value, encodingOrOffset, length) {
+ if (typeof value === 'number') {
+ throw new TypeError('"value" argument must not be a number')
+ }
+
+ if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer) {
+ return fromArrayBuffer(that, value, encodingOrOffset, length)
+ }
+
+ if (typeof value === 'string') {
+ return fromString(that, value, encodingOrOffset)
+ }
+
+ return fromObject(that, value)
+}
+
+/**
+ * Functionally equivalent to Buffer(arg, encoding) but throws a TypeError
+ * if value is a number.
+ * Buffer.from(str[, encoding])
+ * Buffer.from(array)
+ * Buffer.from(buffer)
+ * Buffer.from(arrayBuffer[, byteOffset[, length]])
+ **/
+Buffer.from = function (value, encodingOrOffset, length) {
+ return from(null, value, encodingOrOffset, length)
+}
+
+if (Buffer.TYPED_ARRAY_SUPPORT) {
+ Buffer.prototype.__proto__ = Uint8Array.prototype
+ Buffer.__proto__ = Uint8Array
+ if (typeof Symbol !== 'undefined' && Symbol.species &&
+ Buffer[Symbol.species] === Buffer) {
+ // Fix subarray() in ES2016. See: https://github.com/feross/buffer/pull/97
+ Object.defineProperty(Buffer, Symbol.species, {
+ value: null,
+ configurable: true
+ })
+ }
+}
+
+function assertSize (size) {
+ if (typeof size !== 'number') {
+ throw new TypeError('"size" argument must be a number')
+ } else if (size < 0) {
+ throw new RangeError('"size" argument must not be negative')
+ }
+}
+
+function alloc (that, size, fill, encoding) {
+ assertSize(size)
+ if (size <= 0) {
+ return createBuffer(that, size)
+ }
+ if (fill !== undefined) {
+ // Only pay attention to encoding if it's a string. This
+ // prevents accidentally sending in a number that would
+ // be interpretted as a start offset.
+ return typeof encoding === 'string'
+ ? createBuffer(that, size).fill(fill, encoding)
+ : createBuffer(that, size).fill(fill)
+ }
+ return createBuffer(that, size)
+}
+
+/**
+ * Creates a new filled Buffer instance.
+ * alloc(size[, fill[, encoding]])
+ **/
+Buffer.alloc = function (size, fill, encoding) {
+ return alloc(null, size, fill, encoding)
+}
+
+function allocUnsafe (that, size) {
+ assertSize(size)
+ that = createBuffer(that, size < 0 ? 0 : checked(size) | 0)
+ if (!Buffer.TYPED_ARRAY_SUPPORT) {
+ for (var i = 0; i < size; ++i) {
+ that[i] = 0
+ }
+ }
+ return that
+}
+
+/**
+ * Equivalent to Buffer(num), by default creates a non-zero-filled Buffer instance.
+ * */
+Buffer.allocUnsafe = function (size) {
+ return allocUnsafe(null, size)
+}
+/**
+ * Equivalent to SlowBuffer(num), by default creates a non-zero-filled Buffer instance.
+ */
+Buffer.allocUnsafeSlow = function (size) {
+ return allocUnsafe(null, size)
+}
+
+function fromString (that, string, encoding) {
+ if (typeof encoding !== 'string' || encoding === '') {
+ encoding = 'utf8'
+ }
+
+ if (!Buffer.isEncoding(encoding)) {
+ throw new TypeError('"encoding" must be a valid string encoding')
+ }
+
+ var length = byteLength(string, encoding) | 0
+ that = createBuffer(that, length)
+
+ var actual = that.write(string, encoding)
+
+ if (actual !== length) {
+ // Writing a hex string, for example, that contains invalid characters will
+ // cause everything after the first invalid character to be ignored. (e.g.
+ // 'abxxcd' will be treated as 'ab')
+ that = that.slice(0, actual)
+ }
+
+ return that
+}
+
+function fromArrayLike (that, array) {
+ var length = array.length < 0 ? 0 : checked(array.length) | 0
+ that = createBuffer(that, length)
+ for (var i = 0; i < length; i += 1) {
+ that[i] = array[i] & 255
+ }
+ return that
+}
+
+function fromArrayBuffer (that, array, byteOffset, length) {
+ array.byteLength // this throws if `array` is not a valid ArrayBuffer
+
+ if (byteOffset < 0 || array.byteLength < byteOffset) {
+ throw new RangeError('\'offset\' is out of bounds')
+ }
+
+ if (array.byteLength < byteOffset + (length || 0)) {
+ throw new RangeError('\'length\' is out of bounds')
+ }
+
+ if (byteOffset === undefined && length === undefined) {
+ array = new Uint8Array(array)
+ } else if (length === undefined) {
+ array = new Uint8Array(array, byteOffset)
+ } else {
+ array = new Uint8Array(array, byteOffset, length)
+ }
+
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ // Return an augmented `Uint8Array` instance, for best performance
+ that = array
+ that.__proto__ = Buffer.prototype
+ } else {
+ // Fallback: Return an object instance of the Buffer class
+ that = fromArrayLike(that, array)
+ }
+ return that
+}
+
+function fromObject (that, obj) {
+ if (Buffer.isBuffer(obj)) {
+ var len = checked(obj.length) | 0
+ that = createBuffer(that, len)
+
+ if (that.length === 0) {
+ return that
+ }
+
+ obj.copy(that, 0, 0, len)
+ return that
+ }
+
+ if (obj) {
+ if ((typeof ArrayBuffer !== 'undefined' &&
+ obj.buffer instanceof ArrayBuffer) || 'length' in obj) {
+ if (typeof obj.length !== 'number' || isnan(obj.length)) {
+ return createBuffer(that, 0)
+ }
+ return fromArrayLike(that, obj)
+ }
+
+ if (obj.type === 'Buffer' && isArray(obj.data)) {
+ return fromArrayLike(that, obj.data)
+ }
+ }
+
+ throw new TypeError('First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.')
+}
+
+function checked (length) {
+ // Note: cannot use `length < kMaxLength()` here because that fails when
+ // length is NaN (which is otherwise coerced to zero.)
+ if (length >= kMaxLength()) {
+ throw new RangeError('Attempt to allocate Buffer larger than maximum ' +
+ 'size: 0x' + kMaxLength().toString(16) + ' bytes')
+ }
+ return length | 0
+}
+
+function SlowBuffer (length) {
+ if (+length != length) { // eslint-disable-line eqeqeq
+ length = 0
+ }
+ return Buffer.alloc(+length)
+}
+
+Buffer.isBuffer = function isBuffer (b) {
+ return !!(b != null && b._isBuffer)
+}
+
+Buffer.compare = function compare (a, b) {
+ if (!Buffer.isBuffer(a) || !Buffer.isBuffer(b)) {
+ throw new TypeError('Arguments must be Buffers')
+ }
+
+ if (a === b) return 0
+
+ var x = a.length
+ var y = b.length
+
+ for (var i = 0, len = Math.min(x, y); i < len; ++i) {
+ if (a[i] !== b[i]) {
+ x = a[i]
+ y = b[i]
+ break
+ }
+ }
+
+ if (x < y) return -1
+ if (y < x) return 1
+ return 0
+}
+
+Buffer.isEncoding = function isEncoding (encoding) {
+ switch (String(encoding).toLowerCase()) {
+ case 'hex':
+ case 'utf8':
+ case 'utf-8':
+ case 'ascii':
+ case 'latin1':
+ case 'binary':
+ case 'base64':
+ case 'ucs2':
+ case 'ucs-2':
+ case 'utf16le':
+ case 'utf-16le':
+ return true
+ default:
+ return false
+ }
+}
+
+Buffer.concat = function concat (list, length) {
+ if (!isArray(list)) {
+ throw new TypeError('"list" argument must be an Array of Buffers')
+ }
+
+ if (list.length === 0) {
+ return Buffer.alloc(0)
+ }
+
+ var i
+ if (length === undefined) {
+ length = 0
+ for (i = 0; i < list.length; ++i) {
+ length += list[i].length
+ }
+ }
+
+ var buffer = Buffer.allocUnsafe(length)
+ var pos = 0
+ for (i = 0; i < list.length; ++i) {
+ var buf = list[i]
+ if (!Buffer.isBuffer(buf)) {
+ throw new TypeError('"list" argument must be an Array of Buffers')
+ }
+ buf.copy(buffer, pos)
+ pos += buf.length
+ }
+ return buffer
+}
+
+function byteLength (string, encoding) {
+ if (Buffer.isBuffer(string)) {
+ return string.length
+ }
+ if (typeof ArrayBuffer !== 'undefined' && typeof ArrayBuffer.isView === 'function' &&
+ (ArrayBuffer.isView(string) || string instanceof ArrayBuffer)) {
+ return string.byteLength
+ }
+ if (typeof string !== 'string') {
+ string = '' + string
+ }
+
+ var len = string.length
+ if (len === 0) return 0
+
+ // Use a for loop to avoid recursion
+ var loweredCase = false
+ for (;;) {
+ switch (encoding) {
+ case 'ascii':
+ case 'latin1':
+ case 'binary':
+ return len
+ case 'utf8':
+ case 'utf-8':
+ case undefined:
+ return utf8ToBytes(string).length
+ case 'ucs2':
+ case 'ucs-2':
+ case 'utf16le':
+ case 'utf-16le':
+ return len * 2
+ case 'hex':
+ return len >>> 1
+ case 'base64':
+ return base64ToBytes(string).length
+ default:
+ if (loweredCase) return utf8ToBytes(string).length // assume utf8
+ encoding = ('' + encoding).toLowerCase()
+ loweredCase = true
+ }
+ }
+}
+Buffer.byteLength = byteLength
+
+function slowToString (encoding, start, end) {
+ var loweredCase = false
+
+ // No need to verify that "this.length <= MAX_UINT32" since it's a read-only
+ // property of a typed array.
+
+ // This behaves neither like String nor Uint8Array in that we set start/end
+ // to their upper/lower bounds if the value passed is out of range.
+ // undefined is handled specially as per ECMA-262 6th Edition,
+ // Section 13.3.3.7 Runtime Semantics: KeyedBindingInitialization.
+ if (start === undefined || start < 0) {
+ start = 0
+ }
+ // Return early if start > this.length. Done here to prevent potential uint32
+ // coercion fail below.
+ if (start > this.length) {
+ return ''
+ }
+
+ if (end === undefined || end > this.length) {
+ end = this.length
+ }
+
+ if (end <= 0) {
+ return ''
+ }
+
+ // Force coersion to uint32. This will also coerce falsey/NaN values to 0.
+ end >>>= 0
+ start >>>= 0
+
+ if (end <= start) {
+ return ''
+ }
+
+ if (!encoding) encoding = 'utf8'
+
+ while (true) {
+ switch (encoding) {
+ case 'hex':
+ return hexSlice(this, start, end)
+
+ case 'utf8':
+ case 'utf-8':
+ return utf8Slice(this, start, end)
+
+ case 'ascii':
+ return asciiSlice(this, start, end)
+
+ case 'latin1':
+ case 'binary':
+ return latin1Slice(this, start, end)
+
+ case 'base64':
+ return base64Slice(this, start, end)
+
+ case 'ucs2':
+ case 'ucs-2':
+ case 'utf16le':
+ case 'utf-16le':
+ return utf16leSlice(this, start, end)
+
+ default:
+ if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding)
+ encoding = (encoding + '').toLowerCase()
+ loweredCase = true
+ }
+ }
+}
+
+// The property is used by `Buffer.isBuffer` and `is-buffer` (in Safari 5-7) to detect
+// Buffer instances.
+Buffer.prototype._isBuffer = true
+
+function swap (b, n, m) {
+ var i = b[n]
+ b[n] = b[m]
+ b[m] = i
+}
+
+Buffer.prototype.swap16 = function swap16 () {
+ var len = this.length
+ if (len % 2 !== 0) {
+ throw new RangeError('Buffer size must be a multiple of 16-bits')
+ }
+ for (var i = 0; i < len; i += 2) {
+ swap(this, i, i + 1)
+ }
+ return this
+}
+
+Buffer.prototype.swap32 = function swap32 () {
+ var len = this.length
+ if (len % 4 !== 0) {
+ throw new RangeError('Buffer size must be a multiple of 32-bits')
+ }
+ for (var i = 0; i < len; i += 4) {
+ swap(this, i, i + 3)
+ swap(this, i + 1, i + 2)
+ }
+ return this
+}
+
+Buffer.prototype.swap64 = function swap64 () {
+ var len = this.length
+ if (len % 8 !== 0) {
+ throw new RangeError('Buffer size must be a multiple of 64-bits')
+ }
+ for (var i = 0; i < len; i += 8) {
+ swap(this, i, i + 7)
+ swap(this, i + 1, i + 6)
+ swap(this, i + 2, i + 5)
+ swap(this, i + 3, i + 4)
+ }
+ return this
+}
+
+Buffer.prototype.toString = function toString () {
+ var length = this.length | 0
+ if (length === 0) return ''
+ if (arguments.length === 0) return utf8Slice(this, 0, length)
+ return slowToString.apply(this, arguments)
+}
+
+Buffer.prototype.equals = function equals (b) {
+ if (!Buffer.isBuffer(b)) throw new TypeError('Argument must be a Buffer')
+ if (this === b) return true
+ return Buffer.compare(this, b) === 0
+}
+
+Buffer.prototype.inspect = function inspect () {
+ var str = ''
+ var max = exports.INSPECT_MAX_BYTES
+ if (this.length > 0) {
+ str = this.toString('hex', 0, max).match(/.{2}/g).join(' ')
+ if (this.length > max) str += ' ... '
+ }
+ return '<Buffer ' + str + '>'
+}
+
+Buffer.prototype.compare = function compare (target, start, end, thisStart, thisEnd) {
+ if (!Buffer.isBuffer(target)) {
+ throw new TypeError('Argument must be a Buffer')
+ }
+
+ if (start === undefined) {
+ start = 0
+ }
+ if (end === undefined) {
+ end = target ? target.length : 0
+ }
+ if (thisStart === undefined) {
+ thisStart = 0
+ }
+ if (thisEnd === undefined) {
+ thisEnd = this.length
+ }
+
+ if (start < 0 || end > target.length || thisStart < 0 || thisEnd > this.length) {
+ throw new RangeError('out of range index')
+ }
+
+ if (thisStart >= thisEnd && start >= end) {
+ return 0
+ }
+ if (thisStart >= thisEnd) {
+ return -1
+ }
+ if (start >= end) {
+ return 1
+ }
+
+ start >>>= 0
+ end >>>= 0
+ thisStart >>>= 0
+ thisEnd >>>= 0
+
+ if (this === target) return 0
+
+ var x = thisEnd - thisStart
+ var y = end - start
+ var len = Math.min(x, y)
+
+ var thisCopy = this.slice(thisStart, thisEnd)
+ var targetCopy = target.slice(start, end)
+
+ for (var i = 0; i < len; ++i) {
+ if (thisCopy[i] !== targetCopy[i]) {
+ x = thisCopy[i]
+ y = targetCopy[i]
+ break
+ }
+ }
+
+ if (x < y) return -1
+ if (y < x) return 1
+ return 0
+}
+
+// Finds either the first index of `val` in `buffer` at offset >= `byteOffset`,
+// OR the last index of `val` in `buffer` at offset <= `byteOffset`.
+//
+// Arguments:
+// - buffer - a Buffer to search
+// - val - a string, Buffer, or number
+// - byteOffset - an index into `buffer`; will be clamped to an int32
+// - encoding - an optional encoding, relevant is val is a string
+// - dir - true for indexOf, false for lastIndexOf
+function bidirectionalIndexOf (buffer, val, byteOffset, encoding, dir) {
+ // Empty buffer means no match
+ if (buffer.length === 0) return -1
+
+ // Normalize byteOffset
+ if (typeof byteOffset === 'string') {
+ encoding = byteOffset
+ byteOffset = 0
+ } else if (byteOffset > 0x7fffffff) {
+ byteOffset = 0x7fffffff
+ } else if (byteOffset < -0x80000000) {
+ byteOffset = -0x80000000
+ }
+ byteOffset = +byteOffset // Coerce to Number.
+ if (isNaN(byteOffset)) {
+ // byteOffset: it it's undefined, null, NaN, "foo", etc, search whole buffer
+ byteOffset = dir ? 0 : (buffer.length - 1)
+ }
+
+ // Normalize byteOffset: negative offsets start from the end of the buffer
+ if (byteOffset < 0) byteOffset = buffer.length + byteOffset
+ if (byteOffset >= buffer.length) {
+ if (dir) return -1
+ else byteOffset = buffer.length - 1
+ } else if (byteOffset < 0) {
+ if (dir) byteOffset = 0
+ else return -1
+ }
+
+ // Normalize val
+ if (typeof val === 'string') {
+ val = Buffer.from(val, encoding)
+ }
+
+ // Finally, search either indexOf (if dir is true) or lastIndexOf
+ if (Buffer.isBuffer(val)) {
+ // Special case: looking for empty string/buffer always fails
+ if (val.length === 0) {
+ return -1
+ }
+ return arrayIndexOf(buffer, val, byteOffset, encoding, dir)
+ } else if (typeof val === 'number') {
+ val = val & 0xFF // Search for a byte value [0-255]
+ if (Buffer.TYPED_ARRAY_SUPPORT &&
+ typeof Uint8Array.prototype.indexOf === 'function') {
+ if (dir) {
+ return Uint8Array.prototype.indexOf.call(buffer, val, byteOffset)
+ } else {
+ return Uint8Array.prototype.lastIndexOf.call(buffer, val, byteOffset)
+ }
+ }
+ return arrayIndexOf(buffer, [ val ], byteOffset, encoding, dir)
+ }
+
+ throw new TypeError('val must be string, number or Buffer')
+}
+
+function arrayIndexOf (arr, val, byteOffset, encoding, dir) {
+ var indexSize = 1
+ var arrLength = arr.length
+ var valLength = val.length
+
+ if (encoding !== undefined) {
+ encoding = String(encoding).toLowerCase()
+ if (encoding === 'ucs2' || encoding === 'ucs-2' ||
+ encoding === 'utf16le' || encoding === 'utf-16le') {
+ if (arr.length < 2 || val.length < 2) {
+ return -1
+ }
+ indexSize = 2
+ arrLength /= 2
+ valLength /= 2
+ byteOffset /= 2
+ }
+ }
+
+ function read (buf, i) {
+ if (indexSize === 1) {
+ return buf[i]
+ } else {
+ return buf.readUInt16BE(i * indexSize)
+ }
+ }
+
+ var i
+ if (dir) {
+ var foundIndex = -1
+ for (i = byteOffset; i < arrLength; i++) {
+ if (read(arr, i) === read(val, foundIndex === -1 ? 0 : i - foundIndex)) {
+ if (foundIndex === -1) foundIndex = i
+ if (i - foundIndex + 1 === valLength) return foundIndex * indexSize
+ } else {
+ if (foundIndex !== -1) i -= i - foundIndex
+ foundIndex = -1
+ }
+ }
+ } else {
+ if (byteOffset + valLength > arrLength) byteOffset = arrLength - valLength
+ for (i = byteOffset; i >= 0; i--) {
+ var found = true
+ for (var j = 0; j < valLength; j++) {
+ if (read(arr, i + j) !== read(val, j)) {
+ found = false
+ break
+ }
+ }
+ if (found) return i
+ }
+ }
+
+ return -1
+}
+
+Buffer.prototype.includes = function includes (val, byteOffset, encoding) {
+ return this.indexOf(val, byteOffset, encoding) !== -1
+}
+
+Buffer.prototype.indexOf = function indexOf (val, byteOffset, encoding) {
+ return bidirectionalIndexOf(this, val, byteOffset, encoding, true)
+}
+
+Buffer.prototype.lastIndexOf = function lastIndexOf (val, byteOffset, encoding) {
+ return bidirectionalIndexOf(this, val, byteOffset, encoding, false)
+}
+
+function hexWrite (buf, string, offset, length) {
+ offset = Number(offset) || 0
+ var remaining = buf.length - offset
+ if (!length) {
+ length = remaining
+ } else {
+ length = Number(length)
+ if (length > remaining) {
+ length = remaining
+ }
+ }
+
+ // must be an even number of digits
+ var strLen = string.length
+ if (strLen % 2 !== 0) throw new TypeError('Invalid hex string')
+
+ if (length > strLen / 2) {
+ length = strLen / 2
+ }
+ for (var i = 0; i < length; ++i) {
+ var parsed = parseInt(string.substr(i * 2, 2), 16)
+ if (isNaN(parsed)) return i
+ buf[offset + i] = parsed
+ }
+ return i
+}
+
+function utf8Write (buf, string, offset, length) {
+ return blitBuffer(utf8ToBytes(string, buf.length - offset), buf, offset, length)
+}
+
+function asciiWrite (buf, string, offset, length) {
+ return blitBuffer(asciiToBytes(string), buf, offset, length)
+}
+
+function latin1Write (buf, string, offset, length) {
+ return asciiWrite(buf, string, offset, length)
+}
+
+function base64Write (buf, string, offset, length) {
+ return blitBuffer(base64ToBytes(string), buf, offset, length)
+}
+
+function ucs2Write (buf, string, offset, length) {
+ return blitBuffer(utf16leToBytes(string, buf.length - offset), buf, offset, length)
+}
+
+Buffer.prototype.write = function write (string, offset, length, encoding) {
+ // Buffer#write(string)
+ if (offset === undefined) {
+ encoding = 'utf8'
+ length = this.length
+ offset = 0
+ // Buffer#write(string, encoding)
+ } else if (length === undefined && typeof offset === 'string') {
+ encoding = offset
+ length = this.length
+ offset = 0
+ // Buffer#write(string, offset[, length][, encoding])
+ } else if (isFinite(offset)) {
+ offset = offset | 0
+ if (isFinite(length)) {
+ length = length | 0
+ if (encoding === undefined) encoding = 'utf8'
+ } else {
+ encoding = length
+ length = undefined
+ }
+ // legacy write(string, encoding, offset, length) - remove in v0.13
+ } else {
+ throw new Error(
+ 'Buffer.write(string, encoding, offset[, length]) is no longer supported'
+ )
+ }
+
+ var remaining = this.length - offset
+ if (length === undefined || length > remaining) length = remaining
+
+ if ((string.length > 0 && (length < 0 || offset < 0)) || offset > this.length) {
+ throw new RangeError('Attempt to write outside buffer bounds')
+ }
+
+ if (!encoding) encoding = 'utf8'
+
+ var loweredCase = false
+ for (;;) {
+ switch (encoding) {
+ case 'hex':
+ return hexWrite(this, string, offset, length)
+
+ case 'utf8':
+ case 'utf-8':
+ return utf8Write(this, string, offset, length)
+
+ case 'ascii':
+ return asciiWrite(this, string, offset, length)
+
+ case 'latin1':
+ case 'binary':
+ return latin1Write(this, string, offset, length)
+
+ case 'base64':
+ // Warning: maxLength not taken into account in base64Write
+ return base64Write(this, string, offset, length)
+
+ case 'ucs2':
+ case 'ucs-2':
+ case 'utf16le':
+ case 'utf-16le':
+ return ucs2Write(this, string, offset, length)
+
+ default:
+ if (loweredCase) throw new TypeError('Unknown encoding: ' + encoding)
+ encoding = ('' + encoding).toLowerCase()
+ loweredCase = true
+ }
+ }
+}
+
+Buffer.prototype.toJSON = function toJSON () {
+ return {
+ type: 'Buffer',
+ data: Array.prototype.slice.call(this._arr || this, 0)
+ }
+}
+
+function base64Slice (buf, start, end) {
+ if (start === 0 && end === buf.length) {
+ return base64.fromByteArray(buf)
+ } else {
+ return base64.fromByteArray(buf.slice(start, end))
+ }
+}
+
+function utf8Slice (buf, start, end) {
+ end = Math.min(buf.length, end)
+ var res = []
+
+ var i = start
+ while (i < end) {
+ var firstByte = buf[i]
+ var codePoint = null
+ var bytesPerSequence = (firstByte > 0xEF) ? 4
+ : (firstByte > 0xDF) ? 3
+ : (firstByte > 0xBF) ? 2
+ : 1
+
+ if (i + bytesPerSequence <= end) {
+ var secondByte, thirdByte, fourthByte, tempCodePoint
+
+ switch (bytesPerSequence) {
+ case 1:
+ if (firstByte < 0x80) {
+ codePoint = firstByte
+ }
+ break
+ case 2:
+ secondByte = buf[i + 1]
+ if ((secondByte & 0xC0) === 0x80) {
+ tempCodePoint = (firstByte & 0x1F) << 0x6 | (secondByte & 0x3F)
+ if (tempCodePoint > 0x7F) {
+ codePoint = tempCodePoint
+ }
+ }
+ break
+ case 3:
+ secondByte = buf[i + 1]
+ thirdByte = buf[i + 2]
+ if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80) {
+ tempCodePoint = (firstByte & 0xF) << 0xC | (secondByte & 0x3F) << 0x6 | (thirdByte & 0x3F)
+ if (tempCodePoint > 0x7FF && (tempCodePoint < 0xD800 || tempCodePoint > 0xDFFF)) {
+ codePoint = tempCodePoint
+ }
+ }
+ break
+ case 4:
+ secondByte = buf[i + 1]
+ thirdByte = buf[i + 2]
+ fourthByte = buf[i + 3]
+ if ((secondByte & 0xC0) === 0x80 && (thirdByte & 0xC0) === 0x80 && (fourthByte & 0xC0) === 0x80) {
+ tempCodePoint = (firstByte & 0xF) << 0x12 | (secondByte & 0x3F) << 0xC | (thirdByte & 0x3F) << 0x6 | (fourthByte & 0x3F)
+ if (tempCodePoint > 0xFFFF && tempCodePoint < 0x110000) {
+ codePoint = tempCodePoint
+ }
+ }
+ }
+ }
+
+ if (codePoint === null) {
+ // we did not generate a valid codePoint so insert a
+ // replacement char (U+FFFD) and advance only 1 byte
+ codePoint = 0xFFFD
+ bytesPerSequence = 1
+ } else if (codePoint > 0xFFFF) {
+ // encode to utf16 (surrogate pair dance)
+ codePoint -= 0x10000
+ res.push(codePoint >>> 10 & 0x3FF | 0xD800)
+ codePoint = 0xDC00 | codePoint & 0x3FF
+ }
+
+ res.push(codePoint)
+ i += bytesPerSequence
+ }
+
+ return decodeCodePointsArray(res)
+}
+
+// Based on http://stackoverflow.com/a/22747272/680742, the browser with
+// the lowest limit is Chrome, with 0x10000 args.
+// We go 1 magnitude less, for safety
+var MAX_ARGUMENTS_LENGTH = 0x1000
+
+function decodeCodePointsArray (codePoints) {
+ var len = codePoints.length
+ if (len <= MAX_ARGUMENTS_LENGTH) {
+ return String.fromCharCode.apply(String, codePoints) // avoid extra slice()
+ }
+
+ // Decode in chunks to avoid "call stack size exceeded".
+ var res = ''
+ var i = 0
+ while (i < len) {
+ res += String.fromCharCode.apply(
+ String,
+ codePoints.slice(i, i += MAX_ARGUMENTS_LENGTH)
+ )
+ }
+ return res
+}
+
+function asciiSlice (buf, start, end) {
+ var ret = ''
+ end = Math.min(buf.length, end)
+
+ for (var i = start; i < end; ++i) {
+ ret += String.fromCharCode(buf[i] & 0x7F)
+ }
+ return ret
+}
+
+function latin1Slice (buf, start, end) {
+ var ret = ''
+ end = Math.min(buf.length, end)
+
+ for (var i = start; i < end; ++i) {
+ ret += String.fromCharCode(buf[i])
+ }
+ return ret
+}
+
+function hexSlice (buf, start, end) {
+ var len = buf.length
+
+ if (!start || start < 0) start = 0
+ if (!end || end < 0 || end > len) end = len
+
+ var out = ''
+ for (var i = start; i < end; ++i) {
+ out += toHex(buf[i])
+ }
+ return out
+}
+
+function utf16leSlice (buf, start, end) {
+ var bytes = buf.slice(start, end)
+ var res = ''
+ for (var i = 0; i < bytes.length; i += 2) {
+ res += String.fromCharCode(bytes[i] + bytes[i + 1] * 256)
+ }
+ return res
+}
+
+Buffer.prototype.slice = function slice (start, end) {
+ var len = this.length
+ start = ~~start
+ end = end === undefined ? len : ~~end
+
+ if (start < 0) {
+ start += len
+ if (start < 0) start = 0
+ } else if (start > len) {
+ start = len
+ }
+
+ if (end < 0) {
+ end += len
+ if (end < 0) end = 0
+ } else if (end > len) {
+ end = len
+ }
+
+ if (end < start) end = start
+
+ var newBuf
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ newBuf = this.subarray(start, end)
+ newBuf.__proto__ = Buffer.prototype
+ } else {
+ var sliceLen = end - start
+ newBuf = new Buffer(sliceLen, undefined)
+ for (var i = 0; i < sliceLen; ++i) {
+ newBuf[i] = this[i + start]
+ }
+ }
+
+ return newBuf
+}
+
+/*
+ * Need to make sure that buffer isn't trying to write out of bounds.
+ */
+function checkOffset (offset, ext, length) {
+ if ((offset % 1) !== 0 || offset < 0) throw new RangeError('offset is not uint')
+ if (offset + ext > length) throw new RangeError('Trying to access beyond buffer length')
+}
+
+Buffer.prototype.readUIntLE = function readUIntLE (offset, byteLength, noAssert) {
+ offset = offset | 0
+ byteLength = byteLength | 0
+ if (!noAssert) checkOffset(offset, byteLength, this.length)
+
+ var val = this[offset]
+ var mul = 1
+ var i = 0
+ while (++i < byteLength && (mul *= 0x100)) {
+ val += this[offset + i] * mul
+ }
+
+ return val
+}
+
+Buffer.prototype.readUIntBE = function readUIntBE (offset, byteLength, noAssert) {
+ offset = offset | 0
+ byteLength = byteLength | 0
+ if (!noAssert) {
+ checkOffset(offset, byteLength, this.length)
+ }
+
+ var val = this[offset + --byteLength]
+ var mul = 1
+ while (byteLength > 0 && (mul *= 0x100)) {
+ val += this[offset + --byteLength] * mul
+ }
+
+ return val
+}
+
+Buffer.prototype.readUInt8 = function readUInt8 (offset, noAssert) {
+ if (!noAssert) checkOffset(offset, 1, this.length)
+ return this[offset]
+}
+
+Buffer.prototype.readUInt16LE = function readUInt16LE (offset, noAssert) {
+ if (!noAssert) checkOffset(offset, 2, this.length)
+ return this[offset] | (this[offset + 1] << 8)
+}
+
+Buffer.prototype.readUInt16BE = function readUInt16BE (offset, noAssert) {
+ if (!noAssert) checkOffset(offset, 2, this.length)
+ return (this[offset] << 8) | this[offset + 1]
+}
+
+Buffer.prototype.readUInt32LE = function readUInt32LE (offset, noAssert) {
+ if (!noAssert) checkOffset(offset, 4, this.length)
+
+ return ((this[offset]) |
+ (this[offset + 1] << 8) |
+ (this[offset + 2] << 16)) +
+ (this[offset + 3] * 0x1000000)
+}
+
+Buffer.prototype.readUInt32BE = function readUInt32BE (offset, noAssert) {
+ if (!noAssert) checkOffset(offset, 4, this.length)
+
+ return (this[offset] * 0x1000000) +
+ ((this[offset + 1] << 16) |
+ (this[offset + 2] << 8) |
+ this[offset + 3])
+}
+
+Buffer.prototype.readIntLE = function readIntLE (offset, byteLength, noAssert) {
+ offset = offset | 0
+ byteLength = byteLength | 0
+ if (!noAssert) checkOffset(offset, byteLength, this.length)
+
+ var val = this[offset]
+ var mul = 1
+ var i = 0
+ while (++i < byteLength && (mul *= 0x100)) {
+ val += this[offset + i] * mul
+ }
+ mul *= 0x80
+
+ if (val >= mul) val -= Math.pow(2, 8 * byteLength)
+
+ return val
+}
+
+Buffer.prototype.readIntBE = function readIntBE (offset, byteLength, noAssert) {
+ offset = offset | 0
+ byteLength = byteLength | 0
+ if (!noAssert) checkOffset(offset, byteLength, this.length)
+
+ var i = byteLength
+ var mul = 1
+ var val = this[offset + --i]
+ while (i > 0 && (mul *= 0x100)) {
+ val += this[offset + --i] * mul
+ }
+ mul *= 0x80
+
+ if (val >= mul) val -= Math.pow(2, 8 * byteLength)
+
+ return val
+}
+
+Buffer.prototype.readInt8 = function readInt8 (offset, noAssert) {
+ if (!noAssert) checkOffset(offset, 1, this.length)
+ if (!(this[offset] & 0x80)) return (this[offset])
+ return ((0xff - this[offset] + 1) * -1)
+}
+
+Buffer.prototype.readInt16LE = function readInt16LE (offset, noAssert) {
+ if (!noAssert) checkOffset(offset, 2, this.length)
+ var val = this[offset] | (this[offset + 1] << 8)
+ return (val & 0x8000) ? val | 0xFFFF0000 : val
+}
+
+Buffer.prototype.readInt16BE = function readInt16BE (offset, noAssert) {
+ if (!noAssert) checkOffset(offset, 2, this.length)
+ var val = this[offset + 1] | (this[offset] << 8)
+ return (val & 0x8000) ? val | 0xFFFF0000 : val
+}
+
+Buffer.prototype.readInt32LE = function readInt32LE (offset, noAssert) {
+ if (!noAssert) checkOffset(offset, 4, this.length)
+
+ return (this[offset]) |
+ (this[offset + 1] << 8) |
+ (this[offset + 2] << 16) |
+ (this[offset + 3] << 24)
+}
+
+Buffer.prototype.readInt32BE = function readInt32BE (offset, noAssert) {
+ if (!noAssert) checkOffset(offset, 4, this.length)
+
+ return (this[offset] << 24) |
+ (this[offset + 1] << 16) |
+ (this[offset + 2] << 8) |
+ (this[offset + 3])
+}
+
+Buffer.prototype.readFloatLE = function readFloatLE (offset, noAssert) {
+ if (!noAssert) checkOffset(offset, 4, this.length)
+ return ieee754.read(this, offset, true, 23, 4)
+}
+
+Buffer.prototype.readFloatBE = function readFloatBE (offset, noAssert) {
+ if (!noAssert) checkOffset(offset, 4, this.length)
+ return ieee754.read(this, offset, false, 23, 4)
+}
+
+Buffer.prototype.readDoubleLE = function readDoubleLE (offset, noAssert) {
+ if (!noAssert) checkOffset(offset, 8, this.length)
+ return ieee754.read(this, offset, true, 52, 8)
+}
+
+Buffer.prototype.readDoubleBE = function readDoubleBE (offset, noAssert) {
+ if (!noAssert) checkOffset(offset, 8, this.length)
+ return ieee754.read(this, offset, false, 52, 8)
+}
+
+function checkInt (buf, value, offset, ext, max, min) {
+ if (!Buffer.isBuffer(buf)) throw new TypeError('"buffer" argument must be a Buffer instance')
+ if (value > max || value < min) throw new RangeError('"value" argument is out of bounds')
+ if (offset + ext > buf.length) throw new RangeError('Index out of range')
+}
+
+Buffer.prototype.writeUIntLE = function writeUIntLE (value, offset, byteLength, noAssert) {
+ value = +value
+ offset = offset | 0
+ byteLength = byteLength | 0
+ if (!noAssert) {
+ var maxBytes = Math.pow(2, 8 * byteLength) - 1
+ checkInt(this, value, offset, byteLength, maxBytes, 0)
+ }
+
+ var mul = 1
+ var i = 0
+ this[offset] = value & 0xFF
+ while (++i < byteLength && (mul *= 0x100)) {
+ this[offset + i] = (value / mul) & 0xFF
+ }
+
+ return offset + byteLength
+}
+
+Buffer.prototype.writeUIntBE = function writeUIntBE (value, offset, byteLength, noAssert) {
+ value = +value
+ offset = offset | 0
+ byteLength = byteLength | 0
+ if (!noAssert) {
+ var maxBytes = Math.pow(2, 8 * byteLength) - 1
+ checkInt(this, value, offset, byteLength, maxBytes, 0)
+ }
+
+ var i = byteLength - 1
+ var mul = 1
+ this[offset + i] = value & 0xFF
+ while (--i >= 0 && (mul *= 0x100)) {
+ this[offset + i] = (value / mul) & 0xFF
+ }
+
+ return offset + byteLength
+}
+
+Buffer.prototype.writeUInt8 = function writeUInt8 (value, offset, noAssert) {
+ value = +value
+ offset = offset | 0
+ if (!noAssert) checkInt(this, value, offset, 1, 0xff, 0)
+ if (!Buffer.TYPED_ARRAY_SUPPORT) value = Math.floor(value)
+ this[offset] = (value & 0xff)
+ return offset + 1
+}
+
+function objectWriteUInt16 (buf, value, offset, littleEndian) {
+ if (value < 0) value = 0xffff + value + 1
+ for (var i = 0, j = Math.min(buf.length - offset, 2); i < j; ++i) {
+ buf[offset + i] = (value & (0xff << (8 * (littleEndian ? i : 1 - i)))) >>>
+ (littleEndian ? i : 1 - i) * 8
+ }
+}
+
+Buffer.prototype.writeUInt16LE = function writeUInt16LE (value, offset, noAssert) {
+ value = +value
+ offset = offset | 0
+ if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0)
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ this[offset] = (value & 0xff)
+ this[offset + 1] = (value >>> 8)
+ } else {
+ objectWriteUInt16(this, value, offset, true)
+ }
+ return offset + 2
+}
+
+Buffer.prototype.writeUInt16BE = function writeUInt16BE (value, offset, noAssert) {
+ value = +value
+ offset = offset | 0
+ if (!noAssert) checkInt(this, value, offset, 2, 0xffff, 0)
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ this[offset] = (value >>> 8)
+ this[offset + 1] = (value & 0xff)
+ } else {
+ objectWriteUInt16(this, value, offset, false)
+ }
+ return offset + 2
+}
+
+function objectWriteUInt32 (buf, value, offset, littleEndian) {
+ if (value < 0) value = 0xffffffff + value + 1
+ for (var i = 0, j = Math.min(buf.length - offset, 4); i < j; ++i) {
+ buf[offset + i] = (value >>> (littleEndian ? i : 3 - i) * 8) & 0xff
+ }
+}
+
+Buffer.prototype.writeUInt32LE = function writeUInt32LE (value, offset, noAssert) {
+ value = +value
+ offset = offset | 0
+ if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0)
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ this[offset + 3] = (value >>> 24)
+ this[offset + 2] = (value >>> 16)
+ this[offset + 1] = (value >>> 8)
+ this[offset] = (value & 0xff)
+ } else {
+ objectWriteUInt32(this, value, offset, true)
+ }
+ return offset + 4
+}
+
+Buffer.prototype.writeUInt32BE = function writeUInt32BE (value, offset, noAssert) {
+ value = +value
+ offset = offset | 0
+ if (!noAssert) checkInt(this, value, offset, 4, 0xffffffff, 0)
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ this[offset] = (value >>> 24)
+ this[offset + 1] = (value >>> 16)
+ this[offset + 2] = (value >>> 8)
+ this[offset + 3] = (value & 0xff)
+ } else {
+ objectWriteUInt32(this, value, offset, false)
+ }
+ return offset + 4
+}
+
+Buffer.prototype.writeIntLE = function writeIntLE (value, offset, byteLength, noAssert) {
+ value = +value
+ offset = offset | 0
+ if (!noAssert) {
+ var limit = Math.pow(2, 8 * byteLength - 1)
+
+ checkInt(this, value, offset, byteLength, limit - 1, -limit)
+ }
+
+ var i = 0
+ var mul = 1
+ var sub = 0
+ this[offset] = value & 0xFF
+ while (++i < byteLength && (mul *= 0x100)) {
+ if (value < 0 && sub === 0 && this[offset + i - 1] !== 0) {
+ sub = 1
+ }
+ this[offset + i] = ((value / mul) >> 0) - sub & 0xFF
+ }
+
+ return offset + byteLength
+}
+
+Buffer.prototype.writeIntBE = function writeIntBE (value, offset, byteLength, noAssert) {
+ value = +value
+ offset = offset | 0
+ if (!noAssert) {
+ var limit = Math.pow(2, 8 * byteLength - 1)
+
+ checkInt(this, value, offset, byteLength, limit - 1, -limit)
+ }
+
+ var i = byteLength - 1
+ var mul = 1
+ var sub = 0
+ this[offset + i] = value & 0xFF
+ while (--i >= 0 && (mul *= 0x100)) {
+ if (value < 0 && sub === 0 && this[offset + i + 1] !== 0) {
+ sub = 1
+ }
+ this[offset + i] = ((value / mul) >> 0) - sub & 0xFF
+ }
+
+ return offset + byteLength
+}
+
+Buffer.prototype.writeInt8 = function writeInt8 (value, offset, noAssert) {
+ value = +value
+ offset = offset | 0
+ if (!noAssert) checkInt(this, value, offset, 1, 0x7f, -0x80)
+ if (!Buffer.TYPED_ARRAY_SUPPORT) value = Math.floor(value)
+ if (value < 0) value = 0xff + value + 1
+ this[offset] = (value & 0xff)
+ return offset + 1
+}
+
+Buffer.prototype.writeInt16LE = function writeInt16LE (value, offset, noAssert) {
+ value = +value
+ offset = offset | 0
+ if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000)
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ this[offset] = (value & 0xff)
+ this[offset + 1] = (value >>> 8)
+ } else {
+ objectWriteUInt16(this, value, offset, true)
+ }
+ return offset + 2
+}
+
+Buffer.prototype.writeInt16BE = function writeInt16BE (value, offset, noAssert) {
+ value = +value
+ offset = offset | 0
+ if (!noAssert) checkInt(this, value, offset, 2, 0x7fff, -0x8000)
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ this[offset] = (value >>> 8)
+ this[offset + 1] = (value & 0xff)
+ } else {
+ objectWriteUInt16(this, value, offset, false)
+ }
+ return offset + 2
+}
+
+Buffer.prototype.writeInt32LE = function writeInt32LE (value, offset, noAssert) {
+ value = +value
+ offset = offset | 0
+ if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000)
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ this[offset] = (value & 0xff)
+ this[offset + 1] = (value >>> 8)
+ this[offset + 2] = (value >>> 16)
+ this[offset + 3] = (value >>> 24)
+ } else {
+ objectWriteUInt32(this, value, offset, true)
+ }
+ return offset + 4
+}
+
+Buffer.prototype.writeInt32BE = function writeInt32BE (value, offset, noAssert) {
+ value = +value
+ offset = offset | 0
+ if (!noAssert) checkInt(this, value, offset, 4, 0x7fffffff, -0x80000000)
+ if (value < 0) value = 0xffffffff + value + 1
+ if (Buffer.TYPED_ARRAY_SUPPORT) {
+ this[offset] = (value >>> 24)
+ this[offset + 1] = (value >>> 16)
+ this[offset + 2] = (value >>> 8)
+ this[offset + 3] = (value & 0xff)
+ } else {
+ objectWriteUInt32(this, value, offset, false)
+ }
+ return offset + 4
+}
+
+function checkIEEE754 (buf, value, offset, ext, max, min) {
+ if (offset + ext > buf.length) throw new RangeError('Index out of range')
+ if (offset < 0) throw new RangeError('Index out of range')
+}
+
+function writeFloat (buf, value, offset, littleEndian, noAssert) {
+ if (!noAssert) {
+ checkIEEE754(buf, value, offset, 4, 3.4028234663852886e+38, -3.4028234663852886e+38)
+ }
+ ieee754.write(buf, value, offset, littleEndian, 23, 4)
+ return offset + 4
+}
+
+Buffer.prototype.writeFloatLE = function writeFloatLE (value, offset, noAssert) {
+ return writeFloat(this, value, offset, true, noAssert)
+}
+
+Buffer.prototype.writeFloatBE = function writeFloatBE (value, offset, noAssert) {
+ return writeFloat(this, value, offset, false, noAssert)
+}
+
+function writeDouble (buf, value, offset, littleEndian, noAssert) {
+ if (!noAssert) {
+ checkIEEE754(buf, value, offset, 8, 1.7976931348623157E+308, -1.7976931348623157E+308)
+ }
+ ieee754.write(buf, value, offset, littleEndian, 52, 8)
+ return offset + 8
+}
+
+Buffer.prototype.writeDoubleLE = function writeDoubleLE (value, offset, noAssert) {
+ return writeDouble(this, value, offset, true, noAssert)
+}
+
+Buffer.prototype.writeDoubleBE = function writeDoubleBE (value, offset, noAssert) {
+ return writeDouble(this, value, offset, false, noAssert)
+}
+
+// copy(targetBuffer, targetStart=0, sourceStart=0, sourceEnd=buffer.length)
+Buffer.prototype.copy = function copy (target, targetStart, start, end) {
+ if (!start) start = 0
+ if (!end && end !== 0) end = this.length
+ if (targetStart >= target.length) targetStart = target.length
+ if (!targetStart) targetStart = 0
+ if (end > 0 && end < start) end = start
+
+ // Copy 0 bytes; we're done
+ if (end === start) return 0
+ if (target.length === 0 || this.length === 0) return 0
+
+ // Fatal error conditions
+ if (targetStart < 0) {
+ throw new RangeError('targetStart out of bounds')
+ }
+ if (start < 0 || start >= this.length) throw new RangeError('sourceStart out of bounds')
+ if (end < 0) throw new RangeError('sourceEnd out of bounds')
+
+ // Are we oob?
+ if (end > this.length) end = this.length
+ if (target.length - targetStart < end - start) {
+ end = target.length - targetStart + start
+ }
+
+ var len = end - start
+ var i
+
+ if (this === target && start < targetStart && targetStart < end) {
+ // descending copy from end
+ for (i = len - 1; i >= 0; --i) {
+ target[i + targetStart] = this[i + start]
+ }
+ } else if (len < 1000 || !Buffer.TYPED_ARRAY_SUPPORT) {
+ // ascending copy from start
+ for (i = 0; i < len; ++i) {
+ target[i + targetStart] = this[i + start]
+ }
+ } else {
+ Uint8Array.prototype.set.call(
+ target,
+ this.subarray(start, start + len),
+ targetStart
+ )
+ }
+
+ return len
+}
+
+// Usage:
+// buffer.fill(number[, offset[, end]])
+// buffer.fill(buffer[, offset[, end]])
+// buffer.fill(string[, offset[, end]][, encoding])
+Buffer.prototype.fill = function fill (val, start, end, encoding) {
+ // Handle string cases:
+ if (typeof val === 'string') {
+ if (typeof start === 'string') {
+ encoding = start
+ start = 0
+ end = this.length
+ } else if (typeof end === 'string') {
+ encoding = end
+ end = this.length
+ }
+ if (val.length === 1) {
+ var code = val.charCodeAt(0)
+ if (code < 256) {
+ val = code
+ }
+ }
+ if (encoding !== undefined && typeof encoding !== 'string') {
+ throw new TypeError('encoding must be a string')
+ }
+ if (typeof encoding === 'string' && !Buffer.isEncoding(encoding)) {
+ throw new TypeError('Unknown encoding: ' + encoding)
+ }
+ } else if (typeof val === 'number') {
+ val = val & 255
+ }
+
+ // Invalid ranges are not set to a default, so can range check early.
+ if (start < 0 || this.length < start || this.length < end) {
+ throw new RangeError('Out of range index')
+ }
+
+ if (end <= start) {
+ return this
+ }
+
+ start = start >>> 0
+ end = end === undefined ? this.length : end >>> 0
+
+ if (!val) val = 0
+
+ var i
+ if (typeof val === 'number') {
+ for (i = start; i < end; ++i) {
+ this[i] = val
+ }
+ } else {
+ var bytes = Buffer.isBuffer(val)
+ ? val
+ : utf8ToBytes(new Buffer(val, encoding).toString())
+ var len = bytes.length
+ for (i = 0; i < end - start; ++i) {
+ this[i + start] = bytes[i % len]
+ }
+ }
+
+ return this
+}
+
+// HELPER FUNCTIONS
+// ================
+
+var INVALID_BASE64_RE = /[^+\/0-9A-Za-z-_]/g
+
+function base64clean (str) {
+ // Node strips out invalid characters like \n and \t from the string, base64-js does not
+ str = stringtrim(str).replace(INVALID_BASE64_RE, '')
+ // Node converts strings with length < 2 to ''
+ if (str.length < 2) return ''
+ // Node allows for non-padded base64 strings (missing trailing ===), base64-js does not
+ while (str.length % 4 !== 0) {
+ str = str + '='
+ }
+ return str
+}
+
+function stringtrim (str) {
+ if (str.trim) return str.trim()
+ return str.replace(/^\s+|\s+$/g, '')
+}
+
+function toHex (n) {
+ if (n < 16) return '0' + n.toString(16)
+ return n.toString(16)
+}
+
+function utf8ToBytes (string, units) {
+ units = units || Infinity
+ var codePoint
+ var length = string.length
+ var leadSurrogate = null
+ var bytes = []
+
+ for (var i = 0; i < length; ++i) {
+ codePoint = string.charCodeAt(i)
+
+ // is surrogate component
+ if (codePoint > 0xD7FF && codePoint < 0xE000) {
+ // last char was a lead
+ if (!leadSurrogate) {
+ // no lead yet
+ if (codePoint > 0xDBFF) {
+ // unexpected trail
+ if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD)
+ continue
+ } else if (i + 1 === length) {
+ // unpaired lead
+ if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD)
+ continue
+ }
+
+ // valid lead
+ leadSurrogate = codePoint
+
+ continue
+ }
+
+ // 2 leads in a row
+ if (codePoint < 0xDC00) {
+ if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD)
+ leadSurrogate = codePoint
+ continue
+ }
+
+ // valid surrogate pair
+ codePoint = (leadSurrogate - 0xD800 << 10 | codePoint - 0xDC00) + 0x10000
+ } else if (leadSurrogate) {
+ // valid bmp char, but last char was a lead
+ if ((units -= 3) > -1) bytes.push(0xEF, 0xBF, 0xBD)
+ }
+
+ leadSurrogate = null
+
+ // encode utf8
+ if (codePoint < 0x80) {
+ if ((units -= 1) < 0) break
+ bytes.push(codePoint)
+ } else if (codePoint < 0x800) {
+ if ((units -= 2) < 0) break
+ bytes.push(
+ codePoint >> 0x6 | 0xC0,
+ codePoint & 0x3F | 0x80
+ )
+ } else if (codePoint < 0x10000) {
+ if ((units -= 3) < 0) break
+ bytes.push(
+ codePoint >> 0xC | 0xE0,
+ codePoint >> 0x6 & 0x3F | 0x80,
+ codePoint & 0x3F | 0x80
+ )
+ } else if (codePoint < 0x110000) {
+ if ((units -= 4) < 0) break
+ bytes.push(
+ codePoint >> 0x12 | 0xF0,
+ codePoint >> 0xC & 0x3F | 0x80,
+ codePoint >> 0x6 & 0x3F | 0x80,
+ codePoint & 0x3F | 0x80
+ )
+ } else {
+ throw new Error('Invalid code point')
+ }
+ }
+
+ return bytes
+}
+
+function asciiToBytes (str) {
+ var byteArray = []
+ for (var i = 0; i < str.length; ++i) {
+ // Node's code seems to be doing this and not & 0x7F..
+ byteArray.push(str.charCodeAt(i) & 0xFF)
+ }
+ return byteArray
+}
+
+function utf16leToBytes (str, units) {
+ var c, hi, lo
+ var byteArray = []
+ for (var i = 0; i < str.length; ++i) {
+ if ((units -= 2) < 0) break
+
+ c = str.charCodeAt(i)
+ hi = c >> 8
+ lo = c % 256
+ byteArray.push(lo)
+ byteArray.push(hi)
+ }
+
+ return byteArray
+}
+
+function base64ToBytes (str) {
+ return base64.toByteArray(base64clean(str))
+}
+
+function blitBuffer (src, dst, offset, length) {
+ for (var i = 0; i < length; ++i) {
+ if ((i + offset >= dst.length) || (i >= src.length)) break
+ dst[i + offset] = src[i]
+ }
+ return i
+}
+
+function isnan (val) {
+ return val !== val // eslint-disable-line no-self-compare
+}
+
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
+
+/***/ }),
+/* 19 */
+/***/ (function(module, exports, __webpack_require__) {
+
+exports = module.exports = __webpack_require__(1)(undefined);
+// imports
+
+
+// module
+exports.push([module.i, "\n.cell[data-v-3ac4c361] {\n flex-direction: column;\n}\n", ""]);
+
+// exports
+
+
+/***/ }),
+/* 20 */
+/***/ (function(module, exports, __webpack_require__) {
+
+exports = module.exports = __webpack_require__(1)(undefined);
+// imports
+
+
+// module
+exports.push([module.i, "\n.cell,\n.input,\n.output {\n display: flex;\n width: 100%;\n margin-bottom: 10px;\n}\n.cell pre {\n margin: 0;\n width: 100%;\n}\n", ""]);
+
+// exports
+
+
+/***/ }),
+/* 21 */
+/***/ (function(module, exports, __webpack_require__) {
+
+exports = module.exports = __webpack_require__(1)(undefined);
+// imports
+
+
+// module
+exports.push([module.i, "\n.prompt[data-v-4f6bf458] {\n padding: 0 10px;\n min-width: 7em;\n font-family: monospace;\n}\n", ""]);
+
+// exports
+
+
+/***/ }),
+/* 22 */
+/***/ (function(module, exports, __webpack_require__) {
+
+exports = module.exports = __webpack_require__(1)(undefined);
+// imports
+
+
+// module
+exports.push([module.i, "\n.markdown .katex {\n display: block;\n text-align: center;\n}\n", ""]);
+
+// exports
+
+
+/***/ }),
+/* 23 */
+/***/ (function(module, exports) {
+
+exports.read = function (buffer, offset, isLE, mLen, nBytes) {
+ var e, m
+ var eLen = nBytes * 8 - mLen - 1
+ var eMax = (1 << eLen) - 1
+ var eBias = eMax >> 1
+ var nBits = -7
+ var i = isLE ? (nBytes - 1) : 0
+ var d = isLE ? -1 : 1
+ var s = buffer[offset + i]
+
+ i += d
+
+ e = s & ((1 << (-nBits)) - 1)
+ s >>= (-nBits)
+ nBits += eLen
+ for (; nBits > 0; e = e * 256 + buffer[offset + i], i += d, nBits -= 8) {}
+
+ m = e & ((1 << (-nBits)) - 1)
+ e >>= (-nBits)
+ nBits += mLen
+ for (; nBits > 0; m = m * 256 + buffer[offset + i], i += d, nBits -= 8) {}
+
+ if (e === 0) {
+ e = 1 - eBias
+ } else if (e === eMax) {
+ return m ? NaN : ((s ? -1 : 1) * Infinity)
+ } else {
+ m = m + Math.pow(2, mLen)
+ e = e - eBias
+ }
+ return (s ? -1 : 1) * m * Math.pow(2, e - mLen)
+}
+
+exports.write = function (buffer, value, offset, isLE, mLen, nBytes) {
+ var e, m, c
+ var eLen = nBytes * 8 - mLen - 1
+ var eMax = (1 << eLen) - 1
+ var eBias = eMax >> 1
+ var rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0)
+ var i = isLE ? 0 : (nBytes - 1)
+ var d = isLE ? 1 : -1
+ var s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0
+
+ value = Math.abs(value)
+
+ if (isNaN(value) || value === Infinity) {
+ m = isNaN(value) ? 1 : 0
+ e = eMax
+ } else {
+ e = Math.floor(Math.log(value) / Math.LN2)
+ if (value * (c = Math.pow(2, -e)) < 1) {
+ e--
+ c *= 2
+ }
+ if (e + eBias >= 1) {
+ value += rt / c
+ } else {
+ value += rt * Math.pow(2, 1 - eBias)
+ }
+ if (value * c >= 2) {
+ e++
+ c /= 2
+ }
+
+ if (e + eBias >= eMax) {
+ m = 0
+ e = eMax
+ } else if (e + eBias >= 1) {
+ m = (value * c - 1) * Math.pow(2, mLen)
+ e = e + eBias
+ } else {
+ m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen)
+ e = 0
+ }
+ }
+
+ for (; mLen >= 8; buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8) {}
+
+ e = (e << mLen) | m
+ eLen += mLen
+ for (; eLen > 0; buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8) {}
+
+ buffer[offset + i - d] |= s * 128
+}
+
+
+/***/ }),
+/* 24 */
+/***/ (function(module, exports) {
+
+var toString = {}.toString;
+
+module.exports = Array.isArray || function (arr) {
+ return toString.call(arr) == '[object Array]';
+};
+
+
+/***/ }),
+/* 25 */
+/***/ (function(module, exports, __webpack_require__) {
+
+/* WEBPACK VAR INJECTION */(function(global) {/**
+ * marked - a markdown parser
+ * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed)
+ * https://github.com/chjj/marked
+ */
+
+;(function() {
+
+/**
+ * Block-Level Grammar
+ */
+
+var block = {
+ newline: /^\n+/,
+ code: /^( {4}[^\n]+\n*)+/,
+ fences: noop,
+ hr: /^( *[-*_]){3,} *(?:\n+|$)/,
+ heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,
+ nptable: noop,
+ lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/,
+ blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/,
+ list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,
+ html: /^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/,
+ def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,
+ table: noop,
+ paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,
+ text: /^[^\n]+/
+};
+
+block.bullet = /(?:[*+-]|\d+\.)/;
+block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/;
+block.item = replace(block.item, 'gm')
+ (/bull/g, block.bullet)
+ ();
+
+block.list = replace(block.list)
+ (/bull/g, block.bullet)
+ ('hr', '\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))')
+ ('def', '\\n+(?=' + block.def.source + ')')
+ ();
+
+block.blockquote = replace(block.blockquote)
+ ('def', block.def)
+ ();
+
+block._tag = '(?!(?:'
+ + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code'
+ + '|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo'
+ + '|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b';
+
+block.html = replace(block.html)
+ ('comment', /<!--[\s\S]*?-->/)
+ ('closed', /<(tag)[\s\S]+?<\/\1>/)
+ ('closing', /<tag(?:"[^"]*"|'[^']*'|[^'">])*?>/)
+ (/tag/g, block._tag)
+ ();
+
+block.paragraph = replace(block.paragraph)
+ ('hr', block.hr)
+ ('heading', block.heading)
+ ('lheading', block.lheading)
+ ('blockquote', block.blockquote)
+ ('tag', '<' + block._tag)
+ ('def', block.def)
+ ();
+
+/**
+ * Normal Block Grammar
+ */
+
+block.normal = merge({}, block);
+
+/**
+ * GFM Block Grammar
+ */
+
+block.gfm = merge({}, block.normal, {
+ fences: /^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/,
+ paragraph: /^/,
+ heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/
+});
+
+block.gfm.paragraph = replace(block.paragraph)
+ ('(?!', '(?!'
+ + block.gfm.fences.source.replace('\\1', '\\2') + '|'
+ + block.list.source.replace('\\1', '\\3') + '|')
+ ();
+
+/**
+ * GFM + Tables Block Grammar
+ */
+
+block.tables = merge({}, block.gfm, {
+ nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,
+ table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/
+});
+
+/**
+ * Block Lexer
+ */
+
+function Lexer(options) {
+ this.tokens = [];
+ this.tokens.links = {};
+ this.options = options || marked.defaults;
+ this.rules = block.normal;
+
+ if (this.options.gfm) {
+ if (this.options.tables) {
+ this.rules = block.tables;
+ } else {
+ this.rules = block.gfm;
+ }
+ }
+}
+
+/**
+ * Expose Block Rules
+ */
+
+Lexer.rules = block;
+
+/**
+ * Static Lex Method
+ */
+
+Lexer.lex = function(src, options) {
+ var lexer = new Lexer(options);
+ return lexer.lex(src);
+};
+
+/**
+ * Preprocessing
+ */
+
+Lexer.prototype.lex = function(src) {
+ src = src
+ .replace(/\r\n|\r/g, '\n')
+ .replace(/\t/g, ' ')
+ .replace(/\u00a0/g, ' ')
+ .replace(/\u2424/g, '\n');
+
+ return this.token(src, true);
+};
+
+/**
+ * Lexing
+ */
+
+Lexer.prototype.token = function(src, top, bq) {
+ var src = src.replace(/^ +$/gm, '')
+ , next
+ , loose
+ , cap
+ , bull
+ , b
+ , item
+ , space
+ , i
+ , l;
+
+ while (src) {
+ // newline
+ if (cap = this.rules.newline.exec(src)) {
+ src = src.substring(cap[0].length);
+ if (cap[0].length > 1) {
+ this.tokens.push({
+ type: 'space'
+ });
+ }
+ }
+
+ // code
+ if (cap = this.rules.code.exec(src)) {
+ src = src.substring(cap[0].length);
+ cap = cap[0].replace(/^ {4}/gm, '');
+ this.tokens.push({
+ type: 'code',
+ text: !this.options.pedantic
+ ? cap.replace(/\n+$/, '')
+ : cap
+ });
+ continue;
+ }
+
+ // fences (gfm)
+ if (cap = this.rules.fences.exec(src)) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'code',
+ lang: cap[2],
+ text: cap[3] || ''
+ });
+ continue;
+ }
+
+ // heading
+ if (cap = this.rules.heading.exec(src)) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'heading',
+ depth: cap[1].length,
+ text: cap[2]
+ });
+ continue;
+ }
+
+ // table no leading pipe (gfm)
+ if (top && (cap = this.rules.nptable.exec(src))) {
+ src = src.substring(cap[0].length);
+
+ item = {
+ type: 'table',
+ header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
+ align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
+ cells: cap[3].replace(/\n$/, '').split('\n')
+ };
+
+ for (i = 0; i < item.align.length; i++) {
+ if (/^ *-+: *$/.test(item.align[i])) {
+ item.align[i] = 'right';
+ } else if (/^ *:-+: *$/.test(item.align[i])) {
+ item.align[i] = 'center';
+ } else if (/^ *:-+ *$/.test(item.align[i])) {
+ item.align[i] = 'left';
+ } else {
+ item.align[i] = null;
+ }
+ }
+
+ for (i = 0; i < item.cells.length; i++) {
+ item.cells[i] = item.cells[i].split(/ *\| */);
+ }
+
+ this.tokens.push(item);
+
+ continue;
+ }
+
+ // lheading
+ if (cap = this.rules.lheading.exec(src)) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'heading',
+ depth: cap[2] === '=' ? 1 : 2,
+ text: cap[1]
+ });
+ continue;
+ }
+
+ // hr
+ if (cap = this.rules.hr.exec(src)) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'hr'
+ });
+ continue;
+ }
+
+ // blockquote
+ if (cap = this.rules.blockquote.exec(src)) {
+ src = src.substring(cap[0].length);
+
+ this.tokens.push({
+ type: 'blockquote_start'
+ });
+
+ cap = cap[0].replace(/^ *> ?/gm, '');
+
+ // Pass `top` to keep the current
+ // "toplevel" state. This is exactly
+ // how markdown.pl works.
+ this.token(cap, top, true);
+
+ this.tokens.push({
+ type: 'blockquote_end'
+ });
+
+ continue;
+ }
+
+ // list
+ if (cap = this.rules.list.exec(src)) {
+ src = src.substring(cap[0].length);
+ bull = cap[2];
+
+ this.tokens.push({
+ type: 'list_start',
+ ordered: bull.length > 1
+ });
+
+ // Get each top-level item.
+ cap = cap[0].match(this.rules.item);
+
+ next = false;
+ l = cap.length;
+ i = 0;
+
+ for (; i < l; i++) {
+ item = cap[i];
+
+ // Remove the list item's bullet
+ // so it is seen as the next token.
+ space = item.length;
+ item = item.replace(/^ *([*+-]|\d+\.) +/, '');
+
+ // Outdent whatever the
+ // list item contains. Hacky.
+ if (~item.indexOf('\n ')) {
+ space -= item.length;
+ item = !this.options.pedantic
+ ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '')
+ : item.replace(/^ {1,4}/gm, '');
+ }
+
+ // Determine whether the next list item belongs here.
+ // Backpedal if it does not belong in this list.
+ if (this.options.smartLists && i !== l - 1) {
+ b = block.bullet.exec(cap[i + 1])[0];
+ if (bull !== b && !(bull.length > 1 && b.length > 1)) {
+ src = cap.slice(i + 1).join('\n') + src;
+ i = l - 1;
+ }
+ }
+
+ // Determine whether item is loose or not.
+ // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/
+ // for discount behavior.
+ loose = next || /\n\n(?!\s*$)/.test(item);
+ if (i !== l - 1) {
+ next = item.charAt(item.length - 1) === '\n';
+ if (!loose) loose = next;
+ }
+
+ this.tokens.push({
+ type: loose
+ ? 'loose_item_start'
+ : 'list_item_start'
+ });
+
+ // Recurse.
+ this.token(item, false, bq);
+
+ this.tokens.push({
+ type: 'list_item_end'
+ });
+ }
+
+ this.tokens.push({
+ type: 'list_end'
+ });
+
+ continue;
+ }
+
+ // html
+ if (cap = this.rules.html.exec(src)) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: this.options.sanitize
+ ? 'paragraph'
+ : 'html',
+ pre: !this.options.sanitizer
+ && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'),
+ text: cap[0]
+ });
+ continue;
+ }
+
+ // def
+ if ((!bq && top) && (cap = this.rules.def.exec(src))) {
+ src = src.substring(cap[0].length);
+ this.tokens.links[cap[1].toLowerCase()] = {
+ href: cap[2],
+ title: cap[3]
+ };
+ continue;
+ }
+
+ // table (gfm)
+ if (top && (cap = this.rules.table.exec(src))) {
+ src = src.substring(cap[0].length);
+
+ item = {
+ type: 'table',
+ header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
+ align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
+ cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n')
+ };
+
+ for (i = 0; i < item.align.length; i++) {
+ if (/^ *-+: *$/.test(item.align[i])) {
+ item.align[i] = 'right';
+ } else if (/^ *:-+: *$/.test(item.align[i])) {
+ item.align[i] = 'center';
+ } else if (/^ *:-+ *$/.test(item.align[i])) {
+ item.align[i] = 'left';
+ } else {
+ item.align[i] = null;
+ }
+ }
+
+ for (i = 0; i < item.cells.length; i++) {
+ item.cells[i] = item.cells[i]
+ .replace(/^ *\| *| *\| *$/g, '')
+ .split(/ *\| */);
+ }
+
+ this.tokens.push(item);
+
+ continue;
+ }
+
+ // top-level paragraph
+ if (top && (cap = this.rules.paragraph.exec(src))) {
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'paragraph',
+ text: cap[1].charAt(cap[1].length - 1) === '\n'
+ ? cap[1].slice(0, -1)
+ : cap[1]
+ });
+ continue;
+ }
+
+ // text
+ if (cap = this.rules.text.exec(src)) {
+ // Top-level should never reach here.
+ src = src.substring(cap[0].length);
+ this.tokens.push({
+ type: 'text',
+ text: cap[0]
+ });
+ continue;
+ }
+
+ if (src) {
+ throw new
+ Error('Infinite loop on byte: ' + src.charCodeAt(0));
+ }
+ }
+
+ return this.tokens;
+};
+
+/**
+ * Inline-Level Grammar
+ */
+
+var inline = {
+ escape: /^\\([\\`*{}\[\]()#+\-.!_>])/,
+ autolink: /^<([^ >]+(@|:\/)[^ >]+)>/,
+ url: noop,
+ tag: /^<!--[\s\S]*?-->|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,
+ link: /^!?\[(inside)\]\(href\)/,
+ reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/,
+ nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,
+ strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,
+ em: /^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,
+ code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,
+ br: /^ {2,}\n(?!\s*$)/,
+ del: noop,
+ text: /^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n|$)/
+};
+
+inline._inside = /(?:\[[^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*/;
+inline._href = /\s*<?([\s\S]*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*/;
+
+inline.link = replace(inline.link)
+ ('inside', inline._inside)
+ ('href', inline._href)
+ ();
+
+inline.reflink = replace(inline.reflink)
+ ('inside', inline._inside)
+ ();
+
+/**
+ * Normal Inline Grammar
+ */
+
+inline.normal = merge({}, inline);
+
+/**
+ * Pedantic Inline Grammar
+ */
+
+inline.pedantic = merge({}, inline.normal, {
+ strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,
+ em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/
+});
+
+/**
+ * GFM Inline Grammar
+ */
+
+inline.gfm = merge({}, inline.normal, {
+ escape: replace(inline.escape)('])', '~|])')(),
+ url: /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,
+ del: /^~~(?=\S)([\s\S]*?\S)~~/,
+ text: replace(inline.text)
+ (']|', '~]|')
+ ('|', '|https?://|')
+ ()
+});
+
+/**
+ * GFM + Line Breaks Inline Grammar
+ */
+
+inline.breaks = merge({}, inline.gfm, {
+ br: replace(inline.br)('{2,}', '*')(),
+ text: replace(inline.gfm.text)('{2,}', '*')()
+});
+
+/**
+ * Inline Lexer & Compiler
+ */
+
+function InlineLexer(links, options) {
+ this.options = options || marked.defaults;
+ this.links = links;
+ this.rules = inline.normal;
+ this.renderer = this.options.renderer || new Renderer;
+ this.renderer.options = this.options;
+
+ if (!this.links) {
+ throw new
+ Error('Tokens array requires a `links` property.');
+ }
+
+ if (this.options.gfm) {
+ if (this.options.breaks) {
+ this.rules = inline.breaks;
+ } else {
+ this.rules = inline.gfm;
+ }
+ } else if (this.options.pedantic) {
+ this.rules = inline.pedantic;
+ }
+}
+
+/**
+ * Expose Inline Rules
+ */
+
+InlineLexer.rules = inline;
+
+/**
+ * Static Lexing/Compiling Method
+ */
+
+InlineLexer.output = function(src, links, options) {
+ var inline = new InlineLexer(links, options);
+ return inline.output(src);
+};
+
+/**
+ * Lexing/Compiling
+ */
+
+InlineLexer.prototype.output = function(src) {
+ var out = ''
+ , link
+ , text
+ , href
+ , cap;
+
+ while (src) {
+ // escape
+ if (cap = this.rules.escape.exec(src)) {
+ src = src.substring(cap[0].length);
+ out += cap[1];
+ continue;
+ }
+
+ // autolink
+ if (cap = this.rules.autolink.exec(src)) {
+ src = src.substring(cap[0].length);
+ if (cap[2] === '@') {
+ text = cap[1].charAt(6) === ':'
+ ? this.mangle(cap[1].substring(7))
+ : this.mangle(cap[1]);
+ href = this.mangle('mailto:') + text;
+ } else {
+ text = escape(cap[1]);
+ href = text;
+ }
+ out += this.renderer.link(href, null, text);
+ continue;
+ }
+
+ // url (gfm)
+ if (!this.inLink && (cap = this.rules.url.exec(src))) {
+ src = src.substring(cap[0].length);
+ text = escape(cap[1]);
+ href = text;
+ out += this.renderer.link(href, null, text);
+ continue;
+ }
+
+ // tag
+ if (cap = this.rules.tag.exec(src)) {
+ if (!this.inLink && /^<a /i.test(cap[0])) {
+ this.inLink = true;
+ } else if (this.inLink && /^<\/a>/i.test(cap[0])) {
+ this.inLink = false;
+ }
+ src = src.substring(cap[0].length);
+ out += this.options.sanitize
+ ? this.options.sanitizer
+ ? this.options.sanitizer(cap[0])
+ : escape(cap[0])
+ : cap[0]
+ continue;
+ }
+
+ // link
+ if (cap = this.rules.link.exec(src)) {
+ src = src.substring(cap[0].length);
+ this.inLink = true;
+ out += this.outputLink(cap, {
+ href: cap[2],
+ title: cap[3]
+ });
+ this.inLink = false;
+ continue;
+ }
+
+ // reflink, nolink
+ if ((cap = this.rules.reflink.exec(src))
+ || (cap = this.rules.nolink.exec(src))) {
+ src = src.substring(cap[0].length);
+ link = (cap[2] || cap[1]).replace(/\s+/g, ' ');
+ link = this.links[link.toLowerCase()];
+ if (!link || !link.href) {
+ out += cap[0].charAt(0);
+ src = cap[0].substring(1) + src;
+ continue;
+ }
+ this.inLink = true;
+ out += this.outputLink(cap, link);
+ this.inLink = false;
+ continue;
+ }
+
+ // strong
+ if (cap = this.rules.strong.exec(src)) {
+ src = src.substring(cap[0].length);
+ out += this.renderer.strong(this.output(cap[2] || cap[1]));
+ continue;
+ }
+
+ // em
+ if (cap = this.rules.em.exec(src)) {
+ src = src.substring(cap[0].length);
+ out += this.renderer.em(this.output(cap[2] || cap[1]));
+ continue;
+ }
+
+ // code
+ if (cap = this.rules.code.exec(src)) {
+ src = src.substring(cap[0].length);
+ out += this.renderer.codespan(escape(cap[2], true));
+ continue;
+ }
+
+ // br
+ if (cap = this.rules.br.exec(src)) {
+ src = src.substring(cap[0].length);
+ out += this.renderer.br();
+ continue;
+ }
+
+ // del (gfm)
+ if (cap = this.rules.del.exec(src)) {
+ src = src.substring(cap[0].length);
+ out += this.renderer.del(this.output(cap[1]));
+ continue;
+ }
+
+ // text
+ if (cap = this.rules.text.exec(src)) {
+ src = src.substring(cap[0].length);
+ out += this.renderer.text(escape(this.smartypants(cap[0])));
+ continue;
+ }
+
+ if (src) {
+ throw new
+ Error('Infinite loop on byte: ' + src.charCodeAt(0));
+ }
+ }
+
+ return out;
+};
+
+/**
+ * Compile Link
+ */
+
+InlineLexer.prototype.outputLink = function(cap, link) {
+ var href = escape(link.href)
+ , title = link.title ? escape(link.title) : null;
+
+ return cap[0].charAt(0) !== '!'
+ ? this.renderer.link(href, title, this.output(cap[1]))
+ : this.renderer.image(href, title, escape(cap[1]));
+};
+
+/**
+ * Smartypants Transformations
+ */
+
+InlineLexer.prototype.smartypants = function(text) {
+ if (!this.options.smartypants) return text;
+ return text
+ // em-dashes
+ .replace(/---/g, '\u2014')
+ // en-dashes
+ .replace(/--/g, '\u2013')
+ // opening singles
+ .replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018')
+ // closing singles & apostrophes
+ .replace(/'/g, '\u2019')
+ // opening doubles
+ .replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c')
+ // closing doubles
+ .replace(/"/g, '\u201d')
+ // ellipses
+ .replace(/\.{3}/g, '\u2026');
+};
+
+/**
+ * Mangle Links
+ */
+
+InlineLexer.prototype.mangle = function(text) {
+ if (!this.options.mangle) return text;
+ var out = ''
+ , l = text.length
+ , i = 0
+ , ch;
+
+ for (; i < l; i++) {
+ ch = text.charCodeAt(i);
+ if (Math.random() > 0.5) {
+ ch = 'x' + ch.toString(16);
+ }
+ out += '&#' + ch + ';';
+ }
+
+ return out;
+};
+
+/**
+ * Renderer
+ */
+
+function Renderer(options) {
+ this.options = options || {};
+}
+
+Renderer.prototype.code = function(code, lang, escaped) {
+ if (this.options.highlight) {
+ var out = this.options.highlight(code, lang);
+ if (out != null && out !== code) {
+ escaped = true;
+ code = out;
+ }
+ }
+
+ if (!lang) {
+ return '<pre><code>'
+ + (escaped ? code : escape(code, true))
+ + '\n</code></pre>';
+ }
+
+ return '<pre><code class="'
+ + this.options.langPrefix
+ + escape(lang, true)
+ + '">'
+ + (escaped ? code : escape(code, true))
+ + '\n</code></pre>\n';
+};
+
+Renderer.prototype.blockquote = function(quote) {
+ return '<blockquote>\n' + quote + '</blockquote>\n';
+};
+
+Renderer.prototype.html = function(html) {
+ return html;
+};
+
+Renderer.prototype.heading = function(text, level, raw) {
+ return '<h'
+ + level
+ + ' id="'
+ + this.options.headerPrefix
+ + raw.toLowerCase().replace(/[^\w]+/g, '-')
+ + '">'
+ + text
+ + '</h'
+ + level
+ + '>\n';
+};
+
+Renderer.prototype.hr = function() {
+ return this.options.xhtml ? '<hr/>\n' : '<hr>\n';
+};
+
+Renderer.prototype.list = function(body, ordered) {
+ var type = ordered ? 'ol' : 'ul';
+ return '<' + type + '>\n' + body + '</' + type + '>\n';
+};
+
+Renderer.prototype.listitem = function(text) {
+ return '<li>' + text + '</li>\n';
+};
+
+Renderer.prototype.paragraph = function(text) {
+ return '<p>' + text + '</p>\n';
+};
+
+Renderer.prototype.table = function(header, body) {
+ return '<table>\n'
+ + '<thead>\n'
+ + header
+ + '</thead>\n'
+ + '<tbody>\n'
+ + body
+ + '</tbody>\n'
+ + '</table>\n';
+};
+
+Renderer.prototype.tablerow = function(content) {
+ return '<tr>\n' + content + '</tr>\n';
+};
+
+Renderer.prototype.tablecell = function(content, flags) {
+ var type = flags.header ? 'th' : 'td';
+ var tag = flags.align
+ ? '<' + type + ' style="text-align:' + flags.align + '">'
+ : '<' + type + '>';
+ return tag + content + '</' + type + '>\n';
+};
+
+// span level renderer
+Renderer.prototype.strong = function(text) {
+ return '<strong>' + text + '</strong>';
+};
+
+Renderer.prototype.em = function(text) {
+ return '<em>' + text + '</em>';
+};
+
+Renderer.prototype.codespan = function(text) {
+ return '<code>' + text + '</code>';
+};
+
+Renderer.prototype.br = function() {
+ return this.options.xhtml ? '<br/>' : '<br>';
+};
+
+Renderer.prototype.del = function(text) {
+ return '<del>' + text + '</del>';
+};
+
+Renderer.prototype.link = function(href, title, text) {
+ if (this.options.sanitize) {
+ try {
+ var prot = decodeURIComponent(unescape(href))
+ .replace(/[^\w:]/g, '')
+ .toLowerCase();
+ } catch (e) {
+ return '';
+ }
+ if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0) {
+ return '';
+ }
+ }
+ var out = '<a href="' + href + '"';
+ if (title) {
+ out += ' title="' + title + '"';
+ }
+ out += '>' + text + '</a>';
+ return out;
+};
+
+Renderer.prototype.image = function(href, title, text) {
+ var out = '<img src="' + href + '" alt="' + text + '"';
+ if (title) {
+ out += ' title="' + title + '"';
+ }
+ out += this.options.xhtml ? '/>' : '>';
+ return out;
+};
+
+Renderer.prototype.text = function(text) {
+ return text;
+};
+
+/**
+ * Parsing & Compiling
+ */
+
+function Parser(options) {
+ this.tokens = [];
+ this.token = null;
+ this.options = options || marked.defaults;
+ this.options.renderer = this.options.renderer || new Renderer;
+ this.renderer = this.options.renderer;
+ this.renderer.options = this.options;
+}
+
+/**
+ * Static Parse Method
+ */
+
+Parser.parse = function(src, options, renderer) {
+ var parser = new Parser(options, renderer);
+ return parser.parse(src);
+};
+
+/**
+ * Parse Loop
+ */
+
+Parser.prototype.parse = function(src) {
+ this.inline = new InlineLexer(src.links, this.options, this.renderer);
+ this.tokens = src.reverse();
+
+ var out = '';
+ while (this.next()) {
+ out += this.tok();
+ }
+
+ return out;
+};
+
+/**
+ * Next Token
+ */
+
+Parser.prototype.next = function() {
+ return this.token = this.tokens.pop();
+};
+
+/**
+ * Preview Next Token
+ */
+
+Parser.prototype.peek = function() {
+ return this.tokens[this.tokens.length - 1] || 0;
+};
+
+/**
+ * Parse Text Tokens
+ */
+
+Parser.prototype.parseText = function() {
+ var body = this.token.text;
+
+ while (this.peek().type === 'text') {
+ body += '\n' + this.next().text;
+ }
+
+ return this.inline.output(body);
+};
+
+/**
+ * Parse Current Token
+ */
+
+Parser.prototype.tok = function() {
+ switch (this.token.type) {
+ case 'space': {
+ return '';
+ }
+ case 'hr': {
+ return this.renderer.hr();
+ }
+ case 'heading': {
+ return this.renderer.heading(
+ this.inline.output(this.token.text),
+ this.token.depth,
+ this.token.text);
+ }
+ case 'code': {
+ return this.renderer.code(this.token.text,
+ this.token.lang,
+ this.token.escaped);
+ }
+ case 'table': {
+ var header = ''
+ , body = ''
+ , i
+ , row
+ , cell
+ , flags
+ , j;
+
+ // header
+ cell = '';
+ for (i = 0; i < this.token.header.length; i++) {
+ flags = { header: true, align: this.token.align[i] };
+ cell += this.renderer.tablecell(
+ this.inline.output(this.token.header[i]),
+ { header: true, align: this.token.align[i] }
+ );
+ }
+ header += this.renderer.tablerow(cell);
+
+ for (i = 0; i < this.token.cells.length; i++) {
+ row = this.token.cells[i];
+
+ cell = '';
+ for (j = 0; j < row.length; j++) {
+ cell += this.renderer.tablecell(
+ this.inline.output(row[j]),
+ { header: false, align: this.token.align[j] }
+ );
+ }
+
+ body += this.renderer.tablerow(cell);
+ }
+ return this.renderer.table(header, body);
+ }
+ case 'blockquote_start': {
+ var body = '';
+
+ while (this.next().type !== 'blockquote_end') {
+ body += this.tok();
+ }
+
+ return this.renderer.blockquote(body);
+ }
+ case 'list_start': {
+ var body = ''
+ , ordered = this.token.ordered;
+
+ while (this.next().type !== 'list_end') {
+ body += this.tok();
+ }
+
+ return this.renderer.list(body, ordered);
+ }
+ case 'list_item_start': {
+ var body = '';
+
+ while (this.next().type !== 'list_item_end') {
+ body += this.token.type === 'text'
+ ? this.parseText()
+ : this.tok();
+ }
+
+ return this.renderer.listitem(body);
+ }
+ case 'loose_item_start': {
+ var body = '';
+
+ while (this.next().type !== 'list_item_end') {
+ body += this.tok();
+ }
+
+ return this.renderer.listitem(body);
+ }
+ case 'html': {
+ var html = !this.token.pre && !this.options.pedantic
+ ? this.inline.output(this.token.text)
+ : this.token.text;
+ return this.renderer.html(html);
+ }
+ case 'paragraph': {
+ return this.renderer.paragraph(this.inline.output(this.token.text));
+ }
+ case 'text': {
+ return this.renderer.paragraph(this.parseText());
+ }
+ }
+};
+
+/**
+ * Helpers
+ */
+
+function escape(html, encode) {
+ return html
+ .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/"/g, '&quot;')
+ .replace(/'/g, '&#39;');
+}
+
+function unescape(html) {
+ // explicitly match decimal, hex, and named HTML entities
+ return html.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/g, function(_, n) {
+ n = n.toLowerCase();
+ if (n === 'colon') return ':';
+ if (n.charAt(0) === '#') {
+ return n.charAt(1) === 'x'
+ ? String.fromCharCode(parseInt(n.substring(2), 16))
+ : String.fromCharCode(+n.substring(1));
+ }
+ return '';
+ });
+}
+
+function replace(regex, opt) {
+ regex = regex.source;
+ opt = opt || '';
+ return function self(name, val) {
+ if (!name) return new RegExp(regex, opt);
+ val = val.source || val;
+ val = val.replace(/(^|[^\[])\^/g, '$1');
+ regex = regex.replace(name, val);
+ return self;
+ };
+}
+
+function noop() {}
+noop.exec = noop;
+
+function merge(obj) {
+ var i = 1
+ , target
+ , key;
+
+ for (; i < arguments.length; i++) {
+ target = arguments[i];
+ for (key in target) {
+ if (Object.prototype.hasOwnProperty.call(target, key)) {
+ obj[key] = target[key];
+ }
+ }
+ }
+
+ return obj;
+}
+
+
+/**
+ * Marked
+ */
+
+function marked(src, opt, callback) {
+ if (callback || typeof opt === 'function') {
+ if (!callback) {
+ callback = opt;
+ opt = null;
+ }
+
+ opt = merge({}, marked.defaults, opt || {});
+
+ var highlight = opt.highlight
+ , tokens
+ , pending
+ , i = 0;
+
+ try {
+ tokens = Lexer.lex(src, opt)
+ } catch (e) {
+ return callback(e);
+ }
+
+ pending = tokens.length;
+
+ var done = function(err) {
+ if (err) {
+ opt.highlight = highlight;
+ return callback(err);
+ }
+
+ var out;
+
+ try {
+ out = Parser.parse(tokens, opt);
+ } catch (e) {
+ err = e;
+ }
+
+ opt.highlight = highlight;
+
+ return err
+ ? callback(err)
+ : callback(null, out);
+ };
+
+ if (!highlight || highlight.length < 3) {
+ return done();
+ }
+
+ delete opt.highlight;
+
+ if (!pending) return done();
+
+ for (; i < tokens.length; i++) {
+ (function(token) {
+ if (token.type !== 'code') {
+ return --pending || done();
+ }
+ return highlight(token.text, token.lang, function(err, code) {
+ if (err) return done(err);
+ if (code == null || code === token.text) {
+ return --pending || done();
+ }
+ token.text = code;
+ token.escaped = true;
+ --pending || done();
+ });
+ })(tokens[i]);
+ }
+
+ return;
+ }
+ try {
+ if (opt) opt = merge({}, marked.defaults, opt);
+ return Parser.parse(Lexer.lex(src, opt), opt);
+ } catch (e) {
+ e.message += '\nPlease report this to https://github.com/chjj/marked.';
+ if ((opt || marked.defaults).silent) {
+ return '<p>An error occured:</p><pre>'
+ + escape(e.message + '', true)
+ + '</pre>';
+ }
+ throw e;
+ }
+}
+
+/**
+ * Options
+ */
+
+marked.options =
+marked.setOptions = function(opt) {
+ merge(marked.defaults, opt);
+ return marked;
+};
+
+marked.defaults = {
+ gfm: true,
+ tables: true,
+ breaks: false,
+ pedantic: false,
+ sanitize: false,
+ sanitizer: null,
+ mangle: true,
+ smartLists: false,
+ silent: false,
+ highlight: null,
+ langPrefix: 'lang-',
+ smartypants: false,
+ headerPrefix: '',
+ renderer: new Renderer,
+ xhtml: false
+};
+
+/**
+ * Expose
+ */
+
+marked.Parser = Parser;
+marked.parser = Parser.parse;
+
+marked.Renderer = Renderer;
+
+marked.Lexer = Lexer;
+marked.lexer = Lexer.lex;
+
+marked.InlineLexer = InlineLexer;
+marked.inlineLexer = InlineLexer.output;
+
+marked.parse = marked;
+
+if (true) {
+ module.exports = marked;
+} else if (typeof define === 'function' && define.amd) {
+ define(function() { return marked; });
+} else {
+ this.marked = marked;
+}
+
+}).call(function() {
+ return this || (typeof window !== 'undefined' ? window : global);
+}());
+
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
+
+/***/ }),
+/* 26 */
+/***/ (function(module, exports) {
+
+Prism.languages.python= {
+ 'triple-quoted-string': {
+ pattern: /"""[\s\S]+?"""|'''[\s\S]+?'''/,
+ alias: 'string'
+ },
+ 'comment': {
+ pattern: /(^|[^\\])#.*/,
+ lookbehind: true
+ },
+ 'string': {
+ pattern: /("|')(?:\\\\|\\?[^\\\r\n])*?\1/,
+ greedy: true
+ },
+ 'function' : {
+ pattern: /((?:^|\s)def[ \t]+)[a-zA-Z_][a-zA-Z0-9_]*(?=\()/g,
+ lookbehind: true
+ },
+ 'class-name': {
+ pattern: /(\bclass\s+)[a-z0-9_]+/i,
+ lookbehind: true
+ },
+ 'keyword' : /\b(?:as|assert|async|await|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|pass|print|raise|return|try|while|with|yield)\b/,
+ 'boolean' : /\b(?:True|False)\b/,
+ 'number' : /\b-?(?:0[bo])?(?:(?:\d|0x[\da-f])[\da-f]*\.?\d*|\.\d+)(?:e[+-]?\d+)?j?\b/i,
+ 'operator' : /[-+%=]=?|!=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]|\b(?:or|and|not)\b/,
+ 'punctuation' : /[{}[\];(),.:]/
+};
+
+
+/***/ }),
+/* 27 */
+/***/ (function(module, exports, __webpack_require__) {
+
+/* WEBPACK VAR INJECTION */(function(global) {(function(){
+
+if (
+ (typeof self === 'undefined' || !self.Prism) &&
+ (typeof global === 'undefined' || !global.Prism)
+) {
+ return;
+}
+
+var options = {};
+Prism.plugins.customClass = {
+ map: function map(cm) {
+ options.classMap = cm;
+ },
+ prefix: function prefix(string) {
+ options.prefixString = string;
+ }
+}
+
+Prism.hooks.add('wrap', function (env) {
+ if (!options.classMap && !options.prefixString) {
+ return;
+ }
+ env.classes = env.classes.map(function(c) {
+ return (options.prefixString || '') + (options.classMap[c] || c);
+ });
+});
+
+})();
+
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
+
+/***/ }),
+/* 28 */
+/***/ (function(module, exports, __webpack_require__) {
+
+/* WEBPACK VAR INJECTION */(function(global) {
+/* **********************************************
+ Begin prism-core.js
+********************************************** */
+
+var _self = (typeof window !== 'undefined')
+ ? window // if in browser
+ : (
+ (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope)
+ ? self // if in worker
+ : {} // if in node js
+ );
+
+/**
+ * Prism: Lightweight, robust, elegant syntax highlighting
+ * MIT license http://www.opensource.org/licenses/mit-license.php/
+ * @author Lea Verou http://lea.verou.me
+ */
+
+var Prism = (function(){
+
+// Private helper vars
+var lang = /\blang(?:uage)?-(\w+)\b/i;
+var uniqueId = 0;
+
+var _ = _self.Prism = {
+ util: {
+ encode: function (tokens) {
+ if (tokens instanceof Token) {
+ return new Token(tokens.type, _.util.encode(tokens.content), tokens.alias);
+ } else if (_.util.type(tokens) === 'Array') {
+ return tokens.map(_.util.encode);
+ } else {
+ return tokens.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\u00a0/g, ' ');
+ }
+ },
+
+ type: function (o) {
+ return Object.prototype.toString.call(o).match(/\[object (\w+)\]/)[1];
+ },
+
+ objId: function (obj) {
+ if (!obj['__id']) {
+ Object.defineProperty(obj, '__id', { value: ++uniqueId });
+ }
+ return obj['__id'];
+ },
+
+ // Deep clone a language definition (e.g. to extend it)
+ clone: function (o) {
+ var type = _.util.type(o);
+
+ switch (type) {
+ case 'Object':
+ var clone = {};
+
+ for (var key in o) {
+ if (o.hasOwnProperty(key)) {
+ clone[key] = _.util.clone(o[key]);
+ }
+ }
+
+ return clone;
+
+ case 'Array':
+ // Check for existence for IE8
+ return o.map && o.map(function(v) { return _.util.clone(v); });
+ }
+
+ return o;
+ }
+ },
+
+ languages: {
+ extend: function (id, redef) {
+ var lang = _.util.clone(_.languages[id]);
+
+ for (var key in redef) {
+ lang[key] = redef[key];
+ }
+
+ return lang;
+ },
+
+ /**
+ * Insert a token before another token in a language literal
+ * As this needs to recreate the object (we cannot actually insert before keys in object literals),
+ * we cannot just provide an object, we need anobject and a key.
+ * @param inside The key (or language id) of the parent
+ * @param before The key to insert before. If not provided, the function appends instead.
+ * @param insert Object with the key/value pairs to insert
+ * @param root The object that contains `inside`. If equal to Prism.languages, it can be omitted.
+ */
+ insertBefore: function (inside, before, insert, root) {
+ root = root || _.languages;
+ var grammar = root[inside];
+
+ if (arguments.length == 2) {
+ insert = arguments[1];
+
+ for (var newToken in insert) {
+ if (insert.hasOwnProperty(newToken)) {
+ grammar[newToken] = insert[newToken];
+ }
+ }
+
+ return grammar;
+ }
+
+ var ret = {};
+
+ for (var token in grammar) {
+
+ if (grammar.hasOwnProperty(token)) {
+
+ if (token == before) {
+
+ for (var newToken in insert) {
+
+ if (insert.hasOwnProperty(newToken)) {
+ ret[newToken] = insert[newToken];
+ }
+ }
+ }
+
+ ret[token] = grammar[token];
+ }
+ }
+
+ // Update references in other language definitions
+ _.languages.DFS(_.languages, function(key, value) {
+ if (value === root[inside] && key != inside) {
+ this[key] = ret;
+ }
+ });
+
+ return root[inside] = ret;
+ },
+
+ // Traverse a language definition with Depth First Search
+ DFS: function(o, callback, type, visited) {
+ visited = visited || {};
+ for (var i in o) {
+ if (o.hasOwnProperty(i)) {
+ callback.call(o, i, o[i], type || i);
+
+ if (_.util.type(o[i]) === 'Object' && !visited[_.util.objId(o[i])]) {
+ visited[_.util.objId(o[i])] = true;
+ _.languages.DFS(o[i], callback, null, visited);
+ }
+ else if (_.util.type(o[i]) === 'Array' && !visited[_.util.objId(o[i])]) {
+ visited[_.util.objId(o[i])] = true;
+ _.languages.DFS(o[i], callback, i, visited);
+ }
+ }
+ }
+ }
+ },
+ plugins: {},
+
+ highlightAll: function(async, callback) {
+ var env = {
+ callback: callback,
+ selector: 'code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'
+ };
+
+ _.hooks.run("before-highlightall", env);
+
+ var elements = env.elements || document.querySelectorAll(env.selector);
+
+ for (var i=0, element; element = elements[i++];) {
+ _.highlightElement(element, async === true, env.callback);
+ }
+ },
+
+ highlightElement: function(element, async, callback) {
+ // Find language
+ var language, grammar, parent = element;
+
+ while (parent && !lang.test(parent.className)) {
+ parent = parent.parentNode;
+ }
+
+ if (parent) {
+ language = (parent.className.match(lang) || [,''])[1].toLowerCase();
+ grammar = _.languages[language];
+ }
+
+ // Set language on the element, if not present
+ element.className = element.className.replace(lang, '').replace(/\s+/g, ' ') + ' language-' + language;
+
+ // Set language on the parent, for styling
+ parent = element.parentNode;
+
+ if (/pre/i.test(parent.nodeName)) {
+ parent.className = parent.className.replace(lang, '').replace(/\s+/g, ' ') + ' language-' + language;
+ }
+
+ var code = element.textContent;
+
+ var env = {
+ element: element,
+ language: language,
+ grammar: grammar,
+ code: code
+ };
+
+ _.hooks.run('before-sanity-check', env);
+
+ if (!env.code || !env.grammar) {
+ if (env.code) {
+ env.element.textContent = env.code;
+ }
+ _.hooks.run('complete', env);
+ return;
+ }
+
+ _.hooks.run('before-highlight', env);
+
+ if (async && _self.Worker) {
+ var worker = new Worker(_.filename);
+
+ worker.onmessage = function(evt) {
+ env.highlightedCode = evt.data;
+
+ _.hooks.run('before-insert', env);
+
+ env.element.innerHTML = env.highlightedCode;
+
+ callback && callback.call(env.element);
+ _.hooks.run('after-highlight', env);
+ _.hooks.run('complete', env);
+ };
+
+ worker.postMessage(JSON.stringify({
+ language: env.language,
+ code: env.code,
+ immediateClose: true
+ }));
+ }
+ else {
+ env.highlightedCode = _.highlight(env.code, env.grammar, env.language);
+
+ _.hooks.run('before-insert', env);
+
+ env.element.innerHTML = env.highlightedCode;
+
+ callback && callback.call(element);
+
+ _.hooks.run('after-highlight', env);
+ _.hooks.run('complete', env);
+ }
+ },
+
+ highlight: function (text, grammar, language) {
+ var tokens = _.tokenize(text, grammar);
+ return Token.stringify(_.util.encode(tokens), language);
+ },
+
+ tokenize: function(text, grammar, language) {
+ var Token = _.Token;
+
+ var strarr = [text];
+
+ var rest = grammar.rest;
+
+ if (rest) {
+ for (var token in rest) {
+ grammar[token] = rest[token];
+ }
+
+ delete grammar.rest;
+ }
+
+ tokenloop: for (var token in grammar) {
+ if(!grammar.hasOwnProperty(token) || !grammar[token]) {
+ continue;
+ }
+
+ var patterns = grammar[token];
+ patterns = (_.util.type(patterns) === "Array") ? patterns : [patterns];
+
+ for (var j = 0; j < patterns.length; ++j) {
+ var pattern = patterns[j],
+ inside = pattern.inside,
+ lookbehind = !!pattern.lookbehind,
+ greedy = !!pattern.greedy,
+ lookbehindLength = 0,
+ alias = pattern.alias;
+
+ if (greedy && !pattern.pattern.global) {
+ // Without the global flag, lastIndex won't work
+ var flags = pattern.pattern.toString().match(/[imuy]*$/)[0];
+ pattern.pattern = RegExp(pattern.pattern.source, flags + "g");
+ }
+
+ pattern = pattern.pattern || pattern;
+
+ // Don’t cache length as it changes during the loop
+ for (var i=0, pos = 0; i<strarr.length; pos += strarr[i].length, ++i) {
+
+ var str = strarr[i];
+
+ if (strarr.length > text.length) {
+ // Something went terribly wrong, ABORT, ABORT!
+ break tokenloop;
+ }
+
+ if (str instanceof Token) {
+ continue;
+ }
+
+ pattern.lastIndex = 0;
+
+ var match = pattern.exec(str),
+ delNum = 1;
+
+ // Greedy patterns can override/remove up to two previously matched tokens
+ if (!match && greedy && i != strarr.length - 1) {
+ pattern.lastIndex = pos;
+ match = pattern.exec(text);
+ if (!match) {
+ break;
+ }
+
+ var from = match.index + (lookbehind ? match[1].length : 0),
+ to = match.index + match[0].length,
+ k = i,
+ p = pos;
+
+ for (var len = strarr.length; k < len && p < to; ++k) {
+ p += strarr[k].length;
+ // Move the index i to the element in strarr that is closest to from
+ if (from >= p) {
+ ++i;
+ pos = p;
+ }
+ }
+
+ /*
+ * If strarr[i] is a Token, then the match starts inside another Token, which is invalid
+ * If strarr[k - 1] is greedy we are in conflict with another greedy pattern
+ */
+ if (strarr[i] instanceof Token || strarr[k - 1].greedy) {
+ continue;
+ }
+
+ // Number of tokens to delete and replace with the new match
+ delNum = k - i;
+ str = text.slice(pos, p);
+ match.index -= pos;
+ }
+
+ if (!match) {
+ continue;
+ }
+
+ if(lookbehind) {
+ lookbehindLength = match[1].length;
+ }
+
+ var from = match.index + lookbehindLength,
+ match = match[0].slice(lookbehindLength),
+ to = from + match.length,
+ before = str.slice(0, from),
+ after = str.slice(to);
+
+ var args = [i, delNum];
+
+ if (before) {
+ args.push(before);
+ }
+
+ var wrapped = new Token(token, inside? _.tokenize(match, inside) : match, alias, match, greedy);
+
+ args.push(wrapped);
+
+ if (after) {
+ args.push(after);
+ }
+
+ Array.prototype.splice.apply(strarr, args);
+ }
+ }
+ }
+
+ return strarr;
+ },
+
+ hooks: {
+ all: {},
+
+ add: function (name, callback) {
+ var hooks = _.hooks.all;
+
+ hooks[name] = hooks[name] || [];
+
+ hooks[name].push(callback);
+ },
+
+ run: function (name, env) {
+ var callbacks = _.hooks.all[name];
+
+ if (!callbacks || !callbacks.length) {
+ return;
+ }
+
+ for (var i=0, callback; callback = callbacks[i++];) {
+ callback(env);
+ }
+ }
+ }
+};
+
+var Token = _.Token = function(type, content, alias, matchedStr, greedy) {
+ this.type = type;
+ this.content = content;
+ this.alias = alias;
+ // Copy of the full string this token was created from
+ this.length = (matchedStr || "").length|0;
+ this.greedy = !!greedy;
+};
+
+Token.stringify = function(o, language, parent) {
+ if (typeof o == 'string') {
+ return o;
+ }
+
+ if (_.util.type(o) === 'Array') {
+ return o.map(function(element) {
+ return Token.stringify(element, language, o);
+ }).join('');
+ }
+
+ var env = {
+ type: o.type,
+ content: Token.stringify(o.content, language, parent),
+ tag: 'span',
+ classes: ['token', o.type],
+ attributes: {},
+ language: language,
+ parent: parent
+ };
+
+ if (env.type == 'comment') {
+ env.attributes['spellcheck'] = 'true';
+ }
+
+ if (o.alias) {
+ var aliases = _.util.type(o.alias) === 'Array' ? o.alias : [o.alias];
+ Array.prototype.push.apply(env.classes, aliases);
+ }
+
+ _.hooks.run('wrap', env);
+
+ var attributes = Object.keys(env.attributes).map(function(name) {
+ return name + '="' + (env.attributes[name] || '').replace(/"/g, '&quot;') + '"';
+ }).join(' ');
+
+ return '<' + env.tag + ' class="' + env.classes.join(' ') + '"' + (attributes ? ' ' + attributes : '') + '>' + env.content + '</' + env.tag + '>';
+
+};
+
+if (!_self.document) {
+ if (!_self.addEventListener) {
+ // in Node.js
+ return _self.Prism;
+ }
+ // In worker
+ _self.addEventListener('message', function(evt) {
+ var message = JSON.parse(evt.data),
+ lang = message.language,
+ code = message.code,
+ immediateClose = message.immediateClose;
+
+ _self.postMessage(_.highlight(code, _.languages[lang], lang));
+ if (immediateClose) {
+ _self.close();
+ }
+ }, false);
+
+ return _self.Prism;
+}
+
+//Get current script and highlight
+var script = document.currentScript || [].slice.call(document.getElementsByTagName("script")).pop();
+
+if (script) {
+ _.filename = script.src;
+
+ if (document.addEventListener && !script.hasAttribute('data-manual')) {
+ if(document.readyState !== "loading") {
+ if (window.requestAnimationFrame) {
+ window.requestAnimationFrame(_.highlightAll);
+ } else {
+ window.setTimeout(_.highlightAll, 16);
+ }
+ }
+ else {
+ document.addEventListener('DOMContentLoaded', _.highlightAll);
+ }
+ }
+}
+
+return _self.Prism;
+
+})();
+
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = Prism;
+}
+
+// hack for components to work correctly in node.js
+if (typeof global !== 'undefined') {
+ global.Prism = Prism;
+}
+
+
+/* **********************************************
+ Begin prism-markup.js
+********************************************** */
+
+Prism.languages.markup = {
+ 'comment': /<!--[\w\W]*?-->/,
+ 'prolog': /<\?[\w\W]+?\?>/,
+ 'doctype': /<!DOCTYPE[\w\W]+?>/i,
+ 'cdata': /<!\[CDATA\[[\w\W]*?]]>/i,
+ 'tag': {
+ pattern: /<\/?(?!\d)[^\s>\/=$<]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\\1|\\?(?!\1)[\w\W])*\1|[^\s'">=]+))?)*\s*\/?>/i,
+ inside: {
+ 'tag': {
+ pattern: /^<\/?[^\s>\/]+/i,
+ inside: {
+ 'punctuation': /^<\/?/,
+ 'namespace': /^[^\s>\/:]+:/
+ }
+ },
+ 'attr-value': {
+ pattern: /=(?:('|")[\w\W]*?(\1)|[^\s>]+)/i,
+ inside: {
+ 'punctuation': /[=>"']/
+ }
+ },
+ 'punctuation': /\/?>/,
+ 'attr-name': {
+ pattern: /[^\s>\/]+/,
+ inside: {
+ 'namespace': /^[^\s>\/:]+:/
+ }
+ }
+
+ }
+ },
+ 'entity': /&#?[\da-z]{1,8};/i
+};
+
+// Plugin to make entity title show the real entity, idea by Roman Komarov
+Prism.hooks.add('wrap', function(env) {
+
+ if (env.type === 'entity') {
+ env.attributes['title'] = env.content.replace(/&amp;/, '&');
+ }
+});
+
+Prism.languages.xml = Prism.languages.markup;
+Prism.languages.html = Prism.languages.markup;
+Prism.languages.mathml = Prism.languages.markup;
+Prism.languages.svg = Prism.languages.markup;
+
+
+/* **********************************************
+ Begin prism-css.js
+********************************************** */
+
+Prism.languages.css = {
+ 'comment': /\/\*[\w\W]*?\*\//,
+ 'atrule': {
+ pattern: /@[\w-]+?.*?(;|(?=\s*\{))/i,
+ inside: {
+ 'rule': /@[\w-]+/
+ // See rest below
+ }
+ },
+ 'url': /url\((?:(["'])(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1|.*?)\)/i,
+ 'selector': /[^\{\}\s][^\{\};]*?(?=\s*\{)/,
+ 'string': {
+ pattern: /("|')(\\(?:\r\n|[\w\W])|(?!\1)[^\\\r\n])*\1/,
+ greedy: true
+ },
+ 'property': /(\b|\B)[\w-]+(?=\s*:)/i,
+ 'important': /\B!important\b/i,
+ 'function': /[-a-z0-9]+(?=\()/i,
+ 'punctuation': /[(){};:]/
+};
+
+Prism.languages.css['atrule'].inside.rest = Prism.util.clone(Prism.languages.css);
+
+if (Prism.languages.markup) {
+ Prism.languages.insertBefore('markup', 'tag', {
+ 'style': {
+ pattern: /(<style[\w\W]*?>)[\w\W]*?(?=<\/style>)/i,
+ lookbehind: true,
+ inside: Prism.languages.css,
+ alias: 'language-css'
+ }
+ });
+
+ Prism.languages.insertBefore('inside', 'attr-value', {
+ 'style-attr': {
+ pattern: /\s*style=("|').*?\1/i,
+ inside: {
+ 'attr-name': {
+ pattern: /^\s*style/i,
+ inside: Prism.languages.markup.tag.inside
+ },
+ 'punctuation': /^\s*=\s*['"]|['"]\s*$/,
+ 'attr-value': {
+ pattern: /.+/i,
+ inside: Prism.languages.css
+ }
+ },
+ alias: 'language-css'
+ }
+ }, Prism.languages.markup.tag);
+}
+
+/* **********************************************
+ Begin prism-clike.js
+********************************************** */
+
+Prism.languages.clike = {
+ 'comment': [
+ {
+ pattern: /(^|[^\\])\/\*[\w\W]*?\*\//,
+ lookbehind: true
+ },
+ {
+ pattern: /(^|[^\\:])\/\/.*/,
+ lookbehind: true
+ }
+ ],
+ 'string': {
+ pattern: /(["'])(\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,
+ greedy: true
+ },
+ 'class-name': {
+ pattern: /((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,
+ lookbehind: true,
+ inside: {
+ punctuation: /(\.|\\)/
+ }
+ },
+ 'keyword': /\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,
+ 'boolean': /\b(true|false)\b/,
+ 'function': /[a-z0-9_]+(?=\()/i,
+ 'number': /\b-?(?:0x[\da-f]+|\d*\.?\d+(?:e[+-]?\d+)?)\b/i,
+ 'operator': /--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,
+ 'punctuation': /[{}[\];(),.:]/
+};
+
+
+/* **********************************************
+ Begin prism-javascript.js
+********************************************** */
+
+Prism.languages.javascript = Prism.languages.extend('clike', {
+ 'keyword': /\b(as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|var|void|while|with|yield)\b/,
+ 'number': /\b-?(0x[\dA-Fa-f]+|0b[01]+|0o[0-7]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|Infinity)\b/,
+ // Allow for all non-ASCII characters (See http://stackoverflow.com/a/2008444)
+ 'function': /[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*(?=\()/i,
+ 'operator': /--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*\*?|\/|~|\^|%|\.{3}/
+});
+
+Prism.languages.insertBefore('javascript', 'keyword', {
+ 'regex': {
+ pattern: /(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\\\r\n])+\/[gimyu]{0,5}(?=\s*($|[\r\n,.;})]))/,
+ lookbehind: true,
+ greedy: true
+ }
+});
+
+Prism.languages.insertBefore('javascript', 'string', {
+ 'template-string': {
+ pattern: /`(?:\\\\|\\?[^\\])*?`/,
+ greedy: true,
+ inside: {
+ 'interpolation': {
+ pattern: /\$\{[^}]+\}/,
+ inside: {
+ 'interpolation-punctuation': {
+ pattern: /^\$\{|\}$/,
+ alias: 'punctuation'
+ },
+ rest: Prism.languages.javascript
+ }
+ },
+ 'string': /[\s\S]+/
+ }
+ }
+});
+
+if (Prism.languages.markup) {
+ Prism.languages.insertBefore('markup', 'tag', {
+ 'script': {
+ pattern: /(<script[\w\W]*?>)[\w\W]*?(?=<\/script>)/i,
+ lookbehind: true,
+ inside: Prism.languages.javascript,
+ alias: 'language-javascript'
+ }
+ });
+}
+
+Prism.languages.js = Prism.languages.javascript;
+
+/* **********************************************
+ Begin prism-file-highlight.js
+********************************************** */
+
+(function () {
+ if (typeof self === 'undefined' || !self.Prism || !self.document || !document.querySelector) {
+ return;
+ }
+
+ self.Prism.fileHighlight = function() {
+
+ var Extensions = {
+ 'js': 'javascript',
+ 'py': 'python',
+ 'rb': 'ruby',
+ 'ps1': 'powershell',
+ 'psm1': 'powershell',
+ 'sh': 'bash',
+ 'bat': 'batch',
+ 'h': 'c',
+ 'tex': 'latex'
+ };
+
+ if(Array.prototype.forEach) { // Check to prevent error in IE8
+ Array.prototype.slice.call(document.querySelectorAll('pre[data-src]')).forEach(function (pre) {
+ var src = pre.getAttribute('data-src');
+
+ var language, parent = pre;
+ var lang = /\blang(?:uage)?-(?!\*)(\w+)\b/i;
+ while (parent && !lang.test(parent.className)) {
+ parent = parent.parentNode;
+ }
+
+ if (parent) {
+ language = (pre.className.match(lang) || [, ''])[1];
+ }
+
+ if (!language) {
+ var extension = (src.match(/\.(\w+)$/) || [, ''])[1];
+ language = Extensions[extension] || extension;
+ }
+
+ var code = document.createElement('code');
+ code.className = 'language-' + language;
+
+ pre.textContent = '';
+
+ code.textContent = 'Loading…';
+
+ pre.appendChild(code);
+
+ var xhr = new XMLHttpRequest();
+
+ xhr.open('GET', src, true);
+
+ xhr.onreadystatechange = function () {
+ if (xhr.readyState == 4) {
+
+ if (xhr.status < 400 && xhr.responseText) {
+ code.textContent = xhr.responseText;
+
+ Prism.highlightElement(code);
+ }
+ else if (xhr.status >= 400) {
+ code.textContent = '✖ Error ' + xhr.status + ' while fetching file: ' + xhr.statusText;
+ }
+ else {
+ code.textContent = '✖ Error: File does not exist or is empty';
+ }
+ }
+ };
+
+ xhr.send(null);
+ });
+ }
+
+ };
+
+ document.addEventListener('DOMContentLoaded', self.Prism.fileHighlight);
+
+})();
+
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(4)))
+
+/***/ }),
+/* 29 */
+/***/ (function(module, exports, __webpack_require__) {
+
+
+/* styles */
+__webpack_require__(42)
+
+var Component = __webpack_require__(0)(
+ /* script */
+ __webpack_require__(7),
+ /* template */
+ __webpack_require__(36),
+ /* scopeId */
+ "data-v-3ac4c361",
+ /* cssModules */
+ null
+)
+Component.options.__file = "/Users/phil/Projects/notebooklab/src/cells/code.vue"
+if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")}
+if (Component.options.functional) {console.error("[vue-loader] code.vue: functional components are not supported with templates, they should use render functions.")}
+
+/* hot reload */
+if (false) {(function () {
+ var hotAPI = require("vue-hot-reload-api")
+ hotAPI.install(require("vue"), false)
+ if (!hotAPI.compatible) return
+ module.hot.accept()
+ if (!module.hot.data) {
+ hotAPI.createRecord("data-v-3ac4c361", Component.options)
+ } else {
+ hotAPI.reload("data-v-3ac4c361", Component.options)
+ }
+})()}
+
+module.exports = Component.exports
+
+
+/***/ }),
+/* 30 */
+/***/ (function(module, exports, __webpack_require__) {
+
+
+/* styles */
+__webpack_require__(45)
+
+var Component = __webpack_require__(0)(
+ /* script */
+ __webpack_require__(9),
+ /* template */
+ __webpack_require__(40),
+ /* scopeId */
+ null,
+ /* cssModules */
+ null
+)
+Component.options.__file = "/Users/phil/Projects/notebooklab/src/cells/markdown.vue"
+if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")}
+if (Component.options.functional) {console.error("[vue-loader] markdown.vue: functional components are not supported with templates, they should use render functions.")}
+
+/* hot reload */
+if (false) {(function () {
+ var hotAPI = require("vue-hot-reload-api")
+ hotAPI.install(require("vue"), false)
+ if (!hotAPI.compatible) return
+ module.hot.accept()
+ if (!module.hot.data) {
+ hotAPI.createRecord("data-v-7342b363", Component.options)
+ } else {
+ hotAPI.reload("data-v-7342b363", Component.options)
+ }
+})()}
+
+module.exports = Component.exports
+
+
+/***/ }),
+/* 31 */
+/***/ (function(module, exports, __webpack_require__) {
+
+var Component = __webpack_require__(0)(
+ /* script */
+ __webpack_require__(10),
+ /* template */
+ __webpack_require__(37),
+ /* scopeId */
+ null,
+ /* cssModules */
+ null
+)
+Component.options.__file = "/Users/phil/Projects/notebooklab/src/cells/output/html.vue"
+if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")}
+if (Component.options.functional) {console.error("[vue-loader] html.vue: functional components are not supported with templates, they should use render functions.")}
+
+/* hot reload */
+if (false) {(function () {
+ var hotAPI = require("vue-hot-reload-api")
+ hotAPI.install(require("vue"), false)
+ if (!hotAPI.compatible) return
+ module.hot.accept()
+ if (!module.hot.data) {
+ hotAPI.createRecord("data-v-48ada535", Component.options)
+ } else {
+ hotAPI.reload("data-v-48ada535", Component.options)
+ }
+})()}
+
+module.exports = Component.exports
+
+
+/***/ }),
+/* 32 */
+/***/ (function(module, exports, __webpack_require__) {
+
+var Component = __webpack_require__(0)(
+ /* script */
+ __webpack_require__(11),
+ /* template */
+ __webpack_require__(34),
+ /* scopeId */
+ null,
+ /* cssModules */
+ null
+)
+Component.options.__file = "/Users/phil/Projects/notebooklab/src/cells/output/image.vue"
+if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")}
+if (Component.options.functional) {console.error("[vue-loader] image.vue: functional components are not supported with templates, they should use render functions.")}
+
+/* hot reload */
+if (false) {(function () {
+ var hotAPI = require("vue-hot-reload-api")
+ hotAPI.install(require("vue"), false)
+ if (!hotAPI.compatible) return
+ module.hot.accept()
+ if (!module.hot.data) {
+ hotAPI.createRecord("data-v-09b68c41", Component.options)
+ } else {
+ hotAPI.reload("data-v-09b68c41", Component.options)
+ }
+})()}
+
+module.exports = Component.exports
+
+
+/***/ }),
+/* 33 */
+/***/ (function(module, exports, __webpack_require__) {
+
+var Component = __webpack_require__(0)(
+ /* script */
+ __webpack_require__(12),
+ /* template */
+ __webpack_require__(35),
+ /* scopeId */
+ null,
+ /* cssModules */
+ null
+)
+Component.options.__file = "/Users/phil/Projects/notebooklab/src/cells/output/index.vue"
+if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")}
+if (Component.options.functional) {console.error("[vue-loader] index.vue: functional components are not supported with templates, they should use render functions.")}
+
+/* hot reload */
+if (false) {(function () {
+ var hotAPI = require("vue-hot-reload-api")
+ hotAPI.install(require("vue"), false)
+ if (!hotAPI.compatible) return
+ module.hot.accept()
+ if (!module.hot.data) {
+ hotAPI.createRecord("data-v-0dec7838", Component.options)
+ } else {
+ hotAPI.reload("data-v-0dec7838", Component.options)
+ }
+})()}
+
+module.exports = Component.exports
+
+
+/***/ }),
+/* 34 */
+/***/ (function(module, exports, __webpack_require__) {
+
+module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
+ return _c('div', {
+ staticClass: "output"
+ }, [_c('prompt'), _vm._v(" "), _c('img', {
+ attrs: {
+ "src": 'data:' + _vm.outputType + ';base64,' + _vm.rawCode
+ }
+ })], 1)
+},staticRenderFns: []}
+module.exports.render._withStripped = true
+if (false) {
+ module.hot.accept()
+ if (module.hot.data) {
+ require("vue-hot-reload-api").rerender("data-v-09b68c41", module.exports)
+ }
+}
+
+/***/ }),
+/* 35 */
+/***/ (function(module, exports, __webpack_require__) {
+
+module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
+ return _c(_vm.componentName, {
+ tag: "component",
+ attrs: {
+ "type": "output",
+ "outputType": _vm.outputType,
+ "count": _vm.count,
+ "raw-code": _vm.rawCode,
+ "code-css-class": _vm.codeCssClass
+ }
+ })
+},staticRenderFns: []}
+module.exports.render._withStripped = true
+if (false) {
+ module.hot.accept()
+ if (module.hot.data) {
+ require("vue-hot-reload-api").rerender("data-v-0dec7838", module.exports)
+ }
+}
+
+/***/ }),
+/* 36 */
+/***/ (function(module, exports, __webpack_require__) {
+
+module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
+ return _c('div', {
+ staticClass: "cell"
+ }, [_c('code-cell', {
+ attrs: {
+ "type": "input",
+ "raw-code": _vm.rawInputCode,
+ "count": _vm.cell.execution_count,
+ "code-css-class": _vm.codeCssClass
+ }
+ }), _vm._v(" "), (_vm.hasOutput) ? _c('output-cell', {
+ attrs: {
+ "count": _vm.cell.execution_count,
+ "output": _vm.output,
+ "code-css-class": _vm.codeCssClass
+ }
+ }) : _vm._e()], 1)
+},staticRenderFns: []}
+module.exports.render._withStripped = true
+if (false) {
+ module.hot.accept()
+ if (module.hot.data) {
+ require("vue-hot-reload-api").rerender("data-v-3ac4c361", module.exports)
+ }
+}
+
+/***/ }),
+/* 37 */
+/***/ (function(module, exports, __webpack_require__) {
+
+module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
+ return _c('div', {
+ staticClass: "output"
+ }, [_c('prompt'), _vm._v(" "), _c('div', {
+ domProps: {
+ "innerHTML": _vm._s(_vm.rawCode)
+ }
+ })], 1)
+},staticRenderFns: []}
+module.exports.render._withStripped = true
+if (false) {
+ module.hot.accept()
+ if (module.hot.data) {
+ require("vue-hot-reload-api").rerender("data-v-48ada535", module.exports)
+ }
+}
+
+/***/ }),
+/* 38 */
+/***/ (function(module, exports, __webpack_require__) {
+
+module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
+ return (_vm.hasNotebook) ? _c('div', _vm._l((_vm.cells), function(cell, index) {
+ return _c(_vm.cellType(cell.cell_type), {
+ key: index,
+ tag: "component",
+ attrs: {
+ "cell": cell,
+ "code-css-class": _vm.codeCssClass
+ }
+ })
+ })) : _vm._e()
+},staticRenderFns: []}
+module.exports.render._withStripped = true
+if (false) {
+ module.hot.accept()
+ if (module.hot.data) {
+ require("vue-hot-reload-api").rerender("data-v-4cb2b168", module.exports)
+ }
+}
+
+/***/ }),
+/* 39 */
+/***/ (function(module, exports, __webpack_require__) {
+
+module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
+ return _c('div', {
+ staticClass: "prompt"
+ }, [(_vm.type && _vm.count) ? _c('span', [_vm._v("\n " + _vm._s(_vm.type) + " [" + _vm._s(_vm.count) + "]:\n ")]) : _vm._e()])
+},staticRenderFns: []}
+module.exports.render._withStripped = true
+if (false) {
+ module.hot.accept()
+ if (module.hot.data) {
+ require("vue-hot-reload-api").rerender("data-v-4f6bf458", module.exports)
+ }
+}
+
+/***/ }),
+/* 40 */
+/***/ (function(module, exports, __webpack_require__) {
+
+module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
+ return _c('div', {
+ staticClass: "cell text-cell"
+ }, [_c('prompt'), _vm._v(" "), _c('div', {
+ staticClass: "markdown",
+ domProps: {
+ "innerHTML": _vm._s(_vm.markdown)
+ }
+ })], 1)
+},staticRenderFns: []}
+module.exports.render._withStripped = true
+if (false) {
+ module.hot.accept()
+ if (module.hot.data) {
+ require("vue-hot-reload-api").rerender("data-v-7342b363", module.exports)
+ }
+}
+
+/***/ }),
+/* 41 */
+/***/ (function(module, exports, __webpack_require__) {
+
+module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
+ return _c('div', {
+ class: _vm.type
+ }, [_c('prompt', {
+ attrs: {
+ "type": _vm.promptType,
+ "count": _vm.count
+ }
+ }), _vm._v(" "), _c('pre', {
+ ref: "code",
+ staticClass: "language-python",
+ class: _vm.codeCssClass,
+ domProps: {
+ "textContent": _vm._s(_vm.code)
+ }
+ }, [_vm._v("\n ")])], 1)
+},staticRenderFns: []}
+module.exports.render._withStripped = true
+if (false) {
+ module.hot.accept()
+ if (module.hot.data) {
+ require("vue-hot-reload-api").rerender("data-v-d42105b8", module.exports)
+ }
+}
+
+/***/ }),
+/* 42 */
+/***/ (function(module, exports, __webpack_require__) {
+
+// style-loader: Adds some css to the DOM by adding a <style> tag
+
+// load the styles
+var content = __webpack_require__(19);
+if(typeof content === 'string') content = [[module.i, content, '']];
+if(content.locals) module.exports = content.locals;
+// add the styles to the DOM
+var update = __webpack_require__(3)("06fc6a9f", content, false);
+// Hot Module Replacement
+if(false) {
+ // When the styles change, update the <style> tags
+ if(!content.locals) {
+ module.hot.accept("!!../../node_modules/css-loader/index.js!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-3ac4c361\",\"scoped\":true,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./code.vue", function() {
+ var newContent = require("!!../../node_modules/css-loader/index.js!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-3ac4c361\",\"scoped\":true,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./code.vue");
+ if(typeof newContent === 'string') newContent = [[module.id, newContent, '']];
+ update(newContent);
+ });
+ }
+ // When the module is disposed, remove the <style> tags
+ module.hot.dispose(function() { update(); });
+}
+
+/***/ }),
+/* 43 */
+/***/ (function(module, exports, __webpack_require__) {
+
+// style-loader: Adds some css to the DOM by adding a <style> tag
+
+// load the styles
+var content = __webpack_require__(20);
+if(typeof content === 'string') content = [[module.i, content, '']];
+if(content.locals) module.exports = content.locals;
+// add the styles to the DOM
+var update = __webpack_require__(3)("87c28124", content, false);
+// Hot Module Replacement
+if(false) {
+ // When the styles change, update the <style> tags
+ if(!content.locals) {
+ module.hot.accept("!!../node_modules/css-loader/index.js!../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-4cb2b168\",\"scoped\":false,\"hasInlineConfig\":false}!../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue", function() {
+ var newContent = require("!!../node_modules/css-loader/index.js!../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-4cb2b168\",\"scoped\":false,\"hasInlineConfig\":false}!../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue");
+ if(typeof newContent === 'string') newContent = [[module.id, newContent, '']];
+ update(newContent);
+ });
+ }
+ // When the module is disposed, remove the <style> tags
+ module.hot.dispose(function() { update(); });
+}
+
+/***/ }),
+/* 44 */
+/***/ (function(module, exports, __webpack_require__) {
+
+// style-loader: Adds some css to the DOM by adding a <style> tag
+
+// load the styles
+var content = __webpack_require__(21);
+if(typeof content === 'string') content = [[module.i, content, '']];
+if(content.locals) module.exports = content.locals;
+// add the styles to the DOM
+var update = __webpack_require__(3)("5b60b003", content, false);
+// Hot Module Replacement
+if(false) {
+ // When the styles change, update the <style> tags
+ if(!content.locals) {
+ module.hot.accept("!!../../node_modules/css-loader/index.js!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-4f6bf458\",\"scoped\":true,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./prompt.vue", function() {
+ var newContent = require("!!../../node_modules/css-loader/index.js!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-4f6bf458\",\"scoped\":true,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./prompt.vue");
+ if(typeof newContent === 'string') newContent = [[module.id, newContent, '']];
+ update(newContent);
+ });
+ }
+ // When the module is disposed, remove the <style> tags
+ module.hot.dispose(function() { update(); });
+}
+
+/***/ }),
+/* 45 */
+/***/ (function(module, exports, __webpack_require__) {
+
+// style-loader: Adds some css to the DOM by adding a <style> tag
+
+// load the styles
+var content = __webpack_require__(22);
+if(typeof content === 'string') content = [[module.i, content, '']];
+if(content.locals) module.exports = content.locals;
+// add the styles to the DOM
+var update = __webpack_require__(3)("48dda57c", content, false);
+// Hot Module Replacement
+if(false) {
+ // When the styles change, update the <style> tags
+ if(!content.locals) {
+ module.hot.accept("!!../../node_modules/css-loader/index.js!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7342b363\",\"scoped\":false,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./markdown.vue", function() {
+ var newContent = require("!!../../node_modules/css-loader/index.js!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7342b363\",\"scoped\":false,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./markdown.vue");
+ if(typeof newContent === 'string') newContent = [[module.id, newContent, '']];
+ update(newContent);
+ });
+ }
+ // When the module is disposed, remove the <style> tags
+ module.hot.dispose(function() { update(); });
+}
+
+/***/ }),
+/* 46 */
+/***/ (function(module, exports) {
+
+/**
+ * Translates the list format produced by css-loader into something
+ * easier to manipulate.
+ */
+module.exports = function listToStyles (parentId, list) {
+ var styles = []
+ var newStyles = {}
+ for (var i = 0; i < list.length; i++) {
+ var item = list[i]
+ var id = item[0]
+ var css = item[1]
+ var media = item[2]
+ var sourceMap = item[3]
+ var part = {
+ id: parentId + ':' + i,
+ css: css,
+ media: media,
+ sourceMap: sourceMap
+ }
+ if (!newStyles[id]) {
+ styles.push(newStyles[id] = { id: id, parts: [part] })
+ } else {
+ newStyles[id].parts.push(part)
+ }
+ }
+ return styles
+}
+
+
+/***/ }),
+/* 47 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+var Notebook = __webpack_require__(6);
+
+module.exports = {
+ install: function install(_vue) {
+ _vue.component('notebook-lab', Notebook);
+ }
+};
+
+/***/ })
+/******/ ]);
+}); \ No newline at end of file