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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGrzegorz Bizon <grzesiek.bizon@gmail.com>2017-04-03 10:36:09 +0300
committerGrzegorz Bizon <grzesiek.bizon@gmail.com>2017-04-03 10:36:09 +0300
commitfc550b398be6cdc4584dad79411929305815ffaa (patch)
tree0995b6990fa3599dddfed5bd2720589731bcdf31
parent83d1fe9b5aeb947c1387666205ecaca81f2bf3a2 (diff)
parente7e9307219d1c81427f95444b36471c519dc06c2 (diff)
Merge branch 'master' into feature/multi-level-container-registry-images
* master: (230 commits) Fix N+1 query in loading pipelines in merge requests Fix Spinach and Capybara dependencies Prevent users from disconnecting gitlab account from CAS 30276 Move issue, mr, todos next to profile dropdown in top nav Refactor SearchController#show Properly eagerly-load the Capybara server for JS feature specs only Updating documentation to include a missing step in the update procedure Eager-load the Capybara server to prevent timeouts Increase Capybara's timeout Add metrics button to Environment Overview page Fix link to Jira service documentation Handle parsing OpenBSD ps output properly to display sidekiq infos on ... Eliminate unnecessary queries that add ~500 ms of load time for a large issue 20914 Limits line length for project home page Allow users to import GitHub projects to subgroups Update dpl CI example Fix the docs:check:links job Don't clean up the gitlab-test-fork_bare repo Make GitLab use Gitaly for commit_is_ancestor Remove unnecessary ORDER BY clause from `forked_to_project_id` subquery ...
-rw-r--r--.gitlab-ci.yml92
-rw-r--r--CHANGELOG.md18
-rw-r--r--CONTRIBUTING.md16
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.lock10
-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/notebook/index.js85
-rw-r--r--app/assets/javascripts/blob/notebook_viewer.js3
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js2
-rw-r--r--app/assets/javascripts/boards/components/board_card.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/dispatcher.js3
-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.js19
-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/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.js16
-rw-r--r--app/assets/javascripts/lib/utils/poll.js55
-rw-r--r--app/assets/javascripts/main.js2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js33
-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/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.scss7
-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/environments.scss11
-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.scss33
-rw-r--r--app/assets/stylesheets/pages/todos.scss19
-rw-r--r--app/controllers/admin/application_settings_controller.rb9
-rw-r--r--app/controllers/admin/background_jobs_controller.rb4
-rw-r--r--app/controllers/admin/labels_controller.rb9
-rw-r--r--app/controllers/groups/labels_controller.rb6
-rw-r--r--app/controllers/import/base_controller.rb2
-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/labels_controller.rb6
-rwxr-xr-xapp/controllers/projects/merge_requests_controller.rb52
-rw-r--r--app/controllers/projects/pipelines_controller.rb6
-rw-r--r--app/controllers/projects/wikis_controller.rb2
-rw-r--r--app/controllers/search_controller.rb40
-rw-r--r--app/finders/group_finder.rb17
-rw-r--r--app/finders/labels_finder.rb13
-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/models/blob.rb6
-rw-r--r--app/models/board.rb4
-rw-r--r--app/models/ci/pipeline.rb2
-rw-r--r--app/models/commit_status.rb4
-rw-r--r--app/models/list.rb2
-rw-r--r--app/models/namespace.rb18
-rw-r--r--app/models/note.rb5
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/project_services/prometheus_service.rb6
-rw-r--r--app/models/project_team.rb3
-rw-r--r--app/models/repository.rb8
-rw-r--r--app/models/system_note_metadata.rb11
-rw-r--r--app/models/user.rb7
-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/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/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.rb6
-rw-r--r--app/services/notification_service.rb5
-rw-r--r--app/services/search/global_service.rb4
-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.rb79
-rw-r--r--app/services/todo_service.rb38
-rw-r--r--app/services/users/create_service.rb2
-rw-r--r--app/views/admin/projects/_projects.html.haml15
-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/groups/milestones/show.html.haml2
-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/_notebook.html.haml5
-rw-r--r--app/views/projects/boards/components/_board.html.haml4
-rw-r--r--app/views/projects/boards/components/_board_list.html.haml2
-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.haml2
-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.haml5
-rw-r--r--app/views/projects/new.html.haml2
-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/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/_mr_bold.svg1
-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/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--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/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/24784-system-notes-meta-data.yml4
-rw-r--r--changelogs/unreleased/24861-stringify-group-member-details.yml4
-rw-r--r--changelogs/unreleased/25556-prevent-users-from-disconnecting-gitlab-account-from-cas.yml4
-rw-r--r--changelogs/unreleased/26595-fix-issue-preselected-template.yml4
-rw-r--r--changelogs/unreleased/27293-remove-repeated-labels.yml4
-rw-r--r--changelogs/unreleased/28424-labels-support-color-names-in-backend.yml4
-rw-r--r--changelogs/unreleased/28799-todo-creation.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/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/29871-api-remove-merge-requests-comments-endpoints.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/30098-banzai-filter-mergerequestreferencefilter-has-an-n-1-query-problem.yml4
-rw-r--r--changelogs/unreleased/30112-fix-pipelines-sub-nav-highlight.yml4
-rw-r--r--changelogs/unreleased/30276-move-top-badges.yml4
-rw-r--r--changelogs/unreleased/30289-allow-users-to-import-github-projects-to-subgroups.yml4
-rw-r--r--changelogs/unreleased/better-priority-sorting-2.yml4
-rw-r--r--changelogs/unreleased/better-priority-sorting.yml4
-rw-r--r--changelogs/unreleased/calendar-tooltips.yml4
-rw-r--r--changelogs/unreleased/create-collapsed-todo-button.yml5
-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-environments-folders-route.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/forked-subquery-order.yml4
-rw-r--r--changelogs/unreleased/jej-group-name-disclosure.yml4
-rw-r--r--changelogs/unreleased/make-ci-build-to-lock-on-status-change.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/option-to-be-notified-of-own-activity.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/slow-search-changelog.yml4
-rw-r--r--changelogs/unreleased/update-test-bundle-ignored-files.yml4
-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/rspec_profiling.rb6
-rw-r--r--config/routes/project.rb4
-rw-r--r--config/webpack.config.js8
-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/schema.rb10
-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/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/ux_guide/components.md14
-rw-r--r--doc/development/writing_documentation.md72
-rw-r--r--doc/install/installation.md4
-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/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--features/steps/dashboard/new_project.rb2
-rw-r--r--features/steps/dashboard/todos.rb6
-rw-r--r--features/steps/group/milestones.rb2
-rw-r--r--features/steps/project/merge_requests.rb2
-rw-r--r--features/support/capybara.rb8
-rw-r--r--features/support/env.rb16
-rw-r--r--lib/api/helpers.rb5
-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/notes.rb14
-rw-r--r--lib/api/v3/issues.rb4
-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/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/git/repository.rb35
-rw-r--r--lib/gitlab/gitaly_client.rb34
-rw-r--r--lib/gitlab/gitaly_client/commit.rb20
-rw-r--r--lib/gitlab/gitaly_client/notifications.rb15
-rw-r--r--lib/gitlab/repo_path.rb23
-rw-r--r--lib/gitlab/uploads_transfer.rb2
-rw-r--r--lib/gitlab/workhorse.rb9
-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/profiles/accounts_controller_spec.rb52
-rw-r--r--spec/controllers/profiles/notifications_controller_spec.rb45
-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/merge_requests_controller_spec.rb38
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb20
-rw-r--r--spec/factories/boards.rb2
-rw-r--r--spec/factories/lists.rb4
-rw-r--r--spec/factories/system_note_metadata.rb6
-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/issues/filtered_search/dropdown_hint_spec.rb4
-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/profiles/user_changes_notified_of_own_activity_spec.rb32
-rw-r--r--spec/features/projects/environments/environment_spec.rb19
-rw-r--r--spec/features/projects/group_links_spec.rb22
-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/todos/todos_spec.rb6
-rw-r--r--spec/features/user_callout_spec.rb18
-rw-r--r--spec/fixtures/api/schemas/list.json2
-rw-r--r--spec/helpers/auth_helper_spec.rb14
-rw-r--r--spec/helpers/sidekiq_helper_spec.rb8
-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/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/environments/environment_actions_spec.js7
-rw-r--r--spec/javascripts/environments/environment_monitoring_spec.js23
-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/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.js31
-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/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.js4
-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/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/git/repository_spec.rb27
-rw-r--r--spec/lib/gitlab/gitaly_client/notifications_spec.rb11
-rw-r--r--spec/lib/gitlab/github_import/label_formatter_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/repo_path_spec.rb22
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb12
-rw-r--r--spec/mailers/notify_spec.rb15
-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/commit_status_spec.rb36
-rw-r--r--spec/models/list_spec.rb22
-rw-r--r--spec/models/namespace_spec.rb79
-rw-r--r--spec/models/repository_spec.rb13
-rw-r--r--spec/models/system_note_metadata_spec.rb27
-rw-r--r--spec/policies/issue_policy_spec.rb2
-rw-r--r--spec/policies/project_policy_spec.rb2
-rw-r--r--spec/requests/api/internal_spec.rb6
-rw-r--r--spec/requests/api/issues_spec.rb20
-rw-r--r--spec/requests/api/merge_requests_spec.rb58
-rw-r--r--spec/requests/api/notes_spec.rb60
-rw-r--r--spec/requests/api/v3/issues_spec.rb20
-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/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.rb2
-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.rb71
-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.rb15
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb2
-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.rb202
-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.rb17
-rw-r--r--spec/services/users/destroy_spec.rb4
-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.rb14
-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.rb2
-rw-r--r--spec/support/stored_repositories.rb5
-rw-r--r--spec/support/stub_configuration.rb4
-rw-r--r--spec/support/test_env.rb26
-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.rb2
-rw-r--r--spec/workers/post_receive_spec.rb2
-rw-r--r--spec/workers/process_commit_worker_spec.rb2
-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
500 files changed, 11893 insertions, 1754 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index f271ab4c4c8..476307e7076 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -14,13 +14,15 @@ variables:
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 +41,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 +64,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 +84,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
@@ -122,20 +141,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 +292,35 @@ rake karma:
paths:
- coverage-javascript/
-lint-doc:
+docs:check:apilint:
+ image: "phusion/baseimage"
stage: test
<<: *dedicated-runner
- image: "phusion/baseimage:latest"
+ variables:
+ GIT_DEPTH: "3"
+ 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
+ variables:
+ GIT_DEPTH: "3"
+ 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/CHANGELOG.md b/CHANGELOG.md
index 4291eca8dc7..734c72f5dd2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,24 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 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/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 c4368d79d5d..562ee2d1b9e 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'
diff --git a/Gemfile.lock b/Gemfile.lock
index 2cb0e88962a..8382de2b7a0 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -288,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
@@ -682,7 +682,7 @@ GEM
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)
@@ -905,7 +905,7 @@ DEPENDENCIES
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)
@@ -987,7 +987,7 @@ 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)
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/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/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_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/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/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_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..9c196562c6c 100644
--- a/app/assets/javascripts/environments/components/environment_item.js
+++ b/app/assets/javascripts/environments/components/environment_item.js
@@ -5,6 +5,7 @@ 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';
/**
@@ -22,6 +23,7 @@ export default {
'stop-component': StopComponent,
'rollback-component': RollbackComponent,
'terminal-button-component': TerminalButtonComponent,
+ 'monitoring-button-component': MonitoringButtonComponent,
},
props: {
@@ -392,6 +394,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.
*
@@ -496,13 +506,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/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..4aad0128aef 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
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 9007d661d01..665a59f3183 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -370,4 +370,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/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/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..b6168a293e0 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -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/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 25be7f408d0..3d91e0b22d8 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,
@@ -222,3 +222,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..c2c2f371b87 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -477,20 +477,6 @@ a.deploy-project-label {
}
}
-.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;
- }
- }
-}
-
.project-stats {
font-size: 0;
text-align: center;
@@ -587,9 +573,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 +742,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/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 8d831ffdd70..0bfbe47eb4f 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] = []
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/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..eeee027ef2d 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -11,7 +11,7 @@ class Import::BaseController < ApplicationController
namespace.add_owner(current_user)
namespace
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
- Namespace.find_by_path_or_name(name)
+ Namespace.find_by_full_path(name)
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/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..9621b30b251 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]
@@ -97,31 +97,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 +473,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/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/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/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/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/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/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/pipeline.rb b/app/models/ci/pipeline.rb
index f12be98c80c..ad7e0b23ff4 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -210,7 +210,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/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/namespace.rb b/app/models/namespace.rb
index 1e0bbe69eb1..2c3473f9186 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/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/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..596650353fc 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -981,7 +981,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?
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
new file mode 100644
index 00000000000..5cc66574941
--- /dev/null
+++ b/app/models/system_note_metadata.rb
@@ -0,0 +1,11 @@
+class SystemNoteMetadata < ActiveRecord::Base
+ ICON_TYPES = %w[
+ commit merge confidentiality status label assignee cross_reference
+ title time_tracking branch milestone discussion task moved
+ ].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 612066654dc..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,
@@ -634,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
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/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/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..940e850600f 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -38,7 +38,7 @@ 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
@@ -47,7 +47,7 @@ class NotificationRecipientService
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 +88,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
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index f9aa2346759..2c6f849259e 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -280,8 +280,9 @@ class NotificationService
recipients ||= NotificationRecipientService.new(pipeline.project).build_recipients(
pipeline,
- nil, # The acting user, who won't be added to recipients
- action: pipeline.status).map(&:notification_email)
+ pipeline.user,
+ action: pipeline.status,
+ skip_current_user: false).map(&:notification_email)
if recipients.any?
mailer.public_send(email_template, pipeline, recipients).deliver_later
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index 781cd13b44b..a3c655493a5 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -16,5 +16,9 @@ module Search
Gitlab::SearchResults.new(current_user, projects, params[:search])
end
+
+ def scope
+ @scope ||= %w[issues merge_requests milestones].delete(params[:scope]) { 'projects' }
+ 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..d3e502b66dd 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,57 @@ module SystemNoteService
body = status.dup
body << " via #{source.gfm_reference(project)}" if source
- create_note(noteable: noteable, project: project, author: author, note: body)
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'status'))
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 +257,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
@@ -269,7 +274,8 @@ 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)
+
+ create_note(NoteSummary.new(issue, project, author, body, action: 'confidentiality'))
end
# Called when a branch in Noteable is changed
@@ -288,7 +294,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 +321,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 +333,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 +358,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 +442,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 +465,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 +482,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..2c56cb4c680 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:
@@ -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
index f4f0b80f30a..193fcd85896 100644
--- a/app/services/users/create_service.rb
+++ b/app/services/users/create_service.rb
@@ -94,7 +94,7 @@ module Users
def build_user_params
if current_user&.is_admin?
user_params = params.slice(*admin_create_params)
- user_params[:created_by_id] = current_user.id
+ user_params[:created_by_id] = current_user&.id
if params[:reset_password]
user_params.merge!(force_random_password: true, password_expires_at: nil)
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/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/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index e66a8e0a3b3..8e83b2002b2 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -4,5 +4,5 @@
= 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/_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/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
index 4a4dd84d5d2..4a0b2110601 100644
--- a/app/views/projects/boards/components/_board_list.html.haml
+++ b/app/views/projects/boards/components/_board_list.html.haml
@@ -3,7 +3,7 @@
= icon("spinner spin")
- if can? current_user, :create_issue, @project
%board-new-issue{ ":list" => "list",
- "v-if" => 'list.type !== "done" && showIssueForm' }
+ "v-if" => 'list.type !== "closed" && showIssueForm' }
%ul.board-list{ "ref" => "list",
"v-show" => "!loading",
":data-board" => "list.id",
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..b78de092a60 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
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..f612b5c7d6b 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -36,6 +36,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 +56,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..09ac1fd6794 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -76,7 +76,7 @@
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
- if gitlab_project_import_enabled?
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/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/_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/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index b58640c3ef0..330fa8a5b10 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/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/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/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/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/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/26595-fix-issue-preselected-template.yml b/changelogs/unreleased/26595-fix-issue-preselected-template.yml
new file mode 100644
index 00000000000..a94765f8f2a
--- /dev/null
+++ b/changelogs/unreleased/26595-fix-issue-preselected-template.yml
@@ -0,0 +1,4 @@
+---
+title: Fix linking to new issue with selected template via url parameter
+merge_request:
+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/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/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/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/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/29871-api-remove-merge-requests-comments-endpoints.yml b/changelogs/unreleased/29871-api-remove-merge-requests-comments-endpoints.yml
new file mode 100644
index 00000000000..e3fb62d53b6
--- /dev/null
+++ b/changelogs/unreleased/29871-api-remove-merge-requests-comments-endpoints.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Make the /notes endpoint work with noteable iid instead of id'
+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/30098-banzai-filter-mergerequestreferencefilter-has-an-n-1-query-problem.yml b/changelogs/unreleased/30098-banzai-filter-mergerequestreferencefilter-has-an-n-1-query-problem.yml
new file mode 100644
index 00000000000..f3f4e065aef
--- /dev/null
+++ b/changelogs/unreleased/30098-banzai-filter-mergerequestreferencefilter-has-an-n-1-query-problem.yml
@@ -0,0 +1,4 @@
+---
+title: Improve Markdown rendering when a lot of merge requests are referenced
+merge_request: 10252
+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/30276-move-top-badges.yml b/changelogs/unreleased/30276-move-top-badges.yml
new file mode 100644
index 00000000000..68a8ab359fd
--- /dev/null
+++ b/changelogs/unreleased/30276-move-top-badges.yml
@@ -0,0 +1,4 @@
+---
+title: Move issue, mr, todos next to profile dropdown in top nav
+merge_request:
+author:
diff --git a/changelogs/unreleased/30289-allow-users-to-import-github-projects-to-subgroups.yml b/changelogs/unreleased/30289-allow-users-to-import-github-projects-to-subgroups.yml
new file mode 100644
index 00000000000..a33382a85e3
--- /dev/null
+++ b/changelogs/unreleased/30289-allow-users-to-import-github-projects-to-subgroups.yml
@@ -0,0 +1,4 @@
+---
+title: Allow users to import GitHub projects to subgroups
+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/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/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-environments-folders-route.yml b/changelogs/unreleased/fix-gb-environments-folders-route.yml
new file mode 100644
index 00000000000..fd9d9e6f168
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-environments-folders-route.yml
@@ -0,0 +1,4 @@
+---
+title: Fix environment folder route when special chars present in environment name
+merge_request: 10250
+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/forked-subquery-order.yml b/changelogs/unreleased/forked-subquery-order.yml
new file mode 100644
index 00000000000..06fb8236783
--- /dev/null
+++ b/changelogs/unreleased/forked-subquery-order.yml
@@ -0,0 +1,4 @@
+---
+title: Remove unnecessary ORDER BY clause from `forked_to_project_id` subquery
+merge_request:
+author: mhasbini
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-ci-build-to-lock-on-status-change.yml b/changelogs/unreleased/make-ci-build-to-lock-on-status-change.yml
new file mode 100644
index 00000000000..b0c5eb4ed17
--- /dev/null
+++ b/changelogs/unreleased/make-ci-build-to-lock-on-status-change.yml
@@ -0,0 +1,4 @@
+---
+title: Make CI build to use optimistic locking only on status change
+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/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/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/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/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/config/gitlab.yml.example b/config/gitlab.yml.example
index 3747baf4c3b..bd27f01c872 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
## 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..69c0a91d6f0 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 == 'unix'
+ raise "Unsupported Gitaly address: #{address.inspect}"
+ end
+
+ Gitlab::GitalyClient.configure_channel(name, address)
+ 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 0269857f9fb..f85521fe6d3 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/webpack.config.js b/config/webpack.config.js
index 0859c8416c8..70d98b022c1 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -37,6 +37,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',
@@ -105,6 +106,7 @@ var config = {
'environments_folder',
'issuable',
'merge_conflicts',
+ 'notebook_viewer',
'vue_pipelines',
],
minChunks: function(module, count) {
@@ -115,7 +117,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
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/schema.rb b/db/schema.rb
index 8f32b62bf14..598ee70872b 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1080,6 +1080,14 @@ ActiveRecord::Schema.define(version: 20170322013926) 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"
@@ -1241,6 +1249,7 @@ ActiveRecord::Schema.define(version: 20170322013926) 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
@@ -1312,6 +1321,7 @@ ActiveRecord::Schema.define(version: 20170322013926) 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/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index f58ab4d87af..ffa0831290a 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..b35caf672a8 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://gitab-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/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/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/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/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..7bd3c7ee653 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -28,7 +28,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 +44,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 +56,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!"
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/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..1e46b3faf0b 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,8 @@ 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']
+ require 'spinach/capybara'
+ require 'capybara/rails'
+
+ TestEnv.eager_load_driver_server
end
diff --git a/features/support/env.rb b/features/support/env.rb
index f394c30d52f..26cdd9d746d 100644
--- a/features/support/env.rb
+++ b/features/support/env.rb
@@ -33,3 +33,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/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/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/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/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/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/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/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/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 1ce47ef2b05..a0dbe0a8c11 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -4,28 +4,30 @@ 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)
+ # 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(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/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/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..6fe85af3c30 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
@@ -21,10 +22,10 @@ module Gitlab
RepoPath: repository.path_to_repo,
}
- 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
+ address = Gitlab::GitalyClient.get_address(repository.project.repository_storage)
+ params[:GitalySocketPath] = URI(address).path
+ 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/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/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/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/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/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/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/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/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
index 01b657bcada..bc8cbe30e66 100644
--- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
@@ -43,10 +43,10 @@ describe 'Dropdown hint', js: true, feature: true 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')
- expect(page).to have_css(js_dropdown_hint, text: 'Keep typing and press Enter', visible: false)
+ expect(page).to have_css(js_dropdown_hint, text: 'Press Enter or click to search', visible: false)
expect(dropdown_hint_size).to eq(0)
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/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/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/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/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/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/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/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/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/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/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/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_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/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..7cf39d37181 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -108,6 +108,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/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/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..d1640ffed99 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);
});
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/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/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/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/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/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index e5bba29d85b..d1902766b99 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
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..535c96eeee9 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -184,18 +184,14 @@ describe Gitlab::Workhorse, lib: true do
it { expect(subject).to eq({ GL_ID: "user-#{user.id}", RepoPath: repository.path_to_repo }) }
- context 'when Gitaly socket path is present' do
- let(:gitaly_socket_path) { '/tmp/gitaly.sock' }
-
+ context 'when Gitaly is enabled' do
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 Gitaly params in the returned value' do
- expect(subject).to include({
- GitalyResourcePath: "/projects/#{repository.project.id}/git-http/info-refs",
- GitalySocketPath: gitaly_socket_path,
- })
+ gitaly_socket_path = URI(Gitlab::GitalyClient.get_address('default')).path
+ expect(subject).to include({ GitalySocketPath: gitaly_socket_path })
end
end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 4b72eb2eaa3..f60c5ffb32a 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -184,6 +184,7 @@ 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) }
@@ -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/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/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/namespace_spec.rb b/spec/models/namespace_spec.rb
index 95f4785060c..e1bfd20a6aa 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
@@ -163,16 +163,75 @@ describe Namespace, models: true do
it { expect { @namespace.move_dir }.to raise_error('Namespace cannot be moved, because at least one project has images 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', 'callback' 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/repository_spec.rb b/spec/models/repository_spec.rb
index 585b87b828d..df742ee8084 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1851,4 +1851,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/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/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..91d6fb83c0b 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -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/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/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/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb
index 51021eec63c..383871d5c38 100644
--- a/spec/requests/api/v3/issues_spec.rb
+++ b/spec/requests/api/v3/issues_spec.rb
@@ -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/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/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 fe6a19e97ea..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
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..5c841843b40 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -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,7 +372,7 @@ 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
@@ -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) }
@@ -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)
@@ -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)
@@ -812,7 +849,7 @@ 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' }
@@ -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)
@@ -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 }
@@ -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,7 +1280,7 @@ describe NotificationService, services: true do
describe 'Pipelines' do
describe '#pipeline_finished' do
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
let(:current_user) { create(:user) }
let(:u_member) { create(:user) }
let(:u_other) { create(:user) }
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 193ccd17282..cf1f90becfd 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 e3be1989c93..f8eb34f2ef4 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -7,6 +7,7 @@ describe Projects::ForkService, services: true do
@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
@@ -104,9 +105,10 @@ describe Projects::ForkService, services: true 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/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index a3babaf1e0b..81e15f9dba6 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
@@ -61,7 +61,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) }
@@ -69,7 +69,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..90cde705b85 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
@@ -198,7 +224,9 @@ describe SystemNoteService, services: true do
let(:status) { 'new_status' }
let(:source) { nil }
- it_behaves_like 'a system note'
+ it_behaves_like 'a system note' do
+ let(:action) { 'status' }
+ end
context 'with a source' do
let(:source) { double('commit', gfm_reference: 'commit 123456') }
@@ -216,6 +244,7 @@ describe SystemNoteService, services: true do
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 +252,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 +284,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).
@@ -263,7 +299,9 @@ describe SystemNoteService, services: true do
subject { described_class.change_issue_confidentiality(noteable, project, author) }
context 'when noteable responds to `confidential`' do
- it_behaves_like 'a system note'
+ it_behaves_like 'a system note' do
+ let(:action) { 'confidentiality' }
+ end
it 'sets the note text' do
expect(subject.note).to eq 'made the issue visible to everyone'
@@ -273,10 +311,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 +330,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 +346,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 +364,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 +376,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 +387,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 +413,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 +455,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 +483,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 +576,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 +603,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 +616,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 +643,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 +789,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 +846,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 +880,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 +923,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 +933,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
index 5f79203701a..66f68650f81 100644
--- a/spec/services/users/create_service_spec.rb
+++ b/spec/services/users/create_service_spec.rb
@@ -61,6 +61,23 @@ describe Users::CreateService, services: true do
)
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
diff --git a/spec/services/users/destroy_spec.rb b/spec/services/users/destroy_spec.rb
index 922e82445d0..9a28c03d968 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
@@ -25,7 +25,7 @@ describe Users::DestroyService, services: true do
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..51f1015f43c 100644
--- a/spec/support/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb
@@ -228,5 +228,19 @@ shared_examples 'a GitHub-ish import controller: POST create' do
post :create, { new_name: test_name, format: :js }
end
end
+
+ context 'user has chosen a 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
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..ee492daee30 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,7 +2,7 @@
# 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) }
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..a19a35c2c0d 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
@@ -151,6 +152,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 +169,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..5a7ce2e08c4 100644
--- a/spec/workers/pipeline_notification_worker_spec.rb
+++ b/spec/workers/pipeline_notification_worker_spec.rb
@@ -11,7 +11,7 @@ describe PipelineNotificationWorker do
status: status)
end
- let(:project) { create(:project, public_builds: false) }
+ let(:project) { create(:project, :repository, public_builds: false) }
let(:user) { create(:user) }
let(:pusher) { user }
let(:watcher) { pusher }
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..1c383d0514d 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 }
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