summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md9
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile103
-rw-r--r--Gemfile.lock13
-rw-r--r--app/assets/javascripts/boards/components/board_list.js14
-rw-r--r--app/assets/javascripts/boards/models/issue.js5
-rw-r--r--app/assets/javascripts/boards/models/list.js25
-rw-r--r--app/assets/javascripts/boards/services/board_service.js6
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js6
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js155
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js16
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js10
-rw-r--r--app/assets/javascripts/diff_notes/icons/collapse_icon.svg1
-rw-r--r--app/assets/javascripts/diff_notes/models/discussion.js4
-rw-r--r--app/assets/javascripts/diff_notes/models/note.js13
-rw-r--r--app/assets/javascripts/diff_notes/stores/comments.js6
-rw-r--r--app/assets/javascripts/dispatcher.js7
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax.js6
-rw-r--r--app/assets/javascripts/files_comment_button.js3
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js19
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js7
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js90
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_bundle.js1
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js51
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js185
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js200
-rw-r--r--app/assets/javascripts/issue.js226
-rw-r--r--app/assets/javascripts/main.js6
-rw-r--r--app/assets/javascripts/monitoring/prometheus_graph.js333
-rw-r--r--app/assets/javascripts/notes.js105
-rw-r--r--app/assets/javascripts/test_utils/simulate_drag.js29
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss24
-rw-r--r--app/assets/stylesheets/framework/filters.scss133
-rw-r--r--app/assets/stylesheets/framework/lists.scss38
-rw-r--r--app/assets/stylesheets/framework/mobile.scss3
-rw-r--r--app/assets/stylesheets/framework/nav.scss2
-rw-r--r--app/assets/stylesheets/framework/panels.scss8
-rw-r--r--app/assets/stylesheets/framework/variables.scss9
-rw-r--r--app/assets/stylesheets/pages/commits.scss32
-rw-r--r--app/assets/stylesheets/pages/diff.scss104
-rw-r--r--app/assets/stylesheets/pages/environments.scss68
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss5
-rw-r--r--app/assets/stylesheets/pages/projects.scss2
-rw-r--r--app/assets/stylesheets/pages/tree.scss10
-rw-r--r--app/controllers/admin/applications_controller.rb2
-rw-r--r--app/controllers/admin/impersonation_tokens_controller.rb2
-rw-r--r--app/controllers/concerns/repository_settings_redirect.rb7
-rw-r--r--app/controllers/oauth/authorizations_controller.rb44
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb2
-rw-r--r--app/controllers/projects/boards/issues_controller.rb11
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb30
-rw-r--r--app/controllers/projects/environments_controller.rb15
-rw-r--r--app/controllers/projects/protected_branches_controller.rb36
-rw-r--r--app/controllers/projects/settings/repository_controller.rb50
-rw-r--r--app/controllers/uploads_controller.rb2
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/gitlab_routing_helper.rb4
-rw-r--r--app/helpers/sorting_helper.rb2
-rw-r--r--app/models/chat_team.rb1
-rw-r--r--app/models/ci/build.rb37
-rw-r--r--app/models/concerns/relative_positioning.rb101
-rw-r--r--app/models/environment.rb8
-rw-r--r--app/models/issue.rb1
-rw-r--r--app/models/oauth_access_grant.rb4
-rw-r--r--app/models/oauth_access_token.rb2
-rw-r--r--app/models/personal_access_token.rb11
-rw-r--r--app/models/project.rb11
-rw-r--r--app/models/project_services/monitoring_service.rb16
-rw-r--r--app/models/project_services/prometheus_service.rb93
-rw-r--r--app/models/repository.rb16
-rw-r--r--app/models/service.rb1
-rw-r--r--app/presenters/projects/settings/deploy_keys_presenter.rb60
-rw-r--r--app/services/boards/issues/list_service.rb7
-rw-r--r--app/services/boards/issues/move_service.rb28
-rw-r--r--app/services/issuable_base_service.rb2
-rw-r--r--app/services/issues/create_service.rb1
-rw-r--r--app/services/issues/update_service.rb20
-rw-r--r--app/views/dashboard/issues.html.haml4
-rw-r--r--app/views/dashboard/projects/starred.html.haml2
-rw-r--r--app/views/devise/sessions/two_factor.html.haml2
-rw-r--r--app/views/discussions/_diff_discussion.html.haml2
-rw-r--r--app/views/doorkeeper/authorizations/new.html.haml2
-rw-r--r--app/views/layouts/nav/_project.html.haml29
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml28
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml3
-rw-r--r--app/views/projects/_activity.html.haml1
-rw-r--r--app/views/projects/activity.html.haml1
-rw-r--r--app/views/projects/blob/_blob.html.haml2
-rw-r--r--app/views/projects/boards/components/_board_list.html.haml5
-rw-r--r--app/views/projects/commit/_commit_box.html.haml5
-rw-r--r--app/views/projects/commits/_commit.html.haml51
-rw-r--r--app/views/projects/commits/_commit_list.html.haml2
-rw-r--r--app/views/projects/commits/_commits.html.haml2
-rw-r--r--app/views/projects/deploy_keys/_deploy_key.html.haml2
-rw-r--r--app/views/projects/deploy_keys/_form.html.haml4
-rw-r--r--app/views/projects/deploy_keys/_index.html.haml34
-rw-r--r--app/views/projects/deploy_keys/index.html.haml36
-rw-r--r--app/views/projects/deployments/_actions.haml5
-rw-r--r--app/views/projects/deployments/_deployment.html.haml2
-rw-r--r--app/views/projects/diffs/_line.html.haml18
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml17
-rw-r--r--app/views/projects/edit.html.haml1
-rw-r--r--app/views/projects/environments/_metrics_button.html.haml6
-rw-r--r--app/views/projects/environments/metrics.html.haml21
-rw-r--r--app/views/projects/environments/show.html.haml3
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml6
-rw-r--r--app/views/projects/notes/_note.html.haml10
-rw-r--r--app/views/projects/pages/show.html.haml2
-rw-r--r--app/views/projects/protected_branches/_branches_list.html.haml2
-rw-r--r--app/views/projects/protected_branches/_create_protected_branch.html.haml2
-rw-r--r--app/views/projects/protected_branches/_index.html.haml (renamed from app/views/projects/protected_branches/index.html.haml)7
-rw-r--r--app/views/projects/protected_branches/_protected_branch.html.haml2
-rw-r--r--app/views/projects/settings/_head.html.haml33
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml1
-rw-r--r--app/views/projects/settings/integrations/show.html.haml1
-rw-r--r--app/views/projects/settings/members/show.html.haml1
-rw-r--r--app/views/projects/settings/repository/show.html.haml5
-rw-r--r--app/views/search/_results.html.haml2
-rw-r--r--app/views/shared/icons/_collapse.svg.erb1
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml13
-rw-r--r--app/workers/post_receive.rb4
-rw-r--r--changelogs/unreleased/1381-present-commits-pagination-headers-correctly.yml4
-rw-r--r--changelogs/unreleased/18962-update-issues-button-jumps.yml4
-rw-r--r--changelogs/unreleased/26188-tag-creation-404-for-guests.yml4
-rw-r--r--changelogs/unreleased/26202-change-dropdown-style-slightly.yml4
-rw-r--r--changelogs/unreleased/26732-combine-deploy-keys-and-push-rules-and-mirror-repository-and-protect-branches-settings-pages.yml5
-rw-r--r--changelogs/unreleased/27568-refactor-very-slow-dropdown-asignee-spec.yml4
-rw-r--r--changelogs/unreleased/27936-make-all-uploads-require-revalidation-on-each-browser-fetch.yml4
-rw-r--r--changelogs/unreleased/28402-fix-starred-projects-filter-wrong-message-on-no-results.yml4
-rw-r--r--changelogs/unreleased/28447-hybrid-repository-storages.yml4
-rw-r--r--changelogs/unreleased/28609-fix-redirect-to-home-page-url.yml4
-rw-r--r--changelogs/unreleased/28850-fix-broken-migration.yml4
-rw-r--r--changelogs/unreleased/29034-fix-github-importer.yml4
-rw-r--r--changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml4
-rw-r--r--changelogs/unreleased/add-changelog-filtered-search-visual-tokens.yml4
-rw-r--r--changelogs/unreleased/clear-connections-before-starting-sidekiq.yml4
-rw-r--r--changelogs/unreleased/dm-dont-copy-toolip.yml4
-rw-r--r--changelogs/unreleased/dm-fix-api-create-file-on-empty-repo.yml4
-rw-r--r--changelogs/unreleased/dm-fix-cherry-pick.yml4
-rw-r--r--changelogs/unreleased/es6-class-issue.yml4
-rw-r--r--changelogs/unreleased/feature-openid-connect.yml4
-rw-r--r--changelogs/unreleased/fix-29093.yml4
-rw-r--r--changelogs/unreleased/fix-gb-deprecate-ci-config-types.yml4
-rw-r--r--changelogs/unreleased/issue_16834.yml4
-rw-r--r--changelogs/unreleased/priority-to-label-priority.yml4
-rw-r--r--changelogs/unreleased/rfr-20170307-change-default-project-number-limit.yml4
-rw-r--r--changelogs/unreleased/settings-tab.yml4
-rw-r--r--changelogs/unreleased/tc-api-pipeline-jobs.yml4
-rw-r--r--changelogs/unreleased/use-v3-api-on-frontend.yml4
-rw-r--r--changelogs/unreleased/zj-variables-build-job.yml4
-rw-r--r--config/application.rb1
-rw-r--r--config/gitlab.yml.example6
-rw-r--r--config/initializers/1_settings.rb17
-rw-r--r--config/initializers/6_validations.rb20
-rw-r--r--config/initializers/doorkeeper.rb11
-rw-r--r--config/initializers/doorkeeper_openid_connect.rb36
-rw-r--r--config/initializers/rspec_profiling.rb37
-rw-r--r--config/initializers/secret_token.rb7
-rw-r--r--config/initializers/sidekiq.rb6
-rw-r--r--config/locales/doorkeeper.en.yml1
-rw-r--r--config/routes.rb2
-rw-r--r--config/routes/project.rb2
-rw-r--r--config/webpack.config.js1
-rw-r--r--db/migrate/20140502125220_migrate_repo_size.rb2
-rw-r--r--db/migrate/20160615142710_add_index_on_requested_at_to_members.rb8
-rw-r--r--db/migrate/20160620115026_add_index_on_runners_locked.rb8
-rw-r--r--db/migrate/20160715134306_add_index_for_pipeline_user_id.rb8
-rw-r--r--db/migrate/20160805041956_add_deleted_at_to_namespaces.rb9
-rw-r--r--db/migrate/20160808085602_add_index_for_build_token.rb6
-rw-r--r--db/migrate/20160819221631_add_index_to_note_discussion_id.rb6
-rw-r--r--db/migrate/20160819232256_add_incoming_email_token_to_users.rb9
-rw-r--r--db/migrate/20160919145149_add_group_id_to_labels.rb8
-rw-r--r--db/migrate/20160920160832_add_index_to_labels_title.rb6
-rw-r--r--db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb10
-rw-r--r--db/migrate/20161106185620_add_project_import_data_project_index.rb6
-rw-r--r--db/migrate/20161124111395_add_index_to_parent_id.rb6
-rw-r--r--db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb2
-rw-r--r--db/migrate/20161202152035_add_index_to_routes.rb7
-rw-r--r--db/migrate/20161209153400_add_unique_index_for_environment_slug.rb6
-rw-r--r--db/migrate/20161209165216_create_doorkeeper_openid_connect_tables.rb37
-rw-r--r--db/migrate/20161220141214_remove_dot_git_from_group_names.rb2
-rw-r--r--db/migrate/20161226122833_remove_dot_git_from_usernames.rb4
-rw-r--r--db/migrate/20170131221752_add_relative_position_to_issues.rb37
-rw-r--r--db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb6
-rw-r--r--db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb7
-rw-r--r--db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb6
-rw-r--r--db/migrate/20170210103609_add_index_to_user_agent_detail.rb8
-rw-r--r--db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb8
-rw-r--r--db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb20
-rw-r--r--db/schema.rb12
-rw-r--r--doc/README.md2
-rw-r--r--doc/administration/auth/crowd.md68
-rw-r--r--doc/administration/auth/img/crowd_application.pngbin0 -> 55811 bytes
-rw-r--r--doc/administration/container_registry.md58
-rw-r--r--doc/administration/high_availability/database.md4
-rw-r--r--doc/administration/high_availability/load_balancer.md6
-rw-r--r--doc/administration/repository_storage_paths.md15
-rw-r--r--doc/api/jobs.md120
-rw-r--r--doc/api/projects.md2
-rw-r--r--doc/api/repositories.md12
-rw-r--r--doc/api/repository_files.md39
-rw-r--r--doc/api/settings.md6
-rw-r--r--doc/api/v3_to_v4.md15
-rw-r--r--doc/ci/variables/README.md78
-rw-r--r--doc/ci/yaml/README.md4
-rw-r--r--doc/integration/README.md1
-rw-r--r--doc/integration/crowd.md64
-rw-r--r--doc/integration/omniauth.md2
-rw-r--r--doc/integration/openid_connect_provider.md47
-rw-r--r--doc/update/8.17-to-9.0.md65
-rw-r--r--doc/user/project/container_registry.md2
-rw-r--r--features/project/active_tab.feature20
-rw-r--r--features/steps/project/active_tab.rb32
-rw-r--r--features/steps/project/deploy_keys.rb2
-rw-r--r--features/steps/shared/project_tab.rb6
-rw-r--r--features/support/capybara.rb2
-rw-r--r--fixtures/emojis/emoji-unicode-version-map.json2377
-rw-r--r--lib/api/commits.rb28
-rw-r--r--lib/api/files.rb62
-rw-r--r--lib/api/helpers/internal_helpers.rb6
-rw-r--r--lib/api/jobs.rb39
-rw-r--r--lib/api/repositories.rb55
-rw-r--r--lib/api/services.rb9
-rw-r--r--lib/api/v3/builds.rb24
-rw-r--r--lib/api/v3/repositories.rb54
-rw-r--r--lib/backup/repository.rb5
-rw-r--r--lib/gitlab/auth.rb14
-rw-r--r--lib/gitlab/emoji.rb2
-rw-r--r--lib/gitlab/git/repository.rb12
-rw-r--r--lib/gitlab/github_import/branch_formatter.rb2
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb10
-rw-r--r--lib/gitlab/middleware/go.rb66
-rw-r--r--lib/gitlab/prometheus.rb70
-rw-r--r--lib/tasks/gemojione.rake5
-rw-r--r--lib/tasks/gitlab/check.rake16
-rw-r--r--lib/tasks/gitlab/cleanup.rake6
-rw-r--r--lib/tasks/gitlab/import.rake3
-rw-r--r--lib/tasks/gitlab/info.rake4
-rw-r--r--lib/tasks/gitlab/task_helpers.rb6
-rw-r--r--rubocop/cop/migration/add_concurrent_index.rb34
-rw-r--r--rubocop/rubocop.rb1
-rw-r--r--spec/controllers/admin/applications_controller_spec.rb65
-rw-r--r--spec/controllers/profiles/personal_access_tokens_spec.rb26
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb1
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb46
-rw-r--r--spec/controllers/projects/settings/repository_controller_spec.rb20
-rw-r--r--spec/controllers/uploads_controller_spec.rb82
-rw-r--r--spec/factories/chat_teams.rb9
-rw-r--r--spec/factories/oauth_access_grants.rb11
-rw-r--r--spec/factories/oauth_access_tokens.rb3
-rw-r--r--spec/factories/oauth_applications.rb2
-rw-r--r--spec/factories/projects.rb15
-rw-r--r--spec/features/boards/boards_spec.rb18
-rw-r--r--spec/features/boards/issue_ordering_spec.rb166
-rw-r--r--spec/features/boards/sidebar_spec.rb4
-rw-r--r--spec/features/dashboard/user_filters_projects_spec.rb37
-rw-r--r--spec/features/dashboard_issues_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb82
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb7
-rw-r--r--spec/features/issues/filtered_search/dropdown_hint_spec.rb63
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb31
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb86
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb367
-rw-r--r--spec/features/issues/filtered_search/search_bar_spec.rb4
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb352
-rw-r--r--spec/features/merge_requests/diff_notes_avatars_spec.rb136
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb3
-rw-r--r--spec/features/merge_requests/filter_merge_requests_spec.rb88
-rw-r--r--spec/features/merge_requests/reset_filters_spec.rb32
-rw-r--r--spec/features/projects/environments/environment_metrics_spec.rb39
-rw-r--r--spec/features/projects/environments/environment_spec.rb (renamed from spec/features/environment_spec.rb)19
-rw-r--r--spec/features/projects/environments/environments_spec.rb (renamed from spec/features/environments_spec.rb)0
-rw-r--r--spec/features/projects/labels/issues_sorted_by_priority_spec.rb4
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb8
-rw-r--r--spec/features/search_spec.rb13
-rw-r--r--spec/features/security/project/internal_access_spec.rb14
-rw-r--r--spec/features/security/project/private_access_spec.rb14
-rw-r--r--spec/features/security/project/public_access_spec.rb14
-rw-r--r--spec/features/todos/todos_sorting_spec.rb8
-rw-r--r--spec/fixtures/api/schemas/issue.json1
-rw-r--r--spec/initializers/6_validations_spec.rb28
-rw-r--r--spec/initializers/doorkeeper_spec.rb71
-rw-r--r--spec/initializers/secret_token_spec.rb25
-rw-r--r--spec/javascripts/awards_handler_spec.js3
-rw-r--r--spec/javascripts/boards/boards_store_spec.js75
-rw-r--r--spec/javascripts/boards/issue_spec.js16
-rw-r--r--spec/javascripts/boards/list_spec.js3
-rw-r--r--spec/javascripts/diff_comments_store_spec.js11
-rw-r--r--spec/javascripts/filtered_search/dropdown_user_spec.js8
-rw-r--r--spec/javascripts/filtered_search/dropdown_utils_spec.js25
-rw-r--r--spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js72
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js232
-rw-r--r--spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js600
-rw-r--r--spec/javascripts/fixtures/environments/metrics.html.haml12
-rw-r--r--spec/javascripts/helpers/filtered_search_spec_helper.js52
-rw-r--r--spec/javascripts/issue_spec.js43
-rw-r--r--spec/javascripts/monitoring/prometheus_graph_spec.js78
-rw-r--r--spec/javascripts/monitoring/prometheus_mock_data.js1014
-rw-r--r--spec/lib/gitlab/auth_spec.rb30
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb26
-rw-r--r--spec/lib/gitlab/github_import/branch_formatter_spec.rb12
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/pull_request_formatter_spec.rb40
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb95
-rw-r--r--spec/lib/gitlab/prometheus_spec.rb143
-rw-r--r--spec/models/chat_team_spec.rb5
-rw-r--r--spec/models/ci/build_spec.rb41
-rw-r--r--spec/models/concerns/relative_positioning_spec.rb104
-rw-r--r--spec/models/environment_spec.rb83
-rw-r--r--spec/models/namespace_spec.rb2
-rw-r--r--spec/models/personal_access_token_spec.rb24
-rw-r--r--spec/models/project_services/prometheus_service_spec.rb104
-rw-r--r--spec/models/project_spec.rb8
-rw-r--r--spec/models/repository_spec.rb29
-rw-r--r--spec/presenters/projects/settings/deploy_keys_presenter_spec.rb66
-rw-r--r--spec/requests/api/api_internal_helpers_spec.rb2
-rw-r--r--spec/requests/api/commits_spec.rb94
-rw-r--r--spec/requests/api/doorkeeper_access_spec.rb18
-rw-r--r--spec/requests/api/files_spec.rb182
-rw-r--r--spec/requests/api/jobs_spec.rb76
-rw-r--r--spec/requests/api/oauth_tokens_spec.rb22
-rw-r--r--spec/requests/api/repositories_spec.rb100
-rw-r--r--spec/requests/api/session_spec.rb18
-rw-r--r--spec/requests/api/v3/repositories_spec.rb222
-rw-r--r--spec/requests/api/v3/services_spec.rb4
-rw-r--r--spec/requests/git_http_spec.rb12
-rw-r--r--spec/requests/openid_connect_spec.rb134
-rw-r--r--spec/routing/openid_connect_spec.rb30
-rw-r--r--spec/rubocop/cop/migration/add_concurrent_index_spec.rb41
-rw-r--r--spec/services/boards/issues/list_service_spec.rb26
-rw-r--r--spec/services/boards/issues/move_service_spec.rb18
-rw-r--r--spec/services/issues/update_service_spec.rb16
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb104
-rw-r--r--spec/services/todo_service_spec.rb2
-rw-r--r--spec/support/capybara.rb2
-rw-r--r--spec/support/filtered_search_helpers.rb43
-rw-r--r--spec/support/prometheus_helpers.rb117
-rw-r--r--spec/support/test_env.rb2
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb4
-rw-r--r--spec/views/projects/commit/_commit_box.html.haml_spec.rb28
-rw-r--r--spec/workers/post_receive_spec.rb2
-rw-r--r--vendor/gitignore/Android.gitignore3
-rw-r--r--vendor/gitignore/Global/Eclipse.gitignore5
-rw-r--r--vendor/gitignore/Global/JetBrains.gitignore1
-rw-r--r--vendor/gitignore/Global/SBT.gitignore3
-rw-r--r--vendor/gitignore/Java.gitignore1
-rw-r--r--vendor/gitignore/Maven.gitignore2
-rw-r--r--vendor/gitignore/Node.gitignore2
-rw-r--r--vendor/gitignore/Objective-C.gitignore4
-rw-r--r--vendor/gitignore/PlayFramework.gitignore1
-rw-r--r--vendor/gitignore/Python.gitignore3
-rw-r--r--vendor/gitignore/Scala.gitignore21
-rw-r--r--vendor/gitignore/Swift.gitignore2
-rw-r--r--vendor/gitignore/Symfony.gitignore4
-rw-r--r--vendor/gitignore/TeX.gitignore9
-rw-r--r--vendor/gitignore/VisualStudio.gitignore11
-rw-r--r--vendor/gitlab-ci-yml/Android.gitlab-ci.yml51
-rw-r--r--vendor/gitlab-ci-yml/Bash.gitlab-ci.yml35
-rw-r--r--vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml1
-rw-r--r--vendor/gitlab-ci-yml/Django.gitlab-ci.yml34
-rw-r--r--vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml7
-rw-r--r--vendor/gitlab-ci-yml/LICENSE2
-rw-r--r--vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml78
-rw-r--r--vendor/gitlab-ci-yml/Maven.gitlab-ci.yml5
-rw-r--r--vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml (renamed from vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml)6
-rw-r--r--vendor/gitlab-ci-yml/PHP.gitlab-ci.yml33
-rw-r--r--vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml6
-rw-r--r--vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml14
-rw-r--r--vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml3
-rw-r--r--vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml3
373 files changed, 12167 insertions, 1787 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e075de055e3..42e094bdfc6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,15 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 8.17.3 (2017-03-07)
+
+- Fix the redirect to custom home page URL. !9518
+- Fix broken migration when upgrading straight to 8.17.1. !9613
+- Make projects dropdown only show projects you are a member of. !9614
+- Fix creating a file in an empty repo using the API. !9632
+- Don't copy tooltip when copying GFM.
+- Fix cherry-picking or reverting through an MR.
+
## 8.17.2 (2017-03-01)
- Expire all webpack assets after 8.17.1 included a badly compiled asset. !9602
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 627a3f43a64..0062ac97180 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-4.1.1
+5.0.0
diff --git a/Gemfile b/Gemfile
index 4ac5a0ccfc1..2f813324a35 100644
--- a/Gemfile
+++ b/Gemfile
@@ -18,25 +18,26 @@ gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.24.0'
# Authentication libraries
-gem 'devise', '~> 4.2'
-gem 'doorkeeper', '~> 4.2.0'
-gem 'omniauth', '~> 1.4.2'
-gem 'omniauth-auth0', '~> 1.4.1'
-gem 'omniauth-azure-oauth2', '~> 0.0.6'
-gem 'omniauth-cas3', '~> 1.1.2'
-gem 'omniauth-facebook', '~> 4.0.0'
-gem 'omniauth-github', '~> 1.1.1'
-gem 'omniauth-gitlab', '~> 1.0.2'
+gem 'devise', '~> 4.2'
+gem 'doorkeeper', '~> 4.2.0'
+gem 'doorkeeper-openid_connect', '~> 1.1.0'
+gem 'omniauth', '~> 1.4.2'
+gem 'omniauth-auth0', '~> 1.4.1'
+gem 'omniauth-azure-oauth2', '~> 0.0.6'
+gem 'omniauth-cas3', '~> 1.1.2'
+gem 'omniauth-facebook', '~> 4.0.0'
+gem 'omniauth-github', '~> 1.1.1'
+gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.4.1'
-gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
+gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-oauth2-generic', '~> 0.2.2'
-gem 'omniauth-saml', '~> 1.7.0'
-gem 'omniauth-shibboleth', '~> 1.2.0'
-gem 'omniauth-twitter', '~> 1.2.0'
-gem 'omniauth_crowd', '~> 2.2.0'
-gem 'omniauth-authentiq', '~> 0.3.0'
-gem 'rack-oauth2', '~> 1.2.1'
-gem 'jwt', '~> 1.5.6'
+gem 'omniauth-saml', '~> 1.7.0'
+gem 'omniauth-shibboleth', '~> 1.2.0'
+gem 'omniauth-twitter', '~> 1.2.0'
+gem 'omniauth_crowd', '~> 2.2.0'
+gem 'omniauth-authentiq', '~> 0.3.0'
+gem 'rack-oauth2', '~> 1.2.1'
+gem 'jwt', '~> 1.5.6'
# Spam and anti-bot protection
gem 'recaptcha', '~> 3.0', require: 'recaptcha/rails'
@@ -68,9 +69,9 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
gem 'github-linguist', '~> 4.7.0', require: 'linguist'
# API
-gem 'grape', '~> 0.19.0'
+gem 'grape', '~> 0.19.0'
gem 'grape-entity', '~> 0.6.0'
-gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
+gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
# Pagination
gem 'kaminari', '~> 0.17.0'
@@ -102,19 +103,19 @@ gem 'unf', '~> 0.1.4'
gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing
-gem 'html-pipeline', '~> 1.11.0'
-gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
-gem 'gitlab-markup', '~> 1.5.1'
-gem 'redcarpet', '~> 3.4'
-gem 'RedCloth', '~> 4.3.2'
-gem 'rdoc', '~> 4.2'
-gem 'org-ruby', '~> 0.9.12'
-gem 'creole', '~> 0.5.0'
-gem 'wikicloth', '0.8.1'
-gem 'asciidoctor', '~> 1.5.2'
+gem 'html-pipeline', '~> 1.11.0'
+gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
+gem 'gitlab-markup', '~> 1.5.1'
+gem 'redcarpet', '~> 3.4'
+gem 'RedCloth', '~> 4.3.2'
+gem 'rdoc', '~> 4.2'
+gem 'org-ruby', '~> 0.9.12'
+gem 'creole', '~> 0.5.0'
+gem 'wikicloth', '0.8.1'
+gem 'asciidoctor', '~> 1.5.2'
gem 'asciidoctor-plantuml', '0.0.7'
-gem 'rouge', '~> 2.0'
-gem 'truncato', '~> 0.7.8'
+gem 'rouge', '~> 2.0'
+gem 'truncato', '~> 0.7.8'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
@@ -229,18 +230,18 @@ gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2'
-gem 'addressable', '~> 2.3.8'
-gem 'bootstrap-sass', '~> 3.3.0'
+gem 'addressable', '~> 2.3.8'
+gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.7'
-gem 'gemojione', '~> 3.0'
-gem 'gon', '~> 6.1.0'
+gem 'gemojione', '~> 3.0'
+gem 'gon', '~> 6.1.0'
gem 'jquery-atwho-rails', '~> 1.3.2'
-gem 'jquery-rails', '~> 4.1.0'
-gem 'request_store', '~> 1.3'
-gem 'select2-rails', '~> 3.5.9'
-gem 'virtus', '~> 1.0.1'
-gem 'net-ssh', '~> 3.0.1'
-gem 'base32', '~> 0.3.0'
+gem 'jquery-rails', '~> 4.1.0'
+gem 'request_store', '~> 1.3'
+gem 'select2-rails', '~> 3.5.9'
+gem 'virtus', '~> 1.0.1'
+gem 'net-ssh', '~> 3.0.1'
+gem 'base32', '~> 0.3.0'
# Sentry integration
gem 'sentry-raven', '~> 2.0.0'
@@ -278,13 +279,13 @@ group :development, :test do
gem 'awesome_print', '~> 1.2.0', require: false
gem 'fuubar', '~> 2.0.0'
- gem 'database_cleaner', '~> 1.5.0'
+ gem 'database_cleaner', '~> 1.5.0'
gem 'factory_girl_rails', '~> 4.7.0'
- gem 'rspec-rails', '~> 3.5.0'
- gem 'rspec-retry', '~> 0.4.5'
- gem 'spinach-rails', '~> 0.2.1'
+ gem 'rspec-rails', '~> 3.5.0'
+ gem 'rspec-retry', '~> 0.4.5'
+ gem 'spinach-rails', '~> 0.2.1'
gem 'spinach-rerun-reporter', '~> 0.0.2'
- gem 'rspec_profiling', '~> 0.0.5'
+ gem 'rspec_profiling', '~> 0.0.5'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0'
@@ -292,13 +293,13 @@ group :development, :test do
# Generate Fake data
gem 'ffaker', '~> 2.4'
- gem 'capybara', '~> 2.6.2'
+ gem 'capybara', '~> 2.6.2'
gem 'capybara-screenshot', '~> 1.0.0'
- gem 'poltergeist', '~> 1.9.0'
+ gem 'poltergeist', '~> 1.9.0'
- gem 'spring', '~> 1.7.0'
- gem 'spring-commands-rspec', '~> 1.0.4'
- gem 'spring-commands-spinach', '~> 1.1.0'
+ gem 'spring', '~> 1.7.0'
+ gem 'spring-commands-rspec', '~> 1.0.4'
+ gem 'spring-commands-spinach', '~> 1.1.0'
gem 'rubocop', '~> 0.47.1', require: false
gem 'rubocop-rspec', '~> 1.12.0', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index d4131a3dede..62388628eaa 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -78,6 +78,7 @@ GEM
better_errors (1.0.1)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
+ bindata (2.3.5)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootstrap-sass (3.3.6)
@@ -167,6 +168,9 @@ GEM
unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.2.0)
railties (>= 4.2)
+ doorkeeper-openid_connect (1.1.2)
+ doorkeeper (~> 4.0)
+ json-jwt (~> 1.6)
dropzonejs-rails (0.7.2)
rails (> 3.1)
email_reply_trimmer (0.1.6)
@@ -376,6 +380,12 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (1.8.6)
+ json-jwt (1.7.1)
+ activesupport
+ bindata
+ multi_json (>= 1.3)
+ securecompare
+ url_safe_base64
json-schema (2.6.2)
addressable (~> 2.3.8)
jwt (1.5.6)
@@ -684,6 +694,7 @@ GEM
scss_lint (0.47.1)
rake (>= 0.9, < 11)
sass (~> 3.4.15)
+ securecompare (1.0.0)
seed-fu (2.3.6)
activerecord (>= 3.1)
activesupport (>= 3.1)
@@ -789,6 +800,7 @@ GEM
get_process_mem (~> 0)
unicorn (>= 4, < 6)
uniform_notifier (1.10.0)
+ url_safe_base64 (0.2.2)
validates_hostname (1.0.6)
activerecord (>= 3.0)
activesupport (>= 3.0)
@@ -866,6 +878,7 @@ DEPENDENCIES
devise-two-factor (~> 3.0.0)
diffy (~> 3.1.0)
doorkeeper (~> 4.2.0)
+ doorkeeper-openid_connect (~> 1.1.0)
dropzonejs-rails (~> 0.7.1)
email_reply_trimmer (~> 0.1)
email_spec (~> 1.6.0)
diff --git a/app/assets/javascripts/boards/components/board_list.js b/app/assets/javascripts/boards/components/board_list.js
index 2d52e96e7fb..1330d4ae840 100644
--- a/app/assets/javascripts/boards/components/board_list.js
+++ b/app/assets/javascripts/boards/components/board_list.js
@@ -56,11 +56,6 @@ import boardCard from './board_card';
});
}
},
- computed: {
- orderedIssues () {
- return _.sortBy(this.issues, 'priority');
- },
- },
methods: {
listHeight () {
return this.$refs.list.getBoundingClientRect().height;
@@ -92,9 +87,9 @@ import boardCard from './board_card';
const options = gl.issueBoards.getBoardSortableDefaultOptions({
scroll: document.querySelectorAll('.boards-list')[0],
group: 'issues',
- sort: false,
disabled: this.disabled,
filter: '.board-list-count, .is-disabled',
+ dataIdAttr: 'data-issue-id',
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
@@ -111,6 +106,13 @@ import boardCard from './board_card';
e.item.remove();
});
},
+ onUpdate: (e) => {
+ const sortedArray = this.sortable.toArray().filter(id => id !== '-1');
+ gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray);
+ },
+ onMove(e) {
+ return !e.related.classList.contains('board-list-count');
+ }
});
this.sortable = Sortable.create(this.$refs.list, options);
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 2d0a295ae4d..ca5e6fa7e9d 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -15,6 +15,7 @@ class ListIssue {
this.labels = [];
this.selected = false;
this.assignee = false;
+ this.position = obj.relative_position || Infinity;
if (obj.assignee) {
this.assignee = new ListUser(obj.assignee);
@@ -27,10 +28,6 @@ class ListIssue {
obj.labels.forEach((label) => {
this.labels.push(new ListLabel(label));
});
-
- this.priority = this.labels.reduce((max, label) => {
- return (label.priority < max) ? label.priority : max;
- }, Infinity);
}
addLabel (label) {
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 8158ed4ec2c..f237567208c 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -110,9 +110,20 @@ class List {
}
addIssue (issue, listFrom, newIndex) {
+ let moveBeforeIid = null;
+ let moveAfterIid = null;
+
if (!this.findIssue(issue.id)) {
if (newIndex !== undefined) {
this.issues.splice(newIndex, 0, issue);
+
+ if (this.issues[newIndex - 1]) {
+ moveBeforeIid = this.issues[newIndex - 1].id;
+ }
+
+ if (this.issues[newIndex + 1]) {
+ moveAfterIid = this.issues[newIndex + 1].id;
+ }
} else {
this.issues.push(issue);
}
@@ -123,13 +134,21 @@ class List {
if (listFrom) {
this.issuesSize += 1;
- this.updateIssueLabel(issue, listFrom);
+
+ this.updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid);
}
}
}
- updateIssueLabel(issue, listFrom) {
- gl.boardService.moveIssue(issue.id, listFrom.id, this.id)
+ moveIssue (issue, oldIndex, newIndex, moveBeforeIid, moveAfterIid) {
+ this.issues.splice(oldIndex, 1);
+ this.issues.splice(newIndex, 0, issue);
+
+ gl.boardService.moveIssue(issue.id, null, null, moveBeforeIid, moveAfterIid);
+ }
+
+ updateIssueLabel(issue, listFrom, moveBeforeIid, moveAfterIid) {
+ gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeIid, moveAfterIid)
.then(() => {
listFrom.getIssues(false);
});
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index 065e90518df..e54102814d6 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -64,10 +64,12 @@ class BoardService {
return this.issues.get(data);
}
- moveIssue (id, from_list_id, to_list_id) {
+ moveIssue (id, from_list_id = null, to_list_id = null, move_before_iid = null, move_after_iid = null) {
return this.issue.update({ id }, {
from_list_id,
- to_list_id
+ to_list_id,
+ move_before_iid,
+ move_after_iid,
});
}
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 56436c8fdc7..3866c6bbfc6 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -109,6 +109,12 @@
listFrom.removeIssue(issue);
}
},
+ moveIssueInList (list, issue, oldIndex, newIndex, idArray) {
+ const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
+ const afterId = parseInt(idArray[newIndex + 1], 10) || null;
+
+ list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
+ },
findList (key, val, type = 'label') {
return this.state.lists.filter((list) => {
const byType = type ? list['type'] === type : true;
diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
new file mode 100644
index 00000000000..788daa96b3d
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -0,0 +1,155 @@
+/* global CommentsStore Cookies notes */
+import Vue from 'vue';
+import collapseIcon from '../icons/collapse_icon.svg';
+
+(() => {
+ const DiffNoteAvatars = Vue.extend({
+ props: ['discussionId'],
+ data() {
+ return {
+ isVisible: false,
+ lineType: '',
+ storeState: CommentsStore.state,
+ shownAvatars: 3,
+ collapseIcon,
+ };
+ },
+ template: `
+ <div class="diff-comment-avatar-holders"
+ v-show="notesCount !== 0">
+ <div v-if="!isVisible">
+ <img v-for="note in notesSubset"
+ class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar"
+ width="19"
+ height="19"
+ role="button"
+ data-container="body"
+ data-placement="top"
+ :data-line-type="lineType"
+ :title="note.authorName + ': ' + note.noteTruncated"
+ :src="note.authorAvatar"
+ @click="clickedAvatar($event)" />
+ <span v-if="notesCount > shownAvatars"
+ class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
+ data-container="body"
+ data-placement="top"
+ ref="extraComments"
+ role="button"
+ :data-line-type="lineType"
+ :title="extraNotesTitle"
+ @click="clickedAvatar($event)">{{ moreText }}</span>
+ </div>
+ <button class="diff-notes-collapse js-diff-comment-avatar"
+ type="button"
+ aria-label="Show comments"
+ :data-line-type="lineType"
+ @click="clickedAvatar($event)"
+ v-if="isVisible"
+ v-html="collapseIcon">
+ </button>
+ </div>
+ `,
+ mounted() {
+ this.$nextTick(() => {
+ this.addNoCommentClass();
+ this.setDiscussionVisible();
+
+ this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new';
+ });
+
+ $(document).on('toggle.comments', () => {
+ this.$nextTick(() => {
+ this.setDiscussionVisible();
+ });
+ });
+ },
+ destroyed() {
+ $(document).off('toggle.comments');
+ },
+ watch: {
+ storeState: {
+ handler() {
+ this.$nextTick(() => {
+ $('.has-tooltip', this.$el).tooltip('fixTitle');
+
+ // We need to add/remove a class to an element that is outside the Vue instance
+ this.addNoCommentClass();
+ });
+ },
+ deep: true,
+ },
+ },
+ computed: {
+ notesSubset() {
+ let notes = [];
+
+ if (this.discussion) {
+ notes = Object.keys(this.discussion.notes)
+ .slice(0, this.shownAvatars)
+ .map(noteId => this.discussion.notes[noteId]);
+ }
+
+ return notes;
+ },
+ extraNotesTitle() {
+ if (this.discussion) {
+ const extra = this.discussion.notesCount() - this.shownAvatars;
+
+ return `${extra} more comment${extra > 1 ? 's' : ''}`;
+ }
+
+ return '';
+ },
+ discussion() {
+ return this.storeState[this.discussionId];
+ },
+ notesCount() {
+ if (this.discussion) {
+ return this.discussion.notesCount();
+ }
+
+ return 0;
+ },
+ moreText() {
+ const plusSign = this.notesCount < 100 ? '+' : '';
+
+ return `${plusSign}${this.notesCount - this.shownAvatars}`;
+ },
+ },
+ methods: {
+ clickedAvatar(e) {
+ notes.addDiffNote(e);
+
+ // Toggle the active state of the toggle all button
+ this.toggleDiscussionsToggleState();
+
+ this.$nextTick(() => {
+ this.setDiscussionVisible();
+
+ $('.has-tooltip', this.$el).tooltip('fixTitle');
+ $('.has-tooltip', this.$el).tooltip('hide');
+ });
+ },
+ addNoCommentClass() {
+ const notesCount = this.notesCount;
+
+ $(this.$el).closest('.js-avatar-container')
+ .toggleClass('js-no-comment-btn', notesCount > 0)
+ .nextUntil('.js-avatar-container')
+ .toggleClass('js-no-comment-btn', notesCount > 0);
+ },
+ toggleDiscussionsToggleState() {
+ const $notesHolders = $(this.$el).closest('.code').find('.notes_holder');
+ const $visibleNotesHolders = $notesHolders.filter(':visible');
+ const $toggleDiffCommentsBtn = $(this.$el).closest('.diff-file').find('.js-toggle-diff-comments');
+
+ $toggleDiffCommentsBtn.toggleClass('active', $notesHolders.length === $visibleNotesHolders.length);
+ },
+ setDiscussionVisible() {
+ this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible');
+ },
+ },
+ });
+
+ Vue.component('diff-note-avatars', DiffNoteAvatars);
+})();
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
index d1873d6c7a2..fbd980f0fce 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js
@@ -11,7 +11,10 @@ const Vue = require('vue');
discussionId: String,
resolved: Boolean,
canResolve: Boolean,
- resolvedBy: String
+ resolvedBy: String,
+ authorName: String,
+ authorAvatar: String,
+ noteTruncated: String,
},
data: function () {
return {
@@ -98,7 +101,16 @@ const Vue = require('vue');
CommentsStore.delete(this.discussionId, this.noteId);
},
created: function () {
- CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy);
+ CommentsStore.create({
+ discussionId: this.discussionId,
+ noteId: this.noteId,
+ canResolve: this.canResolve,
+ resolved: this.resolved,
+ resolvedBy: this.resolvedBy,
+ authorName: this.authorName,
+ authorAvatar: this.authorAvatar,
+ noteTruncated: this.noteTruncated,
+ });
this.note = this.discussion.getNote(this.noteId);
}
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
index cadf8b96b87..7d8316dfd63 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
@@ -13,6 +13,7 @@ require('./components/jump_to_discussion');
require('./components/resolve_btn');
require('./components/resolve_count');
require('./components/resolve_discussion_btn');
+require('./components/diff_note_avatars');
$(() => {
const projectPath = document.querySelector('.merge-request').dataset.projectPath;
@@ -24,6 +25,15 @@ $(() => {
window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath);
gl.diffNotesCompileComponents = () => {
+ $('diff-note-avatars').each(function () {
+ const tmp = Vue.extend({
+ template: $(this).get(0).outerHTML
+ });
+ const tmpApp = new tmp().$mount();
+
+ $(this).replaceWith(tmpApp.$el);
+ });
+
const $components = $(COMPONENT_SELECTOR).filter(function () {
return $(this).closest('resolve-count').length !== 1;
});
diff --git a/app/assets/javascripts/diff_notes/icons/collapse_icon.svg b/app/assets/javascripts/diff_notes/icons/collapse_icon.svg
new file mode 100644
index 00000000000..bd4b393cfaa
--- /dev/null
+++ b/app/assets/javascripts/diff_notes/icons/collapse_icon.svg
@@ -0,0 +1 @@
+<svg width="11" height="11" viewBox="0 0 9 13"><path d="M2.57568253,6.49866948 C2.50548852,6.57199715 2.44637866,6.59708255 2.39835118,6.57392645 C2.3503237,6.55077034 2.32631032,6.48902165 2.32631032,6.38867852 L2.32631032,-2.13272614 C2.32631032,-2.23306927 2.3503237,-2.29481796 2.39835118,-2.31797406 C2.44637866,-2.34113017 2.50548852,-2.31604477 2.57568253,-2.24271709 L6.51022184,1.86747129 C6.53977721,1.8983461 6.56379059,1.93500939 6.5822627,1.97746225 L6.5822627,2.27849013 C6.56379059,2.31708364 6.53977721,2.35374693 6.51022184,2.38848109 L2.57568253,6.49866948 Z" transform="translate(4.454287, 2.127976) rotate(90.000000) translate(-4.454287, -2.127976) "></path><path d="M3.74312342,2.09553332 C3.74312342,1.99519019 3.77821989,1.9083561 3.8484139,1.83502843 C3.91860791,1.76170075 4.00173115,1.72503747 4.09778611,1.72503747 L4.80711151,1.72503747 C4.90316647,1.72503747 4.98628971,1.76170075 5.05648372,1.83502843 C5.12667773,1.9083561 5.16177421,1.99519019 5.16177421,2.09553332 L5.16177421,10.2464421 C5.16177421,10.3467853 5.12667773,10.4336194 5.05648372,10.506947 C4.98628971,10.5802747 4.90316647,10.616938 4.80711151,10.616938 L4.09778611,10.616938 C4.00173115,10.616938 3.91860791,10.5802747 3.8484139,10.506947 C3.77821989,10.4336194 3.74312342,10.3467853 3.74312342,10.2464421 L3.74312342,2.09553332 Z" transform="translate(4.452449, 6.170988) rotate(-90.000000) translate(-4.452449, -6.170988) "></path><path d="M2.57568253,14.6236695 C2.50548852,14.6969971 2.44637866,14.7220826 2.39835118,14.6989264 C2.3503237,14.6757703 2.32631032,14.6140216 2.32631032,14.5136785 L2.32631032,5.99227386 C2.32631032,5.89193073 2.3503237,5.83018204 2.39835118,5.80702594 C2.44637866,5.78386983 2.50548852,5.80895523 2.57568253,5.88228291 L6.51022184,9.99247129 C6.53977721,10.0233461 6.56379059,10.0600094 6.5822627,10.1024622 L6.5822627,10.4034901 C6.56379059,10.4420836 6.53977721,10.4787469 6.51022184,10.5134811 L2.57568253,14.6236695 Z" transform="translate(4.454287, 10.252976) scale(1, -1) rotate(90.000000) translate(-4.454287, -10.252976) "></path></svg>
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js b/app/assets/javascripts/diff_notes/models/discussion.js
index fa518ba4d33..dce1a9b58bd 100644
--- a/app/assets/javascripts/diff_notes/models/discussion.js
+++ b/app/assets/javascripts/diff_notes/models/discussion.js
@@ -10,8 +10,8 @@ class DiscussionModel {
this.canResolve = false;
}
- createNote (noteId, canResolve, resolved, resolved_by) {
- Vue.set(this.notes, noteId, new NoteModel(this.id, noteId, canResolve, resolved, resolved_by));
+ createNote (noteObj) {
+ Vue.set(this.notes, noteObj.noteId, new NoteModel(this.id, noteObj));
}
deleteNote (noteId) {
diff --git a/app/assets/javascripts/diff_notes/models/note.js b/app/assets/javascripts/diff_notes/models/note.js
index f3a7cba5ef6..04465aa507e 100644
--- a/app/assets/javascripts/diff_notes/models/note.js
+++ b/app/assets/javascripts/diff_notes/models/note.js
@@ -1,12 +1,15 @@
/* eslint-disable camelcase, no-unused-vars */
class NoteModel {
- constructor(discussionId, noteId, canResolve, resolved, resolved_by) {
+ constructor(discussionId, noteObj) {
this.discussionId = discussionId;
- this.id = noteId;
- this.canResolve = canResolve;
- this.resolved = resolved;
- this.resolved_by = resolved_by;
+ this.id = noteObj.noteId;
+ this.canResolve = noteObj.canResolve;
+ this.resolved = noteObj.resolved;
+ this.resolved_by = noteObj.resolvedBy;
+ this.authorName = noteObj.authorName;
+ this.authorAvatar = noteObj.authorAvatar;
+ this.noteTruncated = noteObj.noteTruncated;
}
}
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js b/app/assets/javascripts/diff_notes/stores/comments.js
index c80d979b977..69c4d7a8434 100644
--- a/app/assets/javascripts/diff_notes/stores/comments.js
+++ b/app/assets/javascripts/diff_notes/stores/comments.js
@@ -21,10 +21,10 @@
return discussion;
},
- create: function (discussionId, noteId, canResolve, resolved, resolved_by) {
- const discussion = this.createDiscussion(discussionId);
+ create: function (noteObj) {
+ const discussion = this.createDiscussion(noteObj.discussionId);
- discussion.createNote(noteId, canResolve, resolved, resolved_by);
+ discussion.createNote(noteObj);
},
update: function (discussionId, noteId, resolved, resolved_by) {
const discussion = this.state[discussionId];
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 31f10f89245..017980271b1 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -1,10 +1,10 @@
+import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make this a bundle
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
/* global UsernameValidator */
/* global ActiveTabMemoizer */
/* global ShortcutsNavigation */
/* global Build */
/* global Issuable */
-/* global Issue */
/* global ShortcutsIssuable */
/* global ZenMode */
/* global Milestone */
@@ -34,6 +34,7 @@
/* global ProjectShow */
/* global Labels */
/* global Shortcuts */
+import Issue from './issue';
import BindInOut from './behaviors/bind_in_out';
import GroupsList from './groups_list';
@@ -286,7 +287,7 @@ const UserCallout = require('./user_callout');
case 'search:show':
new Search();
break;
- case 'projects:protected_branches:index':
+ case 'projects:repository:show':
new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList();
break;
@@ -297,6 +298,8 @@ const UserCallout = require('./user_callout');
case 'ci:lints:show':
new gl.CILintEditor();
break;
+ case 'projects:environments:metrics':
+ new PrometheusGraph();
case 'users:show':
new UserCallout();
break;
diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js
index 5cdf11c6a2c..f61be741b4a 100644
--- a/app/assets/javascripts/droplab/droplab_ajax.js
+++ b/app/assets/javascripts/droplab/droplab_ajax.js
@@ -37,11 +37,14 @@ require('../window')(function(w){
}
}
- self.hook.list[config.method].call(self.hook.list, data);
+ if (!self.destroyed) {
+ self.hook.list[config.method].call(self.hook.list, data);
+ }
},
init: function init(hook) {
var self = this;
+ self.destroyed = false;
self.cache = self.cache || {};
var config = hook.config.droplabAjax;
this.hook = hook;
@@ -79,6 +82,7 @@ require('../window')(function(w){
destroy: function() {
var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+ this.destroyed = true;
if (this.listTemplate && dynamicList) {
dynamicList.outerHTML = this.listTemplate;
}
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index 6d86888dcb8..bf84f2a0a8f 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -38,6 +38,9 @@
FilesCommentButton.prototype.render = function(e) {
var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button;
$currentTarget = $(e.currentTarget);
+
+ if ($currentTarget.hasClass('js-no-comment-btn')) return;
+
lineContentElement = this.getLineContent($currentTarget);
buttonParentElement = this.getButtonParent($currentTarget);
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
index 9e92d544bef..38ff3fb7158 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -28,6 +28,23 @@ require('./filtered_search_dropdown');
const tag = selected.querySelector('.js-filter-tag').innerText.trim();
if (tag.length) {
+ // Get previous input values in the input field and convert them into visual tokens
+ const previousInputValues = this.input.value.split(' ');
+ const searchTerms = [];
+
+ previousInputValues.forEach((value, index) => {
+ searchTerms.push(value);
+
+ if (index === previousInputValues.length - 1
+ && token.indexOf(value.toLowerCase()) !== -1) {
+ searchTerms.pop();
+ }
+ });
+
+ if (searchTerms.length > 0) {
+ gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' '));
+ }
+
gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''));
}
this.dismissDropdown();
@@ -39,7 +56,7 @@ require('./filtered_search_dropdown');
renderContent() {
const dropdownData = [];
- [].forEach.call(this.input.parentElement.querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
+ [].forEach.call(this.input.closest('.filtered-search-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
const { icon, hint, tag } = dropdownMenu.dataset;
if (icon && hint && tag) {
dropdownData.push({
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index 7e9c6f74aa5..04e2afad02f 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -39,7 +39,12 @@ require('./filtered_search_dropdown');
getSearchInput() {
const query = gl.DropdownUtils.getSearchInput(this.input);
const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
- let value = lastToken.value || '';
+
+ let value = lastToken || '';
+
+ if (value[0] === '@') {
+ value = value.slice(1);
+ }
// Removes the first character if it is a quotation so that we can search
// with multiple words
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index de3fa116717..a5a6b56a0d3 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -22,38 +22,40 @@
static filterWithSymbol(filterSymbol, input, item) {
const updatedItem = item;
- const query = gl.DropdownUtils.getSearchInput(input);
- const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query);
+ const searchInput = gl.DropdownUtils.getSearchInput(input);
- if (lastToken !== searchToken) {
- const title = updatedItem.title.toLowerCase();
- let value = lastToken.value.toLowerCase();
+ const title = updatedItem.title.toLowerCase();
+ let value = searchInput.toLowerCase();
+ let symbol = '';
- // Removes the first character if it is a quotation so that we can search
- // with multiple words
- if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
- value = value.slice(1);
- }
-
- // Eg. filterSymbol = ~ for labels
- const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1;
- const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1;
+ // Remove the symbol for filter
+ if (value[0] === filterSymbol) {
+ symbol = value[0];
+ value = value.slice(1);
+ }
- updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
- } else {
- updatedItem.droplab_hidden = false;
+ // Removes the first character if it is a quotation so that we can search
+ // with multiple words
+ if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
+ value = value.slice(1);
}
+ // Eg. filterSymbol = ~ for labels
+ const matchWithoutSymbol = symbol === filterSymbol && title.indexOf(value) !== -1;
+ const match = title.indexOf(`${symbol}${value}`) !== -1;
+
+ updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
+
return updatedItem;
}
static filterHint(input, item) {
const updatedItem = item;
- const query = gl.DropdownUtils.getSearchInput(input);
- let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
+ const searchInput = gl.DropdownUtils.getSearchInput(input);
+ let { lastToken } = gl.FilteredSearchTokenizer.processTokens(searchInput);
lastToken = lastToken.key || lastToken || '';
- if (!lastToken || query.split('').last() === ' ') {
+ if (!lastToken || searchInput.split('').last() === ' ') {
updatedItem.droplab_hidden = false;
} else if (lastToken) {
const split = lastToken.split(':');
@@ -70,13 +72,59 @@
const dataValue = selected.getAttribute('data-value');
if (dataValue) {
- gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue);
+ gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true);
}
// Return boolean based on whether it was set
return dataValue !== null;
}
+ // Determines the full search query (visual tokens + input)
+ static getSearchQuery(untilInput = false) {
+ const tokens = [].slice.call(document.querySelectorAll('.tokens-container li'));
+ const values = [];
+
+ if (untilInput) {
+ const inputIndex = _.findIndex(tokens, t => t.classList.contains('input-token'));
+ // Add one to include input-token to the tokens array
+ tokens.splice(inputIndex + 1);
+ }
+
+ tokens.forEach((token) => {
+ if (token.classList.contains('js-visual-token')) {
+ const name = token.querySelector('.name');
+ const value = token.querySelector('.value');
+ const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
+ let valueText = '';
+
+ if (value && value.innerText) {
+ valueText = value.innerText;
+ }
+
+ if (token.className.indexOf('filtered-search-token') !== -1) {
+ values.push(`${name.innerText.toLowerCase()}:${symbol}${valueText}`);
+ } else {
+ values.push(name.innerText);
+ }
+ } else if (token.classList.contains('input-token')) {
+ const { isLastVisualTokenValid } =
+ gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ const input = document.querySelector('.filtered-search');
+ const inputValue = input && input.value;
+
+ if (isLastVisualTokenValid) {
+ values.push(inputValue);
+ } else {
+ const previous = values.pop();
+ values.push(`${previous}${inputValue}`);
+ }
+ }
+ });
+
+ return values.join(' ');
+ }
+
static getSearchInput(filteredSearchInput) {
const inputValue = filteredSearchInput.value;
const { right } = gl.DropdownUtils.getInputSelectionPosition(filteredSearchInput);
diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
index faaba994f46..856eb6590ee 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
@@ -7,3 +7,4 @@ require('./filtered_search_dropdown');
require('./filtered_search_manager');
require('./filtered_search_token_keys');
require('./filtered_search_tokenizer');
+require('./filtered_search_visual_tokens');
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
index dd565da507e..134bdc6ad80 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
@@ -35,7 +35,7 @@
if (!dataValueSet) {
const value = getValueFunction(selected);
- gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value);
+ gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
}
this.dismissDropdown();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index cecd3518ce3..e1a97070439 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -58,35 +58,15 @@
};
}
- static addWordToInput(tokenName, tokenValue = '') {
+ static addWordToInput(tokenName, tokenValue = '', clicked = false) {
const input = document.querySelector('.filtered-search');
- const inputValue = input.value;
- const word = `${tokenName}:${tokenValue}`;
- // Get the string to replace
- let newCaretPosition = input.selectionStart;
- const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input);
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
+ input.value = '';
- input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`;
-
- // If we have added a tokenValue at the end of the input,
- // add a space and set selection to the end
- if (right >= inputValue.length && tokenValue !== '') {
- input.value += ' ';
- newCaretPosition = input.value.length;
+ if (clicked) {
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
}
-
- gl.FilteredSearchDropdownManager.updateInputCaretPosition(newCaretPosition, input);
- }
-
- static updateInputCaretPosition(selectionStart, input) {
- // Reset the position
- // Sometimes can end up at end of input
- input.setSelectionRange(selectionStart, selectionStart);
-
- const { right } = gl.DropdownUtils.getInputSelectionPosition(input);
-
- input.setSelectionRange(right, right);
}
updateCurrentDropdownOffset() {
@@ -94,19 +74,14 @@
}
updateDropdownOffset(key) {
- if (!this.font) {
- this.font = window.getComputedStyle(this.filteredSearchInput).font;
- }
-
- const input = this.filteredSearchInput;
- const inputText = input.value.slice(0, input.selectionStart);
- const filterIconPadding = 27;
- let offset = gl.text.getTextWidth(inputText, this.font) + filterIconPadding;
+ // Always align dropdown with the input field
+ let offset = this.filteredSearchInput.getBoundingClientRect().left - document.querySelector('.scroll-container').getBoundingClientRect().left;
- const currentDropdownWidth = this.mapping[key].element.clientWidth === 0 ? 200 :
- this.mapping[key].element.clientWidth;
- const offsetMaxWidth = this.filteredSearchInput.clientWidth - currentDropdownWidth;
+ const maxInputWidth = 240;
+ const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
+ // Make sure offset never exceeds the input container
+ const offsetMaxWidth = document.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
if (offsetMaxWidth < offset) {
offset = offsetMaxWidth;
}
@@ -164,8 +139,8 @@
}
setDropdown() {
- const { lastToken, searchToken } = this.tokenizer
- .processTokens(gl.DropdownUtils.getSearchInput(this.filteredSearchInput));
+ const query = gl.DropdownUtils.getSearchQuery(true);
+ const { lastToken, searchToken } = this.tokenizer.processTokens(query);
if (this.currentDropdown) {
this.updateCurrentDropdownOffset();
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index bbafead0305..638fe744668 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -3,6 +3,7 @@
constructor(page) {
this.filteredSearchInput = document.querySelector('.filtered-search');
this.clearSearchButton = document.querySelector('.clear-search');
+ this.tokensContainer = document.querySelector('.tokens-container');
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
if (this.filteredSearchInput) {
@@ -27,36 +28,62 @@
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
+ this.handleInputPlaceholderWrapper = this.handleInputPlaceholder.bind(this);
+ this.handleInputVisualTokenWrapper = this.handleInputVisualToken.bind(this);
this.checkForEnterWrapper = this.checkForEnter.bind(this);
this.clearSearchWrapper = this.clearSearch.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
+ this.removeSelectedTokenWrapper = this.removeSelectedToken.bind(this);
+ this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
+ this.editTokenWrapper = this.editToken.bind(this);
this.tokenChange = this.tokenChange.bind(this);
this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
+ this.filteredSearchInput.addEventListener('input', this.handleInputPlaceholderWrapper);
+ this.filteredSearchInput.addEventListener('input', this.handleInputVisualTokenWrapper);
this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.addEventListener('click', this.tokenChange);
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
+ this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
+ this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
+ document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
+ document.addEventListener('click', this.unselectEditTokensWrapper);
+ document.addEventListener('keydown', this.removeSelectedTokenWrapper);
}
unbindEvents() {
this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
+ this.filteredSearchInput.removeEventListener('input', this.handleInputPlaceholderWrapper);
+ this.filteredSearchInput.removeEventListener('input', this.handleInputVisualTokenWrapper);
this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.removeEventListener('click', this.tokenChange);
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
+ this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
+ this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
+ document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
+ document.removeEventListener('click', this.unselectEditTokensWrapper);
+ document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
}
checkForBackspace(e) {
// 8 = Backspace Key
// 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) {
+ const { lastVisualToken } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (this.filteredSearchInput.value === '' && lastVisualToken) {
+ this.filteredSearchInput.value = gl.FilteredSearchVisualTokens.getLastTokenPartial();
+ gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+ }
+
// Reposition dropdown so that it is aligned with cursor
this.dropdownManager.updateCurrentDropdownOffset();
}
@@ -86,11 +113,68 @@
}
}
- toggleClearSearchButton(e) {
- if (e.target.value) {
- this.clearSearchButton.classList.remove('hidden');
- } else {
- this.clearSearchButton.classList.add('hidden');
+ static selectToken(e) {
+ const button = e.target.closest('.selectable');
+
+ if (button) {
+ e.preventDefault();
+ e.stopPropagation();
+ gl.FilteredSearchVisualTokens.selectToken(button);
+ }
+ }
+
+ unselectEditTokens(e) {
+ const inputContainer = document.querySelector('.filtered-search-input-container');
+ const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
+ const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null;
+ const isElementTokensContainer = e.target.classList.contains('tokens-container');
+
+ if ((!isElementInFilteredSearch && !isElementInFilterDropdown) || isElementTokensContainer) {
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ this.dropdownManager.resetDropdowns();
+ }
+ }
+
+ editToken(e) {
+ const token = e.target.closest('.js-visual-token');
+
+ if (token) {
+ gl.FilteredSearchVisualTokens.editToken(token);
+ this.tokenChange();
+ }
+ }
+
+ toggleClearSearchButton() {
+ const query = gl.DropdownUtils.getSearchQuery();
+ const hidden = 'hidden';
+ const hasHidden = this.clearSearchButton.classList.contains(hidden);
+
+ if (query.length === 0 && !hasHidden) {
+ this.clearSearchButton.classList.add(hidden);
+ } else if (query.length && hasHidden) {
+ this.clearSearchButton.classList.remove(hidden);
+ }
+ }
+
+ handleInputPlaceholder() {
+ const query = gl.DropdownUtils.getSearchQuery();
+ const placeholder = 'Search or filter results...';
+ const currentPlaceholder = this.filteredSearchInput.placeholder;
+
+ if (query.length === 0 && currentPlaceholder !== placeholder) {
+ this.filteredSearchInput.placeholder = placeholder;
+ } else if (query.length > 0 && currentPlaceholder !== '') {
+ this.filteredSearchInput.placeholder = '';
+ }
+ }
+
+ removeSelectedToken(e) {
+ // 8 = Backspace Key
+ // 46 = Delete Key
+ if (e.keyCode === 8 || e.keyCode === 46) {
+ gl.FilteredSearchVisualTokens.removeSelectedToken();
+ this.handleInputPlaceholder();
+ this.toggleClearSearchButton();
}
}
@@ -98,11 +182,67 @@
e.preventDefault();
this.filteredSearchInput.value = '';
+
+ const removeElements = [];
+
+ [].forEach.call(this.tokensContainer.children, (t) => {
+ if (t.classList.contains('js-visual-token')) {
+ removeElements.push(t);
+ }
+ });
+
+ removeElements.forEach((el) => {
+ el.parentElement.removeChild(el);
+ });
+
this.clearSearchButton.classList.add('hidden');
+ this.handleInputPlaceholder();
this.dropdownManager.resetDropdowns();
}
+ handleInputVisualToken() {
+ const input = this.filteredSearchInput;
+ const { tokens, searchToken }
+ = gl.FilteredSearchTokenizer.processTokens(input.value);
+ const { isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (isLastVisualTokenValid) {
+ tokens.forEach((t) => {
+ input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, '');
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`);
+ });
+
+ const fragments = searchToken.split(':');
+ if (fragments.length > 1) {
+ const inputValues = fragments[0].split(' ');
+ const tokenKey = inputValues.last();
+
+ if (inputValues.length > 1) {
+ inputValues.pop();
+ const searchTerms = inputValues.join(' ');
+
+ input.value = input.value.replace(searchTerms, '');
+ gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms);
+ }
+
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenKey);
+ input.value = input.value.replace(`${tokenKey}:`, '');
+ }
+ } else {
+ // Keep listening to token until we determine that the user is done typing the token value
+ const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g;
+
+ if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') {
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(searchToken);
+
+ // Trim the last space as seen in the if statement above
+ input.value = input.value.replace(searchToken, '').trim();
+ }
+ }
+ }
+
handleFormSubmit(e) {
e.preventDefault();
this.search();
@@ -111,7 +251,7 @@
loadSearchParamsFromURL() {
const params = gl.utils.getUrlParamsArray();
const usernameParams = this.getUsernameParams();
- const inputValues = [];
+ let hasFilteredSearch = false;
params.forEach((p) => {
const split = p.split('=');
@@ -122,7 +262,8 @@
const condition = this.filteredSearchTokenKeys.searchByConditionUrl(p);
if (condition) {
- inputValues.push(`${condition.tokenKey}:${condition.value}`);
+ hasFilteredSearch = true;
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(condition.tokenKey, condition.value);
} else {
// Sanitize value since URL converts spaces into +
// Replace before decode so that we know what was originally + versus the encoded +
@@ -140,34 +281,37 @@
quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
}
- inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
+ hasFilteredSearch = true;
+ gl.FilteredSearchVisualTokens.addFilterVisualToken(sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
} else if (!match && keyParam === 'assignee_id') {
const id = parseInt(value, 10);
if (usernameParams[id]) {
- inputValues.push(`assignee:@${usernameParams[id]}`);
+ hasFilteredSearch = true;
+ gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', `@${usernameParams[id]}`);
}
} else if (!match && keyParam === 'author_id') {
const id = parseInt(value, 10);
if (usernameParams[id]) {
- inputValues.push(`author:@${usernameParams[id]}`);
+ hasFilteredSearch = true;
+ gl.FilteredSearchVisualTokens.addFilterVisualToken('author', `@${usernameParams[id]}`);
}
} else if (!match && keyParam === 'search') {
- inputValues.push(sanitizedValue);
+ hasFilteredSearch = true;
+ this.filteredSearchInput.value = sanitizedValue;
}
}
});
- // Trim the last space value
- this.filteredSearchInput.value = inputValues.join(' ');
-
- if (inputValues.length > 0) {
+ if (hasFilteredSearch) {
this.clearSearchButton.classList.remove('hidden');
+ this.handleInputPlaceholder();
}
}
search() {
const paths = [];
- const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value);
+ const { tokens, searchToken }
+ = this.tokenizer.processTokens(gl.DropdownUtils.getSearchQuery());
const currentState = gl.utils.getParameterByName('state') || 'opened';
paths.push(`state=${currentState}`);
@@ -219,10 +363,13 @@
tokenChange() {
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
- const currentDropdownRef = dropdown.reference;
- this.setDropdownWrapper();
- currentDropdownRef.dispatchInputEvent();
+ if (dropdown) {
+ const currentDropdownRef = dropdown.reference;
+
+ this.setDropdownWrapper();
+ currentDropdownRef.dispatchInputEvent();
+ }
}
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
new file mode 100644
index 00000000000..320afa26130
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -0,0 +1,200 @@
+class FilteredSearchVisualTokens {
+ static getLastVisualTokenBeforeInput() {
+ const inputLi = document.querySelector('.input-token');
+ const lastVisualToken = inputLi && inputLi.previousElementSibling;
+
+ return {
+ lastVisualToken,
+ isLastVisualTokenValid: lastVisualToken === null || lastVisualToken.className.indexOf('filtered-search-term') !== -1 || (lastVisualToken && lastVisualToken.querySelector('.value') !== null),
+ };
+ }
+
+ static unselectTokens() {
+ const otherTokens = document.querySelectorAll('.js-visual-token .selectable.selected');
+ [].forEach.call(otherTokens, t => t.classList.remove('selected'));
+ }
+
+ static selectToken(tokenButton) {
+ const selected = tokenButton.classList.contains('selected');
+ FilteredSearchVisualTokens.unselectTokens();
+
+ if (!selected) {
+ tokenButton.classList.add('selected');
+ }
+ }
+
+ static removeSelectedToken() {
+ const selected = document.querySelector('.js-visual-token .selected');
+
+ if (selected) {
+ const li = selected.closest('.js-visual-token');
+ li.parentElement.removeChild(li);
+ }
+ }
+
+ static createVisualTokenElementHTML() {
+ return `
+ <div class="selectable" role="button">
+ <div class="name"></div>
+ <div class="value"></div>
+ </div>
+ `;
+ }
+
+ static addVisualTokenElement(name, value, isSearchTerm) {
+ const li = document.createElement('li');
+ li.classList.add('js-visual-token');
+ li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token');
+
+ if (value) {
+ li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML();
+ li.querySelector('.value').innerText = value;
+ } else {
+ li.innerHTML = '<div class="name"></div>';
+ }
+ li.querySelector('.name').innerText = name;
+
+ const tokensContainer = document.querySelector('.tokens-container');
+ const input = document.querySelector('.filtered-search');
+ tokensContainer.insertBefore(li, input.parentElement);
+ }
+
+ static addValueToPreviousVisualTokenElement(value) {
+ const { lastVisualToken, isLastVisualTokenValid } =
+ FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (!isLastVisualTokenValid && lastVisualToken.classList.contains('filtered-search-token')) {
+ const name = FilteredSearchVisualTokens.getLastTokenPartial();
+ lastVisualToken.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML();
+ lastVisualToken.querySelector('.name').innerText = name;
+ lastVisualToken.querySelector('.value').innerText = value;
+ }
+ }
+
+ static addFilterVisualToken(tokenName, tokenValue) {
+ const { lastVisualToken, isLastVisualTokenValid }
+ = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement;
+
+ if (isLastVisualTokenValid) {
+ addVisualTokenElement(tokenName, tokenValue);
+ } else {
+ const previousTokenName = lastVisualToken.querySelector('.name').innerText;
+ const tokensContainer = document.querySelector('.tokens-container');
+ tokensContainer.removeChild(lastVisualToken);
+
+ const value = tokenValue || tokenName;
+ addVisualTokenElement(previousTokenName, value);
+ }
+ }
+
+ static addSearchVisualToken(searchTerm) {
+ const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (lastVisualToken && lastVisualToken.classList.contains('filtered-search-term')) {
+ lastVisualToken.querySelector('.name').innerText += ` ${searchTerm}`;
+ } else {
+ FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, true);
+ }
+ }
+
+ static getLastTokenPartial() {
+ const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (!lastVisualToken) return '';
+
+ const value = lastVisualToken.querySelector('.value');
+ const name = lastVisualToken.querySelector('.name');
+
+ const valueText = value ? value.innerText : '';
+ const nameText = name ? name.innerText : '';
+
+ return valueText || nameText;
+ }
+
+ static removeLastTokenPartial() {
+ const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (lastVisualToken) {
+ const value = lastVisualToken.querySelector('.value');
+
+ if (value) {
+ const button = lastVisualToken.querySelector('.selectable');
+ button.removeChild(value);
+ lastVisualToken.innerHTML = button.innerHTML;
+ } else {
+ lastVisualToken.closest('.tokens-container').removeChild(lastVisualToken);
+ }
+ }
+ }
+
+ static tokenizeInput() {
+ const input = document.querySelector('.filtered-search');
+ const { isLastVisualTokenValid } =
+ gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (input.value) {
+ if (isLastVisualTokenValid) {
+ gl.FilteredSearchVisualTokens.addSearchVisualToken(input.value);
+ } else {
+ FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement(input.value);
+ }
+
+ input.value = '';
+ }
+ }
+
+ static editToken(token) {
+ const input = document.querySelector('.filtered-search');
+
+ FilteredSearchVisualTokens.tokenizeInput();
+
+ // Replace token with input field
+ const tokenContainer = token.parentElement;
+ const inputLi = input.parentElement;
+ tokenContainer.replaceChild(inputLi, token);
+
+ const name = token.querySelector('.name');
+ const value = token.querySelector('.value');
+
+ if (token.classList.contains('filtered-search-token')) {
+ FilteredSearchVisualTokens.addFilterVisualToken(name.innerText);
+ input.value = value.innerText;
+ } else {
+ // token is a search term
+ input.value = name.innerText;
+ }
+
+ // Opens dropdown
+ const inputEvent = new Event('input');
+ input.dispatchEvent(inputEvent);
+
+ // Adds cursor to input
+ input.focus();
+ }
+
+ static moveInputToTheRight() {
+ const input = document.querySelector('.filtered-search');
+ const inputLi = input.parentElement;
+ const tokenContainer = document.querySelector('.tokens-container');
+
+ FilteredSearchVisualTokens.tokenizeInput();
+
+ if (!tokenContainer.lastElementChild.isEqualNode(inputLi)) {
+ const { isLastVisualTokenValid } =
+ gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ if (!isLastVisualTokenValid) {
+ const lastPartial = gl.FilteredSearchVisualTokens.getLastTokenPartial();
+ gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+ gl.FilteredSearchVisualTokens.addSearchVisualToken(lastPartial);
+ }
+
+ tokenContainer.removeChild(inputLi);
+ tokenContainer.appendChild(inputLi);
+ }
+ }
+}
+
+window.gl = window.gl || {};
+gl.FilteredSearchVisualTokens = FilteredSearchVisualTokens;
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 52457f70d90..ef4029a8623 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -5,131 +5,125 @@ require('./flash');
require('vendor/jquery.waitforimages');
require('./task_list');
-(function() {
- var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
-
- this.Issue = (function() {
- function Issue() {
- this.submitNoteForm = bind(this.submitNoteForm, this);
- if ($('a.btn-close').length) {
- this.taskList = new gl.TaskList({
- dataType: 'issue',
- fieldName: 'description',
- selector: '.detail-page-description',
- onSuccess: (result) => {
- document.querySelector('#task_status').innerText = result.task_status;
- document.querySelector('#task_status_short').innerText = result.task_status_short;
- }
- });
- this.initIssueBtnEventListeners();
- }
- this.initMergeRequests();
- this.initRelatedBranches();
- this.initCanCreateBranch();
+class Issue {
+ constructor() {
+ if ($('a.btn-close').length) {
+ this.taskList = new gl.TaskList({
+ dataType: 'issue',
+ fieldName: 'description',
+ selector: '.detail-page-description',
+ onSuccess: (result) => {
+ document.querySelector('#task_status').innerText = result.task_status;
+ document.querySelector('#task_status_short').innerText = result.task_status_short;
+ }
+ });
+ Issue.initIssueBtnEventListeners();
}
+ Issue.initMergeRequests();
+ Issue.initRelatedBranches();
+ Issue.initCanCreateBranch();
+ }
- Issue.prototype.initIssueBtnEventListeners = function() {
- var _this, issueFailMessage;
- _this = this;
- issueFailMessage = 'Unable to update this issue at this time.';
- return $('a.btn-close, a.btn-reopen').on('click', function(e) {
- var $this, isClose, shouldSubmit, url;
- e.preventDefault();
- e.stopImmediatePropagation();
- $this = $(this);
- isClose = $this.hasClass('btn-close');
- shouldSubmit = $this.hasClass('btn-comment');
- if (shouldSubmit) {
- _this.submitNoteForm($this.closest('form'));
- }
- $this.prop('disabled', true);
- url = $this.attr('href');
- return $.ajax({
- type: 'PUT',
- url: url,
- error: function(jqXHR, textStatus, errorThrown) {
- var issueStatus;
- issueStatus = isClose ? 'close' : 'open';
- return new Flash(issueFailMessage, 'alert');
- },
- success: function(data, textStatus, jqXHR) {
- if ('id' in data) {
- $(document).trigger('issuable:change');
- const currentTotal = Number($('.issue_counter').text());
- if (isClose) {
- $('a.btn-close').addClass('hidden');
- $('a.btn-reopen').removeClass('hidden');
- $('div.status-box-closed').removeClass('hidden');
- $('div.status-box-open').addClass('hidden');
- $('.issue_counter').text(currentTotal - 1);
- } else {
- $('a.btn-reopen').addClass('hidden');
- $('a.btn-close').removeClass('hidden');
- $('div.status-box-closed').addClass('hidden');
- $('div.status-box-open').removeClass('hidden');
- $('.issue_counter').text(currentTotal + 1);
- }
+ static initIssueBtnEventListeners() {
+ var issueFailMessage;
+ issueFailMessage = 'Unable to update this issue at this time.';
+ return $('a.btn-close, a.btn-reopen').on('click', function(e) {
+ var $this, isClose, shouldSubmit, url;
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ $this = $(this);
+ isClose = $this.hasClass('btn-close');
+ shouldSubmit = $this.hasClass('btn-comment');
+ if (shouldSubmit) {
+ Issue.submitNoteForm($this.closest('form'));
+ }
+ $this.prop('disabled', true);
+ url = $this.attr('href');
+ return $.ajax({
+ type: 'PUT',
+ url: url,
+ error: function(jqXHR, textStatus, errorThrown) {
+ var issueStatus;
+ issueStatus = isClose ? 'close' : 'open';
+ return new Flash(issueFailMessage, 'alert');
+ },
+ success: function(data, textStatus, jqXHR) {
+ if ('id' in data) {
+ $(document).trigger('issuable:change');
+ const currentTotal = Number($('.issue_counter').text());
+ if (isClose) {
+ $('a.btn-close').addClass('hidden');
+ $('a.btn-reopen').removeClass('hidden');
+ $('div.status-box-closed').removeClass('hidden');
+ $('div.status-box-open').addClass('hidden');
+ $('.issue_counter').text(currentTotal - 1);
} else {
- new Flash(issueFailMessage, 'alert');
+ $('a.btn-reopen').addClass('hidden');
+ $('a.btn-close').removeClass('hidden');
+ $('div.status-box-closed').addClass('hidden');
+ $('div.status-box-open').removeClass('hidden');
+ $('.issue_counter').text(currentTotal + 1);
}
- return $this.prop('disabled', false);
+ } else {
+ new Flash(issueFailMessage, 'alert');
}
- });
+ return $this.prop('disabled', false);
+ }
});
- };
+ });
+ }
- Issue.prototype.submitNoteForm = function(form) {
- var noteText;
- noteText = form.find("textarea.js-note-text").val();
- if (noteText.trim().length > 0) {
- return form.submit();
- }
- };
+ static submitNoteForm(form) {
+ var noteText;
+ noteText = form.find("textarea.js-note-text").val();
+ if (noteText.trim().length > 0) {
+ return form.submit();
+ }
+ }
- Issue.prototype.initMergeRequests = function() {
- var $container;
- $container = $('#merge-requests');
- return $.getJSON($container.data('url')).error(function() {
- return new Flash('Failed to load referenced merge requests', 'alert');
- }).success(function(data) {
- if ('html' in data) {
- return $container.html(data.html);
- }
- });
- };
+ static initMergeRequests() {
+ var $container;
+ $container = $('#merge-requests');
+ return $.getJSON($container.data('url')).error(function() {
+ return new Flash('Failed to load referenced merge requests', 'alert');
+ }).success(function(data) {
+ if ('html' in data) {
+ return $container.html(data.html);
+ }
+ });
+ }
- Issue.prototype.initRelatedBranches = function() {
- var $container;
- $container = $('#related-branches');
- return $.getJSON($container.data('url')).error(function() {
- return new Flash('Failed to load related branches', 'alert');
- }).success(function(data) {
- if ('html' in data) {
- return $container.html(data.html);
- }
- });
- };
+ static initRelatedBranches() {
+ var $container;
+ $container = $('#related-branches');
+ return $.getJSON($container.data('url')).error(function() {
+ return new Flash('Failed to load related branches', 'alert');
+ }).success(function(data) {
+ if ('html' in data) {
+ return $container.html(data.html);
+ }
+ });
+ }
- Issue.prototype.initCanCreateBranch = function() {
- var $container;
- $container = $('#new-branch');
- // If the user doesn't have the required permissions the container isn't
- // rendered at all.
- if ($container.length === 0) {
- return;
+ static initCanCreateBranch() {
+ var $container;
+ $container = $('#new-branch');
+ // If the user doesn't have the required permissions the container isn't
+ // rendered at all.
+ if ($container.length === 0) {
+ return;
+ }
+ return $.getJSON($container.data('path')).error(function() {
+ $container.find('.unavailable').show();
+ return new Flash('Failed to check if a new branch can be created.', 'alert');
+ }).success(function(data) {
+ if (data.can_create_branch) {
+ $container.find('.available').show();
+ } else {
+ return $container.find('.unavailable').show();
}
- return $.getJSON($container.data('path')).error(function() {
- $container.find('.unavailable').show();
- return new Flash('Failed to check if a new branch can be created.', 'alert');
- }).success(function(data) {
- if (data.can_create_branch) {
- $container.find('.available').show();
- } else {
- return $container.find('.unavailable').show();
- }
- });
- };
+ });
+ }
+}
- return Issue;
- })();
-}).call(window);
+export default Issue;
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index ae4dd64424c..79164edff0e 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -342,11 +342,11 @@ require('./zen_mode');
var notesHolders = $this.closest('.diff-file').find('.notes_holder');
$this.toggleClass('active');
if ($this.hasClass('active')) {
- notesHolders.show().find('.hide').show();
+ notesHolders.show().find('.hide, .content').show();
} else {
- notesHolders.hide();
+ notesHolders.hide().find('.content').hide();
}
- $this.trigger('blur');
+ $(document).trigger('toggle.comments');
return e.preventDefault();
});
$document.off('click', '.js-confirm-danger');
diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js
new file mode 100644
index 00000000000..9384fe3f276
--- /dev/null
+++ b/app/assets/javascripts/monitoring/prometheus_graph.js
@@ -0,0 +1,333 @@
+/* eslint-disable no-new*/
+import d3 from 'd3';
+import _ from 'underscore';
+import statusCodes from '~/lib/utils/http_status';
+import '~/lib/utils/common_utils';
+import Flash from '~/flash';
+
+const prometheusGraphsContainer = '.prometheus-graph';
+const metricsEndpoint = 'metrics.json';
+const timeFormat = d3.time.format('%H:%M');
+const dayFormat = d3.time.format('%b %e, %a');
+const bisectDate = d3.bisector(d => d.time).left;
+const extraAddedWidthParent = 100;
+
+class PrometheusGraph {
+
+ constructor() {
+ this.margin = { top: 80, right: 180, bottom: 80, left: 100 };
+ this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 };
+ const parentContainerWidth = $(prometheusGraphsContainer).parent().width() +
+ extraAddedWidthParent;
+ this.originalWidth = parentContainerWidth;
+ this.originalHeight = 400;
+ this.width = parentContainerWidth - this.margin.left - this.margin.right;
+ this.height = 400 - this.margin.top - this.margin.bottom;
+ this.backOffRequestCounter = 0;
+ this.configureGraph();
+ this.init();
+ }
+
+ createGraph() {
+ const self = this;
+ _.each(this.data, (value, key) => {
+ if (value.length > 0 && (key === 'cpu_values' || key === 'memory_values')) {
+ self.plotValues(value, key);
+ }
+ });
+ }
+
+ init() {
+ const self = this;
+ this.getData().then((metricsResponse) => {
+ if (metricsResponse === {}) {
+ new Flash('Empty metrics', 'alert');
+ } else {
+ self.transformData(metricsResponse);
+ self.createGraph();
+ }
+ });
+ }
+
+ plotValues(valuesToPlot, key) {
+ const x = d3.time.scale()
+ .range([0, this.width]);
+
+ const y = d3.scale.linear()
+ .range([this.height, 0]);
+
+ const prometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
+
+ const graphSpecifics = this.graphSpecificProperties[key];
+
+ const chart = d3.select(prometheusGraphContainer)
+ .attr('width', this.width + this.margin.left + this.margin.right)
+ .attr('height', this.height + this.margin.bottom + this.margin.top)
+ .append('g')
+ .attr('transform', `translate(${this.margin.left},${this.margin.top})`);
+
+ const axisLabelContainer = d3.select(prometheusGraphContainer)
+ .attr('width', this.originalWidth + this.marginLabelContainer.left + this.marginLabelContainer.right)
+ .attr('height', this.originalHeight + this.marginLabelContainer.bottom + this.marginLabelContainer.top)
+ .append('g')
+ .attr('transform', `translate(${this.marginLabelContainer.left},${this.marginLabelContainer.top})`);
+
+ x.domain(d3.extent(valuesToPlot, d => d.time));
+ y.domain([0, d3.max(valuesToPlot.map(metricValue => metricValue.value))]);
+
+ const xAxis = d3.svg.axis()
+ .scale(x)
+ .ticks(this.commonGraphProperties.axis_no_ticks)
+ .orient('bottom');
+
+ const yAxis = d3.svg.axis()
+ .scale(y)
+ .ticks(this.commonGraphProperties.axis_no_ticks)
+ .tickSize(-this.width)
+ .orient('left');
+
+ this.createAxisLabelContainers(axisLabelContainer, key);
+
+ chart.append('g')
+ .attr('class', 'x-axis')
+ .attr('transform', `translate(0,${this.height})`)
+ .call(xAxis);
+
+ chart.append('g')
+ .attr('class', 'y-axis')
+ .call(yAxis);
+
+ const area = d3.svg.area()
+ .x(d => x(d.time))
+ .y0(this.height)
+ .y1(d => y(d.value))
+ .interpolate('linear');
+
+ const line = d3.svg.line()
+ .x(d => x(d.time))
+ .y(d => y(d.value));
+
+ chart.append('path')
+ .datum(valuesToPlot)
+ .attr('d', area)
+ .attr('class', 'metric-area')
+ .attr('fill', graphSpecifics.area_fill_color);
+
+ chart.append('path')
+ .datum(valuesToPlot)
+ .attr('class', 'metric-line')
+ .attr('stroke', graphSpecifics.line_color)
+ .attr('fill', 'none')
+ .attr('stroke-width', this.commonGraphProperties.area_stroke_width)
+ .attr('d', line);
+
+ // Overlay area for the mouseover events
+ chart.append('rect')
+ .attr('class', 'prometheus-graph-overlay')
+ .attr('width', this.width)
+ .attr('height', this.height)
+ .on('mousemove', this.handleMouseOverGraph.bind(this, x, y, valuesToPlot, chart, prometheusGraphContainer, key));
+ }
+
+ // The legends from the metric
+ createAxisLabelContainers(axisLabelContainer, key) {
+ const graphSpecifics = this.graphSpecificProperties[key];
+
+ axisLabelContainer.append('line')
+ .attr('class', 'label-x-axis-line')
+ .attr('stroke', '#000000')
+ .attr('stroke-width', '1')
+ .attr({
+ x1: 0,
+ y1: this.originalHeight - this.marginLabelContainer.top,
+ x2: this.originalWidth - this.margin.right,
+ y2: this.originalHeight - this.marginLabelContainer.top,
+ });
+
+ axisLabelContainer.append('line')
+ .attr('class', 'label-y-axis-line')
+ .attr('stroke', '#000000')
+ .attr('stroke-width', '1')
+ .attr({
+ x1: 0,
+ y1: 0,
+ x2: 0,
+ y2: this.originalHeight - this.marginLabelContainer.top,
+ });
+
+ axisLabelContainer.append('text')
+ .attr('class', 'label-axis-text')
+ .attr('text-anchor', 'middle')
+ .attr('transform', `translate(15, ${(this.originalHeight - this.marginLabelContainer.top) / 2}) rotate(-90)`)
+ .text(graphSpecifics.graph_legend_title);
+
+ axisLabelContainer.append('rect')
+ .attr('class', 'rect-axis-text')
+ .attr('x', (this.originalWidth / 2) - this.margin.right)
+ .attr('y', this.originalHeight - this.marginLabelContainer.top - 20)
+ .attr('width', 30)
+ .attr('height', 80);
+
+ axisLabelContainer.append('text')
+ .attr('class', 'label-axis-text')
+ .attr('x', (this.originalWidth / 2) - this.margin.right)
+ .attr('y', this.originalHeight - this.marginLabelContainer.top)
+ .attr('dy', '.35em')
+ .text('Time');
+
+ // Legends
+
+ // Metric Usage
+ axisLabelContainer.append('rect')
+ .attr('x', this.originalWidth - 170)
+ .attr('y', (this.originalHeight / 2) - 80)
+ .style('fill', graphSpecifics.area_fill_color)
+ .attr('width', 20)
+ .attr('height', 35);
+
+ axisLabelContainer.append('text')
+ .attr('class', 'label-axis-text')
+ .attr('x', this.originalWidth - 140)
+ .attr('y', (this.originalHeight / 2) - 65)
+ .text(graphSpecifics.graph_legend_title);
+
+ axisLabelContainer.append('text')
+ .attr('class', 'text-metric-usage')
+ .attr('x', this.originalWidth - 140)
+ .attr('y', (this.originalHeight / 2) - 50);
+ }
+
+ handleMouseOverGraph(x, y, valuesToPlot, chart, prometheusGraphContainer, key) {
+ const rectOverlay = document.querySelector(`${prometheusGraphContainer} .prometheus-graph-overlay`);
+ const timeValueFromOverlay = x.invert(d3.mouse(rectOverlay)[0]);
+ const timeValueIndex = bisectDate(valuesToPlot, timeValueFromOverlay, 1);
+ const d0 = valuesToPlot[timeValueIndex - 1];
+ const d1 = valuesToPlot[timeValueIndex];
+ const currentData = timeValueFromOverlay - d0.time > d1.time - timeValueFromOverlay ? d1 : d0;
+ const maxValueMetric = y(d3.max(valuesToPlot.map(metricValue => metricValue.value)));
+ const currentTimeCoordinate = x(currentData.time);
+ const graphSpecifics = this.graphSpecificProperties[key];
+ // Remove the current selectors
+ d3.selectAll(`${prometheusGraphContainer} .selected-metric-line`).remove();
+ d3.selectAll(`${prometheusGraphContainer} .circle-metric`).remove();
+ d3.selectAll(`${prometheusGraphContainer} .rect-text-metric`).remove();
+ d3.selectAll(`${prometheusGraphContainer} .text-metric`).remove();
+
+ chart.append('line')
+ .attr('class', 'selected-metric-line')
+ .attr({
+ x1: currentTimeCoordinate,
+ y1: y(0),
+ x2: currentTimeCoordinate,
+ y2: maxValueMetric,
+ });
+
+ chart.append('circle')
+ .attr('class', 'circle-metric')
+ .attr('fill', graphSpecifics.line_color)
+ .attr('cx', currentTimeCoordinate)
+ .attr('cy', y(currentData.value))
+ .attr('r', this.commonGraphProperties.circle_radius_metric);
+
+ // The little box with text
+ const rectTextMetric = chart.append('g')
+ .attr('class', 'rect-text-metric')
+ .attr('translate', `(${currentTimeCoordinate}, ${y(currentData.value)})`);
+
+ rectTextMetric.append('rect')
+ .attr('class', 'rect-metric')
+ .attr('x', currentTimeCoordinate + 10)
+ .attr('y', maxValueMetric)
+ .attr('width', this.commonGraphProperties.rect_text_width)
+ .attr('height', this.commonGraphProperties.rect_text_height);
+
+ rectTextMetric.append('text')
+ .attr('class', 'text-metric')
+ .attr('x', currentTimeCoordinate + 35)
+ .attr('y', maxValueMetric + 35)
+ .text(timeFormat(currentData.time));
+
+ rectTextMetric.append('text')
+ .attr('class', 'text-metric-date')
+ .attr('x', currentTimeCoordinate + 15)
+ .attr('y', maxValueMetric + 15)
+ .text(dayFormat(currentData.time));
+
+ // Update the text
+ d3.select(`${prometheusGraphContainer} .text-metric-usage`)
+ .text(currentData.value.substring(0, 8));
+ }
+
+ configureGraph() {
+ this.graphSpecificProperties = {
+ cpu_values: {
+ area_fill_color: '#edf3fc',
+ line_color: '#5b99f7',
+ graph_legend_title: 'CPU Usage (Cores)',
+ },
+ memory_values: {
+ area_fill_color: '#fca326',
+ line_color: '#fc6d26',
+ graph_legend_title: 'Memory Usage (MB)',
+ },
+ };
+
+ this.commonGraphProperties = {
+ area_stroke_width: 2,
+ median_total_characters: 8,
+ circle_radius_metric: 5,
+ rect_text_width: 90,
+ rect_text_height: 40,
+ axis_no_ticks: 3,
+ };
+ }
+
+ getData() {
+ const maxNumberOfRequests = 3;
+ return gl.utils.backOff((next, stop) => {
+ $.ajax({
+ url: metricsEndpoint,
+ dataType: 'json',
+ })
+ .done((data, statusText, resp) => {
+ if (resp.status === statusCodes.NO_CONTENT) {
+ this.backOffRequestCounter = this.backOffRequestCounter += 1;
+ if (this.backOffRequestCounter < maxNumberOfRequests) {
+ next();
+ } else {
+ stop({
+ status: resp.status,
+ metrics: data,
+ });
+ }
+ } else {
+ stop({
+ status: resp.status,
+ metrics: data,
+ });
+ }
+ }).fail(stop);
+ })
+ .then((resp) => {
+ if (resp.status === statusCodes.NO_CONTENT) {
+ return {};
+ }
+ return resp.metrics;
+ })
+ .catch(() => new Flash('An error occurred while fetching metrics.', 'alert'));
+ }
+
+ transformData(metricsResponse) {
+ const metricTypes = {};
+ _.each(metricsResponse.metrics, (value, key) => {
+ const metricValues = value[0].values;
+ metricTypes[key] = _.map(metricValues, metric => ({
+ time: new Date(metric[0] * 1000),
+ value: metric[1],
+ }));
+ });
+ this.data = metricTypes;
+ }
+}
+
+export default PrometheusGraph;
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index df7a7d2a459..eeab69da941 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -312,7 +312,7 @@ require('./task_list');
*/
Notes.prototype.renderDiscussionNote = function(note) {
- var discussionContainer, form, note_html, row;
+ var discussionContainer, form, note_html, row, lineType, diffAvatarContainer;
if (!this.isNewNote(note)) {
return;
}
@@ -322,6 +322,8 @@ require('./task_list');
form = $("#new-discussion-note-form-" + note.original_discussion_id);
}
row = form.closest("tr");
+ lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
+ diffAvatarContainer = row.prevAll('.line_holder').first().find('.js-avatar-container.' + lineType + '_line');
note_html = $(note.html);
note_html.renderGFM();
// is this the first note of discussion?
@@ -330,10 +332,26 @@ require('./task_list');
discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']");
}
if (discussionContainer.length === 0) {
- // insert the note and the reply button after the temp row
- row.after(note.diff_discussion_html);
- // remove the note (will be added again below)
- row.next().find(".note").remove();
+ if (!this.isParallelView() || row.hasClass('js-temp-notes-holder')) {
+ // insert the note and the reply button after the temp row
+ row.after(note.diff_discussion_html);
+
+ // remove the note (will be added again below)
+ row.next().find(".note").remove();
+ } else {
+ // Merge new discussion HTML in
+ var $discussion = $(note.diff_discussion_html);
+ var $notes = $discussion.find('.notes[data-discussion-id="' + note.discussion_id + '"]');
+ var contentContainerClass = '.' + $notes.closest('.notes_content')
+ .attr('class')
+ .split(' ')
+ .join('.');
+
+ // remove the note (will be added again below)
+ $notes.find('.note').remove();
+
+ row.find(contentContainerClass + ' .content').append($notes.closest('.content').children());
+ }
// Before that, the container didn't exist
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
// Add note to 'Changes' page discussions
@@ -347,14 +365,40 @@ require('./task_list');
discussionContainer.append(note_html);
}
- if (typeof gl.diffNotesCompileComponents !== 'undefined') {
+ if (typeof gl.diffNotesCompileComponents !== 'undefined' && note.discussion_id) {
gl.diffNotesCompileComponents();
+ this.renderDiscussionAvatar(diffAvatarContainer, note);
}
gl.utils.localTimeAgo($('.js-timeago'), false);
return this.updateNotesCount(1);
};
+ Notes.prototype.getLineHolder = function(changesDiscussionContainer) {
+ return $(changesDiscussionContainer).closest('.notes_holder')
+ .prevAll('.line_holder')
+ .first()
+ .get(0);
+ };
+
+ Notes.prototype.renderDiscussionAvatar = function(diffAvatarContainer, note) {
+ var commentButton = diffAvatarContainer.find('.js-add-diff-note-button');
+ var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders');
+
+ if (!avatarHolder.length) {
+ avatarHolder = document.createElement('diff-note-avatars');
+ avatarHolder.setAttribute('discussion-id', note.discussion_id);
+
+ diffAvatarContainer.append(avatarHolder);
+
+ gl.diffNotesCompileComponents();
+ }
+
+ if (commentButton.length) {
+ commentButton.remove();
+ }
+ };
+
/*
Called in response the main target form has been successfully submitted.
@@ -592,9 +636,14 @@ require('./task_list');
*/
Notes.prototype.removeNote = function(e) {
- var noteId;
- noteId = $(e.currentTarget).closest(".note").attr("id");
- $(".note[id='" + noteId + "']").each((function(_this) {
+ var noteElId, noteId, dataNoteId, $note, lineHolder;
+ $note = $(e.currentTarget).closest('.note');
+ noteElId = $note.attr('id');
+ noteId = $note.attr('data-note-id');
+ lineHolder = $(e.currentTarget).closest('.notes[data-discussion-id]')
+ .closest('.notes_holder')
+ .prev('.line_holder');
+ $(".note[id='" + noteElId + "']").each((function(_this) {
// A same note appears in the "Discussion" and in the "Changes" tab, we have
// to remove all. Using $(".note[id='noteId']") ensure we get all the notes,
// where $("#noteId") would return only one.
@@ -604,17 +653,26 @@ require('./task_list');
notes = note.closest(".notes");
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
- if (gl.diffNoteApps[noteId]) {
- gl.diffNoteApps[noteId].$destroy();
+ if (gl.diffNoteApps[noteElId]) {
+ gl.diffNoteApps[noteElId].$destroy();
}
}
+ note.remove();
+
// check if this is the last note for this line
- if (notes.find(".note").length === 1) {
+ if (notes.find(".note").length === 0) {
+ var notesTr = notes.closest("tr");
+
// "Discussions" tab
notes.closest(".timeline-entry").remove();
- // "Changes" tab / commit view
- notes.closest("tr").remove();
+
+ if (!_this.isParallelView() || notesTr.find('.note').length === 0) {
+ // "Changes" tab / commit view
+ notesTr.remove();
+ } else {
+ notes.closest('.content').empty();
+ }
}
return note.remove();
};
@@ -707,15 +765,16 @@ require('./task_list');
*/
Notes.prototype.addDiffNote = function(e) {
- var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent;
+ var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar;
e.preventDefault();
- $link = $(e.currentTarget);
+ $link = $(e.currentTarget || e.target);
row = $link.closest("tr");
nextRow = row.next();
hasNotes = nextRow.is(".notes_holder");
addForm = false;
notesContentSelector = ".notes_content";
rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"><div class=\"content\"></div></td></tr>";
+ isDiffCommentAvatar = $link.hasClass('js-diff-comment-avatar');
// In parallel view, look inside the correct left/right pane
if (this.isParallelView()) {
lineType = $link.data("lineType");
@@ -723,7 +782,9 @@ require('./task_list');
rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line old\"></td><td class=\"notes_content parallel old\"><div class=\"content\"></div></td><td class=\"notes_line new\"></td><td class=\"notes_content parallel new\"><div class=\"content\"></div></td></tr>";
}
notesContentSelector += " .content";
- if (hasNotes) {
+ notesContent = nextRow.find(notesContentSelector);
+
+ if (hasNotes && !isDiffCommentAvatar) {
nextRow.show();
notesContent = nextRow.find(notesContentSelector);
if (notesContent.length) {
@@ -740,13 +801,21 @@ require('./task_list');
}
}
}
- } else {
+ } else if (!isDiffCommentAvatar) {
// add a notes row and insert the form
row.after(rowCssToAdd);
nextRow = row.next();
notesContent = nextRow.find(notesContentSelector);
addForm = true;
+ } else {
+ nextRow.show();
+ notesContent.toggle(!notesContent.is(':visible'));
+
+ if (!nextRow.find('.content:not(:empty)').is(':visible')) {
+ nextRow.hide();
+ }
}
+
if (addForm) {
newForm = this.formClone.clone();
newForm.appendTo(notesContent);
diff --git a/app/assets/javascripts/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js
index 7dba5840c8a..d48f2404fa5 100644
--- a/app/assets/javascripts/test_utils/simulate_drag.js
+++ b/app/assets/javascripts/test_utils/simulate_drag.js
@@ -43,7 +43,14 @@
return event;
}
- function getTraget(target) {
+ function isLast(target) {
+ var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
+ var children = el.children;
+
+ return children.length - 1 === target.index;
+ }
+
+ function getTarget(target) {
var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
var children = el.children;
@@ -75,12 +82,22 @@
function simulateDrag(options, callback) {
options.to.el = options.to.el || options.from.el;
- var fromEl = getTraget(options.from);
- var toEl = getTraget(options.to);
+ var fromEl = getTarget(options.from);
+ var toEl = getTarget(options.to);
+ var firstEl = getTarget({
+ el: options.to.el,
+ index: 'first'
+ });
+ var lastEl = getTarget({
+ el: options.to.el,
+ index: 'last'
+ });
var scrollable = options.scrollable;
var fromRect = getRect(fromEl);
var toRect = getRect(toEl);
+ var firstRect = getRect(firstEl);
+ var lastRect = getRect(lastEl);
var startTime = new Date().getTime();
var duration = options.duration || 1000;
@@ -88,6 +105,12 @@
options.ontap && options.ontap();
window.SIMULATE_DRAG_ACTIVE = 1;
+ if (options.to.index === 0) {
+ toRect.cy = firstRect.y;
+ } else if (isLast(options.to)) {
+ toRect.cy = lastRect.y + lastRect.h + 50;
+ }
+
var dragInterval = setInterval(function loop() {
var progress = (new Date().getTime() - startTime) / duration;
var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 887ab481de4..fe8b37d2c6e 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -159,12 +159,12 @@
li {
text-align: left;
list-style: none;
- padding: 0 8px;
+ padding: 0 10px;
}
.divider {
height: 1px;
- margin: 8px;
+ margin: 6px 10px;
padding: 0;
background-color: $dropdown-divider-color;
}
@@ -181,7 +181,7 @@
display: block;
position: relative;
padding: 5px 8px;
- color: $dropdown-link-color;
+ color: $gl-text-color;
line-height: initial;
text-overflow: ellipsis;
border-radius: 2px;
@@ -218,11 +218,12 @@
}
.dropdown-header {
- color: $gl-text-color-secondary;
+ color: $gl-text-color;
font-size: 13px;
+ font-weight: 600;
line-height: 22px;
text-transform: capitalize;
- padding: 0 10px;
+ padding: 0 16px;
}
.separator + .dropdown-header {
@@ -325,14 +326,17 @@
.dropdown-menu-selectable {
a {
- padding-left: 25px;
+ padding-left: 26px;
&.is-indeterminate,
&.is-active {
+ font-weight: 600;
+ color: $gl-text-color;
+
&::before {
position: absolute;
- left: 5px;
- top: 8px;
+ left: 6px;
+ top: 6px;
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
text-rendering: auto;
@@ -354,7 +358,7 @@
.dropdown-title {
position: relative;
- padding: 0 25px 10px;
+ padding: 2px 25px 10px;
margin: 0 10px 10px;
font-weight: 600;
line-height: 1;
@@ -384,7 +388,7 @@
right: 5px;
width: 20px;
height: 20px;
- top: -3px;
+ top: -1px;
}
.dropdown-menu-back {
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 0ba00cea8b5..8f2150066c7 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -4,6 +4,21 @@
&.reset-filters {
padding: 7px;
}
+
+ &.update-issues-btn {
+ float: right;
+ margin-right: 0;
+
+ @media (max-width: $screen-xs-max) {
+ float: none;
+ }
+ }
+}
+
+.filters-section {
+ @media (max-width: $screen-xs-max) {
+ display: inline-block;
+ }
}
@media (min-width: $screen-sm-min) {
@@ -34,6 +49,11 @@
display: block;
margin: 0 0 10px;
}
+
+ .dropdown-menu-toggle,
+ .update-issues-btn .btn {
+ width: 100%;
+ }
}
.filtered-search-container {
@@ -44,6 +64,89 @@
-webkit-flex-direction: column;
flex-direction: column;
}
+
+ .tokens-container {
+ display: -webkit-flex;
+ display: flex;
+ flex: 1;
+ -webkit-flex: 1;
+ padding-left: 30px;
+ position: relative;
+ margin-bottom: 0;
+ }
+
+ .input-token {
+ flex: 1;
+ -webkit-flex: 1;
+ }
+
+ .filtered-search-token + .input-token:not(:last-child) {
+ max-width: 200px;
+ }
+}
+
+.filtered-search-token,
+.filtered-search-term {
+ display: -webkit-flex;
+ display: flex;
+ margin-top: 5px;
+ margin-bottom: 5px;
+
+ .selectable {
+ display: -webkit-flex;
+ display: flex;
+ }
+
+ .name,
+ .value {
+ display: inline-block;
+ padding: 2px 7px;
+ }
+
+ .name {
+ background-color: $filter-name-resting-color;
+ color: $filter-name-text-color;
+ border-radius: 2px 0 0 2px;
+ margin-right: 1px;
+ text-transform: capitalize;
+ }
+
+ .value {
+ background-color: $white-normal;
+ color: $filter-value-text-color;
+ border-radius: 0 2px 2px 0;
+ margin-right: 5px;
+ }
+
+ .selected {
+ .name {
+ background-color: $filter-name-selected-color;
+ }
+
+ .value {
+ background-color: $filter-value-selected-color;
+ }
+ }
+}
+
+.filtered-search-term {
+ .name {
+ background-color: inherit;
+ color: $black;
+ text-transform: none;
+ }
+
+ .selectable {
+ cursor: text;
+ }
+}
+
+.scroll-container {
+ display: -webkit-flex;
+ display: flex;
+ overflow-x: scroll;
+ white-space: nowrap;
+ width: 100%;
}
.filtered-search-input-container {
@@ -51,6 +154,9 @@
display: flex;
position: relative;
width: 100%;
+ border: 1px solid $border-color;
+ background-color: $white-light;
+ max-width: 87%;
@media (max-width: $screen-xs-min) {
-webkit-flex: 1 1 100%;
@@ -67,12 +173,22 @@
}
.form-control {
- padding-left: 25px;
+ position: relative;
+ min-width: 200px;
+ padding-left: 0;
padding-right: 25px;
+ border-color: transparent;
&:focus ~ .fa-filter {
color: $common-gray-dark;
}
+
+ &:focus,
+ &:hover {
+ outline: none;
+ border-color: transparent;
+ box-shadow: none;
+ }
}
.fa-filter {
@@ -89,12 +205,13 @@
.clear-search {
width: 35px;
- background-color: transparent;
+ background-color: $white-light;
border: none;
position: absolute;
right: 0;
height: 100%;
outline: none;
+ z-index: 1;
&:hover .fa-times {
color: $common-gray-dark;
@@ -111,7 +228,15 @@
overflow: auto;
}
-@media (max-width: $screen-xs-min) {
+@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+ .issues-details-filters {
+ .dropdown-menu-toggle {
+ width: 100px;
+ }
+ }
+}
+
+@media (max-width: $screen-xs-max) {
.issues-details-filters {
padding: 0 0 10px;
background-color: $white-light;
@@ -205,4 +330,4 @@
.filter-dropdown-loading {
padding: 8px 16px;
-} \ No newline at end of file
+}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 55ed4b7b06c..7adbb0a4188 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -229,44 +229,6 @@ ul.content-list {
}
}
-// Table list
-.table-list {
- display: table;
- width: 100%;
-
- .table-list-row {
- display: table-row;
- }
-
- .table-list-cell {
- display: table-cell;
- vertical-align: top;
- padding: 10px 16px;
- border-bottom: 1px solid $gray-darker;
-
- &.avatar-cell {
- width: 36px;
- padding-right: 0;
-
- img {
- margin-right: 0;
- }
- }
- }
-
- &.table-wide {
- .table-list-cell {
- &:last-of-type {
- padding-right: 0;
- }
-
- &:first-of-type {
- padding-left: 0;
- }
- }
- }
-}
-
.panel > .content-list > li {
padding: $gl-padding-top $gl-padding;
}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 8e2c56a8488..eb73f7cc794 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -100,8 +100,7 @@
@media (max-width: $screen-sm-max) {
.issues-filters {
- .milestone-filter,
- .labels-filter {
+ .milestone-filter {
display: none;
}
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 674d3bb45aa..ea45aaa0253 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -294,7 +294,7 @@
.nav-control {
@media (max-width: $screen-sm-max) {
- margin-right: 75px;
+ margin-right: 2px;
}
}
}
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
index efe93724013..9d8d08dff88 100644
--- a/app/assets/stylesheets/framework/panels.scss
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -48,11 +48,3 @@
line-height: inherit;
}
}
-
-.panel-default {
- .table-list-row:last-child {
- .table-list-cell {
- border-bottom: 0;
- }
- }
-}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 832fd0a532c..6841adb637e 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -540,3 +540,12 @@ Pipeline Graph
$stage-hover-bg: #eaf3fc;
$stage-hover-border: #d1e7fc;
$action-icon-color: #d6d6d6;
+
+/*
+Filtered Search
+*/
+$filter-name-resting-color: #f8f8f8;
+$filter-name-text-color: rgba(0, 0, 0, 0.55);
+$filter-value-text-color: rgba(0, 0, 0, 0.85);
+$filter-name-selected-color: #ebebeb;
+$filter-value-selected-color: #d7d7d7;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index c3d45d708c1..2029b6893ef 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -78,6 +78,7 @@
padding: 5px 10px;
background-color: $gray-light;
border-bottom: 1px solid $gray-darker;
+ border-top: 1px solid $gray-darker;
font-size: 14px;
&:first-child {
@@ -117,10 +118,37 @@
}
}
+.commit.flex-list {
+ display: flex;
+}
+
+.avatar-cell {
+ width: 46px;
+ padding-left: 10px;
+
+ img {
+ margin-right: 0;
+ }
+}
+
+.commit-detail {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ flex-grow: 1;
+ padding-left: 10px;
+
+ .merge-request-branches & {
+ flex-direction: column;
+ }
+}
+
+.commit-content {
+ padding-right: 10px;
+}
+
.commit-actions {
@media (min-width: $screen-sm-min) {
- width: 300px;
- text-align: right;
font-size: 0;
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 5d0c247dea8..eab79c2a481 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -113,6 +113,10 @@
td.line_content.parallel {
width: 46%;
}
+
+ .add-diff-note {
+ margin-left: -55px;
+ }
}
.old_line,
@@ -490,3 +494,103 @@
}
}
}
+
+.diff-comment-avatar-holders {
+ position: absolute;
+ height: 19px;
+ width: 19px;
+ margin-left: -15px;
+
+ &:hover {
+ .diff-comment-avatar,
+ .diff-comments-more-count {
+ @for $i from 1 through 4 {
+ $x-pos: 14px;
+
+ &:nth-child(#{$i}) {
+ @if $i == 4 {
+ $x-pos: 14.5px;
+ }
+
+ transform: translateX((($i * $x-pos) - $x-pos));
+
+ &:hover {
+ transform: translateX((($i * $x-pos) - $x-pos)) scale(1.2);
+ }
+ }
+ }
+ }
+
+ .diff-comments-more-count {
+ padding-left: 2px;
+ padding-right: 2px;
+ width: auto;
+ }
+ }
+}
+
+.diff-comment-avatar,
+.diff-comments-more-count {
+ position: absolute;
+ left: 0;
+ width: 19px;
+ height: 19px;
+ margin-right: 0;
+ border-color: $white-light;
+ cursor: pointer;
+ transition: all .1s ease-out;
+
+ @for $i from 1 through 4 {
+ &:nth-child(#{$i}) {
+ z-index: (4 - $i);
+ }
+ }
+}
+
+.diff-comments-more-count {
+ width: 19px;
+ min-width: 19px;
+ padding-left: 0;
+ padding-right: 0;
+ overflow: hidden;
+}
+
+.diff-comments-more-count,
+.diff-notes-collapse {
+ background-color: $gray-darkest;
+ color: $white-light;
+ border: 1px solid $white-light;
+ border-radius: 1em;
+ font-family: $regular_font;
+ font-size: 9px;
+ line-height: 17px;
+ text-align: center;
+}
+
+.diff-notes-collapse {
+ position: relative;
+ width: 19px;
+ height: 19px;
+ padding: 0;
+ transition: transform .1s ease-out;
+
+ svg {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ margin-left: -5.5px;
+ margin-top: -5.5px;
+ }
+
+ path {
+ fill: $white-light;
+ }
+
+ &:hover {
+ transform: scale(1.2);
+ }
+
+ &:focus {
+ outline: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 77e09e66340..0e2b8dba780 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -143,3 +143,71 @@
}
}
}
+
+.prometheus-graph {
+ text {
+ fill: $stat-graph-axis-fill;
+ }
+}
+
+.x-axis path,
+.y-axis path,
+.label-x-axis-line,
+.label-y-axis-line {
+ fill: none;
+ stroke-width: 1;
+ shape-rendering: crispEdges;
+}
+
+.x-axis path,
+.y-axis path {
+ stroke: $stat-graph-axis-fill;
+}
+
+.label-x-axis-line,
+.label-y-axis-line {
+ stroke: $border-color;
+}
+
+.y-axis {
+ line {
+ stroke: $stat-graph-axis-fill;
+ stroke-width: 1;
+ }
+}
+
+.metric-area {
+ opacity: 0.8;
+}
+
+.prometheus-graph-overlay {
+ fill: none;
+ opacity: 0.0;
+ pointer-events: all;
+}
+
+.rect-text-metric {
+ fill: $white-light;
+ stroke-width: 1;
+ stroke: $black;
+}
+
+.rect-axis-text {
+ fill: $white-light;
+}
+
+.text-metric,
+.text-median-metric,
+.text-metric-usage,
+.text-metric-date {
+ fill: $black;
+}
+
+.text-metric-date {
+ font-weight: 200;
+}
+
+.selected-metric-line {
+ stroke: $black;
+ stroke-width: 1;
+}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index f41eeb8ca45..d3496e19dde 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -240,8 +240,7 @@
.commit {
margin: 0;
- padding-top: 2px;
- padding-bottom: 2px;
+ padding: 10px 0;
list-style: none;
&:hover {
@@ -409,7 +408,7 @@
}
.panel-footer {
- padding: 5px 10px;
+ padding: 0;
.btn {
min-width: auto;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 09b85db7d45..4914933430f 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -746,6 +746,8 @@ pre.light-well {
}
.protected-branches-list {
+ margin-bottom: 30px;
+
a {
color: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 8d1063fc26f..fc4da4c495f 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -139,18 +139,10 @@
.blob-commit-info {
list-style: none;
background: $gray-light;
- padding: 6px 0;
+ padding: 16px 16px 16px 6px;
border: 1px solid $border-color;
border-bottom: none;
margin: 0;
-
- .table-list-cell {
- border-bottom: none;
- }
-
- .commit-actions {
- width: 260px;
- }
}
#modal-remove-blob > .modal-dialog { width: 850px; }
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index 62f62e99a97..9c9f420c1e0 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -2,7 +2,7 @@ class Admin::ApplicationsController < Admin::ApplicationController
include OauthApplications
before_action :set_application, only: [:show, :edit, :update, :destroy]
- before_action :load_scopes, only: [:new, :edit]
+ before_action :load_scopes, only: [:new, :create, :edit, :update]
def index
@applications = Doorkeeper::Application.where("owner_id IS NULL")
diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb
index d26004539b5..07c8bf714fc 100644
--- a/app/controllers/admin/impersonation_tokens_controller.rb
+++ b/app/controllers/admin/impersonation_tokens_controller.rb
@@ -44,7 +44,7 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
end
def set_index_vars
- @scopes = Gitlab::Auth::SCOPES
+ @scopes = Gitlab::Auth::API_SCOPES
@impersonation_token ||= finder.build
@inactive_impersonation_tokens = finder(state: 'inactive').execute
diff --git a/app/controllers/concerns/repository_settings_redirect.rb b/app/controllers/concerns/repository_settings_redirect.rb
new file mode 100644
index 00000000000..0854c73a02f
--- /dev/null
+++ b/app/controllers/concerns/repository_settings_redirect.rb
@@ -0,0 +1,7 @@
+module RepositorySettingsRedirect
+ extend ActiveSupport::Concern
+
+ def redirect_to_repository_settings(project)
+ redirect_to namespace_project_settings_repository_path(project.namespace, project)
+ end
+end
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index c721dca58d9..05190103767 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -1,8 +1,8 @@
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
- before_action :authenticate_resource_owner!
-
layout 'profile'
+ # Overriden from Doorkeeper::AuthorizationsController to
+ # include the call to session.delete
def new
if pre_auth.authorizable?
if skip_authorization? || matching_token?
@@ -16,44 +16,4 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
render "doorkeeper/authorizations/error"
end
end
-
- # TODO: Handle raise invalid authorization
- def create
- redirect_or_render authorization.authorize
- end
-
- def destroy
- redirect_or_render authorization.deny
- end
-
- private
-
- def matching_token?
- Doorkeeper::AccessToken.matching_token_for(pre_auth.client,
- current_resource_owner.id,
- pre_auth.scopes)
- end
-
- def redirect_or_render(auth)
- if auth.redirectable?
- redirect_to auth.redirect_uri
- else
- render json: auth.body, status: auth.status
- end
- end
-
- def pre_auth
- @pre_auth ||=
- Doorkeeper::OAuth::PreAuthorization.new(Doorkeeper.configuration,
- server.client_via_uid,
- params)
- end
-
- def authorization
- @authorization ||= strategy.request
- end
-
- def strategy
- @strategy ||= server.authorization_request(pre_auth.response_type)
- end
end
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index d1f2374e9eb..0abe7ea3c9b 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -38,7 +38,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end
def set_index_vars
- @scopes = Gitlab::Auth::SCOPES
+ @scopes = Gitlab::Auth::API_SCOPES
@personal_access_token = finder.build
@inactive_personal_access_tokens = finder(state: 'inactive').execute
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index 61fef4dc133..28c9646910d 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -8,6 +8,7 @@ module Projects
def index
issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute
issues = issues.page(params[:page]).per(params[:per] || 20)
+ make_sure_position_is_set(issues)
render json: {
issues: serialize_as_json(issues),
@@ -38,6 +39,12 @@ module Projects
private
+ def make_sure_position_is_set(issues)
+ issues.each do |issue|
+ issue.move_to_end && issue.save unless issue.relative_position
+ end
+ end
+
def issue
@issue ||=
IssuesFinder.new(current_user, project_id: project.id)
@@ -63,7 +70,7 @@ module Projects
end
def move_params
- params.permit(:board_id, :id, :from_list_id, :to_list_id)
+ params.permit(:board_id, :id, :from_list_id, :to_list_id, :move_before_iid, :move_after_iid)
end
def issue_params
@@ -73,7 +80,7 @@ module Projects
def serialize_as_json(resource)
resource.as_json(
labels: true,
- only: [:id, :iid, :title, :confidential, :due_date],
+ only: [:id, :iid, :title, :confidential, :due_date, :relative_position],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
milestone: { only: [:id, :title] }
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index b094491e006..1502b734f37 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -1,4 +1,5 @@
class Projects::DeployKeysController < Projects::ApplicationController
+ include RepositorySettingsRedirect
respond_to :html
# Authorize
@@ -7,51 +8,36 @@ class Projects::DeployKeysController < Projects::ApplicationController
layout "project_settings"
def index
- @key = DeployKey.new
- set_index_vars
+ redirect_to_repository_settings(@project)
end
def new
- redirect_to namespace_project_deploy_keys_path(@project.namespace, @project)
+ redirect_to_repository_settings(@project)
end
def create
@key = DeployKey.new(deploy_key_params.merge(user: current_user))
- set_index_vars
- if @key.valid? && @project.deploy_keys << @key
- redirect_to namespace_project_deploy_keys_path(@project.namespace, @project)
- else
- render "index"
+ unless @key.valid? && @project.deploy_keys << @key
+ flash[:alert] = @key.errors.full_messages.join(', ').html_safe
end
+ redirect_to_repository_settings(@project)
end
def enable
Projects::EnableDeployKeyService.new(@project, current_user, params).execute
- redirect_to namespace_project_deploy_keys_path(@project.namespace, @project)
+ redirect_to_repository_settings(@project)
end
def disable
@project.deploy_keys_projects.find_by(deploy_key_id: params[:id]).destroy
- redirect_back_or_default(default: { action: 'index' })
+ redirect_to_repository_settings(@project)
end
protected
- def set_index_vars
- @enabled_keys ||= @project.deploy_keys
-
- @available_keys ||= current_user.accessible_deploy_keys - @enabled_keys
- @available_project_keys ||= current_user.project_deploy_keys - @enabled_keys
- @available_public_keys ||= DeployKey.are_public - @enabled_keys
-
- # Public keys that are already used by another accessible project are already
- # in @available_project_keys.
- @available_public_keys -= @available_project_keys
- end
-
def deploy_key_params
params.require(:deploy_key).permit(:key, :title, :can_push)
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index fed75396d6e..fa37963dfd4 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -5,7 +5,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :authorize_create_deployment!, only: [:stop]
before_action :authorize_update_environment!, only: [:edit, :update]
before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize]
- before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize]
+ before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics]
before_action :verify_api_request!, only: :terminal_websocket_authorize
def index
@@ -109,6 +109,19 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
+ def metrics
+ # Currently, this acts as a hint to load the metrics details into the cache
+ # if they aren't there already
+ @metrics = environment.metrics || {}
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: @metrics, status: @metrics.any? ? :ok : :no_content
+ end
+ end
+ end
+
private
def verify_api_request!
diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index ee8c30058a1..a8cb07eb67a 100644
--- a/app/controllers/projects/protected_branches_controller.rb
+++ b/app/controllers/projects/protected_branches_controller.rb
@@ -1,26 +1,22 @@
class Projects::ProtectedBranchesController < Projects::ApplicationController
+ include RepositorySettingsRedirect
# Authorize
before_action :require_non_empty_project
before_action :authorize_admin_project!
before_action :load_protected_branch, only: [:show, :update, :destroy]
- before_action :load_protected_branches, only: [:index]
layout "project_settings"
def index
- @protected_branch = @project.protected_branches.new
- load_gon_index
+ redirect_to_repository_settings(@project)
end
def create
@protected_branch = ::ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute
- if @protected_branch.persisted?
- redirect_to namespace_project_protected_branches_path(@project.namespace, @project)
- else
- load_protected_branches
- load_gon_index
- render :index
+ unless @protected_branch.persisted?
+ flash[:alert] = @protected_branches.errors.full_messages.join(', ').html_safe
end
+ redirect_to_repository_settings(@project)
end
def show
@@ -45,7 +41,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
@protected_branch.destroy
respond_to do |format|
- format.html { redirect_to namespace_project_protected_branches_path }
+ format.html { redirect_to_repository_settings(@project) }
format.js { head :ok }
end
end
@@ -61,24 +57,4 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
merge_access_levels_attributes: [:access_level, :id],
push_access_levels_attributes: [:access_level, :id])
end
-
- def load_protected_branches
- @protected_branches = @project.protected_branches.order(:name).page(params[:page])
- end
-
- def access_levels_options
- {
- push_access_levels: {
- roles: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } },
- },
- merge_access_levels: {
- roles: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } }
- }
- }
- end
-
- def load_gon_index
- params = { open_branches: @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } }
- gon.push(params.merge(access_levels_options))
- end
end
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
new file mode 100644
index 00000000000..b6ce4abca45
--- /dev/null
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -0,0 +1,50 @@
+module Projects
+ module Settings
+ class RepositoryController < Projects::ApplicationController
+ before_action :authorize_admin_project!
+
+ def show
+ @deploy_keys = DeployKeysPresenter
+ .new(@project, current_user: current_user)
+
+ define_protected_branches
+ end
+
+ private
+
+ def define_protected_branches
+ load_protected_branches
+ @protected_branch = @project.protected_branches.new
+ load_gon_index
+ end
+
+ def load_protected_branches
+ @protected_branches = @project.protected_branches.order(:name).page(params[:page])
+ end
+
+ def access_levels_options
+ {
+ push_access_levels: {
+ roles: ProtectedBranch::PushAccessLevel.human_access_levels.map do |id, text|
+ { id: id, text: text, before_divider: true }
+ end
+ },
+ merge_access_levels: {
+ roles: ProtectedBranch::MergeAccessLevel.human_access_levels.map do |id, text|
+ { id: id, text: text, before_divider: true }
+ end
+ }
+ }
+ end
+
+ def open_branches
+ branches = @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } }
+ { open_branches: branches }
+ end
+
+ def load_gon_index
+ gon.push(open_branches.merge(access_levels_options))
+ end
+ end
+ end
+end
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 509f4f412ca..f1bfd574f04 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -14,6 +14,8 @@ class UploadsController < ApplicationController
end
disposition = uploader.image? ? 'inline' : 'attachment'
+
+ expires_in 0.seconds, must_revalidate: true, private: true
send_file uploader.file.path, disposition: disposition
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 4b025669f69..ca326dd0627 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -81,8 +81,8 @@ module ApplicationSettingsHelper
end
def repository_storages_options_for_select
- options = Gitlab.config.repositories.storages.map do |name, path|
- ["#{name} - #{path}", name]
+ options = Gitlab.config.repositories.storages.map do |name, storage|
+ ["#{name} - #{storage['path']}", name]
end
options_for_select(options, @application_setting.repository_storages)
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index f16a63e2178..e9b7cbbad6a 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -74,6 +74,10 @@ module GitlabRoutingHelper
namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args)
end
+ def environment_metrics_path(environment, *args)
+ metrics_namespace_project_environment_path(environment.project.namespace, environment.project, environment, *args)
+ end
+
def issue_path(entity, *args)
namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args)
end
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 8ad3851fb9a..18734f1411f 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -50,7 +50,7 @@ module SortingHelper
end
def sort_title_priority
- 'Priority'
+ 'Label priority'
end
def sort_title_oldest_updated
diff --git a/app/models/chat_team.rb b/app/models/chat_team.rb
index 7952141a0d6..c52b6f15913 100644
--- a/app/models/chat_team.rb
+++ b/app/models/chat_team.rb
@@ -1,5 +1,6 @@
class ChatTeam < ActiveRecord::Base
validates :team_id, presence: true
+ validates :namespace, uniqueness: true
belongs_to :namespace
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 77fa19cfe21..3722047251d 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -564,10 +564,35 @@ module Ci
@unscoped_project ||= Project.unscoped.find_by(id: gl_project_id)
end
+ CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
+
def predefined_variables
variables = [
{ key: 'CI', value: 'true', public: true },
{ key: 'GITLAB_CI', value: 'true', public: true },
+ { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
+ { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
+ { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
+ { key: 'CI_JOB_ID', value: id.to_s, public: true },
+ { key: 'CI_JOB_NAME', value: name, public: true },
+ { key: 'CI_JOB_STAGE', value: stage, public: true },
+ { key: 'CI_JOB_TOKEN', value: token, public: false },
+ { key: 'CI_COMMIT_SHA', value: sha, public: true },
+ { key: 'CI_COMMIT_REF_NAME', value: ref, public: true },
+ { key: 'CI_COMMIT_REF_SLUG', value: ref_slug, public: true },
+ { key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER, public: true },
+ { key: 'CI_REGISTRY_PASSWORD', value: token, public: false },
+ { key: 'CI_REPOSITORY_URL', value: repo_url, public: false }
+ ]
+
+ variables << { key: "CI_COMMIT_TAG", value: ref, public: true } if tag?
+ variables << { key: "CI_PIPELINE_TRIGGERED", value: 'true', public: true } if trigger_request
+ variables << { key: "CI_JOB_MANUAL", value: 'true', public: true } if action?
+ variables.concat(legacy_variables)
+ end
+
+ def legacy_variables
+ variables = [
{ key: 'CI_BUILD_ID', value: id.to_s, public: true },
{ key: 'CI_BUILD_TOKEN', value: token, public: false },
{ key: 'CI_BUILD_REF', value: sha, public: true },
@@ -575,14 +600,12 @@ module Ci
{ key: 'CI_BUILD_REF_NAME', value: ref, public: true },
{ key: 'CI_BUILD_REF_SLUG', value: ref_slug, public: true },
{ key: 'CI_BUILD_NAME', value: name, public: true },
- { key: 'CI_BUILD_STAGE', value: stage, public: true },
- { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
- { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
- { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }
+ { key: 'CI_BUILD_STAGE', value: stage, public: true }
]
- variables << { key: 'CI_BUILD_TAG', value: ref, public: true } if tag?
- variables << { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } if trigger_request
- variables << { key: 'CI_BUILD_MANUAL', value: 'true', public: true } if action?
+
+ variables << { key: "CI_BUILD_TAG", value: ref, public: true } if tag?
+ variables << { key: "CI_BUILD_TRIGGERED", value: 'true', public: true } if trigger_request
+ variables << { key: "CI_BUILD_MANUAL", value: 'true', public: true } if action?
variables
end
diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb
new file mode 100644
index 00000000000..603f2dd7e5d
--- /dev/null
+++ b/app/models/concerns/relative_positioning.rb
@@ -0,0 +1,101 @@
+module RelativePositioning
+ extend ActiveSupport::Concern
+
+ MIN_POSITION = 0
+ MAX_POSITION = Gitlab::Database::MAX_INT_VALUE
+
+ included do
+ after_save :save_positionable_neighbours
+ end
+
+ def min_relative_position
+ self.class.in_projects(project.id).minimum(:relative_position)
+ end
+
+ def max_relative_position
+ self.class.in_projects(project.id).maximum(:relative_position)
+ end
+
+ def prev_relative_position
+ prev_pos = nil
+
+ if self.relative_position
+ prev_pos = self.class.
+ in_projects(project.id).
+ where('relative_position < ?', self.relative_position).
+ maximum(:relative_position)
+ end
+
+ prev_pos || MIN_POSITION
+ end
+
+ def next_relative_position
+ next_pos = nil
+
+ if self.relative_position
+ next_pos = self.class.
+ in_projects(project.id).
+ where('relative_position > ?', self.relative_position).
+ minimum(:relative_position)
+ end
+
+ next_pos || MAX_POSITION
+ end
+
+ def move_between(before, after)
+ return move_after(before) unless after
+ return move_before(after) unless before
+
+ pos_before = before.relative_position
+ pos_after = after.relative_position
+
+ if pos_after && (pos_before == pos_after)
+ self.relative_position = pos_before
+ before.move_before(self)
+ after.move_after(self)
+
+ @positionable_neighbours = [before, after]
+ else
+ self.relative_position = position_between(pos_before, pos_after)
+ end
+ end
+
+ def move_before(after)
+ self.relative_position = position_between(after.prev_relative_position, after.relative_position)
+ end
+
+ def move_after(before)
+ self.relative_position = position_between(before.relative_position, before.next_relative_position)
+ end
+
+ def move_to_end
+ self.relative_position = position_between(max_relative_position, MAX_POSITION)
+ end
+
+ private
+
+ # This method takes two integer values (positions) and
+ # calculates some random position between them. The range is huge as
+ # the maximum integer value is 2147483647. Ideally, the calculated value would be
+ # exactly between those terminating values, but this will introduce possibility of a race condition
+ # so two or more issues can get the same value, we want to avoid that and we also want to avoid
+ # using a lock here. If we have two issues with distance more than one thousand, we are OK.
+ # Given the huge range of possible values that integer can fit we shoud never face a problem.
+ def position_between(pos_before, pos_after)
+ pos_before ||= MIN_POSITION
+ pos_after ||= MAX_POSITION
+
+ pos_before, pos_after = [pos_before, pos_after].sort
+
+ rand(pos_before.next..pos_after.pred)
+ end
+
+ def save_positionable_neighbours
+ return unless @positionable_neighbours
+
+ status = @positionable_neighbours.all?(&:save)
+ @positionable_neighbours = nil
+
+ status
+ end
+end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 1a21b5e52b5..bf33010fd21 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -145,6 +145,14 @@ class Environment < ActiveRecord::Base
project.deployment_service.terminals(self) if has_terminals?
end
+ def has_metrics?
+ project.monitoring_service.present? && available? && last_deployment.present?
+ end
+
+ def metrics
+ project.monitoring_service.metrics(self) if has_metrics?
+ end
+
# An environment name is not necessarily suitable for use in URLs, DNS
# or other third-party contexts, so provide a slugified version. A slug has
# the following properties:
diff --git a/app/models/issue.rb b/app/models/issue.rb
index de90f19f854..0f7a26ee3e1 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -7,6 +7,7 @@ class Issue < ActiveRecord::Base
include Sortable
include Spammable
include FasterCacheKeys
+ include RelativePositioning
DueDateStruct = Struct.new(:title, :name).freeze
NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
diff --git a/app/models/oauth_access_grant.rb b/app/models/oauth_access_grant.rb
new file mode 100644
index 00000000000..3a997406565
--- /dev/null
+++ b/app/models/oauth_access_grant.rb
@@ -0,0 +1,4 @@
+class OauthAccessGrant < Doorkeeper::AccessGrant
+ belongs_to :resource_owner, class_name: 'User'
+ belongs_to :application, class_name: 'Doorkeeper::Application'
+end
diff --git a/app/models/oauth_access_token.rb b/app/models/oauth_access_token.rb
index 116fb71ac08..b85f5dbaf2e 100644
--- a/app/models/oauth_access_token.rb
+++ b/app/models/oauth_access_token.rb
@@ -1,4 +1,4 @@
-class OauthAccessToken < ActiveRecord::Base
+class OauthAccessToken < Doorkeeper::AccessToken
belongs_to :resource_owner, class_name: 'User'
belongs_to :application, class_name: 'Doorkeeper::Application'
end
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 22809fe1487..e8b000ddad6 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -14,6 +14,9 @@ class PersonalAccessToken < ActiveRecord::Base
scope :with_impersonation, -> { where(impersonation: true) }
scope :without_impersonation, -> { where(impersonation: false) }
+ validates :scopes, presence: true
+ validate :validate_api_scopes
+
def revoke!
self.revoked = true
self.save
@@ -22,4 +25,12 @@ class PersonalAccessToken < ActiveRecord::Base
def active?
!revoked? && !expired?
end
+
+ protected
+
+ def validate_api_scopes
+ unless scopes.all? { |scope| Gitlab::Auth::API_SCOPES.include?(scope.to_sym) }
+ errors.add :scopes, "can only contain API scopes"
+ end
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 7d211784c3c..8c2dadf4659 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -113,6 +113,7 @@ class Project < ActiveRecord::Base
has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project
has_one :external_wiki_service, dependent: :destroy
has_one :kubernetes_service, dependent: :destroy, inverse_of: :project
+ has_one :prometheus_service, dependent: :destroy, inverse_of: :project
has_one :mock_ci_service, dependent: :destroy
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
@@ -392,7 +393,7 @@ class Project < ActiveRecord::Base
end
def repository_storage_path
- Gitlab.config.repositories.storages[repository_storage]
+ Gitlab.config.repositories.storages[repository_storage]['path']
end
def team
@@ -771,6 +772,14 @@ class Project < ActiveRecord::Base
@deployment_service ||= deployment_services.reorder(nil).find_by(active: true)
end
+ def monitoring_services
+ services.where(category: :monitoring)
+ end
+
+ def monitoring_service
+ @monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true)
+ end
+
def jira_tracker?
issues_tracker.to_param == 'jira'
end
diff --git a/app/models/project_services/monitoring_service.rb b/app/models/project_services/monitoring_service.rb
new file mode 100644
index 00000000000..ea585721e8f
--- /dev/null
+++ b/app/models/project_services/monitoring_service.rb
@@ -0,0 +1,16 @@
+# Base class for monitoring services
+#
+# These services integrate with a deployment solution like Prometheus
+# to provide additional features for environments.
+class MonitoringService < Service
+ default_value_for :category, 'monitoring'
+
+ def self.supported_events
+ %w()
+ end
+
+ # Environments have a number of metrics
+ def metrics(environment)
+ raise NotImplementedError
+ end
+end
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
new file mode 100644
index 00000000000..375966b9efc
--- /dev/null
+++ b/app/models/project_services/prometheus_service.rb
@@ -0,0 +1,93 @@
+class PrometheusService < MonitoringService
+ include ReactiveCaching
+
+ self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] }
+ self.reactive_cache_lease_timeout = 30.seconds
+ self.reactive_cache_refresh_interval = 30.seconds
+ self.reactive_cache_lifetime = 1.minute
+
+ # Access to prometheus is directly through the API
+ prop_accessor :api_url
+
+ with_options presence: true, if: :activated? do
+ validates :api_url, url: true
+ end
+
+ after_save :clear_reactive_cache!
+
+ def initialize_properties
+ if properties.nil?
+ self.properties = {}
+ end
+ end
+
+ def title
+ 'Prometheus'
+ end
+
+ def description
+ 'Prometheus monitoring'
+ end
+
+ def help
+ 'Retrieves `container_cpu_usage_seconds_total` and `container_memory_usage_bytes` from the configured Prometheus server. An `environment` label is required on each metric to identify the Environment.'
+ end
+
+ def self.to_param
+ 'prometheus'
+ end
+
+ def fields
+ [
+ {
+ type: 'text',
+ name: 'api_url',
+ title: 'API URL',
+ placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/'
+ }
+ ]
+ end
+
+ # Check we can connect to the Prometheus API
+ def test(*args)
+ client.ping
+
+ { success: true, result: 'Checked API endpoint' }
+ rescue Gitlab::PrometheusError => err
+ { success: false, result: err }
+ end
+
+ def metrics(environment)
+ with_reactive_cache(environment.slug) do |data|
+ data
+ end
+ end
+
+ # Cache metrics for specific environment
+ 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}"})/1024/1024}
+ cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="#{environment_slug}"}[2m]))}
+
+ {
+ success: true,
+ metrics: {
+ # Memory used in MB
+ memory_values: client.query_range(memory_query, start: 8.hours.ago),
+ memory_current: client.query(memory_query),
+ # CPU Usage rate in cores.
+ cpu_values: client.query_range(cpu_query, start: 8.hours.ago),
+ cpu_current: client.query(cpu_query)
+ },
+ last_update: Time.now.utc
+ }
+
+ rescue Gitlab::PrometheusError => err
+ { success: false, result: err.message }
+ end
+
+ def client
+ @prometheus ||= Gitlab::Prometheus.new(api_url: api_url)
+ end
+end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index e7cc8d6e083..6ab04440ca8 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -50,10 +50,6 @@ class Repository
end
end
- def self.storages
- Gitlab.config.repositories.storages
- end
-
def initialize(path_with_namespace, project)
@path_with_namespace = path_with_namespace
@project = project
@@ -316,11 +312,13 @@ class Repository
if !branch_name || branch_name == root_ref
branches.each do |branch|
cache.expire(:"diverging_commit_counts_#{branch.name}")
+ cache.expire(:"commit_count_#{branch.name}")
end
# In case a commit is pushed to a non-root branch we only have to flush the
# cache for said branch.
else
cache.expire(:"diverging_commit_counts_#{branch_name}")
+ cache.expire(:"commit_count_#{branch_name}")
end
end
@@ -500,6 +498,16 @@ class Repository
end
cache_method :commit_count, fallback: 0
+ def commit_count_for_ref(ref)
+ return 0 unless exists?
+
+ begin
+ cache.fetch(:"commit_count_#{ref}") { raw_repository.commit_count(ref) }
+ rescue Rugged::ReferenceError
+ 0
+ end
+ end
+
def branch_names
branches.map(&:name)
end
diff --git a/app/models/service.rb b/app/models/service.rb
index 3ef4cbead10..2f75a2e4e7f 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -232,6 +232,7 @@ class Service < ActiveRecord::Base
mattermost
pipelines_email
pivotaltracker
+ prometheus
pushover
redmine
slack_slash_commands
diff --git a/app/presenters/projects/settings/deploy_keys_presenter.rb b/app/presenters/projects/settings/deploy_keys_presenter.rb
new file mode 100644
index 00000000000..86ac513b3c0
--- /dev/null
+++ b/app/presenters/projects/settings/deploy_keys_presenter.rb
@@ -0,0 +1,60 @@
+module Projects
+ module Settings
+ class DeployKeysPresenter < Gitlab::View::Presenter::Simple
+ presents :project
+ delegate :size, to: :enabled_keys, prefix: true
+ delegate :size, to: :available_project_keys, prefix: true
+ delegate :size, to: :available_public_keys, prefix: true
+
+ def new_key
+ @key ||= DeployKey.new
+ end
+
+ def enabled_keys
+ @enabled_keys ||= project.deploy_keys
+ end
+
+ def any_keys_enabled?
+ enabled_keys.any?
+ end
+
+ def available_keys
+ @available_keys ||= current_user.accessible_deploy_keys - enabled_keys
+ end
+
+ def available_project_keys
+ @available_project_keys ||= current_user.project_deploy_keys - enabled_keys
+ end
+
+ def any_available_project_keys_enabled?
+ available_project_keys.any?
+ end
+
+ def key_available?(deploy_key)
+ available_keys.include?(deploy_key)
+ end
+
+ def available_public_keys
+ return @available_public_keys if defined?(@available_public_keys)
+
+ @available_public_keys ||= DeployKey.are_public - enabled_keys
+
+ # Public keys that are already used by another accessible project are already
+ # in @available_project_keys.
+ @available_public_keys -= available_project_keys
+ end
+
+ def any_available_public_keys_enabled?
+ available_public_keys.any?
+ end
+
+ def to_partial_path
+ 'projects/deploy_keys/index'
+ end
+
+ def form_partial_path
+ 'projects/deploy_keys/form'
+ end
+ end
+ end
+end
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 8a94c54b6ab..185838764c1 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -5,7 +5,7 @@ module Boards
issues = IssuesFinder.new(current_user, filter_params).execute
issues = without_board_labels(issues) unless movable_list?
issues = with_list_label(issues) if movable_list?
- issues
+ issues.reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC'))
end
private
@@ -26,7 +26,6 @@ module Boards
def filter_params
set_default_scope
- set_default_sort
set_project
set_state
@@ -37,10 +36,6 @@ module Boards
params[:scope] = 'all'
end
- def set_default_sort
- params[:sort] = 'priority'
- end
-
def set_project
params[:project_id] = project.id
end
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 96554a92a02..2a9981ab884 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -3,7 +3,7 @@ module Boards
class MoveService < BaseService
def execute(issue)
return false unless can?(current_user, :update_issue, issue)
- return false unless valid_move?
+ return false if issue_params.empty?
update_service.execute(issue)
end
@@ -14,7 +14,7 @@ module Boards
@board ||= project.boards.find(params[:board_id])
end
- def valid_move?
+ def move_between_lists?
moving_from_list.present? && moving_to_list.present? &&
moving_from_list != moving_to_list
end
@@ -32,11 +32,19 @@ module Boards
end
def issue_params
- {
- add_label_ids: add_label_ids,
- remove_label_ids: remove_label_ids,
- state_event: issue_state
- }
+ attrs = {}
+
+ if move_between_lists?
+ attrs.merge!(
+ add_label_ids: add_label_ids,
+ remove_label_ids: remove_label_ids,
+ state_event: issue_state,
+ )
+ end
+
+ attrs[:move_between_iids] = move_between_iids if move_between_iids
+
+ attrs
end
def issue_state
@@ -58,6 +66,12 @@ module Boards
Array(label_ids).compact
end
+
+ def move_between_iids
+ return unless params[:move_after_iid] || params[:move_before_iid]
+
+ [params[:move_after_iid], params[:move_before_iid]]
+ end
end
end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index b618c3e038e..b071a398481 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -211,7 +211,7 @@ class IssuableBaseService < BaseService
label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
- if params.present?
+ if issuable.changed? || params.present?
issuable.assign_attributes(params.merge(updated_by: current_user))
before_update(issuable)
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index 366b3572738..85b6eb3fe3d 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -13,6 +13,7 @@ module Issues
def before_create(issue)
spam_check(issue, current_user)
+ issue.move_to_end
end
def after_create(issuable)
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 22e32b13259..a444c78b609 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -3,8 +3,8 @@ module Issues
include SpamCheckService
def execute(issue)
+ handle_move_between_iids(issue)
filter_spam_check_params
-
update(issue)
end
@@ -37,11 +37,13 @@ module Issues
end
added_labels = issue.labels - old_labels
+
if added_labels.present?
notification_service.relabeled_issue(issue, added_labels, current_user)
end
added_mentions = issue.mentioned_users - old_mentioned_users
+
if added_mentions.present?
notification_service.new_mentions_in_issue(issue, added_mentions, current_user)
end
@@ -55,8 +57,24 @@ module Issues
Issues::CloseService
end
+ def handle_move_between_iids(issue)
+ return unless params[:move_between_iids]
+
+ after_iid, before_iid = params.delete(:move_between_iids)
+
+ issue_before = get_issue_if_allowed(issue.project, before_iid) if before_iid
+ issue_after = get_issue_if_allowed(issue.project, after_iid) if after_iid
+
+ issue.move_between(issue_before, issue_after)
+ end
+
private
+ def get_issue_if_allowed(project, iid)
+ issue = project.issues.find_by(iid: iid)
+ issue if can?(current_user, :update_issue, issue)
+ end
+
def create_confidentiality_note(issue)
SystemNoteService.change_issue_confidentiality(issue, issue.project, current_user)
end
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index 9a4e423f896..10867140d4f 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -6,10 +6,8 @@
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls
- = link_to params.merge(rss_url_options), class: 'btn' do
+ = link_to params.merge(rss_url_options), class: 'btn has-tooltip', title: 'Subscribe' do
= icon('rss')
- %span.icon-label
- Subscribe
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues
diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml
index 70705923d42..162ae153b1c 100644
--- a/app/views/dashboard/projects/starred.html.haml
+++ b/app/views/dashboard/projects/starred.html.haml
@@ -6,7 +6,7 @@
- if @last_push
= render "events/event_last_push", event: @last_push
-- if @projects.any?
+- if @projects.any? || params[:filter_projects]
= render 'projects'
- else
%h3 You don't have starred projects yet
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index 951f03083bf..a039756c7e2 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -1,6 +1,6 @@
- if inject_u2f_api?
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('u2f.js')
+ = page_specific_javascript_bundle_tag('u2f')
%div
= render 'devise/shared/tab_single', tab_title: 'Two-Factor Authentication'
diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml
index 2deadbeeceb..ee452add394 100644
--- a/app/views/discussions/_diff_discussion.html.haml
+++ b/app/views/discussions/_diff_discussion.html.haml
@@ -2,5 +2,5 @@
%tr.notes_holder{ class: ('hide' unless expanded) }
%td.notes_line{ colspan: 2 }
%td.notes_content
- .content
+ .content{ class: ('hide' unless expanded) }
= render "discussions/notes", discussion: discussion
diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml
index a196561f381..82aa51f9778 100644
--- a/app/views/doorkeeper/authorizations/new.html.haml
+++ b/app/views/doorkeeper/authorizations/new.html.haml
@@ -27,6 +27,7 @@
= hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :response_type, @pre_auth.response_type
= hidden_field_tag :scope, @pre_auth.scope
+ = hidden_field_tag :nonce, @pre_auth.nonce
= submit_tag "Authorize", class: "btn btn-success wide pull-left"
= form_tag oauth_authorization_path, method: :delete do
= hidden_field_tag :client_id, @pre_auth.client.uid
@@ -34,4 +35,5 @@
= hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :response_type, @pre_auth.response_type
= hidden_field_tag :scope, @pre_auth.scope
+ = hidden_field_tag :nonce, @pre_auth.nonce
= submit_tag "Deny", class: "btn btn-danger prepend-left-10"
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index f351e7feac9..299dace3406 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -1,20 +1,4 @@
-- if current_user
- .controls
- .dropdown.project-settings-dropdown
- %a.dropdown-new.btn.btn-default#project-settings-button{ href: '#', 'data-toggle' => 'dropdown' }
- = icon('cog')
- = icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- - can_edit = can?(current_user, :admin_project, @project)
-
- = render 'layouts/nav/project_settings', can_edit: can_edit
-
- - if can_edit
- %li.divider
- %li
- = link_to edit_project_path(@project) do
- Edit Project
-
+- can_edit = can?(current_user, :admin_project, @project)
.scrolling-tabs-container{ class: nav_control_class }
.fade-left
= icon('angle-left')
@@ -71,6 +55,17 @@
%span
Snippets
+ - if project_nav_tab? :settings
+ = nav_link(path: %w[projects#edit members#show integrations#show repository#show ci_cd#show pages#show]) do
+ = link_to edit_project_path(@project), title: 'Settings', class: 'shortcuts-tree' do
+ %span
+ Settings
+ - else
+ = nav_link(path: %w[members#show]) do
+ = link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Settings', class: 'shortcuts-tree' do
+ %span
+ Settings
+
-# Shortcut to Project > Activity
%li.hidden
= link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
deleted file mode 100644
index 665725f6862..00000000000
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ /dev/null
@@ -1,28 +0,0 @@
-- if project_nav_tab? :team
- = nav_link(controller: [:members, :teams]) do
- = link_to namespace_project_settings_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
- %span
- Members
-- if can_edit
- = nav_link(controller: :deploy_keys) do
- = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
- %span
- Deploy Keys
- = nav_link(controller: :integrations) do
- = link_to namespace_project_settings_integrations_path(@project.namespace, @project), title: 'Integrations' do
- %span
- Integrations
- = nav_link(controller: :protected_branches) do
- = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do
- %span
- Protected Branches
-
- - if @project.feature_available?(:builds, current_user)
- = nav_link(controller: :ci_cd) do
- = link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
- %span
- CI/CD Pipelines
- = nav_link(controller: :pages) do
- = link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages', data: {placement: 'right'} do
- %span
- Pages
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 558a1d56151..7ade5f00d47 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -4,7 +4,7 @@
- if inject_u2f_api?
- content_for :page_specific_javascripts do
- = page_specific_javascript_tag('u2f.js')
+ = page_specific_javascript_bundle_tag('u2f')
.row.prepend-top-default
.col-lg-3
@@ -96,4 +96,3 @@
:javascript
var button = "<a class='btn btn-xs btn-warning pull-right' data-method='patch' href='#{skip_profile_two_factor_auth_path}'>Configure it later</a>";
$(".flash-alert").append(button);
-
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index fb990dd9592..aa0cb3e1a50 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -1,5 +1,4 @@
- @no_container = true
-= render "projects/head"
%div{ class: container_class }
.nav-block.activity-filter-block
diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml
index 3c0f01cbf6f..27c8e3c7fca 100644
--- a/app/views/projects/activity.html.haml
+++ b/app/views/projects/activity.html.haml
@@ -1,4 +1,5 @@
- page_title "Activity"
+= render "projects/head"
= render 'projects/last_push'
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 41a7191302d..24ff74ecb3b 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -18,7 +18,7 @@
- else
= link_to title, '#'
-%ul.blob-commit-info.table-list.hidden-xs
+%ul.blob-commit-info.hidden-xs
- blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)
= render blob_commit, project: @project, ref: @ref
diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml
index 0993e880da9..4a4dd84d5d2 100644
--- a/app/views/projects/boards/components/_board_list.html.haml
+++ b/app/views/projects/boards/components/_board_list.html.haml
@@ -8,7 +8,7 @@
"v-show" => "!loading",
":data-board" => "list.id",
":class" => '{ "is-smaller": showIssueForm }' }
- %board-card{ "v-for" => "(issue, index) in orderedIssues",
+ %board-card{ "v-for" => "(issue, index) in issues",
"ref" => "issue",
":index" => "index",
":list" => "list",
@@ -17,7 +17,8 @@
":root-path" => "rootPath",
":disabled" => "disabled",
":key" => "issue.id" }
- %li.board-list-count.text-center{ "v-if" => "showCount" }
+ %li.board-list-count.text-center{ "v-if" => "showCount",
+ "data-issue-id" => "-1" }
= icon("spinner spin", "v-show" => "list.loadingMore" )
%span{ "v-if" => "list.issues.length === list.issuesSize" }
Showing all issues
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 4d0b7a5ca85..d001e01609a 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -34,8 +34,9 @@
= revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false)
%li.clearfix
= cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false)
- %li.clearfix
- = link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit)
+ - if can_collaborate_with_project?
+ %li.clearfix
+ = link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit)
%li.divider
%li.dropdown-header
Download
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 002e3d345dc..6ab9a80e083 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -9,33 +9,34 @@
- cache_key.push(commit.status(ref)) if commit.status(ref)
= cache(cache_key, expires_in: 1.day) do
- %li.commit.table-list-row.js-toggle-container{ id: "commit-#{commit.short_id}" }
+ %li.commit.flex-list.js-toggle-container{ id: "commit-#{commit.short_id}" }
- .table-list-cell.avatar-cell.hidden-xs
+ .avatar-cell.hidden-xs
= author_avatar(commit, size: 36)
- .table-list-cell.commit-content
- = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message item-title"
- %span.commit-row-message.visible-xs-inline
- &middot;
- = commit.short_id
- - if commit.status(ref)
- .visible-xs-inline
- = render_commit_status(commit, ref: ref)
- - if commit.description?
- %a.text-expander.hidden-xs.js-toggle-button ...
+ .commit-detail
+ .commit-content
+ = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message item-title"
+ %span.commit-row-message.visible-xs-inline
+ &middot;
+ = commit.short_id
+ - if commit.status(ref)
+ .visible-xs-inline
+ = render_commit_status(commit, ref: ref)
+ - if commit.description?
+ %a.text-expander.hidden-xs.js-toggle-button ...
- - if commit.description?
- %pre.commit-row-description.js-toggle-content
- = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author))
- .commiter
- = commit_author_link(commit, avatar: false, size: 24)
- committed
- #{time_ago_with_tooltip(commit.committed_date)}
+ - if commit.description?
+ %pre.commit-row-description.js-toggle-content
+ = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author))
+ .commiter
+ = commit_author_link(commit, avatar: false, size: 24)
+ committed
+ #{time_ago_with_tooltip(commit.committed_date)}
- .table-list-cell.commit-actions.hidden-xs
- - if commit.status(ref)
- = render_commit_status(commit, ref: ref)
- = clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard")
- = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
- = link_to_browse_code(project, commit)
+ .commit-actions.flex-row.hidden-xs
+ - if commit.status(ref)
+ = render_commit_status(commit, ref: ref)
+ = clipboard_button(clipboard_text: commit.id, title: "Copy commit SHA to clipboard")
+ = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
+ = link_to_browse_code(project, commit)
diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml
index 64d93e4141c..6f5835cb9be 100644
--- a/app/views/projects/commits/_commit_list.html.haml
+++ b/app/views/projects/commits/_commit_list.html.haml
@@ -11,4 +11,4 @@
%li.warning-row.unstyled
#{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
- else
- %ul.content-list.table-list= render commits, project: @project, ref: @ref
+ %ul.content-list= render commits, project: @project, ref: @ref
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index 904cdb5767f..88c7d7bc44b 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -4,7 +4,7 @@
- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits|
%li.commit-header #{day.strftime('%d %b, %Y')} #{pluralize(commits.count, 'commit')}
%li.commits-row
- %ul.content-list.commit-list.table-list.table-wide
+ %ul.content-list.commit-list
= render commits, project: project, ref: ref
- if hidden > 0
diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml
index d1e3cb14022..ec8fc4c9ee8 100644
--- a/app/views/projects/deploy_keys/_deploy_key.html.haml
+++ b/app/views/projects/deploy_keys/_deploy_key.html.haml
@@ -18,7 +18,7 @@
%span.key-created-at
created #{time_ago_with_tooltip(deploy_key.created_at)}
.visible-xs-block.visible-sm-block
- - if @available_keys.include?(deploy_key)
+ - if @deploy_keys.key_available?(deploy_key)
= link_to enable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: "btn btn-sm prepend-left-10", method: :put do
Enable
- else
diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml
index c91bb9c255a..1421da72418 100644
--- a/app/views/projects/deploy_keys/_form.html.haml
+++ b/app/views/projects/deploy_keys/_form.html.haml
@@ -1,5 +1,5 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input" } do |f|
- = form_errors(@key)
+= form_for [@project.namespace.becomes(Namespace), @project, @deploy_keys.new_key], url: namespace_project_deploy_keys_path, html: { class: "js-requires-input" } do |f|
+ = form_errors(@deploy_keys.new_key)
.form-group
= f.label :title, class: "label-light"
= f.text_field :title, class: 'form-control', autofocus: true, required: true
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
new file mode 100644
index 00000000000..0cbe9b3275a
--- /dev/null
+++ b/app/views/projects/deploy_keys/_index.html.haml
@@ -0,0 +1,34 @@
+.row.prepend-top-default
+ .col-lg-3.profile-settings-sidebar
+ %h4.prepend-top-0
+ Deploy Keys
+ %p
+ Deploy keys allow read-only access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
+ .col-lg-9
+ %h5.prepend-top-0
+ Create a new deploy key for this project
+ = render @deploy_keys.form_partial_path
+ .col-lg-9.col-lg-offset-3
+ %hr
+ .col-lg-9.col-lg-offset-3.append-bottom-default.deploy-keys
+ %h5.prepend-top-0
+ Enabled deploy keys for this project (#{@deploy_keys.enabled_keys_size})
+ - if @deploy_keys.any_keys_enabled?
+ %ul.well-list
+ = render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.enabled_keys, as: :deploy_key
+ - else
+ .settings-message.text-center
+ No deploy keys found. Create one with the form above.
+ %h5.prepend-top-default
+ Deploy keys from projects you have access to (#{@deploy_keys.available_project_keys_size})
+ - if @deploy_keys.any_available_project_keys_enabled?
+ %ul.well-list
+ = render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.available_project_keys, as: :deploy_key
+ - else
+ .settings-message.text-center
+ No deploy keys from your projects could be found. Create one with the form above or add existing one below.
+ - if @deploy_keys.any_available_public_keys_enabled?
+ %h5.prepend-top-default
+ Public deploy keys available to any project (#{@deploy_keys.available_public_keys_size})
+ %ul.well-list
+ = render partial: 'projects/deploy_keys/deploy_key', collection: @deploy_keys.available_public_keys, as: :deploy_key
diff --git a/app/views/projects/deploy_keys/index.html.haml b/app/views/projects/deploy_keys/index.html.haml
deleted file mode 100644
index 04fbb37d93f..00000000000
--- a/app/views/projects/deploy_keys/index.html.haml
+++ /dev/null
@@ -1,36 +0,0 @@
-- page_title "Deploy Keys"
-
-.row.prepend-top-default
- .col-lg-3.profile-settings-sidebar
- %h4.prepend-top-0
- = page_title
- %p
- Deploy keys allow read-only access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
- .col-lg-9
- %h5.prepend-top-0
- Create a new deploy key for this project
- = render "form"
- .col-lg-9.col-lg-offset-3
- %hr
- .col-lg-9.col-lg-offset-3.append-bottom-default.deploy-keys
- %h5.prepend-top-0
- Enabled deploy keys for this project (#{@enabled_keys.size})
- - if @enabled_keys.any?
- %ul.well-list
- = render @enabled_keys
- - else
- .settings-message.text-center
- No deploy keys found. Create one with the form above or add existing one below.
- %h5.prepend-top-default
- Deploy keys from projects you have access to (#{@available_project_keys.size})
- - if @available_project_keys.any?
- %ul.well-list
- = render @available_project_keys
- - else
- .settings-message.text-center
- No deploy keys from your projects could be found. Create one with the form above or add existing one below.
- - if @available_public_keys.any?
- %h5.prepend-top-default
- Public deploy keys available to any project (#{@available_public_keys.size})
- %ul.well-list
- = render @available_public_keys
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
index a680b1ca017..506246f2ee6 100644
--- a/app/views/projects/deployments/_actions.haml
+++ b/app/views/projects/deployments/_actions.haml
@@ -1,9 +1,9 @@
- if can?(current_user, :create_deployment, deployment)
- actions = deployment.manual_actions
- if actions.present?
- .inline
+ .btn-group
.dropdown
- %a.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' }
+ %button.dropdown.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' }
= custom_icon('icon_play')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
@@ -12,4 +12,3 @@
= link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do
= custom_icon('icon_play')
%span= action.name.humanize
-
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index c468202569f..260c9023daf 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -17,6 +17,6 @@
#{time_ago_with_tooltip(deployment.created_at)}
%td.hidden-xs
- .pull-right
+ .pull-right.btn-group
= render 'projects/deployments/actions', deployment: deployment
= render 'projects/deployments/rollback', deployment: deployment
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index cd18ba2ed00..ed279cfe168 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -1,8 +1,11 @@
- email = local_assigns.fetch(:email, false)
- plain = local_assigns.fetch(:plain, false)
+- discussions = local_assigns.fetch(:discussions, nil)
- type = line.type
- line_code = diff_file.line_code(line)
-%tr.line_holder{ plain ? { class: type} : { class: type, id: line_code } }
+- if discussions && !line.meta?
+ - discussion = discussions[line_code]
+%tr.line_holder{ class: type, id: (line_code unless plain) }
- case type
- when 'match'
= diff_match_line line.old_pos, line.new_pos, text: line.text
@@ -11,12 +14,14 @@
%td.new_line.diff-line-num
%td.line_content.match= line.text
- else
- %td.old_line.diff-line-num{ class: type, data: { linenumber: line.old_pos } }
+ %td.old_line.diff-line-num.js-avatar-container{ class: type, data: { linenumber: line.old_pos } }
- link_text = type == "new" ? " " : line.old_pos
- if plain
= link_text
- else
%a{ href: "##{line_code}", data: { linenumber: link_text } }
+ - if discussion && !plain
+ %diff-note-avatars{ "discussion-id" => discussion.id }
%td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
- link_text = type == "old" ? " " : line.new_pos
- if plain
@@ -29,9 +34,6 @@
- else
= diff_line_content(line.text)
-- discussions = local_assigns.fetch(:discussions, nil)
-- if discussions && !line.meta?
- - discussion = discussions[line_code]
- - if discussion
- - discussion_expanded = local_assigns.fetch(:discussion_expanded, discussion.expanded?)
- = render "discussions/diff_discussion", discussion: discussion, expanded: discussion_expanded
+- if discussion
+ - discussion_expanded = local_assigns.fetch(:discussion_expanded, discussion.expanded?)
+ = render "discussions/diff_discussion", discussion: discussion, expanded: discussion_expanded
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 997bf0fc560..6448748113b 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -4,6 +4,9 @@
- diff_file.parallel_diff_lines.each do |line|
- left = line[:left]
- right = line[:right]
+ - last_line = right.new_pos if right
+ - unless @diff_notes_disabled
+ - discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file)
%tr.line_holder.parallel
- if left
- case left.type
@@ -15,8 +18,10 @@
- else
- left_line_code = diff_file.line_code(left)
- left_position = diff_file.position(left)
- %td.old_line.diff-line-num{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } }
+ %td.old_line.diff-line-num.js-avatar-container{ id: left_line_code, class: left.type, data: { linenumber: left.old_pos } }
%a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } }
+ - if discussion_left
+ %diff-note-avatars{ "discussion-id" => discussion_left.id }
%td.line_content.parallel.noteable_line{ class: left.type, data: diff_view_line_data(left_line_code, left_position, 'old') }= diff_line_content(left.text)
- else
%td.old_line.diff-line-num.empty-cell
@@ -32,17 +37,17 @@
- else
- right_line_code = diff_file.line_code(right)
- right_position = diff_file.position(right)
- %td.new_line.diff-line-num{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } }
+ %td.new_line.diff-line-num.js-avatar-container{ id: right_line_code, class: right.type, data: { linenumber: right.new_pos } }
%a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } }
+ - if discussion_right
+ %diff-note-avatars{ "discussion-id" => discussion_right.id }
%td.line_content.parallel.noteable_line{ class: right.type, data: diff_view_line_data(right_line_code, right_position, 'new') }= diff_line_content(right.text)
- else
%td.old_line.diff-line-num.empty-cell
%td.line_content.parallel
- - unless @diff_notes_disabled
- - discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file)
- - if discussion_left || discussion_right
- = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right
+ - if discussion_left || discussion_right
+ = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right
- if !diff_file.new_file && !diff_file.deleted_file && diff_file.diff_lines.any?
- last_line = diff_file.diff_lines.last
- if last_line.new_pos < total_lines
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 83ae9fd10ec..2802a4eca7b 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -1,3 +1,4 @@
+= render "projects/settings/head"
.project-edit-container
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml
new file mode 100644
index 00000000000..acbac1869fd
--- /dev/null
+++ b/app/views/projects/environments/_metrics_button.html.haml
@@ -0,0 +1,6 @@
+- environment = local_assigns.fetch(:environment)
+
+- return unless environment.has_metrics? && can?(current_user, :read_environment, environment)
+
+= link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do
+ = icon('area-chart')
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
new file mode 100644
index 00000000000..f8e94ca98ae
--- /dev/null
+++ b/app/views/projects/environments/metrics.html.haml
@@ -0,0 +1,21 @@
+- @no_container = true
+- page_title "Metrics for environment", @environment.name
+= render "projects/pipelines/head"
+
+%div{ class: container_class }
+ .top-area
+ .row
+ .col-sm-6
+ %h3.page-title
+ Environment:
+ = @environment.name
+
+ .col-sm-6
+ .nav-controls
+ = render 'projects/deployments/actions', deployment: @environment.last_deployment
+ .row
+ .col-sm-12
+ %svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
+ .row
+ .col-sm-12
+ %svg.prometheus-graph{ 'graph-type' => 'memory_values' }
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 7036325fff8..f463a429f65 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -8,6 +8,7 @@
%h3.page-title= @environment.name
.col-md-3
.nav-controls
+ = render 'projects/environments/metrics_button', environment: @environment
= render 'projects/environments/terminal_button', environment: @environment
= render 'projects/environments/external_url', environment: @environment
- if can?(current_user, :update_environment, @environment)
@@ -15,7 +16,7 @@
- if can?(current_user, :create_deployment, @environment) && @environment.can_stop?
= link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post
- .deployments-container
+ .environments-container
- if @deployments.blank?
.blank-state.blank-state-no-icon
%h2.blank-state-title
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index 466ec1475d8..ad14b4e583e 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -21,7 +21,7 @@
selected: f.object.source_project_id
.merge-request-select.dropdown
= f.hidden_field :source_branch
- = dropdown_toggle f.object.source_branch || "Select source branch", { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
+ = dropdown_toggle local_assigns.fetch(f.object.source_branch, "Select source branch"), { toggle: "dropdown", field_name: "#{f.object_name}[source_branch]" }, { toggle_class: "js-compare-dropdown js-source-branch" }
.dropdown-menu.dropdown-menu-selectable.dropdown-source-branch
= dropdown_title("Select source branch")
= dropdown_filter("Search branches")
@@ -30,7 +30,7 @@
branches: @merge_request.source_branches,
selected: f.object.source_branch
.panel-footer
- = icon('spinner spin', class: 'js-source-loading')
+ .text-center= icon('spinner spin', class: 'js-source-loading')
%ul.list-unstyled.mr_source_commit
.col-md-6
@@ -60,7 +60,7 @@
branches: @merge_request.target_branches,
selected: f.object.target_branch
.panel-footer
- = icon('spinner spin', class: "js-target-loading")
+ .text-center= icon('spinner spin', class: "js-target-loading")
%ul.list-unstyled.mr_target_commit
- if @merge_request.errors.any?
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index a73e8f345e0..a7618370a5d 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -2,7 +2,7 @@
- return if note.cross_reference_not_visible_for?(current_user)
- note_editable = note_editable?(note)
-%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable} }
+%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} }
.timeline-entry-inner
.timeline-icon
%a{ href: user_path(note.author) }
@@ -30,11 +30,15 @@
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
- %resolve-btn{ "discussion-id" => "#{note.discussion_id}",
+ %resolve-btn{ "project-path" => project_path(note.project),
+ "discussion-id" => note.discussion_id,
":note-id" => note.id,
":resolved" => note.resolved?,
":can-resolve" => can_resolve,
- "resolved-by" => "#{note.resolved_by.try(:name)}",
+ ":author-name" => "'#{j(note.author.name)}'",
+ "author-avatar" => note.author.avatar_url,
+ ":note-truncated" => "'#{truncate(note.note, length: 17)}'",
+ ":resolved-by" => "'#{j(note.resolved_by.try(:name))}'",
"v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true,
"ref" => "note_#{note.id}" }
diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml
index b6595269b06..259d5bd63d6 100644
--- a/app/views/projects/pages/show.html.haml
+++ b/app/views/projects/pages/show.html.haml
@@ -1,4 +1,6 @@
- page_title 'Pages'
+= render "projects/settings/head"
+
%h3.page_title
Pages
diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml
index 04b19a8c5a7..cf0db943865 100644
--- a/app/views/projects/protected_branches/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/_branches_list.html.haml
@@ -23,6 +23,6 @@
- if can_admin_project
%th
%tbody
- = render partial: @protected_branches, locals: { can_admin_project: can_admin_project }
+ = render partial: 'projects/protected_branches/protected_branch', collection: @protected_branches, locals: { can_admin_project: can_admin_project}
= paginate @protected_branches, theme: 'gitlab'
diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml
index e95a3b1b4c3..b8e885b4d9a 100644
--- a/app/views/projects/protected_branches/_create_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml
@@ -10,7 +10,7 @@
= f.label :name, class: 'col-md-2 text-right' do
Branch:
.col-md-10
- = render partial: "dropdown", locals: { f: f }
+ = render partial: "projects/protected_branches/dropdown", locals: { f: f }
.help-block
= link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches')
such as
diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/_index.html.haml
index b3b419bd92d..2d8c519c025 100644
--- a/app/views/projects/protected_branches/index.html.haml
+++ b/app/views/projects/protected_branches/_index.html.haml
@@ -1,11 +1,10 @@
-- page_title "Protected branches"
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('protected_branches')
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
- = page_title
+ Protected Branches
%p Keep stable branches secure and force developers to use merge requests.
%p.prepend-top-20
By default, protected branches are designed to:
@@ -17,6 +16,6 @@
%p.append-bottom-0 Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches"), class: "underlined-link"} and #{link_to "project permissions", help_page_path("user/permissions"), class: "underlined-link"}.
.col-lg-9
- if can? current_user, :admin_project, @project
- = render 'create_protected_branch'
+ = render 'projects/protected_branches/create_protected_branch'
- = render "branches_list"
+ = render "projects/protected_branches/branches_list"
diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml
index 0193800dedf..b2a6b8469a3 100644
--- a/app/views/projects/protected_branches/_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_protected_branch.html.haml
@@ -14,7 +14,7 @@
- else
(branch was removed from repository)
- = render partial: 'update_protected_branch', locals: { protected_branch: protected_branch }
+ = render partial: 'projects/protected_branches/update_protected_branch', locals: { protected_branch: protected_branch }
- if can_admin_project
%td
diff --git a/app/views/projects/settings/_head.html.haml b/app/views/projects/settings/_head.html.haml
new file mode 100644
index 00000000000..88bcb541dac
--- /dev/null
+++ b/app/views/projects/settings/_head.html.haml
@@ -0,0 +1,33 @@
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: container_class }
+ - can_edit = can?(current_user, :admin_project, @project)
+ - if can_edit
+ = nav_link(controller: :projects) do
+ = link_to edit_project_path(@project), title: 'General' do
+ %span
+ General
+ = nav_link(controller: :members) do
+ = link_to project_settings_members_path(@project), title: 'Members' do
+ %span
+ Members
+ - if can_edit
+ = nav_link(controller: :integrations) do
+ = link_to project_settings_integrations_path(@project), title: 'Integrations' do
+ %span
+ Integrations
+ = nav_link(controller: :repository) do
+ = link_to namespace_project_settings_repository_path(@project.namespace, @project), title: 'Repository' do
+ %span
+ Repository
+ - if @project.feature_available?(:builds, current_user)
+ = nav_link(controller: :ci_cd) do
+ = link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'CI/CD Pipelines' do
+ %span
+ CI/CD Pipelines
+ = nav_link(controller: :pages) do
+ = link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages' do
+ %span
+ Pages
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 52f5f7b81e2..e2603096014 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -1,4 +1,5 @@
- page_title "CI/CD Pipelines"
+= render "projects/settings/head"
= render 'projects/runners/index'
= render 'projects/variables/index'
diff --git a/app/views/projects/settings/integrations/show.html.haml b/app/views/projects/settings/integrations/show.html.haml
index aa38a889cdd..f69992566b5 100644
--- a/app/views/projects/settings/integrations/show.html.haml
+++ b/app/views/projects/settings/integrations/show.html.haml
@@ -1,3 +1,4 @@
- page_title 'Integrations'
+= render "projects/settings/head"
= render 'projects/hooks/index'
= render 'projects/services/index'
diff --git a/app/views/projects/settings/members/show.html.haml b/app/views/projects/settings/members/show.html.haml
index d81ed7bb609..20e1ad68244 100644
--- a/app/views/projects/settings/members/show.html.haml
+++ b/app/views/projects/settings/members/show.html.haml
@@ -1,4 +1,5 @@
- page_title "Members"
+= render "projects/settings/head"
= render "projects/project_members/index"
- if can?(current_user, :admin_project, @project)
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
new file mode 100644
index 00000000000..4c02302e161
--- /dev/null
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -0,0 +1,5 @@
+- page_title "Repository"
+= render "projects/settings/head"
+
+= render @deploy_keys
+= render "projects/protected_branches/index"
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 22004ecacbc..02133d09cdf 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -11,7 +11,7 @@
.results.prepend-top-10
- if @scope == 'commits'
- %ul.content-list.commit-list.table-list.table-wide
+ %ul.content-list.commit-list
= render partial: "search/results/commit", collection: @search_objects
- else
.search-results
diff --git a/app/views/shared/icons/_collapse.svg.erb b/app/views/shared/icons/_collapse.svg.erb
new file mode 100644
index 00000000000..917753fb343
--- /dev/null
+++ b/app/views/shared/icons/_collapse.svg.erb
@@ -0,0 +1 @@
+<svg width="<%= size %>" height="<%= size %>" viewBox="0 0 9 13"><path d="M2.57568253,6.49866948 C2.50548852,6.57199715 2.44637866,6.59708255 2.39835118,6.57392645 C2.3503237,6.55077034 2.32631032,6.48902165 2.32631032,6.38867852 L2.32631032,-2.13272614 C2.32631032,-2.23306927 2.3503237,-2.29481796 2.39835118,-2.31797406 C2.44637866,-2.34113017 2.50548852,-2.31604477 2.57568253,-2.24271709 L6.51022184,1.86747129 C6.53977721,1.8983461 6.56379059,1.93500939 6.5822627,1.97746225 L6.5822627,2.27849013 C6.56379059,2.31708364 6.53977721,2.35374693 6.51022184,2.38848109 L2.57568253,6.49866948 Z" transform="translate(4.454287, 2.127976) rotate(90.000000) translate(-4.454287, -2.127976) "></path><path d="M3.74312342,2.09553332 C3.74312342,1.99519019 3.77821989,1.9083561 3.8484139,1.83502843 C3.91860791,1.76170075 4.00173115,1.72503747 4.09778611,1.72503747 L4.80711151,1.72503747 C4.90316647,1.72503747 4.98628971,1.76170075 5.05648372,1.83502843 C5.12667773,1.9083561 5.16177421,1.99519019 5.16177421,2.09553332 L5.16177421,10.2464421 C5.16177421,10.3467853 5.12667773,10.4336194 5.05648372,10.506947 C4.98628971,10.5802747 4.90316647,10.616938 4.80711151,10.616938 L4.09778611,10.616938 C4.00173115,10.616938 3.91860791,10.5802747 3.8484139,10.506947 C3.77821989,10.4336194 3.74312342,10.3467853 3.74312342,10.2464421 L3.74312342,2.09553332 Z" transform="translate(4.452449, 6.170988) rotate(-90.000000) translate(-4.452449, -6.170988) "></path><path d="M2.57568253,14.6236695 C2.50548852,14.6969971 2.44637866,14.7220826 2.39835118,14.6989264 C2.3503237,14.6757703 2.32631032,14.6140216 2.32631032,14.5136785 L2.32631032,5.99227386 C2.32631032,5.89193073 2.3503237,5.83018204 2.39835118,5.80702594 C2.44637866,5.78386983 2.50548852,5.80895523 2.57568253,5.88228291 L6.51022184,9.99247129 C6.53977721,10.0233461 6.56379059,10.0600094 6.5822627,10.1024622 L6.5822627,10.4034901 C6.56379059,10.4420836 6.53977721,10.4787469 6.51022184,10.5134811 L2.57568253,14.6236695 Z" transform="translate(4.454287, 10.252976) scale(1, -1) rotate(90.000000) translate(-4.454287, -10.252976) "></path></svg>
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index 62f09cc2dc1..32128f3b3dc 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -11,10 +11,13 @@
class: "check_all_issues left"
.issues-other-filters.filtered-search-container
.filtered-search-input-container
- %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]), 'data-base-endpoint' => namespace_project_path(@project.namespace, @project) }
- = icon('filter')
- %button.clear-search.hidden{ type: 'button' }
- = icon('times')
+ .scroll-container
+ %ul.tokens-container.list-unstyled
+ %li.input-token
+ %input.form-control.filtered-search{ placeholder: 'Search or filter results...', 'data-id' => 'filtered-search', 'data-project-id' => @project.id, 'data-username-params' => @users.to_json(only: [:id, :username]), 'data-base-endpoint' => namespace_project_path(@project.namespace, @project) }
+ = icon('filter')
+ %button.clear-search.hidden{ type: 'button' }
+ = icon('times')
#js-dropdown-hint.dropdown-menu.hint-dropdown
%ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-action' => 'submit' }
@@ -112,7 +115,7 @@
= hidden_field_tag 'update[issuable_ids]', []
= hidden_field_tag :state_event, params[:state_event]
- .filter-item.inline
+ .filter-item.inline.update-issues-btn
= button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
:javascript
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 2fff6b0105d..2cd87895c55 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -3,8 +3,8 @@ class PostReceive
include DedicatedSidekiqQueue
def perform(repo_path, identifier, changes)
- if path = Gitlab.config.repositories.storages.find { |p| repo_path.start_with?(p[1].to_s) }
- repo_path.gsub!(path[1].to_s, "")
+ 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
diff --git a/changelogs/unreleased/1381-present-commits-pagination-headers-correctly.yml b/changelogs/unreleased/1381-present-commits-pagination-headers-correctly.yml
new file mode 100644
index 00000000000..1b7e294bd67
--- /dev/null
+++ b/changelogs/unreleased/1381-present-commits-pagination-headers-correctly.yml
@@ -0,0 +1,4 @@
+---
+title: "GET 'projects/:id/repository/commits' endpoint improvements"
+merge_request: 9679
+author: George Andrinopoulos, Jordan Ryan Reuter
diff --git a/changelogs/unreleased/18962-update-issues-button-jumps.yml b/changelogs/unreleased/18962-update-issues-button-jumps.yml
new file mode 100644
index 00000000000..7be136ac4ff
--- /dev/null
+++ b/changelogs/unreleased/18962-update-issues-button-jumps.yml
@@ -0,0 +1,4 @@
+---
+title: Align bulk update issues button to the right
+merge_request:
+author:
diff --git a/changelogs/unreleased/26188-tag-creation-404-for-guests.yml b/changelogs/unreleased/26188-tag-creation-404-for-guests.yml
new file mode 100644
index 00000000000..fb00d46ea1f
--- /dev/null
+++ b/changelogs/unreleased/26188-tag-creation-404-for-guests.yml
@@ -0,0 +1,4 @@
+---
+title: Don't show links to tag a commit for users that are not permitted
+merge_request: 8407
+author:
diff --git a/changelogs/unreleased/26202-change-dropdown-style-slightly.yml b/changelogs/unreleased/26202-change-dropdown-style-slightly.yml
new file mode 100644
index 00000000000..827224abf5a
--- /dev/null
+++ b/changelogs/unreleased/26202-change-dropdown-style-slightly.yml
@@ -0,0 +1,4 @@
+---
+title: Changed dropdown style slightly
+merge_request:
+author:
diff --git a/changelogs/unreleased/26732-combine-deploy-keys-and-push-rules-and-mirror-repository-and-protect-branches-settings-pages.yml b/changelogs/unreleased/26732-combine-deploy-keys-and-push-rules-and-mirror-repository-and-protect-branches-settings-pages.yml
new file mode 100644
index 00000000000..6fc4615dab8
--- /dev/null
+++ b/changelogs/unreleased/26732-combine-deploy-keys-and-push-rules-and-mirror-repository-and-protect-branches-settings-pages.yml
@@ -0,0 +1,5 @@
+---
+title: Combined deploy keys, push rules, protect branches and mirror repository settings options into a single one called
+ Repository
+merge_request:
+author:
diff --git a/changelogs/unreleased/27568-refactor-very-slow-dropdown-asignee-spec.yml b/changelogs/unreleased/27568-refactor-very-slow-dropdown-asignee-spec.yml
new file mode 100644
index 00000000000..5c738af7704
--- /dev/null
+++ b/changelogs/unreleased/27568-refactor-very-slow-dropdown-asignee-spec.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor dropdown_assignee_spec
+merge_request: 9711
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/27936-make-all-uploads-require-revalidation-on-each-browser-fetch.yml b/changelogs/unreleased/27936-make-all-uploads-require-revalidation-on-each-browser-fetch.yml
new file mode 100644
index 00000000000..adc129d8dca
--- /dev/null
+++ b/changelogs/unreleased/27936-make-all-uploads-require-revalidation-on-each-browser-fetch.yml
@@ -0,0 +1,4 @@
+---
+title: Uploaded files which content can change now require revalidation on each page load
+merge_request: 9453
+author:
diff --git a/changelogs/unreleased/28402-fix-starred-projects-filter-wrong-message-on-no-results.yml b/changelogs/unreleased/28402-fix-starred-projects-filter-wrong-message-on-no-results.yml
new file mode 100644
index 00000000000..dd94b3fe663
--- /dev/null
+++ b/changelogs/unreleased/28402-fix-starred-projects-filter-wrong-message-on-no-results.yml
@@ -0,0 +1,4 @@
+---
+title: Fix wrong message on starred projects filtering
+merge_request:
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/28447-hybrid-repository-storages.yml b/changelogs/unreleased/28447-hybrid-repository-storages.yml
new file mode 100644
index 00000000000..00dfc5781b9
--- /dev/null
+++ b/changelogs/unreleased/28447-hybrid-repository-storages.yml
@@ -0,0 +1,4 @@
+---
+title: Update storage settings to allow extra values per repository storage
+merge_request: 9597
+author:
diff --git a/changelogs/unreleased/28609-fix-redirect-to-home-page-url.yml b/changelogs/unreleased/28609-fix-redirect-to-home-page-url.yml
deleted file mode 100644
index baf832d4495..00000000000
--- a/changelogs/unreleased/28609-fix-redirect-to-home-page-url.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix the redirect to custom home page URL
-merge_request: 9518
-author:
diff --git a/changelogs/unreleased/28850-fix-broken-migration.yml b/changelogs/unreleased/28850-fix-broken-migration.yml
deleted file mode 100644
index 7f59a7708bc..00000000000
--- a/changelogs/unreleased/28850-fix-broken-migration.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix broken migration when upgrading straight to 8.17.1
-merge_request: 9613
-author:
diff --git a/changelogs/unreleased/29034-fix-github-importer.yml b/changelogs/unreleased/29034-fix-github-importer.yml
new file mode 100644
index 00000000000..6d08db3d55d
--- /dev/null
+++ b/changelogs/unreleased/29034-fix-github-importer.yml
@@ -0,0 +1,4 @@
+---
+title: Fix name colision when importing GitHub pull requests from forked repositories
+merge_request: 9719
+author:
diff --git a/changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml b/changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml
new file mode 100644
index 00000000000..ad0c513f525
--- /dev/null
+++ b/changelogs/unreleased/29162-refactor-dropdown-milestone-spec.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor dropdown_milestone_spec.rb
+merge_request:
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/add-changelog-filtered-search-visual-tokens.yml b/changelogs/unreleased/add-changelog-filtered-search-visual-tokens.yml
new file mode 100644
index 00000000000..d10e4cb7c87
--- /dev/null
+++ b/changelogs/unreleased/add-changelog-filtered-search-visual-tokens.yml
@@ -0,0 +1,4 @@
+---
+title: Add filtered search visual tokens
+merge_request: 8969
+author:
diff --git a/changelogs/unreleased/clear-connections-before-starting-sidekiq.yml b/changelogs/unreleased/clear-connections-before-starting-sidekiq.yml
new file mode 100644
index 00000000000..8778fac6e9d
--- /dev/null
+++ b/changelogs/unreleased/clear-connections-before-starting-sidekiq.yml
@@ -0,0 +1,4 @@
+---
+title: Clear ActiveRecord connections before starting Sidekiq
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-dont-copy-toolip.yml b/changelogs/unreleased/dm-dont-copy-toolip.yml
deleted file mode 100644
index 2b134da66ab..00000000000
--- a/changelogs/unreleased/dm-dont-copy-toolip.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Don't copy tooltip when copying GFM
-merge_request:
-author:
diff --git a/changelogs/unreleased/dm-fix-api-create-file-on-empty-repo.yml b/changelogs/unreleased/dm-fix-api-create-file-on-empty-repo.yml
deleted file mode 100644
index 7ac25c0a83e..00000000000
--- a/changelogs/unreleased/dm-fix-api-create-file-on-empty-repo.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix creating a file in an empty repo using the API
-merge_request: 9632
-author:
diff --git a/changelogs/unreleased/dm-fix-cherry-pick.yml b/changelogs/unreleased/dm-fix-cherry-pick.yml
deleted file mode 100644
index e924b821d7e..00000000000
--- a/changelogs/unreleased/dm-fix-cherry-pick.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix cherry-picking or reverting through an MR
-merge_request:
-author:
diff --git a/changelogs/unreleased/es6-class-issue.yml b/changelogs/unreleased/es6-class-issue.yml
new file mode 100644
index 00000000000..9d1c3ac7421
--- /dev/null
+++ b/changelogs/unreleased/es6-class-issue.yml
@@ -0,0 +1,4 @@
+---
+title: Convert Issue into ES6 class
+merge_request: 9636
+author: winniehell
diff --git a/changelogs/unreleased/feature-openid-connect.yml b/changelogs/unreleased/feature-openid-connect.yml
new file mode 100644
index 00000000000..e84eb7aff86
--- /dev/null
+++ b/changelogs/unreleased/feature-openid-connect.yml
@@ -0,0 +1,4 @@
+---
+title: Implement OpenID Connect identity provider
+merge_request: 8018
+author: Markus Koller
diff --git a/changelogs/unreleased/fix-29093.yml b/changelogs/unreleased/fix-29093.yml
new file mode 100644
index 00000000000..791129afe93
--- /dev/null
+++ b/changelogs/unreleased/fix-29093.yml
@@ -0,0 +1,4 @@
+---
+title: Fix 'Object not found - no match for id (sha)' when importing GitHub Pull Requests
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-gb-deprecate-ci-config-types.yml b/changelogs/unreleased/fix-gb-deprecate-ci-config-types.yml
new file mode 100644
index 00000000000..605b5f01d0e
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-deprecate-ci-config-types.yml
@@ -0,0 +1,4 @@
+---
+title: Deprecate usage of `types` configuration entry to describe CI/CD stages
+merge_request: 9766
+author:
diff --git a/changelogs/unreleased/issue_16834.yml b/changelogs/unreleased/issue_16834.yml
new file mode 100644
index 00000000000..06175579ac3
--- /dev/null
+++ b/changelogs/unreleased/issue_16834.yml
@@ -0,0 +1,4 @@
+---
+title: Update API endpoints for raw files
+merge_request:
+author:
diff --git a/changelogs/unreleased/priority-to-label-priority.yml b/changelogs/unreleased/priority-to-label-priority.yml
new file mode 100644
index 00000000000..2d9c58bfd9b
--- /dev/null
+++ b/changelogs/unreleased/priority-to-label-priority.yml
@@ -0,0 +1,4 @@
+---
+title: Rename priority sorting option to label priority
+merge_request:
+author:
diff --git a/changelogs/unreleased/rfr-20170307-change-default-project-number-limit.yml b/changelogs/unreleased/rfr-20170307-change-default-project-number-limit.yml
new file mode 100644
index 00000000000..e799dd3b48d
--- /dev/null
+++ b/changelogs/unreleased/rfr-20170307-change-default-project-number-limit.yml
@@ -0,0 +1,4 @@
+---
+title: Change project count limit from 10 to 100000
+merge_request:
+author:
diff --git a/changelogs/unreleased/settings-tab.yml b/changelogs/unreleased/settings-tab.yml
new file mode 100644
index 00000000000..69990c9a917
--- /dev/null
+++ b/changelogs/unreleased/settings-tab.yml
@@ -0,0 +1,4 @@
+---
+title: Moved project settings from the gear drop-down menu to a tab
+merge_request: 9786
+author:
diff --git a/changelogs/unreleased/tc-api-pipeline-jobs.yml b/changelogs/unreleased/tc-api-pipeline-jobs.yml
new file mode 100644
index 00000000000..993c1b6526a
--- /dev/null
+++ b/changelogs/unreleased/tc-api-pipeline-jobs.yml
@@ -0,0 +1,4 @@
+---
+title: Add GET /projects/:id/pipelines/:pipeline_id/jobs endpoint
+merge_request: 9727
+author:
diff --git a/changelogs/unreleased/use-v3-api-on-frontend.yml b/changelogs/unreleased/use-v3-api-on-frontend.yml
deleted file mode 100644
index 467ad3c8276..00000000000
--- a/changelogs/unreleased/use-v3-api-on-frontend.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Make projects dropdown only show projects you are a member of
-merge_request: 9614
-author:
diff --git a/changelogs/unreleased/zj-variables-build-job.yml b/changelogs/unreleased/zj-variables-build-job.yml
new file mode 100644
index 00000000000..1cb0919f824
--- /dev/null
+++ b/changelogs/unreleased/zj-variables-build-job.yml
@@ -0,0 +1,4 @@
+---
+title: Rename job environment variables to new terminology
+merge_request: 9756
+author:
diff --git a/config/application.rb b/config/application.rb
index cdb93e50e66..1cc092c4da1 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -100,7 +100,6 @@ module Gitlab
config.assets.precompile << "katex.js"
config.assets.precompile << "xterm/xterm.css"
config.assets.precompile << "lib/ace.js"
- config.assets.precompile << "u2f.js"
config.assets.precompile << "vendor/assets/fonts/*"
# Version of your assets, change this if you want to expire all your assets
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index be34a4000fa..720df0cac2d 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -461,7 +461,8 @@ production: &base
# gitlab-shell invokes Dir.pwd inside the repository path and that results
# real path not the symlink.
storages: # You must have at least a `default` storage path.
- default: /home/git/repositories/
+ default:
+ path: /home/git/repositories/
## Backup settings
backup:
@@ -574,7 +575,8 @@ test:
path: tmp/tests/gitlab-satellites/
repositories:
storages:
- default: tmp/tests/repositories/
+ default:
+ path: tmp/tests/repositories/
backup:
path: tmp/tests/backups
gitlab_shell:
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 933844e4ea6..b45d0e23080 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -83,7 +83,7 @@ class Settings < Settingslogic
def base_url(config)
custom_port = on_standard_port?(config) ? nil : ":#{config.port}"
-
+
[
config.protocol,
"://",
@@ -186,7 +186,7 @@ Settings['issues_tracker'] ||= {}
# GitLab
#
Settings['gitlab'] ||= Settingslogic.new({})
-Settings.gitlab['default_projects_limit'] ||= 10
+Settings.gitlab['default_projects_limit'] ||= 100000
Settings.gitlab['default_branch_protection'] ||= 2
Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil?
Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost'
@@ -366,8 +366,13 @@ Settings.gitlab_shell['ssh_path_prefix'] ||= Settings.send(:build_gitlab_shell_s
#
Settings['repositories'] ||= Settingslogic.new({})
Settings.repositories['storages'] ||= {}
-# Setting gitlab_shell.repos_path is DEPRECATED and WILL BE REMOVED in version 9.0
-Settings.repositories.storages['default'] ||= Settings.gitlab_shell['repos_path'] || Settings.gitlab['user_home'] + '/repositories/'
+unless Settings.repositories.storages['default']
+ Settings.repositories.storages['default'] ||= {}
+ # We set the path only if the default storage doesn't exist, in case it exists
+ # but follows the pre-9.0 configuration structure. `6_validations.rb` initializer
+ # will validate all storages and throw a relevant error to the user if necessary.
+ Settings.repositories.storages['default']['path'] ||= Settings.gitlab['user_home'] + '/repositories/'
+end
#
# The repository_downloads_path is used to remove outdated repository
@@ -376,11 +381,11 @@ Settings.repositories.storages['default'] ||= Settings.gitlab_shell['repos_path'
# data-integrity issue. In this case, we sets it to the default
# repository_downloads_path value.
#
-repositories_storages_path = Settings.repositories.storages.values
+repositories_storages = Settings.repositories.storages.values
repository_downloads_path = Settings.gitlab['repository_downloads_path'].to_s.gsub(/\/$/, '')
repository_downloads_full_path = File.expand_path(repository_downloads_path, Settings.gitlab['user_home'])
-if repository_downloads_path.blank? || repositories_storages_path.any? { |path| [repository_downloads_path, repository_downloads_full_path].include?(path.gsub(/\/$/, '')) }
+if repository_downloads_path.blank? || repositories_storages.any? { |rs| [repository_downloads_path, repository_downloads_full_path].include?(rs['path'].gsub(/\/$/, '')) }
Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive')
end
diff --git a/config/initializers/6_validations.rb b/config/initializers/6_validations.rb
index d92f64e1647..abe570f430c 100644
--- a/config/initializers/6_validations.rb
+++ b/config/initializers/6_validations.rb
@@ -4,8 +4,8 @@ end
def find_parent_path(name, path)
parent = Pathname.new(path).realpath.parent
- Gitlab.config.repositories.storages.detect do |n, p|
- name != n && Pathname.new(p).realpath == parent
+ Gitlab.config.repositories.storages.detect do |n, rs|
+ name != n && Pathname.new(rs['path']).realpath == parent
end
end
@@ -16,10 +16,22 @@ end
def validate_storages
storage_validation_error('No repository storage path defined') if Gitlab.config.repositories.storages.empty?
- Gitlab.config.repositories.storages.each do |name, path|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
storage_validation_error("\"#{name}\" is not a valid storage name") unless storage_name_valid?(name)
- parent_name, _parent_path = find_parent_path(name, path)
+ if repository_storage.is_a?(String)
+ error = "#{name} is not a valid storage, because it has no `path` key. " \
+ "It may be configured as:\n\n#{name}:\n path: #{repository_storage}\n\n" \
+ "Refer to gitlab.yml.example for an updated example"
+
+ storage_validation_error(error)
+ end
+
+ if !repository_storage.is_a?(Hash) || repository_storage['path'].nil?
+ storage_validation_error("#{name} is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example")
+ end
+
+ parent_name, _parent_path = find_parent_path(name, repository_storage['path'])
if parent_name
storage_validation_error("#{name} is a nested path of #{parent_name}. Nested paths are not supported for repository storages")
end
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index 88cd0f5f652..a5636765774 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -6,9 +6,14 @@ Doorkeeper.configure do
# This block will be called to check whether the resource owner is authenticated or not.
resource_owner_authenticator do
# Put your resource owner authentication logic here.
- # Ensure user is redirected to redirect_uri after login
- session[:user_return_to] = request.fullpath
- current_user || redirect_to(new_user_session_url)
+ if current_user
+ current_user
+ else
+ # Ensure user is redirected to redirect_uri after login
+ session[:user_return_to] = request.fullpath
+ redirect_to(new_user_session_url)
+ nil
+ end
end
resource_owner_from_credentials do |routes|
diff --git a/config/initializers/doorkeeper_openid_connect.rb b/config/initializers/doorkeeper_openid_connect.rb
new file mode 100644
index 00000000000..700ca25b884
--- /dev/null
+++ b/config/initializers/doorkeeper_openid_connect.rb
@@ -0,0 +1,36 @@
+Doorkeeper::OpenidConnect.configure do
+ issuer Gitlab.config.gitlab.url
+
+ jws_private_key Rails.application.secrets.jws_private_key
+
+ resource_owner_from_access_token do |access_token|
+ User.active.find_by(id: access_token.resource_owner_id)
+ end
+
+ auth_time_from_resource_owner do |user|
+ user.current_sign_in_at
+ end
+
+ reauthenticate_resource_owner do |user, return_to|
+ store_location_for user, return_to
+ sign_out user
+ redirect_to new_user_session_url
+ end
+
+ subject do |user|
+ # hash the user's ID with the Rails secret_key_base to avoid revealing it
+ Digest::SHA256.hexdigest "#{user.id}-#{Rails.application.secrets.secret_key_base}"
+ end
+
+ claims do
+ with_options scope: :openid do |o|
+ o.claim(:name) { |user| user.name }
+ o.claim(:nickname) { |user| user.username }
+ o.claim(:email) { |user| user.public_email }
+ o.claim(:email_verified) { |user| true if user.public_email? }
+ o.claim(:website) { |user| user.full_website_url if user.website_url? }
+ o.claim(:profile) { |user| Rails.application.routes.url_helpers.user_url user }
+ o.claim(:picture) { |user| user.avatar_url }
+ end
+ end
+end
diff --git a/config/initializers/rspec_profiling.rb b/config/initializers/rspec_profiling.rb
index 0ef9f51e5cf..ac353d14499 100644
--- a/config/initializers/rspec_profiling.rb
+++ b/config/initializers/rspec_profiling.rb
@@ -1,22 +1,41 @@
-module RspecProfilingConnection
- def establish_connection
- ::RspecProfiling::Collectors::PSQL::Result.establish_connection(ENV['RSPEC_PROFILING_POSTGRES_URL'])
+module RspecProfilingExt
+ module PSQL
+ def establish_connection
+ ::RspecProfiling::Collectors::PSQL::Result.establish_connection(ENV['RSPEC_PROFILING_POSTGRES_URL'])
+ end
end
-end
-module RspecProfilingGitBranchCi
- def branch
- ENV['CI_BUILD_REF_NAME'] || super
+ module Git
+ def branch
+ ENV['CI_BUILD_REF_NAME'] || super
+ end
+ end
+
+ module Run
+ def example_finished(*args)
+ super
+ rescue => err
+ return if @already_logged_example_finished_error
+
+ $stderr.puts "rspec_profiling couldn't collect an example: #{err}. Further warnings suppressed."
+ @already_logged_example_finished_error = true
+ end
+
+ alias_method :example_passed, :example_finished
+ alias_method :example_failed, :example_finished
end
end
if Rails.env.test?
RspecProfiling.configure do |config|
if ENV['RSPEC_PROFILING_POSTGRES_URL']
- RspecProfiling::Collectors::PSQL.prepend(RspecProfilingConnection)
+ RspecProfiling::Collectors::PSQL.prepend(RspecProfilingExt::PSQL)
config.collector = RspecProfiling::Collectors::PSQL
end
end
- RspecProfiling::VCS::Git.prepend(RspecProfilingGitBranchCi) if ENV.has_key?('CI')
+ if ENV.has_key?('CI')
+ RspecProfiling::VCS::Git.prepend(RspecProfilingExt::Git)
+ RspecProfiling::Run.prepend(RspecProfilingExt::Run)
+ end
end
diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb
index 291fa6c0abc..f9c1d2165d3 100644
--- a/config/initializers/secret_token.rb
+++ b/config/initializers/secret_token.rb
@@ -24,7 +24,8 @@ def create_tokens
defaults = {
secret_key_base: file_secret_key || generate_new_secure_token,
otp_key_base: env_secret_key || file_secret_key || generate_new_secure_token,
- db_key_base: generate_new_secure_token
+ db_key_base: generate_new_secure_token,
+ jws_private_key: generate_new_rsa_private_key
}
missing_secrets = set_missing_keys(defaults)
@@ -41,6 +42,10 @@ def generate_new_secure_token
SecureRandom.hex(64)
end
+def generate_new_rsa_private_key
+ OpenSSL::PKey::RSA.new(2048).to_pem
+end
+
def warn_missing_secret(secret)
warn "Missing Rails.application.secrets.#{secret} for #{Rails.env} environment. The secret will be generated and stored in config/secrets.yml."
end
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 0c4516b70f0..2b018c68703 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -19,6 +19,12 @@ Sidekiq.configure_server do |config|
chain.add Gitlab::SidekiqStatus::ClientMiddleware
end
+ config.on :startup do
+ # Clear any connections that might have been obtained before starting
+ # Sidekiq (e.g. in an initializer).
+ ActiveRecord::Base.clear_all_connections!
+ end
+
# Sidekiq-cron: load recurring jobs from gitlab.yml
# UGLY Hack to get nested hash from settingslogic
cron_jobs = JSON.parse(Gitlab.config.cron_jobs.to_json)
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
index 1d728282d90..14d49885fb3 100644
--- a/config/locales/doorkeeper.en.yml
+++ b/config/locales/doorkeeper.en.yml
@@ -60,6 +60,7 @@ en:
scopes:
api: Access your API
read_user: Read user information
+ openid: Authenticate using OpenID Connect
flash:
applications:
diff --git a/config/routes.rb b/config/routes.rb
index 06293316937..1a851da6203 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -22,6 +22,8 @@ Rails.application.routes.draw do
authorizations: 'oauth/authorizations'
end
+ use_doorkeeper_openid_connect
+
# Autocomplete
get '/autocomplete/users' => 'autocomplete#users'
get '/autocomplete/users/:id' => 'autocomplete#user'
diff --git a/config/routes/project.rb b/config/routes/project.rb
index df39c3e200c..44b8ae7aedd 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -159,6 +159,7 @@ constraints(ProjectUrlConstrainer.new) do
member do
post :stop
get :terminal
+ get :metrics
get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil }
end
@@ -328,6 +329,7 @@ constraints(ProjectUrlConstrainer.new) do
resource :members, only: [:show]
resource :ci_cd, only: [:show], controller: 'ci_cd'
resource :integrations, only: [:show]
+ resource :repository, only: [:show], controller: :repository
end
# Since both wiki and repository routing contains wildcard characters
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 7298e7109c6..ff5f1412261 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -40,6 +40,7 @@ var config = {
protected_branches: './protected_branches/protected_branches_bundle.js',
snippet: './snippet/snippet_bundle.js',
terminal: './terminal/terminal_bundle.js',
+ u2f: ['vendor/u2f'],
users: './users/users_bundle.js',
vue_pipelines: './vue_pipelines_index/index.js',
},
diff --git a/db/migrate/20140502125220_migrate_repo_size.rb b/db/migrate/20140502125220_migrate_repo_size.rb
index e8de7ccf3db..66203486d53 100644
--- a/db/migrate/20140502125220_migrate_repo_size.rb
+++ b/db/migrate/20140502125220_migrate_repo_size.rb
@@ -8,7 +8,7 @@ class MigrateRepoSize < ActiveRecord::Migration
project_data.each do |project|
id = project['id']
namespace_path = project['namespace_path'] || ''
- repos_path = Gitlab.config.gitlab_shell['repos_path'] || Gitlab.config.repositories.storages.default
+ repos_path = Gitlab.config.gitlab_shell['repos_path'] || Gitlab.config.repositories.storages.default['path']
path = File.join(repos_path, namespace_path, project['project_path'] + '.git')
begin
diff --git a/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb b/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb
index 63f7392e54f..7a8ed99c68f 100644
--- a/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb
+++ b/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb
@@ -1,9 +1,15 @@
class AddIndexOnRequestedAtToMembers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :members, :requested_at
end
+
+ def down
+ remove_index :members, :requested_at if index_exists? :members, :requested_at
+ end
end
diff --git a/db/migrate/20160620115026_add_index_on_runners_locked.rb b/db/migrate/20160620115026_add_index_on_runners_locked.rb
index dfa5110dea4..6ca486c63d1 100644
--- a/db/migrate/20160620115026_add_index_on_runners_locked.rb
+++ b/db/migrate/20160620115026_add_index_on_runners_locked.rb
@@ -4,9 +4,15 @@
class AddIndexOnRunnersLocked < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :ci_runners, :locked
end
+
+ def down
+ remove_index :ci_runners, :locked if index_exists? :ci_runners, :locked
+ end
end
diff --git a/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb b/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb
index 7c991c6d998..a05a4c679e3 100644
--- a/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb
+++ b/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb
@@ -1,9 +1,15 @@
class AddIndexForPipelineUserId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :ci_commits, :user_id
end
+
+ def down
+ remove_index :ci_commits, :user_id if index_exists? :ci_commits, :user_id
+ end
end
diff --git a/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb b/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb
index a853de3abfb..3f074723b4a 100644
--- a/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb
+++ b/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb
@@ -5,8 +5,15 @@ class AddDeletedAtToNamespaces < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_column :namespaces, :deleted_at, :datetime
+
add_concurrent_index :namespaces, :deleted_at
end
+
+ def down
+ remove_index :namespaces, :deleted_at if index_exists? :namespaces, :deleted_at
+
+ remove_column :namespaces, :deleted_at
+ end
end
diff --git a/db/migrate/20160808085602_add_index_for_build_token.rb b/db/migrate/20160808085602_add_index_for_build_token.rb
index 10ef42afce1..6c5d7268e72 100644
--- a/db/migrate/20160808085602_add_index_for_build_token.rb
+++ b/db/migrate/20160808085602_add_index_for_build_token.rb
@@ -6,7 +6,11 @@ class AddIndexForBuildToken < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :ci_builds, :token, unique: true
end
+
+ def down
+ remove_index :ci_builds, :token, unique: true if index_exists? :ci_builds, :token, unique: true
+ end
end
diff --git a/db/migrate/20160819221631_add_index_to_note_discussion_id.rb b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb
index b6e8bb18e7b..8f693e97a58 100644
--- a/db/migrate/20160819221631_add_index_to_note_discussion_id.rb
+++ b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb
@@ -8,7 +8,11 @@ class AddIndexToNoteDiscussionId < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :notes, :discussion_id
end
+
+ def down
+ remove_index :notes, :discussion_id if index_exists? :notes, :discussion_id
+ end
end
diff --git a/db/migrate/20160819232256_add_incoming_email_token_to_users.rb b/db/migrate/20160819232256_add_incoming_email_token_to_users.rb
index f2cf956adc9..bcad3416d04 100644
--- a/db/migrate/20160819232256_add_incoming_email_token_to_users.rb
+++ b/db/migrate/20160819232256_add_incoming_email_token_to_users.rb
@@ -9,8 +9,15 @@ class AddIncomingEmailTokenToUsers < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_column :users, :incoming_email_token, :string
+
add_concurrent_index :users, :incoming_email_token
end
+
+ def down
+ remove_index :users, :incoming_email_token if index_exists? :users, :incoming_email_token
+
+ remove_column :users, :incoming_email_token
+ end
end
diff --git a/db/migrate/20160919145149_add_group_id_to_labels.rb b/db/migrate/20160919145149_add_group_id_to_labels.rb
index d10f3a6d104..828b6afddb1 100644
--- a/db/migrate/20160919145149_add_group_id_to_labels.rb
+++ b/db/migrate/20160919145149_add_group_id_to_labels.rb
@@ -5,9 +5,15 @@ class AddGroupIdToLabels < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_column :labels, :group_id, :integer
add_foreign_key :labels, :namespaces, column: :group_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey
add_concurrent_index :labels, :group_id
end
+
+ def down
+ remove_index :labels, :group_id if index_exists? :labels, :group_id
+ remove_foreign_key :labels, :namespaces, column: :group_id
+ remove_column :labels, :group_id
+ end
end
diff --git a/db/migrate/20160920160832_add_index_to_labels_title.rb b/db/migrate/20160920160832_add_index_to_labels_title.rb
index b5de552b98c..19f7b1076a7 100644
--- a/db/migrate/20160920160832_add_index_to_labels_title.rb
+++ b/db/migrate/20160920160832_add_index_to_labels_title.rb
@@ -5,7 +5,11 @@ class AddIndexToLabelsTitle < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :labels, :title
end
+
+ def down
+ remove_index :labels, :title if index_exists? :labels, :title
+ end
end
diff --git a/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb
index 2abfe47b776..ad3eb4a26f9 100644
--- a/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb
+++ b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb
@@ -25,9 +25,15 @@ class AddPipelineIdToMergeRequestMetrics < ActiveRecord::Migration
# comments:
# disable_ddl_transaction!
- def change
+ def up
add_column :merge_request_metrics, :pipeline_id, :integer
- add_concurrent_index :merge_request_metrics, :pipeline_id
add_foreign_key :merge_request_metrics, :ci_commits, column: :pipeline_id, on_delete: :cascade # rubocop: disable Migration/AddConcurrentForeignKey
+ add_concurrent_index :merge_request_metrics, :pipeline_id
+ end
+
+ def down
+ remove_index :merge_request_metrics, :pipeline_id if index_exists? :merge_request_metrics, :pipeline_id
+ remove_foreign_key :merge_request_metrics, :ci_commits, column: :pipeline_id
+ remove_column :merge_request_metrics, :pipeline_id
end
end
diff --git a/db/migrate/20161106185620_add_project_import_data_project_index.rb b/db/migrate/20161106185620_add_project_import_data_project_index.rb
index 750a6a8c51e..94b8ddd46f5 100644
--- a/db/migrate/20161106185620_add_project_import_data_project_index.rb
+++ b/db/migrate/20161106185620_add_project_import_data_project_index.rb
@@ -6,7 +6,11 @@ class AddProjectImportDataProjectIndex < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :project_import_data, :project_id
end
+
+ def down
+ remove_index :project_import_data, :project_id if index_exists? :project_import_data, :project_id
+ end
end
diff --git a/db/migrate/20161124111395_add_index_to_parent_id.rb b/db/migrate/20161124111395_add_index_to_parent_id.rb
index eab74c01dfd..73f9d92bb22 100644
--- a/db/migrate/20161124111395_add_index_to_parent_id.rb
+++ b/db/migrate/20161124111395_add_index_to_parent_id.rb
@@ -8,7 +8,11 @@ class AddIndexToParentId < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index(:namespaces, [:parent_id, :id], unique: true)
end
+
+ def down
+ remove_index :namespaces, [:parent_id, :id] if index_exists? :namespaces, [:parent_id, :id]
+ end
end
diff --git a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
index 3e1f6b1627d..e5292cfba07 100644
--- a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
+++ b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb
@@ -12,7 +12,7 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration
end
def repository_storage_path
- Gitlab.config.repositories.storages[repository_storage]
+ Gitlab.config.repositories.storages[repository_storage]['path']
end
def repository_path
diff --git a/db/migrate/20161202152035_add_index_to_routes.rb b/db/migrate/20161202152035_add_index_to_routes.rb
index 4a51337bda6..6d6c8906204 100644
--- a/db/migrate/20161202152035_add_index_to_routes.rb
+++ b/db/migrate/20161202152035_add_index_to_routes.rb
@@ -9,8 +9,13 @@ class AddIndexToRoutes < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index(:routes, :path, unique: true)
add_concurrent_index(:routes, [:source_type, :source_id], unique: true)
end
+
+ def down
+ remove_index(:routes, :path) if index_exists? :routes, :path
+ remove_index(:routes, [:source_type, :source_id]) if index_exists? :routes, [:source_type, :source_id]
+ end
end
diff --git a/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
index e9fcef1cd45..d7ef1aa83d9 100644
--- a/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
+++ b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
@@ -9,7 +9,11 @@ class AddUniqueIndexForEnvironmentSlug < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :environments, [:project_id, :slug], unique: true
end
+
+ def down
+ remove_index :environments, [:project_id, :slug], unique: true if index_exists? :environments, [:project_id, :slug]
+ end
end
diff --git a/db/migrate/20161209165216_create_doorkeeper_openid_connect_tables.rb b/db/migrate/20161209165216_create_doorkeeper_openid_connect_tables.rb
new file mode 100644
index 00000000000..e63d5927f86
--- /dev/null
+++ b/db/migrate/20161209165216_create_doorkeeper_openid_connect_tables.rb
@@ -0,0 +1,37 @@
+class CreateDoorkeeperOpenidConnectTables < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ create_table :oauth_openid_requests do |t|
+ t.integer :access_grant_id, null: false
+ t.string :nonce, null: false
+ end
+
+ if Gitlab::Database.postgresql?
+ # add foreign key without validation to avoid downtime on PostgreSQL,
+ # also see db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb
+ execute %q{
+ ALTER TABLE "oauth_openid_requests"
+ ADD CONSTRAINT "fk_oauth_openid_requests_oauth_access_grants_access_grant_id"
+ FOREIGN KEY ("access_grant_id")
+ REFERENCES "oauth_access_grants" ("id")
+ NOT VALID;
+ }
+ else
+ execute %q{
+ ALTER TABLE oauth_openid_requests
+ ADD CONSTRAINT fk_oauth_openid_requests_oauth_access_grants_access_grant_id
+ FOREIGN KEY (access_grant_id)
+ REFERENCES oauth_access_grants (id);
+ }
+ end
+ end
+
+ def down
+ drop_table :oauth_openid_requests
+ end
+end
diff --git a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
index 241afc6b097..8fb1f9d5e73 100644
--- a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
+++ b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb
@@ -60,7 +60,7 @@ class RemoveDotGitFromGroupNames < ActiveRecord::Migration
def move_namespace(group_id, path_was, path)
repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{group_id}").map do |row|
- Gitlab.config.repositories.storages[row['repository_storage']]
+ Gitlab.config.repositories.storages[row['repository_storage']]['path']
end.compact
# Move the namespace directory in all storages paths used by member projects
diff --git a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
index a0ce927161f..61dcc8c54f5 100644
--- a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
+++ b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb
@@ -71,7 +71,7 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
route_exists = route_exists?(path)
Gitlab.config.repositories.storages.each_value do |storage|
- if route_exists || path_exists?(path, storage)
+ if route_exists || path_exists?(path, storage['path'])
counter += 1
path = "#{base}#{counter}"
@@ -84,7 +84,7 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
def move_namespace(namespace_id, path_was, path)
repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{namespace_id}").map do |row|
- Gitlab.config.repositories.storages[row['repository_storage']]
+ Gitlab.config.repositories.storages[row['repository_storage']]['path']
end.compact
# Move the namespace directory in all storages paths used by member projects
diff --git a/db/migrate/20170131221752_add_relative_position_to_issues.rb b/db/migrate/20170131221752_add_relative_position_to_issues.rb
new file mode 100644
index 00000000000..1baad0893e3
--- /dev/null
+++ b/db/migrate/20170131221752_add_relative_position_to_issues.rb
@@ -0,0 +1,37 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddRelativePositionToIssues < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ disable_ddl_transaction!
+
+ def up
+ add_column :issues, :relative_position, :integer
+
+ add_concurrent_index :issues, :relative_position
+ end
+
+ def down
+ remove_column :issues, :relative_position
+
+ remove_index :issues, :relative_position if index_exists? :issues, :relative_position
+ end
+end
diff --git a/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb
index 8f944930807..31ef458c44f 100644
--- a/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb
+++ b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb
@@ -5,7 +5,11 @@ class AddIndexToLabelsForTypeAndProject < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :labels, [:type, :project_id]
end
+
+ def down
+ remove_index :labels, [:type, :project_id] if index_exists? :labels, [:type, :project_id]
+ end
end
diff --git a/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb b/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb
index f922ed209aa..70fb0ef12f9 100644
--- a/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb
+++ b/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb
@@ -5,8 +5,13 @@ class AddIndexToLabelsForTitleAndProject < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :labels, :title
add_concurrent_index :labels, :project_id
end
+
+ def down
+ remove_index :labels, :title if index_exists? :labels, :title
+ remove_index :labels, :project_id if index_exists? :labels, :project_id
+ end
end
diff --git a/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb
index 61e49c14fc0..07d4f8af27f 100644
--- a/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb
+++ b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb
@@ -5,7 +5,11 @@ class AddIndexToCiTriggerRequestsForCommitId < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_index :ci_trigger_requests, :commit_id
end
+
+ def down
+ remove_index :ci_trigger_requests, :commit_id if index_exists? :ci_trigger_requests, :commit_id
+ end
end
diff --git a/db/migrate/20170210103609_add_index_to_user_agent_detail.rb b/db/migrate/20170210103609_add_index_to_user_agent_detail.rb
index c01753cfbd2..2d8329b7862 100644
--- a/db/migrate/20170210103609_add_index_to_user_agent_detail.rb
+++ b/db/migrate/20170210103609_add_index_to_user_agent_detail.rb
@@ -8,7 +8,11 @@ class AddIndexToUserAgentDetail < ActiveRecord::Migration
disable_ddl_transaction!
- def change
- add_concurrent_index(:user_agent_details, [:subject_id, :subject_type])
+ def up
+ add_concurrent_index :user_agent_details, [:subject_id, :subject_type]
+ end
+
+ def down
+ remove_index :user_agent_details, [:subject_id, :subject_type] if index_exists? :user_agent_details, [:subject_id, :subject_type]
end
end
diff --git a/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb
index 7b1e687977b..65adc90c2c1 100644
--- a/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb
+++ b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb
@@ -4,7 +4,11 @@ class AddIndexForLatestSuccessfulPipeline < ActiveRecord::Migration
disable_ddl_transaction!
- def change
- add_concurrent_index(:ci_commits, [:gl_project_id, :ref, :status])
+ def up
+ add_concurrent_index :ci_commits, [:gl_project_id, :ref, :status]
+ end
+
+ def down
+ remove_index :ci_commits, [:gl_project_id, :ref, :status] if index_exists? :ci_commits, [:gl_project_id, :ref, :status]
end
end
diff --git a/db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb b/db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb
new file mode 100644
index 00000000000..e206f9af636
--- /dev/null
+++ b/db/post_migrate/20170209140523_validate_foreign_keys_on_oauth_openid_requests.rb
@@ -0,0 +1,20 @@
+class ValidateForeignKeysOnOauthOpenidRequests < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ if Gitlab::Database.postgresql?
+ execute %q{
+ ALTER TABLE "oauth_openid_requests"
+ VALIDATE CONSTRAINT "fk_oauth_openid_requests_oauth_access_grants_access_grant_id";
+ }
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 14f60999f15..3ec5461f600 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -61,6 +61,7 @@ ActiveRecord::Schema.define(version: 20170306170512) do
t.boolean "shared_runners_enabled", default: true, null: false
t.integer "max_artifacts_size", default: 100, null: false
t.string "runners_registration_token"
+ t.integer "max_pages_size", default: 100, null: false
t.boolean "require_two_factor_authentication", default: false
t.integer "two_factor_grace_period", default: 48
t.boolean "metrics_enabled", default: false
@@ -109,7 +110,6 @@ ActiveRecord::Schema.define(version: 20170306170512) do
t.boolean "html_emails_enabled", default: true
t.string "plantuml_url"
t.boolean "plantuml_enabled"
- t.integer "max_pages_size", default: 100, null: false
t.integer "terminal_max_session_time", default: 0, null: false
t.string "default_artifacts_expire_in", default: "0", null: false
t.integer "unique_ips_limit_per_user"
@@ -530,6 +530,7 @@ ActiveRecord::Schema.define(version: 20170306170512) do
t.text "title_html"
t.text "description_html"
t.integer "time_estimate"
+ t.integer "relative_position"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -541,6 +542,7 @@ ActiveRecord::Schema.define(version: 20170306170512) do
add_index "issues", ["due_date"], name: "index_issues_on_due_date", using: :btree
add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
+ add_index "issues", ["relative_position"], name: "index_issues_on_relative_position", using: :btree
add_index "issues", ["state"], name: "index_issues_on_state", using: :btree
add_index "issues", ["title"], name: "index_issues_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
@@ -771,8 +773,8 @@ ActiveRecord::Schema.define(version: 20170306170512) do
t.integer "visibility_level", default: 20, null: false
t.boolean "request_access_enabled", default: false, null: false
t.datetime "deleted_at"
- t.boolean "lfs_enabled"
t.text "description_html"
+ t.boolean "lfs_enabled"
t.integer "parent_id"
end
@@ -878,6 +880,11 @@ ActiveRecord::Schema.define(version: 20170306170512) do
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
+ create_table "oauth_openid_requests", force: :cascade do |t|
+ t.integer "access_grant_id", null: false
+ t.string "nonce", null: false
+ end
+
create_table "pages_domains", force: :cascade do |t|
t.integer "project_id"
t.text "certificate"
@@ -1375,6 +1382,7 @@ ActiveRecord::Schema.define(version: 20170306170512) do
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
+ add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", name: "fk_oauth_openid_requests_oauth_access_grants_access_grant_id"
add_foreign_key "personal_access_tokens", "users"
add_foreign_key "project_authorizations", "projects", on_delete: :cascade
add_foreign_key "project_authorizations", "users", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index 46a1ed0e148..57d85d770e7 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -19,7 +19,7 @@
- [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab.
- [Permissions](user/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do.
- [Profile Settings](profile/README.md)
-- [Project Services](user/project/integrations//project_services.md) Integrate a project with external services, such as CI and chat.
+- [Project Services](user/project/integrations/project_services.md) Integrate a project with external services, such as CI and chat.
- [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects.
- [Snippets](user/snippets.md) Snippets allow you to create little bits of code.
- [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects.
diff --git a/doc/administration/auth/crowd.md b/doc/administration/auth/crowd.md
new file mode 100644
index 00000000000..2c289c67a6d
--- /dev/null
+++ b/doc/administration/auth/crowd.md
@@ -0,0 +1,68 @@
+# Atlassian Crowd OmniAuth Provider
+
+## Configure a new Crowd application
+
+1. Choose 'Applications' in the top menu, then 'Add application'.
+1. Go through the 'Add application' steps, entering the appropriate details.
+ The screenshot below shows an example configuration.
+
+ ![Example Crowd application configuration](img/crowd_application.png)
+
+## Configure GitLab
+
+1. On your GitLab server, open the configuration file.
+
+ **Omnibus:**
+
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ **Source:**
+
+ ```sh
+ cd /home/git/gitlab
+
+ sudo -u git -H editor config/gitlab.yml
+ ```
+
+1. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration)
+ for initial settings.
+
+1. Add the provider configuration:
+
+ **Omnibus:**
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "crowd",
+ "args" => {
+ "crowd_server_url" => "CROWD_SERVER_URL",
+ "application_name" => "YOUR_APP_NAME",
+ "application_password" => "YOUR_APP_PASSWORD"
+ }
+ }
+ ]
+ ```
+
+ **Source:**
+
+ ```
+ - { name: 'crowd',
+ args: {
+ crowd_server_url: 'CROWD_SERVER_URL',
+ application_name: 'YOUR_APP_NAME',
+ application_password: 'YOUR_APP_PASSWORD' } }
+ ```
+1. Change `CROWD_SERVER_URL` to the URL of your Crowd server.
+1. Change `YOUR_APP_NAME` to the application name from Crowd applications page.
+1. Change `YOUR_APP_PASSWORD` to the application password you've set.
+1. Save the configuration file.
+1. [Reconfigure][] or [restart][] for the changes to take effect if you
+ installed GitLab via Omnibus or from source respectively.
+
+On the sign in page there should now be a Crowd tab in the sign in form.
+
+[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: ../restart_gitlab.md#installations-from-source
diff --git a/doc/administration/auth/img/crowd_application.png b/doc/administration/auth/img/crowd_application.png
new file mode 100644
index 00000000000..7deea9dac8e
--- /dev/null
+++ b/doc/administration/auth/img/crowd_application.png
Binary files differ
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index 28e413ef447..f707039827b 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -512,6 +512,62 @@ Currently, there is no storage limitation, which means a user can upload an
infinite amount of Docker images with arbitrary sizes. This setting will be
configurable in future releases.
+## Configure Container Registry notifications
+
+You can configure the Container Registry to send webhook notifications in
+response to events happening within the registry.
+
+Read more about the Container Registry notifications config options in the
+[Docker Registry notifications documentation][notifications-config].
+
+>**Note:**
+Multiple endpoints can be configured for the Container Registry.
+
+
+**Omnibus GitLab installations**
+
+To configure a notification endpoint in Omnibus:
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ registry['notifications'] = [
+ {
+ 'name' => 'test_endpoint',
+ 'url' => 'https://gitlab.example.com/notify',
+ 'timeout' => '500ms',
+ 'threshold' => 5,
+ 'backoff' => '1s',
+ 'headers' => {
+ "Authorization" => ["AUTHORIZATION_EXAMPLE_TOKEN"]
+ }
+ }
+ ]
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**Installations from source**
+
+Configuring the notification endpoint is done in your registry config YML file created
+when you [deployed your docker registry][registry-deploy].
+
+Example:
+
+```
+notifications:
+ endpoints:
+ - name: alistener
+ disabled: false
+ url: https://my.listener.com/event
+ headers: <http.Header>
+ timeout: 500
+ threshold: 5
+ backoff: 1000
+```
+
## Changelog
**GitLab 8.8 ([source docs][8-8-docs])**
@@ -532,3 +588,5 @@ configurable in future releases.
[registry-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/registry-ssl
[existing-domain]: #configure-container-registry-under-an-existing-gitlab-domain
[new-domain]: #configure-container-registry-under-its-own-domain
+[notifications-config]: https://docs.docker.com/registry/notifications/
+[registry-notifications-config]: https://docs.docker.com/registry/configuration/#notifications \ No newline at end of file
diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md
index e4f94eb7cb6..0a08591c3ce 100644
--- a/doc/administration/high_availability/database.md
+++ b/doc/administration/high_availability/database.md
@@ -16,7 +16,7 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
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.
- This step is covered in [Configuring GitLab for HA](gitlab.md)
+ This step is covered in [Configuring GitLab for HA](gitlab.md).
## Configure using Omnibus
@@ -105,6 +105,8 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
1. Exit the database prompt by typing `\q` and Enter.
1. Exit the `gitlab-psql` user by running `exit` twice.
1. Run `sudo gitlab-ctl reconfigure` a final time.
+1. Configure the GitLab application servers with the appropriate details.
+ This step is covered in [Configuring GitLab for HA](gitlab.md).
---
diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md
index dad8e956c0e..3245988fc14 100644
--- a/doc/administration/high_availability/load_balancer.md
+++ b/doc/administration/high_availability/load_balancer.md
@@ -19,8 +19,8 @@ you need to use with GitLab.
## GitLab Pages Ports
If you're using GitLab Pages you will need some additional port configurations.
-GitLab Pages requires a separate VIP. Configure DNS to point the
-`pages_external_url` from `/etc/gitlab/gitlab.rb` at the new VIP. See the
+GitLab Pages requires a separate virtual IP address. Configure DNS to point the
+`pages_external_url` from `/etc/gitlab/gitlab.rb` at the new virtual IP address. See the
[GitLab Pages documentation][gitlab-pages] for more information.
| LB Port | Backend Port | Protocol |
@@ -32,7 +32,7 @@ GitLab Pages requires a separate VIP. Configure DNS to point the
Some organizations have policies against opening SSH port 22. In this case,
it may be helpful to configure an alternate SSH hostname that allows users
-to use SSH on port 443. An alternate SSH hostname will require a new VIP
+to use SSH on port 443. An alternate SSH hostname will require a new virtual IP address
compared to the other GitLab HTTP configuration above.
Configure DNS for an alternate SSH hostname such as altssh.gitlab.example.com.
diff --git a/doc/administration/repository_storage_paths.md b/doc/administration/repository_storage_paths.md
index d6aa6101026..55a45119525 100644
--- a/doc/administration/repository_storage_paths.md
+++ b/doc/administration/repository_storage_paths.md
@@ -52,9 +52,12 @@ respectively.
# Paths where repositories can be stored. Give the canonicalized absolute pathname.
# NOTE: REPOS PATHS MUST NOT CONTAIN ANY SYMLINK!!!
storages: # You must have at least a 'default' storage path.
- default: /home/git/repositories
- nfs: /mnt/nfs/repositories
- cephfs: /mnt/cephfs/repositories
+ default:
+ path: /home/git/repositories
+ nfs:
+ path: /mnt/nfs/repositories
+ cephfs:
+ path: /mnt/cephfs/repositories
```
1. [Restart GitLab] for the changes to take effect.
@@ -75,9 +78,9 @@ working, you can remove the `repos_path` line.
```ruby
git_data_dirs({
- "default" => "/var/opt/gitlab/git-data",
- "nfs" => "/mnt/nfs/git-data",
- "cephfs" => "/mnt/cephfs/git-data"
+ "default" => { "path" => "/var/opt/gitlab/git-data" },
+ "nfs" => { "path" => "/mnt/nfs/git-data" },
+ "cephfs" => { "path" => "/mnt/cephfs/git-data" }
})
```
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index 296f1d025dd..7340123e09d 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -1,6 +1,6 @@
# Jobs API
-## List project jobs
+## List project jobs
Get a list of jobs in a project.
@@ -14,7 +14,123 @@ GET /projects/:id/jobs
| `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all jobs if none provided |
```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/jobs?scope%5B0%5D=pending&scope%5B1%5D=running'
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/jobs?scope[]=pending&scope[]=running'
+```
+
+Example of response
+
+```json
+[
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.802Z",
+ "artifacts_file": {
+ "filename": "artifacts.zip",
+ "size": 1000
+ },
+ "finished_at": "2015-12-24T17:54:27.895Z",
+ "id": 7,
+ "name": "teaspoon",
+ "pipeline": {
+ "id": 6,
+ "ref": "master",
+ "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "status": "pending"
+ },
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:27.722Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/root",
+ "website_url": ""
+ }
+ },
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.727Z",
+ "artifacts_file": null,
+ "finished_at": "2015-12-24T17:54:24.921Z",
+ "id": 6,
+ "name": "spinach:other",
+ "pipeline": {
+ "id": 6,
+ "ref": "master",
+ "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "status": "pending"
+ },
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:24.729Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/root",
+ "website_url": ""
+ }
+ }
+]
+```
+
+## List pipeline jobs
+
+Get a list of jobs for a pipeline.
+
+```
+GET /projects/:id/pipeline/:pipeline_id/jobs
+```
+
+| Attribute | Type | Required | Description |
+|---------------|--------------------------------|----------|----------------------|
+| `id` | integer | yes | The ID of a project |
+| `pipeline_id` | integer | yes | The ID of a pipeline |
+| `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all jobs if none provided |
+
+```
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/pipelines/6/jobs?scope[]=pending&scope[]=running'
```
Example of response
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 28e4bfe39dc..686f3dba35d 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -20,7 +20,7 @@ Constants for project visibility levels are next:
## List projects
-Get a list of projects for which the authenticated user is a member.
+Get a list of visible projects for authenticated user. When being accessed without authentication, all public projects are returned.
```
GET /projects
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index ddd11bb2a14..b1bf9ca07cc 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -15,7 +15,7 @@ Parameters:
- `id` (required) - The ID of a project
- `path` (optional) - The path inside repository. Used to get contend of subdirectories
-- `ref_name` (optional) - The name of a repository branch or tag or if not given the default branch
+- `ref` (optional) - The name of a repository branch or tag or if not given the default branch
- `recursive` (optional) - Boolean value used to get a recursive tree (false by default)
```json
@@ -72,10 +72,11 @@ Parameters:
]
```
-## Raw file content
+## Get a blob from repository
-Get the raw file contents for a file by commit SHA and path. This endpoint can
-be accessed without authentication if the repository is publicly accessible.
+Allows you to receive information about blob in repository like size and
+content. Note that blob content is Base64 encoded. This endpoint can be accessed
+without authentication if the repository is publicly accessible.
```
GET /projects/:id/repository/blobs/:sha
@@ -85,7 +86,6 @@ Parameters:
- `id` (required) - The ID of a project
- `sha` (required) - The commit or branch name
-- `filepath` (required) - The path the file
## Raw blob content
@@ -93,7 +93,7 @@ Get the raw file contents for a blob by blob SHA. This endpoint can be accessed
without authentication if the repository is publicly accessible.
```
-GET /projects/:id/repository/raw_blobs/:sha
+GET /projects/:id/repository/blobs/:sha/raw
```
Parameters:
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index ec56d0efa1c..aec91abd390 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -11,11 +11,11 @@ content. Note that file content is Base64 encoded. This endpoint can be accessed
without authentication if the repository is publicly accessible.
```
-GET /projects/:id/repository/files
+GET /projects/:id/repository/files/:file_path
```
```bash
-curl --request GET --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files?file_path=app/models/key.rb&ref=master'
+curl --request GET --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fmodels%2Fkey%2Erb?ref=master'
```
Example response:
@@ -36,17 +36,32 @@ Example response:
Parameters:
-- `file_path` (required) - Full path to new file. Ex. lib/class.rb
+- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
+- `ref` (required) - The name of branch, tag or commit
+
+## Get raw file from repository
+
+```
+GET /projects/:id/repository/files/:file_path/raw
+```
+
+```bash
+curl --request GET --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fmodels%2Fkey%2Erb/raw?ref=master'
+```
+
+Parameters:
+
+- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
- `ref` (required) - The name of branch, tag or commit
## Create new file in repository
```
-POST /projects/:id/repository/files
+POST /projects/:id/repository/files/:file_path
```
```bash
-curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file'
+curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fprojectrb%2E?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file'
```
Example response:
@@ -60,7 +75,7 @@ Example response:
Parameters:
-- `file_path` (required) - Full path to new file. Ex. lib/class.rb
+- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
- `branch` (required) - The name of branch
- `encoding` (optional) - Change encoding to 'base64'. Default is text.
- `author_email` (optional) - Specify the commit author's email address
@@ -71,11 +86,11 @@ Parameters:
## Update existing file in repository
```
-PUT /projects/:id/repository/files
+PUT /projects/:id/repository/files/:file_path
```
```bash
-curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file'
+curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file'
```
Example response:
@@ -89,7 +104,7 @@ Example response:
Parameters:
-- `file_path` (required) - Full path to file. Ex. lib/class.rb
+- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
- `branch` (required) - The name of branch
- `encoding` (optional) - Change encoding to 'base64'. Default is text.
- `author_email` (optional) - Specify the commit author's email address
@@ -109,11 +124,11 @@ Currently gitlab-shell has a boolean return code, preventing GitLab from specify
## Delete existing file in repository
```
-DELETE /projects/:id/repository/files
+DELETE /projects/:id/repository/files/:file_path
```
```bash
-curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files?file_path=app/project.rb&branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file'
+curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file'
```
Example response:
@@ -127,7 +142,7 @@ Example response:
Parameters:
-- `file_path` (required) - Full path to file. Ex. lib/class.rb
+- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
- `branch` (required) - The name of branch
- `author_email` (optional) - Specify the commit author's email address
- `author_name` (optional) - Specify the commit author's name
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 38a37cd920c..ad975e2e325 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -20,7 +20,7 @@ Example response:
```json
{
- "default_projects_limit" : 10,
+ "default_projects_limit" : 100000,
"signup_enabled" : true,
"id" : 1,
"default_branch_protection" : 2,
@@ -60,7 +60,7 @@ PUT /application/settings
| Attribute | Type | Required | Description |
| --------- | ---- | :------: | ----------- |
-| `default_projects_limit` | integer | no | Project limit per user. Default is `10` |
+| `default_projects_limit` | integer | no | Project limit per user. Default is `100000` |
| `signup_enabled` | boolean | no | Enable registration. Default is `true`. |
| `signin_enabled` | boolean | no | Enable login via a GitLab account. Default is `true`. |
| `gravatar_enabled` | boolean | no | Enable Gravatar |
@@ -98,7 +98,7 @@ Example response:
```json
{
"id": 1,
- "default_projects_limit": 10,
+ "default_projects_limit": 100000,
"signup_enabled": true,
"signin_enabled": true,
"gravatar_enabled": true,
diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md
index f42a5e9158b..0794156bc39 100644
--- a/doc/api/v3_to_v4.md
+++ b/doc/api/v3_to_v4.md
@@ -1,8 +1,10 @@
# V3 to V4 version
-Our V4 API version is currently available as *Beta*! It means that V3
-will still be supported and remain unchanged for now, but be aware that the following
-changes are in V4:
+Since GitLab 9.0, API V4 is the preferred version to be used.
+
+V3 will remain working until at least GitLab 9.3. The V3 API documentation is still [available](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/doc/api/README.md).
+
+Below are the changes made between V3 and V4.
### 8.17
@@ -71,3 +73,10 @@ changes are in V4:
- Simplify project payload exposed on Environment endpoints [!9675](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9675)
- API uses merge request `IID`s (internal ID, as in the web UI) rather than `ID`s. This affects the merge requests, award emoji, todos, and time tracking APIs. [!9530](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9530)
- API uses issue `IID`s (internal ID, as in the web UI) rather than `ID`s. This affects the issues, award emoji, todos, and time tracking APIs. [!9530](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9530)
+- Change initial page from `0` to `1` on `GET projects/:id/repository/commits` (like on the rest of the API) [!9679] (https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9679)
+- Return correct `Link` header data for `GET projects/:id/repository/commits` [!9679] (https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9679)
+- Update endpoints for repository files [!9637](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9637)
+ - Moved `/projects/:id/repository/files?file_path=:file_path` to `/projects/:id/repository/files/:file_path` (`:file_path` should be URL-encoded)
+ - `/projects/:id/repository/blobs/:sha` now returns JSON attributes for the blob identified by `:sha`, instead of finding the commit identified by `:sha` and returning the raw content of the blob in that commit identified by the required `?filepath=:filepath`
+ - Moved `/projects/:id/repository/commits/:sha/blob?file_path=:file_path` and `/projects/:id/repository/blobs/:sha?file_path=:file_path` to `/projects/:id/repository/files/:file_path/raw?ref=:sha`
+ - `/projects/:id/repository/tree` parameter `ref_name` has been renamed to `ref` for consistency
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index a9e25187b88..4c3e7c4e86e 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -35,17 +35,28 @@ version of Runner required.
| **CI_SERVER_NAME** | all | all | The name of CI server that is used to coordinate jobs |
| **CI_SERVER_VERSION** | all | all | GitLab version that is used to schedule jobs |
| **CI_SERVER_REVISION** | all | all | GitLab revision that is used to schedule jobs |
-| **CI_BUILD_ID** | all | all | The unique id of the current job that GitLab CI uses internally |
-| **CI_BUILD_REF** | all | all | The commit revision for which project is built |
-| **CI_BUILD_TAG** | all | 0.5 | The commit tag name. Present only when building tags. |
-| **CI_BUILD_NAME** | all | 0.5 | The name of the job as defined in `.gitlab-ci.yml` |
-| **CI_BUILD_STAGE** | all | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` |
-| **CI_BUILD_REF_NAME** | all | all | The branch or tag name for which project is built |
-| **CI_BUILD_REF_SLUG** | 8.15 | all | `$CI_BUILD_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
-| **CI_BUILD_REPO** | all | all | The URL to clone the Git repository |
-| **CI_BUILD_TRIGGERED** | all | 0.5 | The flag to indicate that job was [triggered] |
-| **CI_BUILD_MANUAL** | 8.12 | all | The flag to indicate that job was manually started |
-| **CI_BUILD_TOKEN** | all | 1.2 | Token used for authenticating with the GitLab Container Registry |
+| **CI_BUILD_ID** | all | all | The unique id of the current job that GitLab CI uses internally. Deprecated, use CI_JOB_ID |
+| **CI_JOB_ID** | 9.0 | all | The unique id of the current job that GitLab CI uses internally |
+| **CI_BUILD_REF** | all | all | The commit revision for which project is built. Deprecated, use CI_COMMIT_REF |
+| **CI_COMMIT_SHA** | 9.0 | all | The commit revision for which project is built |
+| **CI_BUILD_TAG** | all | 0.5 | The commit tag name. Present only when building tags. Deprecated, use CI_COMMIT_TAG |
+| **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. |
+| **CI_BUILD_NAME** | all | 0.5 | The name of the job as defined in `.gitlab-ci.yml`. Deprecated, use CI_JOB_NAME |
+| **CI_JOB_NAME** | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` |
+| **CI_BUILD_STAGE** | all | 0.5 | The name of the stage as defined in `.gitlab-ci.yml`. Deprecated, use CI_JOB_STAGE |
+| **CI_JOB_STAGE** | 9.0 | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` |
+| **CI_BUILD_REF_NAME** | all | all | The branch or tag name for which project is built. Deprecated, use CI_COMMIT_REF_NAME |
+| **CI_COMMIT_REF_NAME** | 9.0 | all | The branch or tag name for which project is built |
+| **CI_BUILD_REF_SLUG** | 8.15 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. Deprecated, use CI_COMMIT_REF_SLUG |
+| **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
+| **CI_BUILD_REPO** | all | all | The URL to clone the Git repository. Deprecated, use CI_REPOSITORY |
+| **CI_REPOSITORY_URL** | 9.0 | all | The URL to clone the Git repository |
+| **CI_BUILD_TRIGGERED** | all | 0.5 | The flag to indicate that job was [triggered]. Deprecated, use CI_PIPELINE_TRIGGERED |
+| **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] |
+| **CI_BUILD_MANUAL** | 8.12 | all | The flag to indicate that job was manually started. Deprecated, use CI_JOB_MANUAL |
+| **CI_JOB_MANUAL** | 8.12 | all | The flag to indicate that job was manually started |
+| **CI_BUILD_TOKEN** | all | 1.2 | Token used for authenticating with the GitLab Container Registry. Deprecated, use CI_JOB_TOKEN |
+| **CI_JOB_TOKEN** | 9.0 | 1.2 | Token used for authenticating with the GitLab Container Registry |
| **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally |
| **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally |
| **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built |
@@ -66,21 +77,22 @@ version of Runner required.
| **RESTORE_CACHE_ATTEMPTS** | 8.15 | 1.9 | Number of attempts to restore the cache running a job |
| **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the job |
| **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the job |
-
+| **CI_REGISTRY_USER** | 9.0 | all | The username to use to push containers to the GitLab Container Registry |
+| **CI_REGISTRY_PASSWORD** | 9.0 | all | The password to use to push containers to the GitLab Container Registry |
Example values:
```bash
-export CI_BUILD_ID="50"
-export CI_BUILD_REF="1ecfd275763eff1d6b4844ea3168962458c9f27a"
-export CI_BUILD_REF_NAME="master"
-export CI_BUILD_REPO="https://gitab-ci-token:abcde-1234ABCD5678ef@example.com/gitlab-org/gitlab-ce.git"
-export CI_BUILD_TAG="1.0.0"
-export CI_BUILD_NAME="spec:other"
-export CI_BUILD_STAGE="test"
-export CI_BUILD_MANUAL="true"
-export CI_BUILD_TRIGGERED="true"
-export CI_BUILD_TOKEN="abcde-1234ABCD5678ef"
+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_COMMIT_TAG="1.0.0"
+export CI_JOB_NAME="spec:other"
+export CI_JOB_STAGE="test"
+export CI_JOB_MANUAL="true"
+export CI_JOB_TRIGGERED="true"
+export CI_JOB_TOKEN="abcde-1234ABCD5678ef"
export CI_PIPELINE_ID="1000"
export CI_PROJECT_ID="34"
export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce"
@@ -99,8 +111,30 @@ export CI_SERVER_REVISION="70606bf"
export CI_SERVER_VERSION="8.9.0"
export GITLAB_USER_ID="42"
export GITLAB_USER_EMAIL="user@example.com"
+export CI_REGISTRY_USER="gitlab-ci-token"
+export CI_REGISTRY_PASSWORD="longalfanumstring"
```
+## 9.0 Renaming
+
+To follow conventions of naming across GitLab, and to futher move away from the
+`build` term and toward `job` CI variables have been renamed for the 9.0
+release.
+
+| 8.X name | 9.0 name |
+|----------|----------|
+| CI_BUILD_ID | CI_JOB_ID |
+| CI_BUILD_REF | CI_COMMIT_SHA |
+| CI_BUILD_TAG | CI_COMMIT_TAG |
+| CI_BUILD_REF_NAME | CI_COMMIT_REF_NAME |
+| CI_BUILD_REF_SLUG | CI_COMMIT_REF_SLUG |
+| CI_BUILD_NAME | CI_JOB_NAME |
+| CI_BUILD_STAGE | CI_JOB_STAGE |
+| CI_BUILD_REPO | CI_REPOSITORY |
+| CI_BUILD_TRIGGERED | CI_PIPELINE_TRIGGERED |
+| CI_BUILD_MANUAL | CI_JOB_MANUAL |
+| CI_BUILD_TOKEN | CI_JOB_TOKEN |
+
## `.gitlab-ci.yaml` defined variables
>**Note:**
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index b25ccd4376e..49fa8761e5e 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -70,7 +70,7 @@ There are a few reserved `keywords` that **cannot** be used as job names:
| image | no | Use docker image, covered in [Use Docker](../docker/README.md) |
| services | no | Use docker services, covered in [Use Docker](../docker/README.md) |
| stages | no | Define build stages |
-| types | no | Alias for `stages` |
+| types | no | Alias for `stages` (deprecated) |
| before_script | no | Define commands that run before each job's script |
| after_script | no | Define commands that run after each job's script |
| variables | no | Define build variables |
@@ -130,6 +130,8 @@ There are also two edge cases worth mentioning:
### types
+> Deprecated, and will be removed in 10.0. Use [stages](#stages) instead.
+
Alias for [stages](#stages).
### variables
diff --git a/doc/integration/README.md b/doc/integration/README.md
index 22bdf33443d..e56e58498a6 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -12,6 +12,7 @@ See the documentation below for details on how to configure these services.
- [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [CAS](cas.md) Configure GitLab to sign in using CAS
- [OAuth2 provider](oauth_provider.md) OAuth2 application creation
+- [OpenID Connect](openid_connect_provider.md) Use GitLab as an identity provider
- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
- [Akismet](akismet.md) Configure Akismet to stop spam
diff --git a/doc/integration/crowd.md b/doc/integration/crowd.md
index f8370cd349e..2bc526dc3db 100644
--- a/doc/integration/crowd.md
+++ b/doc/integration/crowd.md
@@ -1,63 +1 @@
-# Crowd OmniAuth Provider
-
-To enable the Crowd OmniAuth provider you must register your application with Crowd. To configure Crowd integration you need an application name and password.
-
-1. On your GitLab server, open the configuration file.
-
- For omnibus package:
-
- ```sh
- sudo editor /etc/gitlab/gitlab.rb
- ```
-
- For installations from source:
-
- ```sh
- cd /home/git/gitlab
-
- sudo -u git -H editor config/gitlab.yml
- ```
-
-1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
-
-1. Add the provider configuration:
-
- For omnibus package:
-
- ```ruby
- gitlab_rails['omniauth_providers'] = [
- {
- "name" => "crowd",
- "args" => {
- "crowd_server_url" => "CROWD",
- "application_name" => "YOUR_APP_NAME",
- "application_password" => "YOUR_APP_PASSWORD"
- }
- }
- ]
- ```
-
- For installations from source:
-
- ```
- - { name: 'crowd',
- args: {
- crowd_server_url: 'CROWD SERVER URL',
- application_name: 'YOUR_APP_NAME',
- application_password: 'YOUR_APP_PASSWORD' } }
- ```
-
-1. Change 'YOUR_APP_NAME' to the application name from Crowd applications page.
-
-1. Change 'YOUR_APP_PASSWORD' to the application password you've set.
-
-1. Save the configuration file.
-
-1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
- installed GitLab via Omnibus or from source respectively.
-
-On the sign in page there should now be a Crowd tab in the sign in form.
-
-[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
-[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
-
+This document was moved to [`administration/auth/crowd`](../administration/auth/crowd.md).
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index 47e20d7566a..6c11f46a70a 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -27,7 +27,7 @@ contains some settings that are common for all providers.
- [Twitter](twitter.md)
- [Shibboleth](shibboleth.md)
- [SAML](saml.md)
-- [Crowd](crowd.md)
+- [Crowd](../administration/auth/crowd.md)
- [Azure](azure.md)
- [Auth0](auth0.md)
- [Authentiq](../administration/auth/authentiq.md)
diff --git a/doc/integration/openid_connect_provider.md b/doc/integration/openid_connect_provider.md
new file mode 100644
index 00000000000..56f367d841e
--- /dev/null
+++ b/doc/integration/openid_connect_provider.md
@@ -0,0 +1,47 @@
+# GitLab as OpenID Connect identity provider
+
+This document is about using GitLab as an OpenID Connect identity provider
+to sign in to other services.
+
+## Introduction to OpenID Connect
+
+[OpenID Connect] \(OIC) is a simple identity layer on top of the
+OAuth 2.0 protocol. It allows clients to verify the identity of the end-user
+based on the authentication performed by GitLab, as well as to obtain
+basic profile information about the end-user in an interoperable and
+REST-like manner. OIC performs many of the same tasks as OpenID 2.0,
+but does so in a way that is API-friendly, and usable by native and
+mobile applications.
+
+On the client side, you can use [omniauth-openid-connect] for Rails
+applications, or any of the other available [client implementations].
+
+GitLab's implementation uses the [doorkeeper-openid_connect] gem, refer
+to its README for more details about which parts of the specifications
+are supported.
+
+## Enabling OpenID Connect for OAuth applications
+
+Refer to the [OAuth guide] for basic information on how to set up OAuth
+applications in GitLab. To enable OIC for an application, all you have to do
+is select the `openid` scope in the application settings.
+
+Currently the following user information is shared with clients:
+
+| Claim | Type | Description |
+|:-----------------|:----------|:------------|
+| `sub` | `string` | An opaque token that uniquely identifies the user
+| `auth_time` | `integer` | The timestamp for the user's last authentication
+| `name` | `string` | The user's full name
+| `nickname` | `string` | The user's GitLab username
+| `email` | `string` | The user's public email address
+| `email_verified` | `boolean` | Whether the user's public email address was verified
+| `website` | `string` | URL for the user's website
+| `profile` | `string` | URL for the user's GitLab profile
+| `picture` | `string` | URL for the user's GitLab avatar
+
+[OpenID Connect]: http://openid.net/connect/ "OpenID Connect website"
+[doorkeeper-openid_connect]: https://github.com/doorkeeper-gem/doorkeeper-openid_connect "Doorkeeper::OpenidConnect website"
+[OAuth guide]: oauth_provider.md "GitLab as OAuth2 authentication service provider"
+[omniauth-openid-connect]: https://github.com/jjbohn/omniauth-openid-connect/ "OmniAuth::OpenIDConnect website"
+[client implementations]: http://openid.net/developers/libraries#connect "List of available client implementations"
diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md
index 7b934ecd87a..4cc8be752c4 100644
--- a/doc/update/8.17-to-9.0.md
+++ b/doc/update/8.17-to-9.0.md
@@ -1,3 +1,66 @@
+#### 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. Upate 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:
@@ -12,7 +75,7 @@ git diff origin/8-17-stable:lib/support/nginx/gitlab-ssl origin/9-0-stable:lib/s
git diff origin/8-17-stable:lib/support/nginx/gitlab origin/9-0-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
+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].
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index 91b35c73b34..b6221620e58 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -249,4 +249,4 @@ Once the right permissions were set, the error will go away.
[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040
[docker-docs]: https://docs.docker.com/engine/userguide/intro/
-[private-docker]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-docker-registry
+[private-docker]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-container-registry
diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature
index 8570c637b36..0d6f7350181 100644
--- a/features/project/active_tab.feature
+++ b/features/project/active_tab.feature
@@ -52,28 +52,28 @@ Feature: Project Active Tab
Scenario: On Project Settings/Integrations
Given I visit my project's settings page
And I click the "Integrations" tab
- Then the active sub nav should be Integrations
- And no other sub navs should be active
+ Then the active sub tab should be Integrations
+ And no other sub tabs should be active
And the active main tab should be Settings
- Scenario: On Project Settings/Deploy Keys
+ Scenario: On Project Settings/Repository
Given I visit my project's settings page
- And I click the "Deploy Keys" tab
- Then the active sub nav should be Deploy Keys
- And no other sub navs should be active
+ And I click the "Repository" tab
+ Then the active sub tab should be Repository
+ And no other sub tabs should be active
And the active main tab should be Settings
Scenario: On Project Settings/Pages
Given I visit my project's settings page
And I click the "Pages" tab
- Then the active sub nav should be Pages
- And no other sub navs should be active
+ Then the active sub tab should be Pages
+ And no other sub tabs should be active
And the active main tab should be Settings
Scenario: On Project Members
Given I visit my project's members page
- Then the active sub nav should be Members
- And no other sub navs should be active
+ Then the active sub tab should be Members
+ And no other sub tabs should be active
And the active main tab should be Settings
# Sub Tabs: Repository
diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb
index d29b22d42ec..4befd49ac81 100644
--- a/features/steps/project/active_tab.rb
+++ b/features/steps/project/active_tab.rb
@@ -22,21 +22,27 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
end
step 'I click the "Edit Project"' do
- page.within '.layout-nav .controls' do
+ page.within '.sub-nav' do
click_link('Edit Project')
end
end
step 'I click the "Integrations" tab' do
- click_link('Integrations')
+ page.within '.sub-nav' do
+ click_link('Integrations')
+ end
end
- step 'I click the "Deploy Keys" tab' do
- click_link('Deploy Keys')
+ step 'I click the "Repository" tab' do
+ page.within '.sub-nav' do
+ click_link('Repository')
+ end
end
step 'I click the "Pages" tab' do
- click_link('Pages')
+ page.within '.sub-nav' do
+ click_link('Pages')
+ end
end
step 'I click the "Activity" tab' do
@@ -45,20 +51,20 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps
end
end
- step 'the active sub nav should be Members' do
- ensure_active_sub_nav('Members')
+ step 'the active sub tab should be Members' do
+ ensure_active_sub_tab('Members')
end
- step 'the active sub nav should be Integrations' do
- ensure_active_sub_nav('Integrations')
+ step 'the active sub tab should be Integrations' do
+ ensure_active_sub_tab('Integrations')
end
- step 'the active sub nav should be Deploy Keys' do
- ensure_active_sub_nav('Deploy Keys')
+ step 'the active sub tab should be Repository' do
+ ensure_active_sub_tab('Repository')
end
- step 'the active sub nav should be Pages' do
- ensure_active_sub_nav('Pages')
+ step 'the active sub tab should be Pages' do
+ ensure_active_sub_tab('Pages')
end
step 'the active sub tab should be Activity' do
diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb
index edf78f62f9a..580a19494c2 100644
--- a/features/steps/project/deploy_keys.rb
+++ b/features/steps/project/deploy_keys.rb
@@ -36,7 +36,7 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
end
step 'I should be on deploy keys page' do
- expect(current_path).to eq namespace_project_deploy_keys_path(@project.namespace, @project)
+ expect(current_path).to eq namespace_project_settings_repository_path(@project.namespace, @project)
end
step 'I should see newly created deploy key' do
diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb
index 400114f03c0..0cb9229dbae 100644
--- a/features/steps/shared/project_tab.rb
+++ b/features/steps/shared/project_tab.rb
@@ -16,8 +16,8 @@ module SharedProjectTab
ensure_active_main_tab('Issues')
end
- step 'the active main tab should be Members' do
- ensure_active_main_tab('Members')
+ step 'the active sub tab should be Members' do
+ ensure_active_sub_tab('Members')
end
step 'the active main tab should be Merge Requests' do
@@ -33,7 +33,7 @@ module SharedProjectTab
end
step 'the active main tab should be Settings' do
- expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 0)
+ ensure_active_main_tab('Settings')
end
step 'the active sub tab should be Graph' do
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
index 47372df152d..a5fcbb65131 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -2,7 +2,7 @@ require 'spinach/capybara'
require 'capybara/poltergeist'
# Give CI some extra time
-timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 15
+timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 30 : 10
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
diff --git a/fixtures/emojis/emoji-unicode-version-map.json b/fixtures/emojis/emoji-unicode-version-map.json
new file mode 100644
index 00000000000..5164fe39426
--- /dev/null
+++ b/fixtures/emojis/emoji-unicode-version-map.json
@@ -0,0 +1,2377 @@
+{
+ "100": "6.0",
+ "1234": "6.0",
+ "grinning": "6.1",
+ "grin": "6.0",
+ "joy": "6.0",
+ "rofl": "9.0",
+ "rolling_on_the_floor_laughing": "9.0",
+ "smiley": "6.0",
+ "smile": "6.0",
+ "sweat_smile": "6.0",
+ "laughing": "6.0",
+ "satisfied": "6.0",
+ "wink": "6.0",
+ "blush": "6.0",
+ "yum": "6.0",
+ "sunglasses": "6.0",
+ "heart_eyes": "6.0",
+ "kissing_heart": "6.0",
+ "kissing": "6.1",
+ "kissing_smiling_eyes": "6.1",
+ "kissing_closed_eyes": "6.0",
+ "relaxed": "1.1",
+ "slight_smile": "7.0",
+ "slightly_smiling_face": "7.0",
+ "hugging": "8.0",
+ "hugging_face": "8.0",
+ "thinking": "8.0",
+ "thinking_face": "8.0",
+ "neutral_face": "6.0",
+ "expressionless": "6.1",
+ "no_mouth": "6.0",
+ "rolling_eyes": "8.0",
+ "face_with_rolling_eyes": "8.0",
+ "smirk": "6.0",
+ "persevere": "6.0",
+ "disappointed_relieved": "6.0",
+ "open_mouth": "6.1",
+ "zipper_mouth": "8.0",
+ "zipper_mouth_face": "8.0",
+ "hushed": "6.1",
+ "sleepy": "6.0",
+ "tired_face": "6.0",
+ "sleeping": "6.1",
+ "relieved": "6.0",
+ "nerd": "8.0",
+ "nerd_face": "8.0",
+ "stuck_out_tongue": "6.1",
+ "stuck_out_tongue_winking_eye": "6.0",
+ "stuck_out_tongue_closed_eyes": "6.0",
+ "drooling_face": "9.0",
+ "drool": "9.0",
+ "unamused": "6.0",
+ "sweat": "6.0",
+ "pensive": "6.0",
+ "confused": "6.1",
+ "upside_down": "8.0",
+ "upside_down_face": "8.0",
+ "money_mouth": "8.0",
+ "money_mouth_face": "8.0",
+ "astonished": "6.0",
+ "frowning2": "1.1",
+ "white_frowning_face": "1.1",
+ "slight_frown": "7.0",
+ "slightly_frowning_face": "7.0",
+ "confounded": "6.0",
+ "disappointed": "6.0",
+ "worried": "6.1",
+ "triumph": "6.0",
+ "cry": "6.0",
+ "sob": "6.0",
+ "frowning": "6.1",
+ "anguished": "6.1",
+ "fearful": "6.0",
+ "weary": "6.0",
+ "grimacing": "6.1",
+ "cold_sweat": "6.0",
+ "scream": "6.0",
+ "flushed": "6.0",
+ "dizzy_face": "6.0",
+ "rage": "6.0",
+ "angry": "6.0",
+ "innocent": "6.0",
+ "cowboy": "9.0",
+ "face_with_cowboy_hat": "9.0",
+ "clown": "9.0",
+ "clown_face": "9.0",
+ "lying_face": "9.0",
+ "liar": "9.0",
+ "mask": "6.0",
+ "thermometer_face": "8.0",
+ "face_with_thermometer": "8.0",
+ "head_bandage": "8.0",
+ "face_with_head_bandage": "8.0",
+ "nauseated_face": "9.0",
+ "sick": "9.0",
+ "sneezing_face": "9.0",
+ "sneeze": "9.0",
+ "smiling_imp": "6.0",
+ "imp": "6.0",
+ "japanese_ogre": "6.0",
+ "japanese_goblin": "6.0",
+ "skull": "6.0",
+ "skeleton": "6.0",
+ "skull_crossbones": "1.1",
+ "skull_and_crossbones": "1.1",
+ "ghost": "6.0",
+ "alien": "6.0",
+ "space_invader": "6.0",
+ "robot": "8.0",
+ "robot_face": "8.0",
+ "poop": "6.0",
+ "shit": "6.0",
+ "hankey": "6.0",
+ "poo": "6.0",
+ "smiley_cat": "6.0",
+ "smile_cat": "6.0",
+ "joy_cat": "6.0",
+ "heart_eyes_cat": "6.0",
+ "smirk_cat": "6.0",
+ "kissing_cat": "6.0",
+ "scream_cat": "6.0",
+ "crying_cat_face": "6.0",
+ "pouting_cat": "6.0",
+ "see_no_evil": "6.0",
+ "hear_no_evil": "6.0",
+ "speak_no_evil": "6.0",
+ "boy": "6.0",
+ "boy_tone1": "8.0",
+ "boy_tone2": "8.0",
+ "boy_tone3": "8.0",
+ "boy_tone4": "8.0",
+ "boy_tone5": "8.0",
+ "girl": "6.0",
+ "girl_tone1": "8.0",
+ "girl_tone2": "8.0",
+ "girl_tone3": "8.0",
+ "girl_tone4": "8.0",
+ "girl_tone5": "8.0",
+ "man": "6.0",
+ "man_tone1": "8.0",
+ "man_tone2": "8.0",
+ "man_tone3": "8.0",
+ "man_tone4": "8.0",
+ "man_tone5": "8.0",
+ "woman": "6.0",
+ "woman_tone1": "8.0",
+ "woman_tone2": "8.0",
+ "woman_tone3": "8.0",
+ "woman_tone4": "8.0",
+ "woman_tone5": "8.0",
+ "older_man": "6.0",
+ "older_man_tone1": "8.0",
+ "older_man_tone2": "8.0",
+ "older_man_tone3": "8.0",
+ "older_man_tone4": "8.0",
+ "older_man_tone5": "8.0",
+ "older_woman": "6.0",
+ "grandma": "6.0",
+ "older_woman_tone1": "8.0",
+ "grandma_tone1": "8.0",
+ "older_woman_tone2": "8.0",
+ "grandma_tone2": "8.0",
+ "older_woman_tone3": "8.0",
+ "grandma_tone3": "8.0",
+ "older_woman_tone4": "8.0",
+ "grandma_tone4": "8.0",
+ "older_woman_tone5": "8.0",
+ "grandma_tone5": "8.0",
+ "baby": "6.0",
+ "baby_tone1": "8.0",
+ "baby_tone2": "8.0",
+ "baby_tone3": "8.0",
+ "baby_tone4": "8.0",
+ "baby_tone5": "8.0",
+ "angel": "6.0",
+ "angel_tone1": "8.0",
+ "angel_tone2": "8.0",
+ "angel_tone3": "8.0",
+ "angel_tone4": "8.0",
+ "angel_tone5": "8.0",
+ "cop": "6.0",
+ "cop_tone1": "8.0",
+ "cop_tone2": "8.0",
+ "cop_tone3": "8.0",
+ "cop_tone4": "8.0",
+ "cop_tone5": "8.0",
+ "spy": "7.0",
+ "sleuth_or_spy": "7.0",
+ "spy_tone1": "8.0",
+ "sleuth_or_spy_tone1": "8.0",
+ "spy_tone2": "8.0",
+ "sleuth_or_spy_tone2": "8.0",
+ "spy_tone3": "8.0",
+ "sleuth_or_spy_tone3": "8.0",
+ "spy_tone4": "8.0",
+ "sleuth_or_spy_tone4": "8.0",
+ "spy_tone5": "8.0",
+ "sleuth_or_spy_tone5": "8.0",
+ "guardsman": "6.0",
+ "guardsman_tone1": "8.0",
+ "guardsman_tone2": "8.0",
+ "guardsman_tone3": "8.0",
+ "guardsman_tone4": "8.0",
+ "guardsman_tone5": "8.0",
+ "construction_worker": "6.0",
+ "construction_worker_tone1": "8.0",
+ "construction_worker_tone2": "8.0",
+ "construction_worker_tone3": "8.0",
+ "construction_worker_tone4": "8.0",
+ "construction_worker_tone5": "8.0",
+ "man_with_turban": "6.0",
+ "man_with_turban_tone1": "8.0",
+ "man_with_turban_tone2": "8.0",
+ "man_with_turban_tone3": "8.0",
+ "man_with_turban_tone4": "8.0",
+ "man_with_turban_tone5": "8.0",
+ "person_with_blond_hair": "6.0",
+ "person_with_blond_hair_tone1": "8.0",
+ "person_with_blond_hair_tone2": "8.0",
+ "person_with_blond_hair_tone3": "8.0",
+ "person_with_blond_hair_tone4": "8.0",
+ "person_with_blond_hair_tone5": "8.0",
+ "santa": "6.0",
+ "santa_tone1": "8.0",
+ "santa_tone2": "8.0",
+ "santa_tone3": "8.0",
+ "santa_tone4": "8.0",
+ "santa_tone5": "8.0",
+ "mrs_claus": "9.0",
+ "mother_christmas": "9.0",
+ "mrs_claus_tone1": "9.0",
+ "mother_christmas_tone1": "9.0",
+ "mrs_claus_tone2": "9.0",
+ "mother_christmas_tone2": "9.0",
+ "mrs_claus_tone3": "9.0",
+ "mother_christmas_tone3": "9.0",
+ "mrs_claus_tone4": "9.0",
+ "mother_christmas_tone4": "9.0",
+ "mrs_claus_tone5": "9.0",
+ "mother_christmas_tone5": "9.0",
+ "princess": "6.0",
+ "princess_tone1": "8.0",
+ "princess_tone2": "8.0",
+ "princess_tone3": "8.0",
+ "princess_tone4": "8.0",
+ "princess_tone5": "8.0",
+ "prince": "9.0",
+ "prince_tone1": "9.0",
+ "prince_tone2": "9.0",
+ "prince_tone3": "9.0",
+ "prince_tone4": "9.0",
+ "prince_tone5": "9.0",
+ "bride_with_veil": "6.0",
+ "bride_with_veil_tone1": "8.0",
+ "bride_with_veil_tone2": "8.0",
+ "bride_with_veil_tone3": "8.0",
+ "bride_with_veil_tone4": "8.0",
+ "bride_with_veil_tone5": "8.0",
+ "man_in_tuxedo": "9.0",
+ "man_in_tuxedo_tone1": "9.0",
+ "tuxedo_tone1": "9.0",
+ "man_in_tuxedo_tone2": "9.0",
+ "tuxedo_tone2": "9.0",
+ "man_in_tuxedo_tone3": "9.0",
+ "tuxedo_tone3": "9.0",
+ "man_in_tuxedo_tone4": "9.0",
+ "tuxedo_tone4": "9.0",
+ "man_in_tuxedo_tone5": "9.0",
+ "tuxedo_tone5": "9.0",
+ "pregnant_woman": "9.0",
+ "expecting_woman": "9.0",
+ "pregnant_woman_tone1": "9.0",
+ "expecting_woman_tone1": "9.0",
+ "pregnant_woman_tone2": "9.0",
+ "expecting_woman_tone2": "9.0",
+ "pregnant_woman_tone3": "9.0",
+ "expecting_woman_tone3": "9.0",
+ "pregnant_woman_tone4": "9.0",
+ "expecting_woman_tone4": "9.0",
+ "pregnant_woman_tone5": "9.0",
+ "expecting_woman_tone5": "9.0",
+ "man_with_gua_pi_mao": "6.0",
+ "man_with_gua_pi_mao_tone1": "8.0",
+ "man_with_gua_pi_mao_tone2": "8.0",
+ "man_with_gua_pi_mao_tone3": "8.0",
+ "man_with_gua_pi_mao_tone4": "8.0",
+ "man_with_gua_pi_mao_tone5": "8.0",
+ "person_frowning": "6.0",
+ "person_frowning_tone1": "8.0",
+ "person_frowning_tone2": "8.0",
+ "person_frowning_tone3": "8.0",
+ "person_frowning_tone4": "8.0",
+ "person_frowning_tone5": "8.0",
+ "person_with_pouting_face": "6.0",
+ "person_with_pouting_face_tone1": "8.0",
+ "person_with_pouting_face_tone2": "8.0",
+ "person_with_pouting_face_tone3": "8.0",
+ "person_with_pouting_face_tone4": "8.0",
+ "person_with_pouting_face_tone5": "8.0",
+ "no_good": "6.0",
+ "no_good_tone1": "8.0",
+ "no_good_tone2": "8.0",
+ "no_good_tone3": "8.0",
+ "no_good_tone4": "8.0",
+ "no_good_tone5": "8.0",
+ "ok_woman": "6.0",
+ "ok_woman_tone1": "8.0",
+ "ok_woman_tone2": "8.0",
+ "ok_woman_tone3": "8.0",
+ "ok_woman_tone4": "8.0",
+ "ok_woman_tone5": "8.0",
+ "information_desk_person": "6.0",
+ "information_desk_person_tone1": "8.0",
+ "information_desk_person_tone2": "8.0",
+ "information_desk_person_tone3": "8.0",
+ "information_desk_person_tone4": "8.0",
+ "information_desk_person_tone5": "8.0",
+ "raising_hand": "6.0",
+ "raising_hand_tone1": "8.0",
+ "raising_hand_tone2": "8.0",
+ "raising_hand_tone3": "8.0",
+ "raising_hand_tone4": "8.0",
+ "raising_hand_tone5": "8.0",
+ "bow": "6.0",
+ "bow_tone1": "8.0",
+ "bow_tone2": "8.0",
+ "bow_tone3": "8.0",
+ "bow_tone4": "8.0",
+ "bow_tone5": "8.0",
+ "face_palm": "9.0",
+ "facepalm": "9.0",
+ "face_palm_tone1": "9.0",
+ "facepalm_tone1": "9.0",
+ "face_palm_tone2": "9.0",
+ "facepalm_tone2": "9.0",
+ "face_palm_tone3": "9.0",
+ "facepalm_tone3": "9.0",
+ "face_palm_tone4": "9.0",
+ "facepalm_tone4": "9.0",
+ "face_palm_tone5": "9.0",
+ "facepalm_tone5": "9.0",
+ "shrug": "9.0",
+ "shrug_tone1": "9.0",
+ "shrug_tone2": "9.0",
+ "shrug_tone3": "9.0",
+ "shrug_tone4": "9.0",
+ "shrug_tone5": "9.0",
+ "massage": "6.0",
+ "massage_tone1": "8.0",
+ "massage_tone2": "8.0",
+ "massage_tone3": "8.0",
+ "massage_tone4": "8.0",
+ "massage_tone5": "8.0",
+ "haircut": "6.0",
+ "haircut_tone1": "8.0",
+ "haircut_tone2": "8.0",
+ "haircut_tone3": "8.0",
+ "haircut_tone4": "8.0",
+ "haircut_tone5": "8.0",
+ "walking": "6.0",
+ "walking_tone1": "8.0",
+ "walking_tone2": "8.0",
+ "walking_tone3": "8.0",
+ "walking_tone4": "8.0",
+ "walking_tone5": "8.0",
+ "runner": "6.0",
+ "runner_tone1": "8.0",
+ "runner_tone2": "8.0",
+ "runner_tone3": "8.0",
+ "runner_tone4": "8.0",
+ "runner_tone5": "8.0",
+ "dancer": "6.0",
+ "dancer_tone1": "8.0",
+ "dancer_tone2": "8.0",
+ "dancer_tone3": "8.0",
+ "dancer_tone4": "8.0",
+ "dancer_tone5": "8.0",
+ "man_dancing": "9.0",
+ "male_dancer": "9.0",
+ "man_dancing_tone1": "9.0",
+ "male_dancer_tone1": "9.0",
+ "man_dancing_tone2": "9.0",
+ "male_dancer_tone2": "9.0",
+ "man_dancing_tone3": "9.0",
+ "male_dancer_tone3": "9.0",
+ "man_dancing_tone4": "9.0",
+ "male_dancer_tone4": "9.0",
+ "man_dancing_tone5": "9.0",
+ "male_dancer_tone5": "9.0",
+ "dancers": "6.0",
+ "levitate": "7.0",
+ "man_in_business_suit_levitating": "7.0",
+ "speaking_head": "7.0",
+ "speaking_head_in_silhouette": "7.0",
+ "bust_in_silhouette": "6.0",
+ "busts_in_silhouette": "6.0",
+ "fencer": "9.0",
+ "fencing": "9.0",
+ "horse_racing": "6.0",
+ "horse_racing_tone1": "8.0",
+ "horse_racing_tone2": "8.0",
+ "horse_racing_tone3": "8.0",
+ "horse_racing_tone4": "8.0",
+ "horse_racing_tone5": "8.0",
+ "skier": "5.2",
+ "snowboarder": "6.0",
+ "golfer": "7.0",
+ "surfer": "6.0",
+ "surfer_tone1": "8.0",
+ "surfer_tone2": "8.0",
+ "surfer_tone3": "8.0",
+ "surfer_tone4": "8.0",
+ "surfer_tone5": "8.0",
+ "rowboat": "6.0",
+ "rowboat_tone1": "8.0",
+ "rowboat_tone2": "8.0",
+ "rowboat_tone3": "8.0",
+ "rowboat_tone4": "8.0",
+ "rowboat_tone5": "8.0",
+ "swimmer": "6.0",
+ "swimmer_tone1": "8.0",
+ "swimmer_tone2": "8.0",
+ "swimmer_tone3": "8.0",
+ "swimmer_tone4": "8.0",
+ "swimmer_tone5": "8.0",
+ "basketball_player": "5.2",
+ "person_with_ball": "5.2",
+ "basketball_player_tone1": "8.0",
+ "person_with_ball_tone1": "8.0",
+ "basketball_player_tone2": "8.0",
+ "person_with_ball_tone2": "8.0",
+ "basketball_player_tone3": "8.0",
+ "person_with_ball_tone3": "8.0",
+ "basketball_player_tone4": "8.0",
+ "person_with_ball_tone4": "8.0",
+ "basketball_player_tone5": "8.0",
+ "person_with_ball_tone5": "8.0",
+ "lifter": "7.0",
+ "weight_lifter": "7.0",
+ "lifter_tone1": "8.0",
+ "weight_lifter_tone1": "8.0",
+ "lifter_tone2": "8.0",
+ "weight_lifter_tone2": "8.0",
+ "lifter_tone3": "8.0",
+ "weight_lifter_tone3": "8.0",
+ "lifter_tone4": "8.0",
+ "weight_lifter_tone4": "8.0",
+ "lifter_tone5": "8.0",
+ "weight_lifter_tone5": "8.0",
+ "bicyclist": "6.0",
+ "bicyclist_tone1": "8.0",
+ "bicyclist_tone2": "8.0",
+ "bicyclist_tone3": "8.0",
+ "bicyclist_tone4": "8.0",
+ "bicyclist_tone5": "8.0",
+ "mountain_bicyclist": "6.0",
+ "mountain_bicyclist_tone1": "8.0",
+ "mountain_bicyclist_tone2": "8.0",
+ "mountain_bicyclist_tone3": "8.0",
+ "mountain_bicyclist_tone4": "8.0",
+ "mountain_bicyclist_tone5": "8.0",
+ "race_car": "7.0",
+ "racing_car": "7.0",
+ "motorcycle": "7.0",
+ "racing_motorcycle": "7.0",
+ "cartwheel": "9.0",
+ "person_doing_cartwheel": "9.0",
+ "cartwheel_tone1": "9.0",
+ "person_doing_cartwheel_tone1": "9.0",
+ "cartwheel_tone2": "9.0",
+ "person_doing_cartwheel_tone2": "9.0",
+ "cartwheel_tone3": "9.0",
+ "person_doing_cartwheel_tone3": "9.0",
+ "cartwheel_tone4": "9.0",
+ "person_doing_cartwheel_tone4": "9.0",
+ "cartwheel_tone5": "9.0",
+ "person_doing_cartwheel_tone5": "9.0",
+ "wrestlers": "9.0",
+ "wrestling": "9.0",
+ "wrestlers_tone1": "9.0",
+ "wrestling_tone1": "9.0",
+ "wrestlers_tone2": "9.0",
+ "wrestling_tone2": "9.0",
+ "wrestlers_tone3": "9.0",
+ "wrestling_tone3": "9.0",
+ "wrestlers_tone4": "9.0",
+ "wrestling_tone4": "9.0",
+ "wrestlers_tone5": "9.0",
+ "wrestling_tone5": "9.0",
+ "water_polo": "9.0",
+ "water_polo_tone1": "9.0",
+ "water_polo_tone2": "9.0",
+ "water_polo_tone3": "9.0",
+ "water_polo_tone4": "9.0",
+ "water_polo_tone5": "9.0",
+ "handball": "9.0",
+ "handball_tone1": "9.0",
+ "handball_tone2": "9.0",
+ "handball_tone3": "9.0",
+ "handball_tone4": "9.0",
+ "handball_tone5": "9.0",
+ "juggling": "9.0",
+ "juggler": "9.0",
+ "juggling_tone1": "9.0",
+ "juggler_tone1": "9.0",
+ "juggling_tone2": "9.0",
+ "juggler_tone2": "9.0",
+ "juggling_tone3": "9.0",
+ "juggler_tone3": "9.0",
+ "juggling_tone4": "9.0",
+ "juggler_tone4": "9.0",
+ "juggling_tone5": "9.0",
+ "juggler_tone5": "9.0",
+ "couple": "6.0",
+ "two_men_holding_hands": "6.0",
+ "two_women_holding_hands": "6.0",
+ "couplekiss": "6.0",
+ "kiss_mm": "6.0",
+ "couplekiss_mm": "6.0",
+ "kiss_ww": "6.0",
+ "couplekiss_ww": "6.0",
+ "couple_with_heart": "6.0",
+ "couple_mm": "6.0",
+ "couple_with_heart_mm": "6.0",
+ "couple_ww": "6.0",
+ "couple_with_heart_ww": "6.0",
+ "family": "6.0",
+ "family_mwg": "6.0",
+ "family_mwgb": "6.0",
+ "family_mwbb": "6.0",
+ "family_mwgg": "6.0",
+ "family_mmb": "6.0",
+ "family_mmg": "6.0",
+ "family_mmgb": "6.0",
+ "family_mmbb": "6.0",
+ "family_mmgg": "6.0",
+ "family_wwb": "6.0",
+ "family_wwg": "6.0",
+ "family_wwgb": "6.0",
+ "family_wwbb": "6.0",
+ "family_wwgg": "6.0",
+ "tone1": "8.0",
+ "tone2": "8.0",
+ "tone3": "8.0",
+ "tone4": "8.0",
+ "tone5": "8.0",
+ "muscle": "6.0",
+ "muscle_tone1": "8.0",
+ "muscle_tone2": "8.0",
+ "muscle_tone3": "8.0",
+ "muscle_tone4": "8.0",
+ "muscle_tone5": "8.0",
+ "selfie": "9.0",
+ "selfie_tone1": "9.0",
+ "selfie_tone2": "9.0",
+ "selfie_tone3": "9.0",
+ "selfie_tone4": "9.0",
+ "selfie_tone5": "9.0",
+ "point_left": "6.0",
+ "point_left_tone1": "8.0",
+ "point_left_tone2": "8.0",
+ "point_left_tone3": "8.0",
+ "point_left_tone4": "8.0",
+ "point_left_tone5": "8.0",
+ "point_right": "6.0",
+ "point_right_tone1": "8.0",
+ "point_right_tone2": "8.0",
+ "point_right_tone3": "8.0",
+ "point_right_tone4": "8.0",
+ "point_right_tone5": "8.0",
+ "point_up": "1.1",
+ "point_up_tone1": "8.0",
+ "point_up_tone2": "8.0",
+ "point_up_tone3": "8.0",
+ "point_up_tone4": "8.0",
+ "point_up_tone5": "8.0",
+ "point_up_2": "6.0",
+ "point_up_2_tone1": "8.0",
+ "point_up_2_tone2": "8.0",
+ "point_up_2_tone3": "8.0",
+ "point_up_2_tone4": "8.0",
+ "point_up_2_tone5": "8.0",
+ "middle_finger": "7.0",
+ "reversed_hand_with_middle_finger_extended": "7.0",
+ "middle_finger_tone1": "8.0",
+ "reversed_hand_with_middle_finger_extended_tone1": "8.0",
+ "middle_finger_tone2": "8.0",
+ "reversed_hand_with_middle_finger_extended_tone2": "8.0",
+ "middle_finger_tone3": "8.0",
+ "reversed_hand_with_middle_finger_extended_tone3": "8.0",
+ "middle_finger_tone4": "8.0",
+ "reversed_hand_with_middle_finger_extended_tone4": "8.0",
+ "middle_finger_tone5": "8.0",
+ "reversed_hand_with_middle_finger_extended_tone5": "8.0",
+ "point_down": "6.0",
+ "point_down_tone1": "8.0",
+ "point_down_tone2": "8.0",
+ "point_down_tone3": "8.0",
+ "point_down_tone4": "8.0",
+ "point_down_tone5": "8.0",
+ "v": "1.1",
+ "v_tone1": "8.0",
+ "v_tone2": "8.0",
+ "v_tone3": "8.0",
+ "v_tone4": "8.0",
+ "v_tone5": "8.0",
+ "fingers_crossed": "9.0",
+ "hand_with_index_and_middle_finger_crossed": "9.0",
+ "fingers_crossed_tone1": "9.0",
+ "hand_with_index_and_middle_fingers_crossed_tone1": "9.0",
+ "fingers_crossed_tone2": "9.0",
+ "hand_with_index_and_middle_fingers_crossed_tone2": "9.0",
+ "fingers_crossed_tone3": "9.0",
+ "hand_with_index_and_middle_fingers_crossed_tone3": "9.0",
+ "fingers_crossed_tone4": "9.0",
+ "hand_with_index_and_middle_fingers_crossed_tone4": "9.0",
+ "fingers_crossed_tone5": "9.0",
+ "hand_with_index_and_middle_fingers_crossed_tone5": "9.0",
+ "vulcan": "7.0",
+ "raised_hand_with_part_between_middle_and_ring_fingers": "7.0",
+ "vulcan_tone1": "8.0",
+ "raised_hand_with_part_between_middle_and_ring_fingers_tone1": "8.0",
+ "vulcan_tone2": "8.0",
+ "raised_hand_with_part_between_middle_and_ring_fingers_tone2": "8.0",
+ "vulcan_tone3": "8.0",
+ "raised_hand_with_part_between_middle_and_ring_fingers_tone3": "8.0",
+ "vulcan_tone4": "8.0",
+ "raised_hand_with_part_between_middle_and_ring_fingers_tone4": "8.0",
+ "vulcan_tone5": "8.0",
+ "raised_hand_with_part_between_middle_and_ring_fingers_tone5": "8.0",
+ "metal": "8.0",
+ "sign_of_the_horns": "8.0",
+ "metal_tone1": "8.0",
+ "sign_of_the_horns_tone1": "8.0",
+ "metal_tone2": "8.0",
+ "sign_of_the_horns_tone2": "8.0",
+ "metal_tone3": "8.0",
+ "sign_of_the_horns_tone3": "8.0",
+ "metal_tone4": "8.0",
+ "sign_of_the_horns_tone4": "8.0",
+ "metal_tone5": "8.0",
+ "sign_of_the_horns_tone5": "8.0",
+ "call_me": "9.0",
+ "call_me_hand": "9.0",
+ "call_me_tone1": "9.0",
+ "call_me_hand_tone1": "9.0",
+ "call_me_tone2": "9.0",
+ "call_me_hand_tone2": "9.0",
+ "call_me_tone3": "9.0",
+ "call_me_hand_tone3": "9.0",
+ "call_me_tone4": "9.0",
+ "call_me_hand_tone4": "9.0",
+ "call_me_tone5": "9.0",
+ "call_me_hand_tone5": "9.0",
+ "hand_splayed": "7.0",
+ "raised_hand_with_fingers_splayed": "7.0",
+ "hand_splayed_tone1": "8.0",
+ "raised_hand_with_fingers_splayed_tone1": "8.0",
+ "hand_splayed_tone2": "8.0",
+ "raised_hand_with_fingers_splayed_tone2": "8.0",
+ "hand_splayed_tone3": "8.0",
+ "raised_hand_with_fingers_splayed_tone3": "8.0",
+ "hand_splayed_tone4": "8.0",
+ "raised_hand_with_fingers_splayed_tone4": "8.0",
+ "hand_splayed_tone5": "8.0",
+ "raised_hand_with_fingers_splayed_tone5": "8.0",
+ "raised_hand": "6.0",
+ "raised_hand_tone1": "8.0",
+ "raised_hand_tone2": "8.0",
+ "raised_hand_tone3": "8.0",
+ "raised_hand_tone4": "8.0",
+ "raised_hand_tone5": "8.0",
+ "ok_hand": "6.0",
+ "ok_hand_tone1": "8.0",
+ "ok_hand_tone2": "8.0",
+ "ok_hand_tone3": "8.0",
+ "ok_hand_tone4": "8.0",
+ "ok_hand_tone5": "8.0",
+ "thumbsup": "6.0",
+ "+1": "6.0",
+ "thumbup": "6.0",
+ "thumbsup_tone1": "8.0",
+ "+1_tone1": "8.0",
+ "thumbup_tone1": "8.0",
+ "thumbsup_tone2": "8.0",
+ "+1_tone2": "8.0",
+ "thumbup_tone2": "8.0",
+ "thumbsup_tone3": "8.0",
+ "+1_tone3": "8.0",
+ "thumbup_tone3": "8.0",
+ "thumbsup_tone4": "8.0",
+ "+1_tone4": "8.0",
+ "thumbup_tone4": "8.0",
+ "thumbsup_tone5": "8.0",
+ "+1_tone5": "8.0",
+ "thumbup_tone5": "8.0",
+ "thumbsdown": "6.0",
+ "-1": "6.0",
+ "thumbdown": "6.0",
+ "thumbsdown_tone1": "8.0",
+ "-1_tone1": "8.0",
+ "thumbdown_tone1": "8.0",
+ "thumbsdown_tone2": "8.0",
+ "-1_tone2": "8.0",
+ "thumbdown_tone2": "8.0",
+ "thumbsdown_tone3": "8.0",
+ "-1_tone3": "8.0",
+ "thumbdown_tone3": "8.0",
+ "thumbsdown_tone4": "8.0",
+ "-1_tone4": "8.0",
+ "thumbdown_tone4": "8.0",
+ "thumbsdown_tone5": "8.0",
+ "-1_tone5": "8.0",
+ "thumbdown_tone5": "8.0",
+ "fist": "6.0",
+ "fist_tone1": "8.0",
+ "fist_tone2": "8.0",
+ "fist_tone3": "8.0",
+ "fist_tone4": "8.0",
+ "fist_tone5": "8.0",
+ "punch": "6.0",
+ "punch_tone1": "8.0",
+ "punch_tone2": "8.0",
+ "punch_tone3": "8.0",
+ "punch_tone4": "8.0",
+ "punch_tone5": "8.0",
+ "left_facing_fist": "9.0",
+ "left_fist": "9.0",
+ "left_facing_fist_tone1": "9.0",
+ "left_fist_tone1": "9.0",
+ "left_facing_fist_tone2": "9.0",
+ "left_fist_tone2": "9.0",
+ "left_facing_fist_tone3": "9.0",
+ "left_fist_tone3": "9.0",
+ "left_facing_fist_tone4": "9.0",
+ "left_fist_tone4": "9.0",
+ "left_facing_fist_tone5": "9.0",
+ "left_fist_tone5": "9.0",
+ "right_facing_fist": "9.0",
+ "right_fist": "9.0",
+ "right_facing_fist_tone1": "9.0",
+ "right_fist_tone1": "9.0",
+ "right_facing_fist_tone2": "9.0",
+ "right_fist_tone2": "9.0",
+ "right_facing_fist_tone3": "9.0",
+ "right_fist_tone3": "9.0",
+ "right_facing_fist_tone4": "9.0",
+ "right_fist_tone4": "9.0",
+ "right_facing_fist_tone5": "9.0",
+ "right_fist_tone5": "9.0",
+ "raised_back_of_hand": "9.0",
+ "back_of_hand": "9.0",
+ "raised_back_of_hand_tone1": "9.0",
+ "back_of_hand_tone1": "9.0",
+ "raised_back_of_hand_tone2": "9.0",
+ "back_of_hand_tone2": "9.0",
+ "raised_back_of_hand_tone3": "9.0",
+ "back_of_hand_tone3": "9.0",
+ "raised_back_of_hand_tone4": "9.0",
+ "back_of_hand_tone4": "9.0",
+ "raised_back_of_hand_tone5": "9.0",
+ "back_of_hand_tone5": "9.0",
+ "wave": "6.0",
+ "wave_tone1": "8.0",
+ "wave_tone2": "8.0",
+ "wave_tone3": "8.0",
+ "wave_tone4": "8.0",
+ "wave_tone5": "8.0",
+ "clap": "6.0",
+ "clap_tone1": "8.0",
+ "clap_tone2": "8.0",
+ "clap_tone3": "8.0",
+ "clap_tone4": "8.0",
+ "clap_tone5": "8.0",
+ "writing_hand": "1.1",
+ "writing_hand_tone1": "8.0",
+ "writing_hand_tone2": "8.0",
+ "writing_hand_tone3": "8.0",
+ "writing_hand_tone4": "8.0",
+ "writing_hand_tone5": "8.0",
+ "open_hands": "6.0",
+ "open_hands_tone1": "8.0",
+ "open_hands_tone2": "8.0",
+ "open_hands_tone3": "8.0",
+ "open_hands_tone4": "8.0",
+ "open_hands_tone5": "8.0",
+ "raised_hands": "6.0",
+ "raised_hands_tone1": "8.0",
+ "raised_hands_tone2": "8.0",
+ "raised_hands_tone3": "8.0",
+ "raised_hands_tone4": "8.0",
+ "raised_hands_tone5": "8.0",
+ "pray": "6.0",
+ "pray_tone1": "8.0",
+ "pray_tone2": "8.0",
+ "pray_tone3": "8.0",
+ "pray_tone4": "8.0",
+ "pray_tone5": "8.0",
+ "handshake": "9.0",
+ "shaking_hands": "9.0",
+ "handshake_tone1": "9.0",
+ "shaking_hands_tone1": "9.0",
+ "handshake_tone2": "9.0",
+ "shaking_hands_tone2": "9.0",
+ "handshake_tone3": "9.0",
+ "shaking_hands_tone3": "9.0",
+ "handshake_tone4": "9.0",
+ "shaking_hands_tone4": "9.0",
+ "handshake_tone5": "9.0",
+ "shaking_hands_tone5": "9.0",
+ "nail_care": "6.0",
+ "nail_care_tone1": "8.0",
+ "nail_care_tone2": "8.0",
+ "nail_care_tone3": "8.0",
+ "nail_care_tone4": "8.0",
+ "nail_care_tone5": "8.0",
+ "ear": "6.0",
+ "ear_tone1": "8.0",
+ "ear_tone2": "8.0",
+ "ear_tone3": "8.0",
+ "ear_tone4": "8.0",
+ "ear_tone5": "8.0",
+ "nose": "6.0",
+ "nose_tone1": "8.0",
+ "nose_tone2": "8.0",
+ "nose_tone3": "8.0",
+ "nose_tone4": "8.0",
+ "nose_tone5": "8.0",
+ "footprints": "6.0",
+ "eyes": "6.0",
+ "eye": "7.0",
+ "eye_in_speech_bubble": "7.0",
+ "tongue": "6.0",
+ "lips": "6.0",
+ "kiss": "6.0",
+ "cupid": "6.0",
+ "heart": "1.1",
+ "heartbeat": "6.0",
+ "broken_heart": "6.0",
+ "two_hearts": "6.0",
+ "sparkling_heart": "6.0",
+ "heartpulse": "6.0",
+ "blue_heart": "6.0",
+ "green_heart": "6.0",
+ "yellow_heart": "6.0",
+ "purple_heart": "6.0",
+ "black_heart": "9.0",
+ "gift_heart": "6.0",
+ "revolving_hearts": "6.0",
+ "heart_decoration": "6.0",
+ "heart_exclamation": "1.1",
+ "heavy_heart_exclamation_mark_ornament": "1.1",
+ "love_letter": "6.0",
+ "zzz": "6.0",
+ "anger": "6.0",
+ "bomb": "6.0",
+ "boom": "6.0",
+ "sweat_drops": "6.0",
+ "dash": "6.0",
+ "dizzy": "6.0",
+ "speech_balloon": "6.0",
+ "speech_left": "7.0",
+ "left_speech_bubble": "7.0",
+ "anger_right": "7.0",
+ "right_anger_bubble": "7.0",
+ "thought_balloon": "6.0",
+ "hole": "7.0",
+ "eyeglasses": "6.0",
+ "dark_sunglasses": "7.0",
+ "necktie": "6.0",
+ "shirt": "6.0",
+ "jeans": "6.0",
+ "dress": "6.0",
+ "kimono": "6.0",
+ "bikini": "6.0",
+ "womans_clothes": "6.0",
+ "purse": "6.0",
+ "handbag": "6.0",
+ "pouch": "6.0",
+ "shopping_bags": "7.0",
+ "school_satchel": "6.0",
+ "mans_shoe": "6.0",
+ "athletic_shoe": "6.0",
+ "high_heel": "6.0",
+ "sandal": "6.0",
+ "boot": "6.0",
+ "crown": "6.0",
+ "womans_hat": "6.0",
+ "tophat": "6.0",
+ "mortar_board": "6.0",
+ "helmet_with_cross": "5.2",
+ "helmet_with_white_cross": "5.2",
+ "prayer_beads": "8.0",
+ "lipstick": "6.0",
+ "ring": "6.0",
+ "gem": "6.0",
+ "monkey_face": "6.0",
+ "monkey": "6.0",
+ "gorilla": "9.0",
+ "dog": "6.0",
+ "dog2": "6.0",
+ "poodle": "6.0",
+ "wolf": "6.0",
+ "fox": "9.0",
+ "fox_face": "9.0",
+ "cat": "6.0",
+ "cat2": "6.0",
+ "lion_face": "8.0",
+ "lion": "8.0",
+ "tiger": "6.0",
+ "tiger2": "6.0",
+ "leopard": "6.0",
+ "horse": "6.0",
+ "racehorse": "6.0",
+ "deer": "9.0",
+ "unicorn": "8.0",
+ "unicorn_face": "8.0",
+ "cow": "6.0",
+ "ox": "6.0",
+ "water_buffalo": "6.0",
+ "cow2": "6.0",
+ "pig": "6.0",
+ "pig2": "6.0",
+ "boar": "6.0",
+ "pig_nose": "6.0",
+ "ram": "6.0",
+ "sheep": "6.0",
+ "goat": "6.0",
+ "dromedary_camel": "6.0",
+ "camel": "6.0",
+ "elephant": "6.0",
+ "rhino": "9.0",
+ "rhinoceros": "9.0",
+ "mouse": "6.0",
+ "mouse2": "6.0",
+ "rat": "6.0",
+ "hamster": "6.0",
+ "rabbit": "6.0",
+ "rabbit2": "6.0",
+ "chipmunk": "7.0",
+ "bat": "9.0",
+ "bear": "6.0",
+ "koala": "6.0",
+ "panda_face": "6.0",
+ "feet": "6.0",
+ "paw_prints": "6.0",
+ "turkey": "8.0",
+ "chicken": "6.0",
+ "rooster": "6.0",
+ "hatching_chick": "6.0",
+ "baby_chick": "6.0",
+ "hatched_chick": "6.0",
+ "bird": "6.0",
+ "penguin": "6.0",
+ "dove": "7.0",
+ "dove_of_peace": "7.0",
+ "eagle": "9.0",
+ "duck": "9.0",
+ "owl": "9.0",
+ "frog": "6.0",
+ "crocodile": "6.0",
+ "turtle": "6.0",
+ "lizard": "9.0",
+ "snake": "6.0",
+ "dragon_face": "6.0",
+ "dragon": "6.0",
+ "whale": "6.0",
+ "whale2": "6.0",
+ "dolphin": "6.0",
+ "fish": "6.0",
+ "tropical_fish": "6.0",
+ "blowfish": "6.0",
+ "shark": "9.0",
+ "octopus": "6.0",
+ "shell": "6.0",
+ "crab": "8.0",
+ "shrimp": "9.0",
+ "squid": "9.0",
+ "butterfly": "9.0",
+ "snail": "6.0",
+ "bug": "6.0",
+ "ant": "6.0",
+ "bee": "6.0",
+ "beetle": "6.0",
+ "spider": "7.0",
+ "spider_web": "7.0",
+ "scorpion": "8.0",
+ "bouquet": "6.0",
+ "cherry_blossom": "6.0",
+ "white_flower": "6.0",
+ "rosette": "7.0",
+ "rose": "6.0",
+ "wilted_rose": "9.0",
+ "wilted_flower": "9.0",
+ "hibiscus": "6.0",
+ "sunflower": "6.0",
+ "blossom": "6.0",
+ "tulip": "6.0",
+ "seedling": "6.0",
+ "evergreen_tree": "6.0",
+ "deciduous_tree": "6.0",
+ "palm_tree": "6.0",
+ "cactus": "6.0",
+ "ear_of_rice": "6.0",
+ "herb": "6.0",
+ "shamrock": "4.1",
+ "four_leaf_clover": "6.0",
+ "maple_leaf": "6.0",
+ "fallen_leaf": "6.0",
+ "leaves": "6.0",
+ "grapes": "6.0",
+ "melon": "6.0",
+ "watermelon": "6.0",
+ "tangerine": "6.0",
+ "lemon": "6.0",
+ "banana": "6.0",
+ "pineapple": "6.0",
+ "apple": "6.0",
+ "green_apple": "6.0",
+ "pear": "6.0",
+ "peach": "6.0",
+ "cherries": "6.0",
+ "strawberry": "6.0",
+ "kiwi": "9.0",
+ "kiwifruit": "9.0",
+ "tomato": "6.0",
+ "avocado": "9.0",
+ "eggplant": "6.0",
+ "potato": "9.0",
+ "carrot": "9.0",
+ "corn": "6.0",
+ "hot_pepper": "7.0",
+ "cucumber": "9.0",
+ "mushroom": "6.0",
+ "peanuts": "9.0",
+ "shelled_peanut": "9.0",
+ "chestnut": "6.0",
+ "bread": "6.0",
+ "croissant": "9.0",
+ "french_bread": "9.0",
+ "baguette_bread": "9.0",
+ "pancakes": "9.0",
+ "cheese": "8.0",
+ "cheese_wedge": "8.0",
+ "meat_on_bone": "6.0",
+ "poultry_leg": "6.0",
+ "bacon": "9.0",
+ "hamburger": "6.0",
+ "fries": "6.0",
+ "pizza": "6.0",
+ "hotdog": "8.0",
+ "hot_dog": "8.0",
+ "taco": "8.0",
+ "burrito": "8.0",
+ "stuffed_flatbread": "9.0",
+ "stuffed_pita": "9.0",
+ "egg": "9.0",
+ "cooking": "6.0",
+ "shallow_pan_of_food": "9.0",
+ "paella": "9.0",
+ "stew": "6.0",
+ "salad": "9.0",
+ "green_salad": "9.0",
+ "popcorn": "8.0",
+ "bento": "6.0",
+ "rice_cracker": "6.0",
+ "rice_ball": "6.0",
+ "rice": "6.0",
+ "curry": "6.0",
+ "ramen": "6.0",
+ "spaghetti": "6.0",
+ "sweet_potato": "6.0",
+ "oden": "6.0",
+ "sushi": "6.0",
+ "fried_shrimp": "6.0",
+ "fish_cake": "6.0",
+ "dango": "6.0",
+ "icecream": "6.0",
+ "shaved_ice": "6.0",
+ "ice_cream": "6.0",
+ "doughnut": "6.0",
+ "cookie": "6.0",
+ "birthday": "6.0",
+ "cake": "6.0",
+ "chocolate_bar": "6.0",
+ "candy": "6.0",
+ "lollipop": "6.0",
+ "custard": "6.0",
+ "pudding": "6.0",
+ "flan": "6.0",
+ "honey_pot": "6.0",
+ "baby_bottle": "6.0",
+ "milk": "9.0",
+ "glass_of_milk": "9.0",
+ "coffee": "4.0",
+ "tea": "6.0",
+ "sake": "6.0",
+ "champagne": "8.0",
+ "bottle_with_popping_cork": "8.0",
+ "wine_glass": "6.0",
+ "cocktail": "6.0",
+ "tropical_drink": "6.0",
+ "beer": "6.0",
+ "beers": "6.0",
+ "champagne_glass": "9.0",
+ "clinking_glass": "9.0",
+ "tumbler_glass": "9.0",
+ "whisky": "9.0",
+ "fork_knife_plate": "7.0",
+ "fork_and_knife_with_plate": "7.0",
+ "fork_and_knife": "6.0",
+ "spoon": "9.0",
+ "knife": "6.0",
+ "amphora": "8.0",
+ "earth_africa": "6.0",
+ "earth_americas": "6.0",
+ "earth_asia": "6.0",
+ "globe_with_meridians": "6.0",
+ "map": "7.0",
+ "world_map": "7.0",
+ "japan": "6.0",
+ "mountain_snow": "7.0",
+ "snow_capped_mountain": "7.0",
+ "mountain": "5.2",
+ "volcano": "6.0",
+ "mount_fuji": "6.0",
+ "camping": "7.0",
+ "beach": "7.0",
+ "beach_with_umbrella": "7.0",
+ "desert": "7.0",
+ "island": "7.0",
+ "desert_island": "7.0",
+ "park": "7.0",
+ "national_park": "7.0",
+ "stadium": "7.0",
+ "classical_building": "7.0",
+ "construction_site": "7.0",
+ "building_construction": "7.0",
+ "homes": "7.0",
+ "house_buildings": "7.0",
+ "cityscape": "7.0",
+ "house_abandoned": "7.0",
+ "derelict_house_building": "7.0",
+ "house": "6.0",
+ "house_with_garden": "6.0",
+ "office": "6.0",
+ "post_office": "6.0",
+ "european_post_office": "6.0",
+ "hospital": "6.0",
+ "bank": "6.0",
+ "hotel": "6.0",
+ "love_hotel": "6.0",
+ "convenience_store": "6.0",
+ "school": "6.0",
+ "department_store": "6.0",
+ "factory": "6.0",
+ "japanese_castle": "6.0",
+ "european_castle": "6.0",
+ "wedding": "6.0",
+ "tokyo_tower": "6.0",
+ "statue_of_liberty": "6.0",
+ "church": "5.2",
+ "mosque": "8.0",
+ "synagogue": "8.0",
+ "shinto_shrine": "5.2",
+ "kaaba": "8.0",
+ "fountain": "5.2",
+ "tent": "5.2",
+ "foggy": "6.0",
+ "night_with_stars": "6.0",
+ "sunrise_over_mountains": "6.0",
+ "sunrise": "6.0",
+ "city_dusk": "6.0",
+ "city_sunset": "6.0",
+ "city_sunrise": "6.0",
+ "bridge_at_night": "6.0",
+ "hotsprings": "1.1",
+ "milky_way": "6.0",
+ "carousel_horse": "6.0",
+ "ferris_wheel": "6.0",
+ "roller_coaster": "6.0",
+ "barber": "6.0",
+ "circus_tent": "6.0",
+ "performing_arts": "6.0",
+ "frame_photo": "7.0",
+ "frame_with_picture": "7.0",
+ "art": "6.0",
+ "slot_machine": "6.0",
+ "steam_locomotive": "6.0",
+ "railway_car": "6.0",
+ "bullettrain_side": "6.0",
+ "bullettrain_front": "6.0",
+ "train2": "6.0",
+ "metro": "6.0",
+ "light_rail": "6.0",
+ "station": "6.0",
+ "tram": "6.0",
+ "monorail": "6.0",
+ "mountain_railway": "6.0",
+ "train": "6.0",
+ "bus": "6.0",
+ "oncoming_bus": "6.0",
+ "trolleybus": "6.0",
+ "minibus": "6.0",
+ "ambulance": "6.0",
+ "fire_engine": "6.0",
+ "police_car": "6.0",
+ "oncoming_police_car": "6.0",
+ "taxi": "6.0",
+ "oncoming_taxi": "6.0",
+ "red_car": "6.0",
+ "oncoming_automobile": "6.0",
+ "blue_car": "6.0",
+ "truck": "6.0",
+ "articulated_lorry": "6.0",
+ "tractor": "6.0",
+ "bike": "6.0",
+ "scooter": "9.0",
+ "motor_scooter": "9.0",
+ "motorbike": "9.0",
+ "busstop": "6.0",
+ "motorway": "7.0",
+ "railway_track": "7.0",
+ "railroad_track": "7.0",
+ "fuelpump": "5.2",
+ "rotating_light": "6.0",
+ "traffic_light": "6.0",
+ "vertical_traffic_light": "6.0",
+ "construction": "6.0",
+ "octagonal_sign": "9.0",
+ "stop_sign": "9.0",
+ "anchor": "4.1",
+ "sailboat": "5.2",
+ "canoe": "9.0",
+ "kayak": "9.0",
+ "speedboat": "6.0",
+ "cruise_ship": "7.0",
+ "passenger_ship": "7.0",
+ "ferry": "5.2",
+ "motorboat": "7.0",
+ "ship": "6.0",
+ "airplane": "1.1",
+ "airplane_small": "7.0",
+ "small_airplane": "7.0",
+ "airplane_departure": "7.0",
+ "airplane_arriving": "7.0",
+ "seat": "6.0",
+ "helicopter": "6.0",
+ "suspension_railway": "6.0",
+ "mountain_cableway": "6.0",
+ "aerial_tramway": "6.0",
+ "rocket": "6.0",
+ "satellite_orbital": "7.0",
+ "bellhop": "7.0",
+ "bellhop_bell": "7.0",
+ "door": "6.0",
+ "sleeping_accommodation": "7.0",
+ "bed": "7.0",
+ "couch": "7.0",
+ "couch_and_lamp": "7.0",
+ "toilet": "6.0",
+ "shower": "6.0",
+ "bath": "6.0",
+ "bath_tone1": "8.0",
+ "bath_tone2": "8.0",
+ "bath_tone3": "8.0",
+ "bath_tone4": "8.0",
+ "bath_tone5": "8.0",
+ "bathtub": "6.0",
+ "hourglass": "1.1",
+ "hourglass_flowing_sand": "6.0",
+ "watch": "1.1",
+ "alarm_clock": "6.0",
+ "stopwatch": "6.0",
+ "timer": "6.0",
+ "timer_clock": "6.0",
+ "clock": "7.0",
+ "mantlepiece_clock": "7.0",
+ "clock12": "6.0",
+ "clock1230": "6.0",
+ "clock1": "6.0",
+ "clock130": "6.0",
+ "clock2": "6.0",
+ "clock230": "6.0",
+ "clock3": "6.0",
+ "clock330": "6.0",
+ "clock4": "6.0",
+ "clock430": "6.0",
+ "clock5": "6.0",
+ "clock530": "6.0",
+ "clock6": "6.0",
+ "clock630": "6.0",
+ "clock7": "6.0",
+ "clock730": "6.0",
+ "clock8": "6.0",
+ "clock830": "6.0",
+ "clock9": "6.0",
+ "clock930": "6.0",
+ "clock10": "6.0",
+ "clock1030": "6.0",
+ "clock11": "6.0",
+ "clock1130": "6.0",
+ "new_moon": "6.0",
+ "waxing_crescent_moon": "6.0",
+ "first_quarter_moon": "6.0",
+ "waxing_gibbous_moon": "6.0",
+ "full_moon": "6.0",
+ "waning_gibbous_moon": "6.0",
+ "last_quarter_moon": "6.0",
+ "waning_crescent_moon": "6.0",
+ "crescent_moon": "6.0",
+ "new_moon_with_face": "6.0",
+ "first_quarter_moon_with_face": "6.0",
+ "last_quarter_moon_with_face": "6.0",
+ "thermometer": "7.0",
+ "sunny": "1.1",
+ "full_moon_with_face": "6.0",
+ "sun_with_face": "6.0",
+ "star": "5.1",
+ "star2": "6.0",
+ "stars": "6.0",
+ "cloud": "1.1",
+ "partly_sunny": "5.2",
+ "thunder_cloud_rain": "5.2",
+ "thunder_cloud_and_rain": "5.2",
+ "white_sun_small_cloud": "7.0",
+ "white_sun_with_small_cloud": "7.0",
+ "white_sun_cloud": "7.0",
+ "white_sun_behind_cloud": "7.0",
+ "white_sun_rain_cloud": "7.0",
+ "white_sun_behind_cloud_with_rain": "7.0",
+ "cloud_rain": "7.0",
+ "cloud_with_rain": "7.0",
+ "cloud_snow": "7.0",
+ "cloud_with_snow": "7.0",
+ "cloud_lightning": "7.0",
+ "cloud_with_lightning": "7.0",
+ "cloud_tornado": "7.0",
+ "cloud_with_tornado": "7.0",
+ "fog": "7.0",
+ "wind_blowing_face": "7.0",
+ "cyclone": "6.0",
+ "rainbow": "6.0",
+ "closed_umbrella": "6.0",
+ "umbrella2": "1.1",
+ "umbrella": "4.0",
+ "beach_umbrella": "5.2",
+ "umbrella_on_ground": "5.2",
+ "zap": "4.0",
+ "snowflake": "1.1",
+ "snowman2": "1.1",
+ "snowman": "5.2",
+ "comet": "1.1",
+ "fire": "6.0",
+ "flame": "6.0",
+ "droplet": "6.0",
+ "ocean": "6.0",
+ "jack_o_lantern": "6.0",
+ "christmas_tree": "6.0",
+ "fireworks": "6.0",
+ "sparkler": "6.0",
+ "sparkles": "6.0",
+ "balloon": "6.0",
+ "tada": "6.0",
+ "confetti_ball": "6.0",
+ "tanabata_tree": "6.0",
+ "bamboo": "6.0",
+ "dolls": "6.0",
+ "flags": "6.0",
+ "wind_chime": "6.0",
+ "rice_scene": "6.0",
+ "ribbon": "6.0",
+ "gift": "6.0",
+ "reminder_ribbon": "7.0",
+ "tickets": "7.0",
+ "admission_tickets": "7.0",
+ "ticket": "6.0",
+ "military_medal": "7.0",
+ "trophy": "6.0",
+ "medal": "7.0",
+ "sports_medal": "7.0",
+ "first_place": "9.0",
+ "first_place_medal": "9.0",
+ "second_place": "9.0",
+ "second_place_medal": "9.0",
+ "third_place": "9.0",
+ "third_place_medal": "9.0",
+ "soccer": "5.2",
+ "baseball": "5.2",
+ "basketball": "6.0",
+ "volleyball": "8.0",
+ "football": "6.0",
+ "rugby_football": "6.0",
+ "tennis": "6.0",
+ "8ball": "6.0",
+ "bowling": "6.0",
+ "cricket": "8.0",
+ "cricket_bat_ball": "8.0",
+ "field_hockey": "8.0",
+ "hockey": "8.0",
+ "ping_pong": "8.0",
+ "table_tennis": "8.0",
+ "badminton": "8.0",
+ "boxing_glove": "9.0",
+ "boxing_gloves": "9.0",
+ "martial_arts_uniform": "9.0",
+ "karate_uniform": "9.0",
+ "goal": "9.0",
+ "goal_net": "9.0",
+ "dart": "6.0",
+ "golf": "5.2",
+ "ice_skate": "5.2",
+ "fishing_pole_and_fish": "6.0",
+ "running_shirt_with_sash": "6.0",
+ "ski": "6.0",
+ "video_game": "6.0",
+ "joystick": "7.0",
+ "game_die": "6.0",
+ "spades": "1.1",
+ "hearts": "1.1",
+ "diamonds": "1.1",
+ "clubs": "1.1",
+ "black_joker": "6.0",
+ "mahjong": "5.1",
+ "flower_playing_cards": "6.0",
+ "mute": "6.0",
+ "speaker": "6.0",
+ "sound": "6.0",
+ "loud_sound": "6.0",
+ "loudspeaker": "6.0",
+ "mega": "6.0",
+ "postal_horn": "6.0",
+ "bell": "6.0",
+ "no_bell": "6.0",
+ "musical_score": "6.0",
+ "musical_note": "6.0",
+ "notes": "6.0",
+ "microphone2": "7.0",
+ "studio_microphone": "7.0",
+ "level_slider": "7.0",
+ "control_knobs": "7.0",
+ "microphone": "6.0",
+ "headphones": "6.0",
+ "radio": "6.0",
+ "saxophone": "6.0",
+ "guitar": "6.0",
+ "musical_keyboard": "6.0",
+ "trumpet": "6.0",
+ "violin": "6.0",
+ "drum": "9.0",
+ "drum_with_drumsticks": "9.0",
+ "iphone": "6.0",
+ "calling": "6.0",
+ "telephone": "1.1",
+ "telephone_receiver": "6.0",
+ "pager": "6.0",
+ "fax": "6.0",
+ "battery": "6.0",
+ "electric_plug": "6.0",
+ "computer": "6.0",
+ "desktop": "7.0",
+ "desktop_computer": "7.0",
+ "printer": "7.0",
+ "keyboard": "1.1",
+ "mouse_three_button": "7.0",
+ "three_button_mouse": "7.0",
+ "trackball": "7.0",
+ "minidisc": "6.0",
+ "floppy_disk": "6.0",
+ "cd": "6.0",
+ "dvd": "6.0",
+ "movie_camera": "6.0",
+ "film_frames": "7.0",
+ "projector": "7.0",
+ "film_projector": "7.0",
+ "clapper": "6.0",
+ "tv": "6.0",
+ "camera": "6.0",
+ "camera_with_flash": "7.0",
+ "video_camera": "6.0",
+ "vhs": "6.0",
+ "mag": "6.0",
+ "mag_right": "6.0",
+ "microscope": "6.0",
+ "telescope": "6.0",
+ "satellite": "6.0",
+ "candle": "7.0",
+ "bulb": "6.0",
+ "flashlight": "6.0",
+ "izakaya_lantern": "6.0",
+ "notebook_with_decorative_cover": "6.0",
+ "closed_book": "6.0",
+ "book": "6.0",
+ "green_book": "6.0",
+ "blue_book": "6.0",
+ "orange_book": "6.0",
+ "books": "6.0",
+ "notebook": "6.0",
+ "ledger": "6.0",
+ "page_with_curl": "6.0",
+ "scroll": "6.0",
+ "page_facing_up": "6.0",
+ "newspaper": "6.0",
+ "newspaper2": "7.0",
+ "rolled_up_newspaper": "7.0",
+ "bookmark_tabs": "6.0",
+ "bookmark": "6.0",
+ "label": "7.0",
+ "moneybag": "6.0",
+ "yen": "6.0",
+ "dollar": "6.0",
+ "euro": "6.0",
+ "pound": "6.0",
+ "money_with_wings": "6.0",
+ "credit_card": "6.0",
+ "chart": "6.0",
+ "currency_exchange": "6.0",
+ "heavy_dollar_sign": "6.0",
+ "envelope": "1.1",
+ "e-mail": "6.0",
+ "email": "6.0",
+ "incoming_envelope": "6.0",
+ "envelope_with_arrow": "6.0",
+ "outbox_tray": "6.0",
+ "inbox_tray": "6.0",
+ "package": "6.0",
+ "mailbox": "6.0",
+ "mailbox_closed": "6.0",
+ "mailbox_with_mail": "6.0",
+ "mailbox_with_no_mail": "6.0",
+ "postbox": "6.0",
+ "ballot_box": "7.0",
+ "ballot_box_with_ballot": "7.0",
+ "pencil2": "1.1",
+ "black_nib": "1.1",
+ "pen_fountain": "7.0",
+ "lower_left_fountain_pen": "7.0",
+ "pen_ballpoint": "7.0",
+ "lower_left_ballpoint_pen": "7.0",
+ "paintbrush": "7.0",
+ "lower_left_paintbrush": "7.0",
+ "crayon": "7.0",
+ "lower_left_crayon": "7.0",
+ "pencil": "6.0",
+ "briefcase": "6.0",
+ "file_folder": "6.0",
+ "open_file_folder": "6.0",
+ "dividers": "7.0",
+ "card_index_dividers": "7.0",
+ "date": "6.0",
+ "calendar": "6.0",
+ "notepad_spiral": "7.0",
+ "spiral_note_pad": "7.0",
+ "calendar_spiral": "7.0",
+ "spiral_calendar_pad": "7.0",
+ "card_index": "6.0",
+ "chart_with_upwards_trend": "6.0",
+ "chart_with_downwards_trend": "6.0",
+ "bar_chart": "6.0",
+ "clipboard": "6.0",
+ "pushpin": "6.0",
+ "round_pushpin": "6.0",
+ "paperclip": "6.0",
+ "paperclips": "7.0",
+ "linked_paperclips": "7.0",
+ "straight_ruler": "6.0",
+ "triangular_ruler": "6.0",
+ "scissors": "1.1",
+ "card_box": "7.0",
+ "card_file_box": "7.0",
+ "file_cabinet": "7.0",
+ "wastebasket": "7.0",
+ "lock": "6.0",
+ "unlock": "6.0",
+ "lock_with_ink_pen": "6.0",
+ "closed_lock_with_key": "6.0",
+ "key": "6.0",
+ "key2": "7.0",
+ "old_key": "7.0",
+ "hammer": "6.0",
+ "pick": "5.2",
+ "hammer_pick": "4.1",
+ "hammer_and_pick": "4.1",
+ "tools": "7.0",
+ "hammer_and_wrench": "7.0",
+ "dagger": "7.0",
+ "dagger_knife": "7.0",
+ "crossed_swords": "4.1",
+ "gun": "6.0",
+ "bow_and_arrow": "8.0",
+ "archery": "8.0",
+ "shield": "7.0",
+ "wrench": "6.0",
+ "nut_and_bolt": "6.0",
+ "gear": "4.1",
+ "compression": "7.0",
+ "alembic": "4.1",
+ "scales": "4.1",
+ "link": "6.0",
+ "chains": "5.2",
+ "syringe": "6.0",
+ "pill": "6.0",
+ "smoking": "6.0",
+ "coffin": "4.1",
+ "urn": "4.1",
+ "funeral_urn": "4.1",
+ "moyai": "6.0",
+ "oil": "7.0",
+ "oil_drum": "7.0",
+ "crystal_ball": "6.0",
+ "shopping_cart": "9.0",
+ "shopping_trolley": "9.0",
+ "atm": "6.0",
+ "put_litter_in_its_place": "6.0",
+ "potable_water": "6.0",
+ "wheelchair": "4.1",
+ "mens": "6.0",
+ "womens": "6.0",
+ "restroom": "6.0",
+ "baby_symbol": "6.0",
+ "wc": "6.0",
+ "passport_control": "6.0",
+ "customs": "6.0",
+ "baggage_claim": "6.0",
+ "left_luggage": "6.0",
+ "warning": "4.0",
+ "children_crossing": "6.0",
+ "no_entry": "5.2",
+ "no_entry_sign": "6.0",
+ "no_bicycles": "6.0",
+ "no_smoking": "6.0",
+ "do_not_litter": "6.0",
+ "non-potable_water": "6.0",
+ "no_pedestrians": "6.0",
+ "no_mobile_phones": "6.0",
+ "underage": "6.0",
+ "radioactive": "1.1",
+ "radioactive_sign": "1.1",
+ "biohazard": "1.1",
+ "biohazard_sign": "1.1",
+ "arrow_up": "4.0",
+ "arrow_upper_right": "1.1",
+ "arrow_right": "1.1",
+ "arrow_lower_right": "1.1",
+ "arrow_down": "4.0",
+ "arrow_lower_left": "1.1",
+ "arrow_left": "4.0",
+ "arrow_upper_left": "1.1",
+ "arrow_up_down": "1.1",
+ "left_right_arrow": "1.1",
+ "leftwards_arrow_with_hook": "1.1",
+ "arrow_right_hook": "1.1",
+ "arrow_heading_up": "3.2",
+ "arrow_heading_down": "3.2",
+ "arrows_clockwise": "6.0",
+ "arrows_counterclockwise": "6.0",
+ "back": "6.0",
+ "end": "6.0",
+ "on": "6.0",
+ "soon": "6.0",
+ "top": "6.0",
+ "place_of_worship": "8.0",
+ "worship_symbol": "8.0",
+ "atom": "4.1",
+ "atom_symbol": "4.1",
+ "om_symbol": "7.0",
+ "star_of_david": "1.1",
+ "wheel_of_dharma": "1.1",
+ "yin_yang": "1.1",
+ "cross": "1.1",
+ "latin_cross": "1.1",
+ "orthodox_cross": "1.1",
+ "star_and_crescent": "1.1",
+ "peace": "1.1",
+ "peace_symbol": "1.1",
+ "menorah": "8.0",
+ "six_pointed_star": "6.0",
+ "aries": "1.1",
+ "taurus": "1.1",
+ "gemini": "1.1",
+ "cancer": "1.1",
+ "leo": "1.1",
+ "virgo": "1.1",
+ "libra": "1.1",
+ "scorpius": "1.1",
+ "sagittarius": "1.1",
+ "capricorn": "1.1",
+ "aquarius": "1.1",
+ "pisces": "1.1",
+ "ophiuchus": "6.0",
+ "twisted_rightwards_arrows": "6.0",
+ "repeat": "6.0",
+ "repeat_one": "6.0",
+ "arrow_forward": "1.1",
+ "fast_forward": "6.0",
+ "track_next": "6.0",
+ "next_track": "6.0",
+ "play_pause": "6.0",
+ "arrow_backward": "1.1",
+ "rewind": "6.0",
+ "track_previous": "6.0",
+ "previous_track": "6.0",
+ "arrow_up_small": "6.0",
+ "arrow_double_up": "6.0",
+ "arrow_down_small": "6.0",
+ "arrow_double_down": "6.0",
+ "pause_button": "7.0",
+ "double_vertical_bar": "7.0",
+ "stop_button": "7.0",
+ "record_button": "7.0",
+ "eject": "4.0",
+ "eject_symbol": "4.0",
+ "cinema": "6.0",
+ "low_brightness": "6.0",
+ "high_brightness": "6.0",
+ "signal_strength": "6.0",
+ "vibration_mode": "6.0",
+ "mobile_phone_off": "6.0",
+ "recycle": "3.2",
+ "name_badge": "6.0",
+ "fleur-de-lis": "4.1",
+ "beginner": "6.0",
+ "trident": "6.0",
+ "o": "5.2",
+ "white_check_mark": "6.0",
+ "ballot_box_with_check": "1.1",
+ "heavy_check_mark": "1.1",
+ "heavy_multiplication_x": "1.1",
+ "x": "6.0",
+ "negative_squared_cross_mark": "6.0",
+ "heavy_plus_sign": "6.0",
+ "heavy_minus_sign": "6.0",
+ "heavy_division_sign": "6.0",
+ "curly_loop": "6.0",
+ "loop": "6.0",
+ "part_alternation_mark": "3.2",
+ "eight_spoked_asterisk": "1.1",
+ "eight_pointed_black_star": "1.1",
+ "sparkle": "1.1",
+ "bangbang": "1.1",
+ "interrobang": "3.0",
+ "question": "6.0",
+ "grey_question": "6.0",
+ "grey_exclamation": "6.0",
+ "exclamation": "5.2",
+ "wavy_dash": "1.1",
+ "copyright": "1.1",
+ "registered": "1.1",
+ "tm": "1.1",
+ "hash": "3.0",
+ "asterisk": "3.0",
+ "keycap_asterisk": "3.0",
+ "zero": "3.0",
+ "one": "3.0",
+ "two": "3.0",
+ "three": "3.0",
+ "four": "3.0",
+ "five": "3.0",
+ "six": "3.0",
+ "seven": "3.0",
+ "eight": "3.0",
+ "nine": "3.0",
+ "keycap_ten": "6.0",
+ "capital_abcd": "6.0",
+ "abcd": "6.0",
+ "symbols": "6.0",
+ "abc": "6.0",
+ "a": "6.0",
+ "ab": "6.0",
+ "b": "6.0",
+ "cl": "6.0",
+ "cool": "6.0",
+ "free": "6.0",
+ "information_source": "3.0",
+ "id": "6.0",
+ "m": "1.1",
+ "new": "6.0",
+ "ng": "6.0",
+ "o2": "6.0",
+ "ok": "6.0",
+ "parking": "5.2",
+ "sos": "6.0",
+ "up": "6.0",
+ "vs": "6.0",
+ "koko": "6.0",
+ "sa": "6.0",
+ "u6708": "6.0",
+ "u6709": "6.0",
+ "u6307": "5.2",
+ "ideograph_advantage": "6.0",
+ "u5272": "6.0",
+ "u7121": "5.2",
+ "u7981": "6.0",
+ "accept": "6.0",
+ "u7533": "6.0",
+ "u5408": "6.0",
+ "u7a7a": "6.0",
+ "congratulations": "1.1",
+ "secret": "1.1",
+ "u55b6": "6.0",
+ "u6e80": "6.0",
+ "black_small_square": "1.1",
+ "white_small_square": "1.1",
+ "white_medium_square": "3.2",
+ "black_medium_square": "3.2",
+ "white_medium_small_square": "3.2",
+ "black_medium_small_square": "3.2",
+ "black_large_square": "5.1",
+ "white_large_square": "5.1",
+ "large_orange_diamond": "6.0",
+ "large_blue_diamond": "6.0",
+ "small_orange_diamond": "6.0",
+ "small_blue_diamond": "6.0",
+ "small_red_triangle": "6.0",
+ "small_red_triangle_down": "6.0",
+ "diamond_shape_with_a_dot_inside": "6.0",
+ "radio_button": "6.0",
+ "black_square_button": "6.0",
+ "white_square_button": "6.0",
+ "white_circle": "4.1",
+ "black_circle": "4.1",
+ "red_circle": "6.0",
+ "blue_circle": "6.0",
+ "checkered_flag": "6.0",
+ "triangular_flag_on_post": "6.0",
+ "crossed_flags": "6.0",
+ "flag_black": "6.0",
+ "waving_black_flag": "6.0",
+ "flag_white": "6.0",
+ "waving_white_flag": "6.0",
+ "rainbow_flag": "6.0",
+ "gay_pride_flag": "6.0",
+ "flag_ac": "6.0",
+ "ac": "6.0",
+ "flag_ad": "6.0",
+ "ad": "6.0",
+ "flag_ae": "6.0",
+ "ae": "6.0",
+ "flag_af": "6.0",
+ "af": "6.0",
+ "flag_ag": "6.0",
+ "ag": "6.0",
+ "flag_ai": "6.0",
+ "ai": "6.0",
+ "flag_al": "6.0",
+ "al": "6.0",
+ "flag_am": "6.0",
+ "am": "6.0",
+ "flag_ao": "6.0",
+ "ao": "6.0",
+ "flag_aq": "6.0",
+ "aq": "6.0",
+ "flag_ar": "6.0",
+ "ar": "6.0",
+ "flag_as": "6.0",
+ "as": "6.0",
+ "flag_at": "6.0",
+ "at": "6.0",
+ "flag_au": "6.0",
+ "au": "6.0",
+ "flag_aw": "6.0",
+ "aw": "6.0",
+ "flag_ax": "6.0",
+ "ax": "6.0",
+ "flag_az": "6.0",
+ "az": "6.0",
+ "flag_ba": "6.0",
+ "ba": "6.0",
+ "flag_bb": "6.0",
+ "bb": "6.0",
+ "flag_bd": "6.0",
+ "bd": "6.0",
+ "flag_be": "6.0",
+ "be": "6.0",
+ "flag_bf": "6.0",
+ "bf": "6.0",
+ "flag_bg": "6.0",
+ "bg": "6.0",
+ "flag_bh": "6.0",
+ "bh": "6.0",
+ "flag_bi": "6.0",
+ "bi": "6.0",
+ "flag_bj": "6.0",
+ "bj": "6.0",
+ "flag_bl": "6.0",
+ "bl": "6.0",
+ "flag_bm": "6.0",
+ "bm": "6.0",
+ "flag_bn": "6.0",
+ "bn": "6.0",
+ "flag_bo": "6.0",
+ "bo": "6.0",
+ "flag_bq": "6.0",
+ "bq": "6.0",
+ "flag_br": "6.0",
+ "br": "6.0",
+ "flag_bs": "6.0",
+ "bs": "6.0",
+ "flag_bt": "6.0",
+ "bt": "6.0",
+ "flag_bv": "6.0",
+ "bv": "6.0",
+ "flag_bw": "6.0",
+ "bw": "6.0",
+ "flag_by": "6.0",
+ "by": "6.0",
+ "flag_bz": "6.0",
+ "bz": "6.0",
+ "flag_ca": "6.0",
+ "ca": "6.0",
+ "flag_cc": "6.0",
+ "cc": "6.0",
+ "flag_cd": "6.0",
+ "congo": "6.0",
+ "flag_cf": "6.0",
+ "cf": "6.0",
+ "flag_cg": "6.0",
+ "cg": "6.0",
+ "flag_ch": "6.0",
+ "ch": "6.0",
+ "flag_ci": "6.0",
+ "ci": "6.0",
+ "flag_ck": "6.0",
+ "ck": "6.0",
+ "flag_cl": "6.0",
+ "chile": "6.0",
+ "flag_cm": "6.0",
+ "cm": "6.0",
+ "flag_cn": "6.0",
+ "cn": "6.0",
+ "flag_co": "6.0",
+ "co": "6.0",
+ "flag_cp": "6.0",
+ "cp": "6.0",
+ "flag_cr": "6.0",
+ "cr": "6.0",
+ "flag_cu": "6.0",
+ "cu": "6.0",
+ "flag_cv": "6.0",
+ "cv": "6.0",
+ "flag_cw": "6.0",
+ "cw": "6.0",
+ "flag_cx": "6.0",
+ "cx": "6.0",
+ "flag_cy": "6.0",
+ "cy": "6.0",
+ "flag_cz": "6.0",
+ "cz": "6.0",
+ "flag_de": "6.0",
+ "de": "6.0",
+ "flag_dg": "6.0",
+ "dg": "6.0",
+ "flag_dj": "6.0",
+ "dj": "6.0",
+ "flag_dk": "6.0",
+ "dk": "6.0",
+ "flag_dm": "6.0",
+ "dm": "6.0",
+ "flag_do": "6.0",
+ "do": "6.0",
+ "flag_dz": "6.0",
+ "dz": "6.0",
+ "flag_ea": "6.0",
+ "ea": "6.0",
+ "flag_ec": "6.0",
+ "ec": "6.0",
+ "flag_ee": "6.0",
+ "ee": "6.0",
+ "flag_eg": "6.0",
+ "eg": "6.0",
+ "flag_eh": "6.0",
+ "eh": "6.0",
+ "flag_er": "6.0",
+ "er": "6.0",
+ "flag_es": "6.0",
+ "es": "6.0",
+ "flag_et": "6.0",
+ "et": "6.0",
+ "flag_eu": "6.0",
+ "eu": "6.0",
+ "flag_fi": "6.0",
+ "fi": "6.0",
+ "flag_fj": "6.0",
+ "fj": "6.0",
+ "flag_fk": "6.0",
+ "fk": "6.0",
+ "flag_fm": "6.0",
+ "fm": "6.0",
+ "flag_fo": "6.0",
+ "fo": "6.0",
+ "flag_fr": "6.0",
+ "fr": "6.0",
+ "flag_ga": "6.0",
+ "ga": "6.0",
+ "flag_gb": "6.0",
+ "gb": "6.0",
+ "flag_gd": "6.0",
+ "gd": "6.0",
+ "flag_ge": "6.0",
+ "ge": "6.0",
+ "flag_gf": "6.0",
+ "gf": "6.0",
+ "flag_gg": "6.0",
+ "gg": "6.0",
+ "flag_gh": "6.0",
+ "gh": "6.0",
+ "flag_gi": "6.0",
+ "gi": "6.0",
+ "flag_gl": "6.0",
+ "gl": "6.0",
+ "flag_gm": "6.0",
+ "gm": "6.0",
+ "flag_gn": "6.0",
+ "gn": "6.0",
+ "flag_gp": "6.0",
+ "gp": "6.0",
+ "flag_gq": "6.0",
+ "gq": "6.0",
+ "flag_gr": "6.0",
+ "gr": "6.0",
+ "flag_gs": "6.0",
+ "gs": "6.0",
+ "flag_gt": "6.0",
+ "gt": "6.0",
+ "flag_gu": "6.0",
+ "gu": "6.0",
+ "flag_gw": "6.0",
+ "gw": "6.0",
+ "flag_gy": "6.0",
+ "gy": "6.0",
+ "flag_hk": "6.0",
+ "hk": "6.0",
+ "flag_hm": "6.0",
+ "hm": "6.0",
+ "flag_hn": "6.0",
+ "hn": "6.0",
+ "flag_hr": "6.0",
+ "hr": "6.0",
+ "flag_ht": "6.0",
+ "ht": "6.0",
+ "flag_hu": "6.0",
+ "hu": "6.0",
+ "flag_ic": "6.0",
+ "ic": "6.0",
+ "flag_id": "6.0",
+ "indonesia": "6.0",
+ "flag_ie": "6.0",
+ "ie": "6.0",
+ "flag_il": "6.0",
+ "il": "6.0",
+ "flag_im": "6.0",
+ "im": "6.0",
+ "flag_in": "6.0",
+ "in": "6.0",
+ "flag_io": "6.0",
+ "io": "6.0",
+ "flag_iq": "6.0",
+ "iq": "6.0",
+ "flag_ir": "6.0",
+ "ir": "6.0",
+ "flag_is": "6.0",
+ "is": "6.0",
+ "flag_it": "6.0",
+ "it": "6.0",
+ "flag_je": "6.0",
+ "je": "6.0",
+ "flag_jm": "6.0",
+ "jm": "6.0",
+ "flag_jo": "6.0",
+ "jo": "6.0",
+ "flag_jp": "6.0",
+ "jp": "6.0",
+ "flag_ke": "6.0",
+ "ke": "6.0",
+ "flag_kg": "6.0",
+ "kg": "6.0",
+ "flag_kh": "6.0",
+ "kh": "6.0",
+ "flag_ki": "6.0",
+ "ki": "6.0",
+ "flag_km": "6.0",
+ "km": "6.0",
+ "flag_kn": "6.0",
+ "kn": "6.0",
+ "flag_kp": "6.0",
+ "kp": "6.0",
+ "flag_kr": "6.0",
+ "kr": "6.0",
+ "flag_kw": "6.0",
+ "kw": "6.0",
+ "flag_ky": "6.0",
+ "ky": "6.0",
+ "flag_kz": "6.0",
+ "kz": "6.0",
+ "flag_la": "6.0",
+ "la": "6.0",
+ "flag_lb": "6.0",
+ "lb": "6.0",
+ "flag_lc": "6.0",
+ "lc": "6.0",
+ "flag_li": "6.0",
+ "li": "6.0",
+ "flag_lk": "6.0",
+ "lk": "6.0",
+ "flag_lr": "6.0",
+ "lr": "6.0",
+ "flag_ls": "6.0",
+ "ls": "6.0",
+ "flag_lt": "6.0",
+ "lt": "6.0",
+ "flag_lu": "6.0",
+ "lu": "6.0",
+ "flag_lv": "6.0",
+ "lv": "6.0",
+ "flag_ly": "6.0",
+ "ly": "6.0",
+ "flag_ma": "6.0",
+ "ma": "6.0",
+ "flag_mc": "6.0",
+ "mc": "6.0",
+ "flag_md": "6.0",
+ "md": "6.0",
+ "flag_me": "6.0",
+ "me": "6.0",
+ "flag_mf": "6.0",
+ "mf": "6.0",
+ "flag_mg": "6.0",
+ "mg": "6.0",
+ "flag_mh": "6.0",
+ "mh": "6.0",
+ "flag_mk": "6.0",
+ "mk": "6.0",
+ "flag_ml": "6.0",
+ "ml": "6.0",
+ "flag_mm": "6.0",
+ "mm": "6.0",
+ "flag_mn": "6.0",
+ "mn": "6.0",
+ "flag_mo": "6.0",
+ "mo": "6.0",
+ "flag_mp": "6.0",
+ "mp": "6.0",
+ "flag_mq": "6.0",
+ "mq": "6.0",
+ "flag_mr": "6.0",
+ "mr": "6.0",
+ "flag_ms": "6.0",
+ "ms": "6.0",
+ "flag_mt": "6.0",
+ "mt": "6.0",
+ "flag_mu": "6.0",
+ "mu": "6.0",
+ "flag_mv": "6.0",
+ "mv": "6.0",
+ "flag_mw": "6.0",
+ "mw": "6.0",
+ "flag_mx": "6.0",
+ "mx": "6.0",
+ "flag_my": "6.0",
+ "my": "6.0",
+ "flag_mz": "6.0",
+ "mz": "6.0",
+ "flag_na": "6.0",
+ "na": "6.0",
+ "flag_nc": "6.0",
+ "nc": "6.0",
+ "flag_ne": "6.0",
+ "ne": "6.0",
+ "flag_nf": "6.0",
+ "nf": "6.0",
+ "flag_ng": "6.0",
+ "nigeria": "6.0",
+ "flag_ni": "6.0",
+ "ni": "6.0",
+ "flag_nl": "6.0",
+ "nl": "6.0",
+ "flag_no": "6.0",
+ "no": "6.0",
+ "flag_np": "6.0",
+ "np": "6.0",
+ "flag_nr": "6.0",
+ "nr": "6.0",
+ "flag_nu": "6.0",
+ "nu": "6.0",
+ "flag_nz": "6.0",
+ "nz": "6.0",
+ "flag_om": "6.0",
+ "om": "6.0",
+ "flag_pa": "6.0",
+ "pa": "6.0",
+ "flag_pe": "6.0",
+ "pe": "6.0",
+ "flag_pf": "6.0",
+ "pf": "6.0",
+ "flag_pg": "6.0",
+ "pg": "6.0",
+ "flag_ph": "6.0",
+ "ph": "6.0",
+ "flag_pk": "6.0",
+ "pk": "6.0",
+ "flag_pl": "6.0",
+ "pl": "6.0",
+ "flag_pm": "6.0",
+ "pm": "6.0",
+ "flag_pn": "6.0",
+ "pn": "6.0",
+ "flag_pr": "6.0",
+ "pr": "6.0",
+ "flag_ps": "6.0",
+ "ps": "6.0",
+ "flag_pt": "6.0",
+ "pt": "6.0",
+ "flag_pw": "6.0",
+ "pw": "6.0",
+ "flag_py": "6.0",
+ "py": "6.0",
+ "flag_qa": "6.0",
+ "qa": "6.0",
+ "flag_re": "6.0",
+ "re": "6.0",
+ "flag_ro": "6.0",
+ "ro": "6.0",
+ "flag_rs": "6.0",
+ "rs": "6.0",
+ "flag_ru": "6.0",
+ "ru": "6.0",
+ "flag_rw": "6.0",
+ "rw": "6.0",
+ "flag_sa": "6.0",
+ "saudiarabia": "6.0",
+ "saudi": "6.0",
+ "flag_sb": "6.0",
+ "sb": "6.0",
+ "flag_sc": "6.0",
+ "sc": "6.0",
+ "flag_sd": "6.0",
+ "sd": "6.0",
+ "flag_se": "6.0",
+ "se": "6.0",
+ "flag_sg": "6.0",
+ "sg": "6.0",
+ "flag_sh": "6.0",
+ "sh": "6.0",
+ "flag_si": "6.0",
+ "si": "6.0",
+ "flag_sj": "6.0",
+ "sj": "6.0",
+ "flag_sk": "6.0",
+ "sk": "6.0",
+ "flag_sl": "6.0",
+ "sl": "6.0",
+ "flag_sm": "6.0",
+ "sm": "6.0",
+ "flag_sn": "6.0",
+ "sn": "6.0",
+ "flag_so": "6.0",
+ "so": "6.0",
+ "flag_sr": "6.0",
+ "sr": "6.0",
+ "flag_ss": "6.0",
+ "ss": "6.0",
+ "flag_st": "6.0",
+ "st": "6.0",
+ "flag_sv": "6.0",
+ "sv": "6.0",
+ "flag_sx": "6.0",
+ "sx": "6.0",
+ "flag_sy": "6.0",
+ "sy": "6.0",
+ "flag_sz": "6.0",
+ "sz": "6.0",
+ "flag_ta": "6.0",
+ "ta": "6.0",
+ "flag_tc": "6.0",
+ "tc": "6.0",
+ "flag_td": "6.0",
+ "td": "6.0",
+ "flag_tf": "6.0",
+ "tf": "6.0",
+ "flag_tg": "6.0",
+ "tg": "6.0",
+ "flag_th": "6.0",
+ "th": "6.0",
+ "flag_tj": "6.0",
+ "tj": "6.0",
+ "flag_tk": "6.0",
+ "tk": "6.0",
+ "flag_tl": "6.0",
+ "tl": "6.0",
+ "flag_tm": "6.0",
+ "turkmenistan": "6.0",
+ "flag_tn": "6.0",
+ "tn": "6.0",
+ "flag_to": "6.0",
+ "to": "6.0",
+ "flag_tr": "6.0",
+ "tr": "6.0",
+ "flag_tt": "6.0",
+ "tt": "6.0",
+ "flag_tv": "6.0",
+ "tuvalu": "6.0",
+ "flag_tw": "6.0",
+ "tw": "6.0",
+ "flag_tz": "6.0",
+ "tz": "6.0",
+ "flag_ua": "6.0",
+ "ua": "6.0",
+ "flag_ug": "6.0",
+ "ug": "6.0",
+ "flag_um": "6.0",
+ "um": "6.0",
+ "flag_us": "6.0",
+ "us": "6.0",
+ "flag_uy": "6.0",
+ "uy": "6.0",
+ "flag_uz": "6.0",
+ "uz": "6.0",
+ "flag_va": "6.0",
+ "va": "6.0",
+ "flag_vc": "6.0",
+ "vc": "6.0",
+ "flag_ve": "6.0",
+ "ve": "6.0",
+ "flag_vg": "6.0",
+ "vg": "6.0",
+ "flag_vi": "6.0",
+ "vi": "6.0",
+ "flag_vn": "6.0",
+ "vn": "6.0",
+ "flag_vu": "6.0",
+ "vu": "6.0",
+ "flag_wf": "6.0",
+ "wf": "6.0",
+ "flag_ws": "6.0",
+ "ws": "6.0",
+ "flag_xk": "6.0",
+ "xk": "6.0",
+ "flag_ye": "6.0",
+ "ye": "6.0",
+ "flag_yt": "6.0",
+ "yt": "6.0",
+ "flag_za": "6.0",
+ "za": "6.0",
+ "flag_zm": "6.0",
+ "zm": "6.0",
+ "flag_zw": "6.0",
+ "zw": "6.0",
+ "regional_indicator_z": "6.0",
+ "regional_indicator_y": "6.0",
+ "regional_indicator_x": "6.0",
+ "regional_indicator_w": "6.0",
+ "regional_indicator_v": "6.0",
+ "regional_indicator_u": "6.0",
+ "regional_indicator_t": "6.0",
+ "regional_indicator_s": "6.0",
+ "regional_indicator_r": "6.0",
+ "regional_indicator_q": "6.0",
+ "regional_indicator_p": "6.0",
+ "regional_indicator_o": "6.0",
+ "regional_indicator_n": "6.0",
+ "regional_indicator_m": "6.0",
+ "regional_indicator_l": "6.0",
+ "regional_indicator_k": "6.0",
+ "regional_indicator_j": "6.0",
+ "regional_indicator_i": "6.0",
+ "regional_indicator_h": "6.0",
+ "regional_indicator_g": "6.0",
+ "regional_indicator_f": "6.0",
+ "regional_indicator_e": "6.0",
+ "regional_indicator_d": "6.0",
+ "regional_indicator_c": "6.0",
+ "regional_indicator_b": "6.0",
+ "regional_indicator_a": "6.0",
+ "large_blue_circle": "6.0",
+ "ten": "6.0"
+} \ No newline at end of file
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index b0aa10f8bf2..42401abfe0f 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -18,22 +18,34 @@ module API
optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
optional :since, type: DateTime, desc: 'Only commits after or on this date will be returned'
optional :until, type: DateTime, desc: 'Only commits before or on this date will be returned'
- optional :page, type: Integer, default: 0, desc: 'The page for pagination'
- optional :per_page, type: Integer, default: 20, desc: 'The number of results per page'
optional :path, type: String, desc: 'The file path'
+ use :pagination
end
get ":id/repository/commits" do
- ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
- offset = params[:page] * params[:per_page]
+ path = params[:path]
+ before = params[:until]
+ after = params[:since]
+ ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
+ offset = (params[:page] - 1) * params[:per_page]
commits = user_project.repository.commits(ref,
- path: params[:path],
+ path: path,
limit: params[:per_page],
offset: offset,
- after: params[:since],
- before: params[:until])
+ before: before,
+ after: after)
+
+ commit_count =
+ if path || before || after
+ user_project.repository.count_commits(ref: ref, path: path, before: before, after: after)
+ else
+ # Cacheable commit count.
+ user_project.repository.commit_count_for_ref(ref)
+ end
+
+ paginated_commits = Kaminari.paginate_array(commits, total_count: commit_count)
- present commits, with: Entities::RepoCommit
+ present paginate(paginated_commits), with: Entities::RepoCommit
end
desc 'Commit multiple file changes as one commit' do
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 9c4e43d77cc..bb8f5c3076d 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -14,6 +14,19 @@ module API
}
end
+ def assign_file_vars!
+ authorize! :download_code, user_project
+
+ @commit = user_project.commit(params[:ref])
+ not_found!('Commit') unless @commit
+
+ @repo = user_project.repository
+ @blob = @repo.blob_at(@commit.sha, params[:file_path])
+
+ not_found!('File') unless @blob
+ @blob.load_all_data!(@repo)
+ end
+
def commit_response(attrs)
{
file_path: attrs[:file_path],
@@ -22,7 +35,7 @@ module API
end
params :simple_file_params do
- requires :file_path, type: String, desc: 'The path to new file. Ex. lib/class.rb'
+ requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
requires :branch, type: String, desc: 'The name of branch'
requires :commit_message, type: String, desc: 'Commit Message'
optional :author_email, type: String, desc: 'The email of the author'
@@ -40,34 +53,35 @@ module API
requires :id, type: String, desc: 'The project ID'
end
resource :projects do
- desc 'Get a file from repository'
+ desc 'Get raw file contents from the repository'
params do
- requires :file_path, type: String, desc: 'The path to the file. Ex. lib/class.rb'
- requires :ref, type: String, desc: 'The name of branch, tag, or commit'
+ requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
+ requires :ref, type: String, desc: 'The name of branch, tag commit'
end
- get ":id/repository/files" do
- authorize! :download_code, user_project
-
- commit = user_project.commit(params[:ref])
- not_found!('Commit') unless commit
+ get ":id/repository/files/:file_path/raw" do
+ assign_file_vars!
- repo = user_project.repository
- blob = repo.blob_at(commit.sha, params[:file_path])
- not_found!('File') unless blob
+ send_git_blob @repo, @blob
+ end
- blob.load_all_data!(repo)
- status(200)
+ desc 'Get a file from the repository'
+ params do
+ requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
+ requires :ref, type: String, desc: 'The name of branch, tag or commit'
+ end
+ get ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do
+ assign_file_vars!
{
- file_name: blob.name,
- file_path: blob.path,
- size: blob.size,
+ file_name: @blob.name,
+ file_path: @blob.path,
+ size: @blob.size,
encoding: "base64",
- content: Base64.strict_encode64(blob.data),
+ content: Base64.strict_encode64(@blob.data),
ref: params[:ref],
- blob_id: blob.id,
- commit_id: commit.id,
- last_commit_id: repo.last_commit_id_for_path(commit.sha, params[:file_path])
+ blob_id: @blob.id,
+ commit_id: @commit.id,
+ last_commit_id: @repo.last_commit_id_for_path(@commit.sha, params[:file_path])
}
end
@@ -75,7 +89,7 @@ module API
params do
use :extended_file_params
end
- post ":id/repository/files" do
+ post ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do
authorize! :push_code, user_project
file_params = declared_params(include_missing: false)
@@ -93,7 +107,7 @@ module API
params do
use :extended_file_params
end
- put ":id/repository/files" do
+ put ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do
authorize! :push_code, user_project
file_params = declared_params(include_missing: false)
@@ -112,7 +126,7 @@ module API
params do
use :simple_file_params
end
- delete ":id/repository/files" do
+ delete ":id/repository/files/:file_path", requirements: { file_path: /.+/ } do
authorize! :push_code, user_project
file_params = declared_params(include_missing: false)
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index 080a6274957..2135a787b11 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -9,11 +9,11 @@ module API
# In addition, they may have a '.git' extension and multiple namespaces
#
# Transform all these cases to 'namespace/project'
- def clean_project_path(project_path, storage_paths = Repository.storages.values)
+ def clean_project_path(project_path, storages = Gitlab.config.repositories.storages.values)
project_path = project_path.sub(/\.git\z/, '')
- storage_paths.each do |storage_path|
- storage_path = File.expand_path(storage_path)
+ storages.each do |storage|
+ storage_path = File.expand_path(storage['path'])
if project_path.start_with?(storage_path)
project_path = project_path.sub(storage_path, '')
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 33c05e8aa63..44118522abe 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -18,6 +18,8 @@ module API
[scope]
when Hashie::Mash
scope.values
+ when Hashie::Array
+ scope
else
['unknown']
end
@@ -36,8 +38,23 @@ module API
builds = user_project.builds.order('id DESC')
builds = filter_builds(builds, params[:scope])
- present paginate(builds), with: Entities::Job,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ present paginate(builds), with: Entities::Job
+ end
+
+ desc 'Get pipeline jobs' do
+ success Entities::Job
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ use :optional_scope
+ use :pagination
+ end
+ get ':id/pipelines/:pipeline_id/jobs' do
+ pipeline = user_project.pipelines.find(params[:pipeline_id])
+ builds = pipeline.builds
+ builds = filter_builds(builds, params[:scope])
+
+ present paginate(builds), with: Entities::Job
end
desc 'Get a specific job of a project' do
@@ -51,8 +68,7 @@ module API
build = get_build!(params[:job_id])
- present build, with: Entities::Job,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ present build, with: Entities::Job
end
desc 'Download the artifacts file from a job' do
@@ -119,8 +135,7 @@ module API
build.cancel
- present build, with: Entities::Job,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ present build, with: Entities::Job
end
desc 'Retry a specific build of a project' do
@@ -137,8 +152,7 @@ module API
build = Ci::Build.retry(build, current_user)
- present build, with: Entities::Job,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ present build, with: Entities::Job
end
desc 'Erase job (remove artifacts and the trace)' do
@@ -154,8 +168,7 @@ module API
return forbidden!('Job is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
- present build, with: Entities::Job,
- user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
+ present build, with: Entities::Job
end
desc 'Keep the artifacts to prevent them from being deleted' do
@@ -173,8 +186,7 @@ module API
build.keep_artifacts!
status 200
- present build, with: Entities::Job,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ present build, with: Entities::Job
end
desc 'Trigger a manual job' do
@@ -194,8 +206,7 @@ module API
build.play(current_user)
status 200
- present build, with: Entities::Job,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ present build, with: Entities::Job
end
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 36166780149..531ef5a63ea 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -17,19 +17,34 @@ module API
end
not_found!
end
+
+ def assign_blob_vars!
+ authorize! :download_code, user_project
+
+ @repo = user_project.repository
+
+ begin
+ @blob = Gitlab::Git::Blob.raw(@repo, params[:sha])
+ @blob.load_all_data!(@repo)
+ rescue
+ not_found! 'Blob'
+ end
+
+ not_found! 'Blob' unless @blob
+ end
end
desc 'Get a project repository tree' do
success Entities::RepoTreeObject
end
params do
- optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
+ optional :ref, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used'
optional :path, type: String, desc: 'The path of the tree'
optional :recursive, type: Boolean, default: false, desc: 'Used to get a recursive tree'
use :pagination
end
get ':id/repository/tree' do
- ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
+ ref = params[:ref] || user_project.try(:default_branch) || 'master'
path = params[:path] || nil
commit = user_project.commit(ref)
@@ -40,39 +55,29 @@ module API
present paginate(entries), with: Entities::RepoTreeObject
end
- desc 'Get a raw file contents'
+ desc 'Get raw blob contents from the repository'
params do
requires :sha, type: String, desc: 'The commit, branch name, or tag name'
- requires :filepath, type: String, desc: 'The path to the file to display'
end
- get [":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob"] do
- repo = user_project.repository
-
- commit = repo.commit(params[:sha])
- not_found! "Commit" unless commit
+ get ':id/repository/blobs/:sha/raw' do
+ assign_blob_vars!
- blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath])
- not_found! "File" unless blob
-
- send_git_blob repo, blob
+ send_git_blob @repo, @blob
end
- desc 'Get a raw blob contents by blob sha'
+ desc 'Get a blob from the repository'
params do
requires :sha, type: String, desc: 'The commit, branch name, or tag name'
end
- get ':id/repository/raw_blobs/:sha' do
- repo = user_project.repository
-
- begin
- blob = Gitlab::Git::Blob.raw(repo, params[:sha])
- rescue
- not_found! 'Blob'
- end
-
- not_found! 'Blob' unless blob
+ get ':id/repository/blobs/:sha' do
+ assign_blob_vars!
- send_git_blob repo, blob
+ {
+ size: @blob.size,
+ encoding: "base64",
+ content: Base64.strict_encode64(@blob.data),
+ sha: @blob.id
+ }
end
desc 'Get an archive of the repository'
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 1cf29d9a1a3..5aa2f5eba7b 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -422,6 +422,14 @@ module API
desc: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.'
}
],
+ 'prometheus' => [
+ {
+ required: true,
+ name: :api_url,
+ type: String,
+ desc: 'Prometheus API Base URL, like http://prometheus.example.com/'
+ }
+ ],
'pushover' => [
{
required: true,
@@ -558,6 +566,7 @@ module API
SlackSlashCommandsService,
PipelinesEmailService,
PivotaltrackerService,
+ PrometheusService,
PushoverService,
RedmineService,
SlackService,
diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb
index c8feba13527..6f97102c6ef 100644
--- a/lib/api/v3/builds.rb
+++ b/lib/api/v3/builds.rb
@@ -36,8 +36,7 @@ module API
builds = user_project.builds.order('id DESC')
builds = filter_builds(builds, params[:scope])
- present paginate(builds), with: ::API::V3::Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ present paginate(builds), with: ::API::V3::Entities::Build
end
desc 'Get builds for a specific commit of a project' do
@@ -57,8 +56,7 @@ module API
builds = user_project.builds.where(pipeline: pipelines).order('id DESC')
builds = filter_builds(builds, params[:scope])
- present paginate(builds), with: ::API::V3::Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ present paginate(builds), with: ::API::V3::Entities::Build
end
desc 'Get a specific build of a project' do
@@ -72,8 +70,7 @@ module API
build = get_build!(params[:build_id])
- present build, with: ::API::V3::Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ present build, with: ::API::V3::Entities::Build
end
desc 'Download the artifacts file from build' do
@@ -140,8 +137,7 @@ module API
build.cancel
- present build, with: ::API::V3::Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ present build, with: ::API::V3::Entities::Build
end
desc 'Retry a specific build of a project' do
@@ -158,8 +154,7 @@ module API
build = Ci::Build.retry(build, current_user)
- present build, with: ::API::V3::Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ present build, with: ::API::V3::Entities::Build
end
desc 'Erase build (remove artifacts and build trace)' do
@@ -175,8 +170,7 @@ module API
return forbidden!('Build is not erasable!') unless build.erasable?
build.erase(erased_by: current_user)
- present build, with: ::API::V3::Entities::Build,
- user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
+ present build, with: ::API::V3::Entities::Build
end
desc 'Keep the artifacts to prevent them from being deleted' do
@@ -194,8 +188,7 @@ module API
build.keep_artifacts!
status 200
- present build, with: ::API::V3::Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ present build, with: ::API::V3::Entities::Build
end
desc 'Trigger a manual build' do
@@ -215,8 +208,7 @@ module API
build.play(current_user)
status 200
- present build, with: ::API::V3::Entities::Build,
- user_can_download_artifacts: can?(current_user, :read_build, user_project)
+ present build, with: ::API::V3::Entities::Build
end
end
diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb
index 3549ea225ef..44584e2eb70 100644
--- a/lib/api/v3/repositories.rb
+++ b/lib/api/v3/repositories.rb
@@ -38,6 +38,60 @@ module API
present tree.sorted_entries, with: ::API::Entities::RepoTreeObject
end
+ desc 'Get a raw file contents'
+ params do
+ requires :sha, type: String, desc: 'The commit, branch name, or tag name'
+ requires :filepath, type: String, desc: 'The path to the file to display'
+ end
+ get [":id/repository/blobs/:sha", ":id/repository/commits/:sha/blob"] do
+ repo = user_project.repository
+ commit = repo.commit(params[:sha])
+ not_found! "Commit" unless commit
+ blob = Gitlab::Git::Blob.find(repo, commit.id, params[:filepath])
+ not_found! "File" unless blob
+ send_git_blob repo, blob
+ end
+
+ desc 'Get a raw blob contents by blob sha'
+ params do
+ requires :sha, type: String, desc: 'The commit, branch name, or tag name'
+ end
+ get ':id/repository/raw_blobs/:sha' do
+ repo = user_project.repository
+ begin
+ blob = Gitlab::Git::Blob.raw(repo, params[:sha])
+ rescue
+ not_found! 'Blob'
+ end
+ not_found! 'Blob' unless blob
+ send_git_blob repo, blob
+ end
+
+ desc 'Get an archive of the repository'
+ params do
+ optional :sha, type: String, desc: 'The commit sha of the archive to be downloaded'
+ optional :format, type: String, desc: 'The archive format'
+ end
+ get ':id/repository/archive', requirements: { format: Gitlab::Regex.archive_formats_regex } do
+ begin
+ send_git_archive user_project.repository, ref: params[:sha], format: params[:format]
+ rescue
+ not_found!('File')
+ end
+ end
+
+ desc 'Compare two branches, tags, or commits' do
+ success ::API::Entities::Compare
+ end
+ params do
+ requires :from, type: String, desc: 'The commit, branch name, or tag name to start comparison'
+ requires :to, type: String, desc: 'The commit, branch name, or tag name to stop comparison'
+ end
+ get ':id/repository/compare' do
+ compare = Gitlab::Git::Compare.new(user_project.repository.raw_repository, params[:from], params[:to])
+ present compare, with: ::API::Entities::Compare
+ end
+
desc 'Get repository contributors' do
success ::API::Entities::Contributor
end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 3c4ba5d50e6..cd745d35e7c 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -68,7 +68,8 @@ module Backup
end
def restore
- Gitlab.config.repositories.storages.each do |name, path|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ path = repository_storage['path']
next unless File.exist?(path)
# Move repos dir to 'repositories.old' dir
@@ -199,7 +200,7 @@ module Backup
private
def repository_storage_paths_args
- Gitlab.config.repositories.storages.values
+ Gitlab.config.repositories.storages.values.map { |rs| rs['path'] }
end
end
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 6d69efb0bf6..eee5601b0ed 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -2,9 +2,17 @@ module Gitlab
module Auth
MissingPersonalTokenError = Class.new(StandardError)
- SCOPES = [:api, :read_user].freeze
+ # Scopes used for GitLab API access
+ API_SCOPES = [:api, :read_user].freeze
+
+ # Scopes used for OpenID Connect
+ OPENID_SCOPES = [:openid].freeze
+
+ # Default scopes for OAuth applications that don't define their own
DEFAULT_SCOPES = [:api].freeze
- OPTIONAL_SCOPES = SCOPES - DEFAULT_SCOPES
+
+ # Other available scopes
+ OPTIONAL_SCOPES = (API_SCOPES + OPENID_SCOPES - DEFAULT_SCOPES).freeze
class << self
def find_for_git_client(login, password, project:, ip:)
@@ -40,7 +48,7 @@ module Gitlab
Gitlab::LDAP::Authentication.login(login, password)
else
- user if user.valid_password?(password)
+ user if user.active? && user.valid_password?(password)
end
end
end
diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb
index 42703545c4f..35871fd1b7b 100644
--- a/lib/gitlab/emoji.rb
+++ b/lib/gitlab/emoji.rb
@@ -31,7 +31,7 @@ module Gitlab
end
def emoji_unicode_version(name)
- @emoji_unicode_versions_by_name ||= JSON.parse(File.read(Rails.root.join('node_modules', 'emoji-unicode-version', 'emoji-unicode-version-map.json')))
+ @emoji_unicode_versions_by_name ||= JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json')))
@emoji_unicode_versions_by_name[name]
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 6540730ca7a..228ef7bb7a9 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -354,6 +354,18 @@ module Gitlab
lines.map! { |c| Rugged::Commit.new(rugged, c.strip) }
end
+ def count_commits(options)
+ cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} rev-list]
+ cmd << "--after=#{options[:after].iso8601}" if options[:after]
+ cmd << "--before=#{options[:before].iso8601}" if options[:before]
+ cmd += %W[--count #{options[:ref]}]
+ cmd += %W[-- #{options[:path]}] if options[:path].present?
+
+ raw_output = IO.popen(cmd) { |io| io.read }
+
+ raw_output.to_i
+ end
+
def sha_from_ref(ref)
rev_parse_target(ref).oid
end
diff --git a/lib/gitlab/github_import/branch_formatter.rb b/lib/gitlab/github_import/branch_formatter.rb
index 0a8d05b5fe1..5d29e698b27 100644
--- a/lib/gitlab/github_import/branch_formatter.rb
+++ b/lib/gitlab/github_import/branch_formatter.rb
@@ -18,7 +18,7 @@ module Gitlab
end
def commit_exists?
- project.repository.commit(sha).present?
+ project.repository.branch_names_contains(sha).include?(ref)
end
def short_id
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
index 4ea0200e89b..28812fd0cb9 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -38,7 +38,11 @@ module Gitlab
def source_branch_name
@source_branch_name ||= begin
- source_branch_exists? ? source_branch_ref : "pull/#{number}/#{source_branch_ref}"
+ if cross_project?
+ "pull/#{number}/#{source_branch_repo.full_name}/#{source_branch_ref}"
+ else
+ source_branch_exists? ? source_branch_ref : "pull/#{number}/#{source_branch_ref}"
+ end
end
end
@@ -52,6 +56,10 @@ module Gitlab
end
end
+ def cross_project?
+ source_branch.repo.id != target_branch.repo.id
+ end
+
private
def state
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
index 5764ab15652..6023fa1820f 100644
--- a/lib/gitlab/middleware/go.rb
+++ b/lib/gitlab/middleware/go.rb
@@ -30,21 +30,69 @@ module Gitlab
end
def go_body(request)
- base_url = Gitlab.config.gitlab.url
- # Go subpackages may be in the form of namespace/project/path1/path2/../pathN
- # We can just ignore the paths and leave the namespace/project
- path_info = request.env["PATH_INFO"]
- path_info.sub!(/^\//, '')
- project_path = path_info.split('/').first(2).join('/')
- request_url = URI.join(base_url, project_path)
- domain_path = strip_url(request_url.to_s)
+ project_url = URI.join(Gitlab.config.gitlab.url, project_path(request))
+ import_prefix = strip_url(project_url.to_s)
- "<!DOCTYPE html><html><head><meta content='#{domain_path} git #{request_url}.git' name='go-import'></head></html>\n"
+ "<!DOCTYPE html><html><head><meta content='#{import_prefix} git #{project_url}.git' name='go-import'></head></html>\n"
end
def strip_url(url)
url.gsub(/\Ahttps?:\/\//, '')
end
+
+ def project_path(request)
+ path_info = request.env["PATH_INFO"]
+ path_info.sub!(/^\//, '')
+
+ # Go subpackages may be in the form of `namespace/project/path1/path2/../pathN`.
+ # In a traditional project with a single namespace, this would denote repo
+ # `namespace/project` with subpath `path1/path2/../pathN`, but with nested
+ # groups, this could also be `namespace/project/path1` with subpath
+ # `path2/../pathN`, for example.
+
+ # We find all potential project paths out of the path segments
+ path_segments = path_info.split('/')
+ simple_project_path = path_segments.first(2).join('/')
+
+ # If the path is at most 2 segments long, it is a simple `namespace/project` path and we're done
+ return simple_project_path if path_segments.length <= 2
+
+ project_paths = []
+ begin
+ project_paths << path_segments.join('/')
+ path_segments.pop
+ end while path_segments.length >= 2
+
+ # We see if a project exists with any of these potential paths
+ project = project_for_paths(project_paths, request)
+
+ if project
+ # If a project is found and the user has access, we return the full project path
+ project.full_path
+ else
+ # If not, we return the first two components as if it were a simple `namespace/project` path,
+ # so that we don't reveal the existence of a nested project the user doesn't have access to.
+ # This means that for an unauthenticated request to `group/subgroup/project/subpackage`
+ # for a private `group/subgroup/project` with subpackage path `subpackage`, GitLab will respond
+ # as if the user is looking for project `group/subgroup`, with subpackage path `project/subpackage`.
+ # Since `go get` doesn't authenticate by default, this means that
+ # `go get gitlab.com/group/subgroup/project/subpackage` will not work for private projects.
+ # `go get gitlab.com/group/subgroup/project.git/subpackage` will work, since Go is smart enough
+ # to figure that out. `import 'gitlab.com/...'` behaves the same as `go get`.
+ simple_project_path
+ end
+ end
+
+ def project_for_paths(paths, request)
+ project = Project.where_full_path_in(paths).first
+ return unless Ability.allowed?(current_user(request), :read_project, project)
+
+ project
+ end
+
+ def current_user(request)
+ request.env['warden']&.authenticate
+ end
end
end
end
diff --git a/lib/gitlab/prometheus.rb b/lib/gitlab/prometheus.rb
new file mode 100644
index 00000000000..62239779454
--- /dev/null
+++ b/lib/gitlab/prometheus.rb
@@ -0,0 +1,70 @@
+module Gitlab
+ PrometheusError = Class.new(StandardError)
+
+ # Helper methods to interact with Prometheus network services & resources
+ class Prometheus
+ attr_reader :api_url
+
+ def initialize(api_url:)
+ @api_url = api_url
+ end
+
+ def ping
+ json_api_get('query', query: '1')
+ end
+
+ def query(query)
+ get_result('vector') do
+ json_api_get('query', query: query)
+ end
+ end
+
+ def query_range(query, start: 8.hours.ago)
+ get_result('matrix') do
+ json_api_get('query_range',
+ query: query,
+ start: start.to_f,
+ end: Time.now.utc.to_f,
+ step: 1.minute.to_i)
+ end
+ end
+
+ private
+
+ def json_api_get(type, args = {})
+ get(join_api_url(type, args))
+ rescue Errno::ECONNREFUSED
+ raise PrometheusError, 'Connection refused'
+ end
+
+ def join_api_url(type, args = {})
+ url = URI.parse(api_url)
+ rescue URI::Error
+ raise PrometheusError, "Invalid API URL: #{api_url}"
+ else
+ url.path = [url.path.sub(%r{/+\z}, ''), 'api', 'v1', type].join('/')
+ url.query = args.to_query
+
+ url.to_s
+ end
+
+ def get(url)
+ handle_response(HTTParty.get(url))
+ end
+
+ def handle_response(response)
+ if response.code == 200 && response['status'] == 'success'
+ response['data'] || {}
+ elsif response.code == 400
+ raise PrometheusError, response['error'] || 'Bad data received'
+ else
+ raise PrometheusError, "#{response.code} - #{response.body}"
+ end
+ end
+
+ def get_result(expected_type)
+ data = yield
+ data['result'] if data['resultType'] == expected_type
+ end
+ end
+end
diff --git a/lib/tasks/gemojione.rake b/lib/tasks/gemojione.rake
index 1f93b5a4dd2..5293f5af12d 100644
--- a/lib/tasks/gemojione.rake
+++ b/lib/tasks/gemojione.rake
@@ -1,9 +1,12 @@
namespace :gemojione do
desc 'Generates Emoji SHA256 digests'
- task digests: :environment do
+ task digests: ['yarn:check', 'environment'] do
require 'digest/sha2'
require 'json'
+ # We don't have `node_modules` available in built versions of GitLab
+ FileUtils.cp_r(Rails.root.join('node_modules', 'emoji-unicode-version', 'emoji-unicode-version-map.json'), File.join(Rails.root, 'fixtures', 'emojis'))
+
dir = Gemojione.images_path
resultant_emoji_map = {}
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 38edd49b6ed..a6f8c4ced5d 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -354,7 +354,8 @@ namespace :gitlab do
def check_repo_base_exists
puts "Repo base directory exists?"
- Gitlab.config.repositories.storages.each do |name, repo_base_path|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ repo_base_path = repository_storage['path']
print "#{name}... "
if File.exist?(repo_base_path)
@@ -378,7 +379,8 @@ namespace :gitlab do
def check_repo_base_is_not_symlink
puts "Repo storage directories are symlinks?"
- Gitlab.config.repositories.storages.each do |name, repo_base_path|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ repo_base_path = repository_storage['path']
print "#{name}... "
unless File.exist?(repo_base_path)
@@ -401,7 +403,8 @@ namespace :gitlab do
def check_repo_base_permissions
puts "Repo paths access is drwxrws---?"
- Gitlab.config.repositories.storages.each do |name, repo_base_path|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ repo_base_path = repository_storage['path']
print "#{name}... "
unless File.exist?(repo_base_path)
@@ -431,7 +434,8 @@ namespace :gitlab do
gitlab_shell_owner_group = Gitlab.config.gitlab_shell.owner_group
puts "Repo paths owned by #{gitlab_shell_ssh_user}:#{gitlab_shell_owner_group}?"
- Gitlab.config.repositories.storages.each do |name, repo_base_path|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ repo_base_path = repository_storage['path']
print "#{name}... "
unless File.exist?(repo_base_path)
@@ -810,8 +814,8 @@ namespace :gitlab do
namespace :repo do
desc "GitLab | Check the integrity of the repositories managed by GitLab"
task check: :environment do
- Gitlab.config.repositories.storages.each do |name, path|
- namespace_dirs = Dir.glob(File.join(path, '*'))
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ namespace_dirs = Dir.glob(File.join(repository_storage['path'], '*'))
namespace_dirs.each do |namespace_dir|
repo_dirs = Dir.glob(File.join(namespace_dir, '*'))
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index daf7382dd02..f76bef5f4bf 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -6,7 +6,8 @@ namespace :gitlab do
remove_flag = ENV['REMOVE']
namespaces = Namespace.pluck(:path)
- Gitlab.config.repositories.storages.each do |name, git_base_path|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ git_base_path = repository_storage['path']
all_dirs = Dir.glob(git_base_path + '/*')
puts git_base_path.color(:yellow)
@@ -47,7 +48,8 @@ namespace :gitlab do
warn_user_is_not_gitlab
move_suffix = "+orphaned+#{Time.now.to_i}"
- Gitlab.config.repositories.storages.each do |name, repo_root|
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ repo_root = repository_storage['path']
# Look for global repos (legacy, depth 1) and normal repos (depth 2)
IO.popen(%W(find #{repo_root} -mindepth 1 -maxdepth 2 -name *.git)) do |find|
find.each_line do |path|
diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake
index 66e7b7685f7..48bd9139ce8 100644
--- a/lib/tasks/gitlab/import.rake
+++ b/lib/tasks/gitlab/import.rake
@@ -11,7 +11,8 @@ namespace :gitlab do
#
desc "GitLab | Import bare repositories from repositories -> storages into GitLab project instance"
task repos: :environment do
- Gitlab.config.repositories.storages.each do |name, git_base_path|
+ Gitlab.config.repositories.storages.each_value do |repository_storage|
+ git_base_path = repository_storage['path']
repos_to_import = Dir.glob(git_base_path + '/**/*.git')
repos_to_import.each do |repo_path|
diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake
index b8dd654b9a9..a2a2db487b7 100644
--- a/lib/tasks/gitlab/info.rake
+++ b/lib/tasks/gitlab/info.rake
@@ -65,8 +65,8 @@ namespace :gitlab do
puts "GitLab Shell".color(:yellow)
puts "Version:\t#{gitlab_shell_version || "unknown".color(:red)}"
puts "Repository storage paths:"
- Gitlab.config.repositories.storages.each do |name, path|
- puts "- #{name}: \t#{path}"
+ Gitlab.config.repositories.storages.each do |name, repository_storage|
+ puts "- #{name}: \t#{repository_storage['path']}"
end
puts "Hooks:\t\t#{Gitlab.config.gitlab_shell.hooks_path}"
puts "Git:\t\t#{Gitlab.config.git.bin_path}"
diff --git a/lib/tasks/gitlab/task_helpers.rb b/lib/tasks/gitlab/task_helpers.rb
index 2a999ad6959..bb755ae689b 100644
--- a/lib/tasks/gitlab/task_helpers.rb
+++ b/lib/tasks/gitlab/task_helpers.rb
@@ -130,8 +130,8 @@ module Gitlab
end
def all_repos
- Gitlab.config.repositories.storages.each do |name, path|
- IO.popen(%W(find #{path} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find|
+ Gitlab.config.repositories.storages.each_value do |repository_storage|
+ IO.popen(%W(find #{repository_storage['path']} -mindepth 2 -maxdepth 2 -type d -name *.git)) do |find|
find.each_line do |path|
yield path.chomp
end
@@ -140,7 +140,7 @@ module Gitlab
end
def repository_storage_paths_args
- Gitlab.config.repositories.storages.values
+ Gitlab.config.repositories.storages.values.map { |rs| rs['path'] }
end
def user_home
diff --git a/rubocop/cop/migration/add_concurrent_index.rb b/rubocop/cop/migration/add_concurrent_index.rb
new file mode 100644
index 00000000000..332fb7dcbd7
--- /dev/null
+++ b/rubocop/cop/migration/add_concurrent_index.rb
@@ -0,0 +1,34 @@
+require_relative '../../migration_helpers'
+
+module RuboCop
+ module Cop
+ module Migration
+ # Cop that checks if `add_concurrent_index` is used with `up`/`down` methods
+ # and not `change`.
+ class AddConcurrentIndex < RuboCop::Cop::Cop
+ include MigrationHelpers
+
+ MSG = '`add_concurrent_index` is not reversible so you must manually define ' \
+ 'the `up` and `down` methods in your migration class, using `remove_index` in `down`'.freeze
+
+ def on_send(node)
+ return unless in_migration?(node)
+
+ name = node.children[1]
+
+ return unless name == :add_concurrent_index
+
+ node.each_ancestor(:def) do |def_node|
+ next unless method_name(def_node) == :change
+
+ add_offense(def_node, :name)
+ end
+ end
+
+ def method_name(node)
+ node.children.first
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index ea8e0f64b0d..a50a522cf9d 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -3,4 +3,5 @@ require_relative 'cop/gem_fetcher'
require_relative 'cop/migration/add_column'
require_relative 'cop/migration/add_column_with_default'
require_relative 'cop/migration/add_concurrent_foreign_key'
+require_relative 'cop/migration/add_concurrent_index'
require_relative 'cop/migration/add_index'
diff --git a/spec/controllers/admin/applications_controller_spec.rb b/spec/controllers/admin/applications_controller_spec.rb
new file mode 100644
index 00000000000..e311b8a63b2
--- /dev/null
+++ b/spec/controllers/admin/applications_controller_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Admin::ApplicationsController do
+ let(:admin) { create(:admin) }
+ let(:application) { create(:oauth_application, owner_id: nil, owner_type: nil) }
+
+ before do
+ sign_in(admin)
+ end
+
+ describe 'GET #new' do
+ it 'renders the application form' do
+ get :new
+
+ expect(response).to render_template :new
+ expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+ end
+ end
+
+ describe 'GET #edit' do
+ it 'renders the application form' do
+ get :edit, id: application.id
+
+ expect(response).to render_template :edit
+ expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+ end
+ end
+
+ describe 'POST #create' do
+ it 'creates the application' do
+ expect do
+ post :create, doorkeeper_application: attributes_for(:application)
+ end.to change { Doorkeeper::Application.count }.by(1)
+
+ application = Doorkeeper::Application.last
+
+ expect(response).to redirect_to(admin_application_path(application))
+ end
+
+ it 'renders the application form on errors' do
+ expect do
+ post :create, doorkeeper_application: attributes_for(:application).merge(redirect_uri: nil)
+ end.not_to change { Doorkeeper::Application.count }
+
+ expect(response).to render_template :new
+ expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+ end
+ end
+
+ describe 'PATCH #update' do
+ it 'updates the application' do
+ patch :update, id: application.id, doorkeeper_application: { redirect_uri: 'http://example.com/' }
+
+ expect(response).to redirect_to(admin_application_path(application))
+ expect(application.reload.redirect_uri).to eq 'http://example.com/'
+ end
+
+ it 'renders the application form on errors' do
+ patch :update, id: application.id, doorkeeper_application: { redirect_uri: nil }
+
+ expect(response).to render_template :edit
+ expect(assigns[:scopes]).to be_kind_of(Doorkeeper::OAuth::Scopes)
+ end
+ end
+end
diff --git a/spec/controllers/profiles/personal_access_tokens_spec.rb b/spec/controllers/profiles/personal_access_tokens_spec.rb
index ae8031a45f6..dfed1de2046 100644
--- a/spec/controllers/profiles/personal_access_tokens_spec.rb
+++ b/spec/controllers/profiles/personal_access_tokens_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe Profiles::PersonalAccessTokensController do
let(:user) { create(:user) }
+ let(:token_attributes) { attributes_for(:personal_access_token) }
before { sign_in(user) }
@@ -10,41 +11,26 @@ describe Profiles::PersonalAccessTokensController do
PersonalAccessToken.order(:created_at).last
end
- it "allows creation of a token" do
+ it "allows creation of a token with scopes" do
name = FFaker::Product.brand
+ scopes = %w[api read_user]
- post :create, personal_access_token: { name: name }
+ post :create, personal_access_token: token_attributes.merge(scopes: scopes, name: name)
expect(created_token).not_to be_nil
expect(created_token.name).to eq(name)
- expect(created_token.expires_at).to be_nil
+ expect(created_token.scopes).to eq(scopes)
expect(PersonalAccessToken.active).to include(created_token)
end
it "allows creation of a token with an expiry date" do
expires_at = 5.days.from_now.to_date
- post :create, personal_access_token: { name: FFaker::Product.brand, expires_at: expires_at }
+ post :create, personal_access_token: token_attributes.merge(expires_at: expires_at)
expect(created_token).not_to be_nil
expect(created_token.expires_at).to eq(expires_at)
end
-
- context "scopes" do
- it "allows creation of a token with scopes" do
- post :create, personal_access_token: { name: FFaker::Product.brand, scopes: %w(api read_user) }
-
- expect(created_token).not_to be_nil
- expect(created_token.scopes).to eq(%w(api read_user))
- end
-
- it "allows creation of a token with no scopes" do
- post :create, personal_access_token: { name: FFaker::Product.brand, scopes: [] }
-
- expect(created_token).not_to be_nil
- expect(created_token.scopes).to eq([])
- end
- end
end
describe '#index' do
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
index 3d0533cb516..15667e8d4b1 100644
--- a/spec/controllers/projects/boards/issues_controller_spec.rb
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -43,6 +43,7 @@ describe Projects::Boards::IssuesController do
expect(response).to match_response_schema('issues')
expect(parsed_response.length).to eq 2
+ expect(development.issues.map(&:relative_position)).not_to include(nil)
end
end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index 84d119f1867..83d80b376fb 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -187,6 +187,52 @@ describe Projects::EnvironmentsController do
end
end
+ describe 'GET #metrics' do
+ before do
+ allow(controller).to receive(:environment).and_return(environment)
+ end
+
+ context 'when environment has no metrics' do
+ before do
+ expect(environment).to receive(:metrics).and_return(nil)
+ end
+
+ it 'returns a metrics page' do
+ get :metrics, environment_params
+
+ expect(response).to be_ok
+ end
+
+ context 'when requesting metrics as JSON' do
+ it 'returns a metrics JSON document' do
+ get :metrics, environment_params(format: :json)
+
+ expect(response).to have_http_status(204)
+ expect(json_response).to eq({})
+ end
+ end
+ end
+
+ context 'when environment has some metrics' do
+ before do
+ expect(environment).to receive(:metrics).and_return({
+ success: true,
+ metrics: {},
+ last_update: 42
+ })
+ end
+
+ it 'returns a metrics JSON document' do
+ get :metrics, environment_params(format: :json)
+
+ expect(response).to be_ok
+ expect(json_response['success']).to be(true)
+ expect(json_response['metrics']).to eq({})
+ expect(json_response['last_update']).to eq(42)
+ end
+ end
+ end
+
def environment_params(opts = {})
opts.reverse_merge(namespace_id: project.namespace,
project_id: project,
diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb
new file mode 100644
index 00000000000..f73471f8ca8
--- /dev/null
+++ b/spec/controllers/projects/settings/repository_controller_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Projects::Settings::RepositoryController do
+ let(:project) { create(:project_empty_repo, :public) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ describe 'GET show' do
+ it 'renders show with 200 status code' do
+ get :show, namespace_id: project.namespace, project_id: project
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template(:show)
+ end
+ end
+end
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index c9584ddf18c..f67d26da0ac 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -1,4 +1,9 @@
require 'spec_helper'
+shared_examples 'content not cached without revalidation' do
+ it 'ensures content will not be cached without revalidation' do
+ expect(subject['Cache-Control']).to eq('max-age=0, private, must-revalidate')
+ end
+end
describe UploadsController do
let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
@@ -50,6 +55,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png'
+ response
+ end
+ end
end
end
@@ -59,6 +71,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png'
+ response
+ end
+ end
end
end
@@ -76,6 +95,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+ response
+ end
+ end
end
context "when signed in" do
@@ -88,6 +114,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+ response
+ end
+ end
end
end
@@ -133,6 +166,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+ response
+ end
+ end
end
end
@@ -157,6 +197,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+ response
+ end
+ end
end
context "when signed in" do
@@ -169,6 +216,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+ response
+ end
+ end
end
end
@@ -205,6 +259,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+ response
+ end
+ end
end
end
@@ -234,6 +295,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+ response
+ end
+ end
end
context "when signed in" do
@@ -246,6 +314,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+ response
+ end
+ end
end
end
@@ -291,6 +366,13 @@ describe UploadsController do
expect(response).to have_http_status(200)
end
+
+ it_behaves_like 'content not cached without revalidation' do
+ subject do
+ get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+ response
+ end
+ end
end
end
diff --git a/spec/factories/chat_teams.rb b/spec/factories/chat_teams.rb
new file mode 100644
index 00000000000..82f44fa3d15
--- /dev/null
+++ b/spec/factories/chat_teams.rb
@@ -0,0 +1,9 @@
+FactoryGirl.define do
+ factory :chat_team, class: ChatTeam do
+ sequence :team_id do |n|
+ "abcdefghijklm#{n}"
+ end
+
+ namespace factory: :group
+ end
+end
diff --git a/spec/factories/oauth_access_grants.rb b/spec/factories/oauth_access_grants.rb
new file mode 100644
index 00000000000..543b3e99274
--- /dev/null
+++ b/spec/factories/oauth_access_grants.rb
@@ -0,0 +1,11 @@
+FactoryGirl.define do
+ factory :oauth_access_grant do
+ resource_owner_id { create(:user).id }
+ application
+ token { Doorkeeper::OAuth::Helpers::UniqueToken.generate }
+ expires_in 2.hours
+
+ redirect_uri { application.redirect_uri }
+ scopes { application.scopes }
+ end
+end
diff --git a/spec/factories/oauth_access_tokens.rb b/spec/factories/oauth_access_tokens.rb
index ccf02d0719b..a46bc1d8ce8 100644
--- a/spec/factories/oauth_access_tokens.rb
+++ b/spec/factories/oauth_access_tokens.rb
@@ -2,6 +2,7 @@ FactoryGirl.define do
factory :oauth_access_token do
resource_owner
application
- token '123456'
+ token { Doorkeeper::OAuth::Helpers::UniqueToken.generate }
+ scopes { application.scopes }
end
end
diff --git a/spec/factories/oauth_applications.rb b/spec/factories/oauth_applications.rb
index d116a573830..86cdc208268 100644
--- a/spec/factories/oauth_applications.rb
+++ b/spec/factories/oauth_applications.rb
@@ -1,7 +1,7 @@
FactoryGirl.define do
factory :oauth_application, class: 'Doorkeeper::Application', aliases: [:application] do
name { FFaker::Name.name }
- uid { FFaker::Name.name }
+ uid { Doorkeeper::OAuth::Helpers::UniqueToken.generate }
redirect_uri { FFaker::Internet.uri('http') }
owner
owner_type 'User'
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 70c65bc693a..0db2fe04edd 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -38,7 +38,7 @@ FactoryGirl.define do
trait :empty_repo do
after(:create) do |project|
- project.create_repository
+ raise "Failed to create repository!" unless project.create_repository
# We delete hooks so that gitlab-shell will not try to authenticate with
# an API that isn't running
@@ -48,7 +48,7 @@ FactoryGirl.define do
trait :broken_repo do
after(:create) do |project|
- project.create_repository
+ raise "Failed to create repository!" unless project.create_repository
FileUtils.rm_r(File.join(project.repository_storage_path, "#{project.path_with_namespace}.git", 'refs'))
end
@@ -195,4 +195,15 @@ FactoryGirl.define do
factory :kubernetes_project, parent: :empty_project do
kubernetes_service
end
+
+ factory :prometheus_project, parent: :empty_project do
+ after :create do |project|
+ project.create_prometheus_service(
+ active: true,
+ properties: {
+ api_url: 'https://prometheus.example.com'
+ }
+ )
+ end
+ end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index e247bfa2980..ecc356f2505 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -71,16 +71,16 @@ describe 'Issue Boards', feature: true, js: true do
let!(:list1) { create(:list, board: board, label: planning, position: 0) }
let!(:list2) { create(:list, board: board, label: development, position: 1) }
- let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning]) }
- let!(:issue1) { create(:labeled_issue, project: project, assignee: user, labels: [planning]) }
- let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning]) }
- let!(:issue3) { create(:labeled_issue, project: project, labels: [planning]) }
- let!(:issue4) { create(:labeled_issue, project: project, labels: [planning]) }
- let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone) }
- let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) }
- let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) }
+ let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) }
+ let!(:issue1) { create(:labeled_issue, project: project, assignee: user, labels: [planning], relative_position: 8) }
+ let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning], relative_position: 7) }
+ let!(:issue3) { create(:labeled_issue, project: project, labels: [planning], relative_position: 6) }
+ let!(:issue4) { create(:labeled_issue, project: project, labels: [planning], relative_position: 5) }
+ let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone, relative_position: 4) }
+ let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development], relative_position: 3) }
+ let!(:issue7) { create(:labeled_issue, project: project, labels: [development], relative_position: 2) }
let!(:issue8) { create(:closed_issue, project: project) }
- let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting]) }
+ let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting], relative_position: 1) }
before do
visit namespace_project_board_path(project.namespace, project, board)
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
new file mode 100644
index 00000000000..c50155a6d14
--- /dev/null
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -0,0 +1,166 @@
+require 'rails_helper'
+
+describe 'Issue Boards', :feature, :js do
+ include WaitForVueResource
+ include DragTo
+
+ let(:project) { create(:empty_project, :public) }
+ let(:board) { create(:board, project: project) }
+ let(:user) { create(:user) }
+ let(:label) { create(:label, project: project) }
+ let!(:list1) { create(:list, board: board, label: label, position: 0) }
+ let!(:issue1) { create(:labeled_issue, project: project, title: 'testing 1', labels: [label], relative_position: 3) }
+ let!(:issue2) { create(:labeled_issue, project: project, title: 'testing 2', labels: [label], relative_position: 2) }
+ let!(:issue3) { create(:labeled_issue, project: project, title: 'testing 3', labels: [label], relative_position: 1) }
+
+ before do
+ project.team << [user, :master]
+
+ login_as(user)
+ end
+
+ context 'un-ordered issues' do
+ let!(:issue4) { create(:labeled_issue, project: project, labels: [label]) }
+
+ before do
+ visit namespace_project_board_path(project.namespace, project, board)
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 2)
+ end
+
+ it 'has un-ordered issue as last issue' do
+ page.within(first('.board')) do
+ expect(all('.card').last).to have_content(issue4.title)
+ end
+ end
+
+ it 'moves un-ordered issue to top of list' do
+ drag(from_index: 3, to_index: 0)
+
+ page.within(first('.board')) do
+ expect(first('.card')).to have_content(issue4.title)
+ end
+ end
+ end
+
+ context 'ordering in list' do
+ before do
+ visit namespace_project_board_path(project.namespace, project, board)
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 2)
+ end
+
+ it 'moves from middle to top' do
+ drag(from_index: 1, to_index: 0)
+
+ wait_for_vue_resource
+
+ expect(first('.card')).to have_content(issue2.title)
+ end
+
+ it 'moves from middle to bottom' do
+ drag(from_index: 1, to_index: 2)
+
+ wait_for_vue_resource
+
+ expect(all('.card').last).to have_content(issue2.title)
+ end
+
+ it 'moves from top to bottom' do
+ drag(from_index: 0, to_index: 2)
+
+ wait_for_vue_resource
+
+ expect(all('.card').last).to have_content(issue3.title)
+ end
+
+ it 'moves from bottom to top' do
+ drag(from_index: 2, to_index: 0)
+
+ wait_for_vue_resource
+
+ expect(first('.card')).to have_content(issue1.title)
+ end
+
+ it 'moves from top to middle' do
+ drag(from_index: 0, to_index: 1)
+
+ wait_for_vue_resource
+
+ expect(first('.card')).to have_content(issue2.title)
+ end
+
+ it 'moves from bottom to middle' do
+ drag(from_index: 2, to_index: 1)
+
+ wait_for_vue_resource
+
+ expect(all('.card').last).to have_content(issue2.title)
+ end
+ end
+
+ context 'ordering when changing list' do
+ let(:label2) { create(:label, project: project) }
+ let!(:list2) { create(:list, board: board, label: label2, position: 1) }
+ let!(:issue4) { create(:labeled_issue, project: project, title: 'testing 1', labels: [label2], relative_position: 3.0) }
+ let!(:issue5) { create(:labeled_issue, project: project, title: 'testing 2', labels: [label2], relative_position: 2.0) }
+ let!(:issue6) { create(:labeled_issue, project: project, title: 'testing 3', labels: [label2], relative_position: 1.0) }
+
+ before do
+ visit namespace_project_board_path(project.namespace, project, board)
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 3)
+ end
+
+ it 'moves to top of another list' do
+ drag(list_from_index: 0, list_to_index: 1)
+
+ wait_for_vue_resource
+
+ expect(first('.board')).to have_selector('.card', count: 2)
+ expect(all('.board')[1]).to have_selector('.card', count: 4)
+
+ page.within(all('.board')[1]) do
+ expect(first('.card')).to have_content(issue3.title)
+ end
+ end
+
+ it 'moves to bottom of another list' do
+ drag(list_from_index: 0, list_to_index: 1, to_index: 2)
+
+ wait_for_vue_resource
+
+ expect(first('.board')).to have_selector('.card', count: 2)
+ expect(all('.board')[1]).to have_selector('.card', count: 4)
+
+ page.within(all('.board')[1]) do
+ expect(all('.card').last).to have_content(issue3.title)
+ end
+ end
+
+ it 'moves to index of another list' do
+ drag(list_from_index: 0, list_to_index: 1, to_index: 1)
+
+ wait_for_vue_resource
+
+ expect(first('.board')).to have_selector('.card', count: 2)
+ expect(all('.board')[1]).to have_selector('.card', count: 4)
+
+ page.within(all('.board')[1]) do
+ expect(all('.card')[1]).to have_content(issue3.title)
+ end
+ end
+ end
+
+ def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0)
+ drag_to(selector: selector,
+ scrollable: '#board-app',
+ list_from_index: list_from_index,
+ from_index: from_index,
+ to_index: to_index,
+ list_to_index: list_to_index)
+ end
+end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 59e87b3f69c..3332e07ec31 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -11,8 +11,8 @@ describe 'Issue Boards', feature: true, js: true do
let!(:bug) { create(:label, project: project, name: 'Bug') }
let!(:regression) { create(:label, project: project, name: 'Regression') }
let!(:stretch) { create(:label, project: project, name: 'Stretch') }
- let!(:issue1) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [development]) }
- let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch]) }
+ let!(:issue1) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [development], relative_position: 2) }
+ let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) }
let(:board) { create(:board, project: project) }
let!(:list) { create(:list, board: board, label: development, position: 0) }
let(:card) { first('.board').first('.card') }
diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb
index c2e0612aef8..34d6257f5fd 100644
--- a/spec/features/dashboard/user_filters_projects_spec.rb
+++ b/spec/features/dashboard/user_filters_projects_spec.rb
@@ -1,26 +1,45 @@
require 'spec_helper'
-describe "Dashboard > User filters projects", feature: true do
+describe 'Dashboard > User filters projects', :feature do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, name: 'Victorialand', namespace: user.namespace) }
+ let(:user2) { create(:user) }
+ let(:project2) { create(:project, name: 'Treasure', namespace: user2.namespace) }
+
+ before do
+ project.team << [user, :master]
+
+ login_as(user)
+ end
+
describe 'filtering personal projects' do
before do
- user = create(:user)
- project = create(:project, name: "Victorialand", namespace: user.namespace)
- project.team << [user, :master]
-
- user2 = create(:user)
- project2 = create(:project, name: "Treasure", namespace: user2.namespace)
project2.team << [user, :developer]
- login_as(user)
visit dashboard_projects_path
end
it 'filters by projects "Owned by me"' do
- click_link "Owned by me"
+ click_link 'Owned by me'
expect(page).to have_css('.is-active', text: 'Owned by me')
expect(page).to have_content('Victorialand')
expect(page).not_to have_content('Treasure')
end
end
+
+ describe 'filtering starred projects', :js do
+ before do
+ user.toggle_star(project)
+
+ visit dashboard_projects_path
+ end
+
+ it 'returns message when starred projects fitler returns no results' do
+ fill_in 'project-filter-form-field', with: 'Beta\n'
+
+ expect(page).to have_content('No projects found')
+ expect(page).not_to have_content('You don\'t have starred projects yet')
+ end
+ end
end
diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb
index aa75e1140f6..8c61cdebc4b 100644
--- a/spec/features/dashboard_issues_spec.rb
+++ b/spec/features/dashboard_issues_spec.rb
@@ -48,7 +48,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
it 'updates atom feed link' do
visit_issues(milestone_title: '', assignee_id: user.id)
- link = find('.nav-controls a', text: 'Subscribe')
+ link = find('.nav-controls a[title="Subscribe"]')
params = CGI.parse(URI.parse(link[:href]).query)
auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index 93763f092fb..4dcc56a97d1 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -1,6 +1,7 @@
require 'rails_helper'
-describe 'Dropdown assignee', js: true, feature: true do
+describe 'Dropdown assignee', :feature, :js do
+ include FilteredSearchHelpers
include WaitForAjax
let!(:project) { create(:empty_project) }
@@ -9,17 +10,10 @@ describe 'Dropdown assignee', js: true, feature: true do
let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') }
let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_assignee) { '#js-dropdown-assignee' }
-
- def send_keys_to_filtered_search(input)
- input.split("").each do |i|
- filtered_search.send_keys(i)
- sleep 5
- wait_for_ajax
- end
- end
+ let(:filter_dropdown) { find("#{js_dropdown_assignee} .filter-dropdown") }
def dropdown_assignee_size
- page.all('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item').size
+ filter_dropdown.all('.filter-dropdown-item').size
end
def click_assignee(text)
@@ -56,63 +50,80 @@ describe 'Dropdown assignee', js: true, feature: true do
end
it 'should hide loading indicator when loaded' do
- send_keys_to_filtered_search('assignee:')
+ filtered_search.set('assignee:')
- expect(page).not_to have_css('#js-dropdown-assignee .filter-dropdown-loading')
+ expect(find(js_dropdown_assignee)).to have_css('.filter-dropdown-loading')
+ expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading')
end
it 'should load all the assignees when opened' do
- send_keys_to_filtered_search('assignee:')
+ filtered_search.set('assignee:')
expect(dropdown_assignee_size).to eq(3)
end
it 'shows current user at top of dropdown' do
- send_keys_to_filtered_search('assignee:')
+ filtered_search.set('assignee:')
- expect(first('#js-dropdown-assignee .filter-dropdown li')).to have_content(user.name)
+ expect(filter_dropdown.first('.filter-dropdown-item')).to have_content(user.name)
end
end
describe 'filtering' do
before do
- send_keys_to_filtered_search('assignee:')
+ filtered_search.set('assignee:')
+
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_john.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
end
it 'filters by name' do
- send_keys_to_filtered_search('j')
+ filtered_search.send_keys('j')
- expect(dropdown_assignee_size).to eq(2)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_john.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user.name)
end
it 'filters by case insensitive name' do
- send_keys_to_filtered_search('J')
+ filtered_search.send_keys('J')
- expect(dropdown_assignee_size).to eq(2)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_john.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user.name)
end
it 'filters by username with symbol' do
- send_keys_to_filtered_search('@ot')
+ filtered_search.send_keys('@ot')
- expect(dropdown_assignee_size).to eq(2)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user_john.name)
end
it 'filters by case insensitive username with symbol' do
- send_keys_to_filtered_search('@OT')
+ filtered_search.send_keys('@OT')
- expect(dropdown_assignee_size).to eq(2)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user_john.name)
end
it 'filters by username without symbol' do
- send_keys_to_filtered_search('ot')
+ filtered_search.send_keys('ot')
- expect(dropdown_assignee_size).to eq(2)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user_john.name)
end
it 'filters by case insensitive username without symbol' do
- send_keys_to_filtered_search('OT')
+ filtered_search.send_keys('OT')
- expect(dropdown_assignee_size).to eq(2)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name)
+ expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user_john.name)
end
end
@@ -125,22 +136,25 @@ describe 'Dropdown assignee', js: true, feature: true do
click_assignee(user_jacob.name)
expect(page).to have_css(js_dropdown_assignee, visible: false)
- expect(filtered_search.value).to eq("assignee:@#{user_jacob.username} ")
+ expect_tokens([{ name: 'assignee', value: "@#{user_jacob.username}" }])
+ expect_filtered_search_input_empty
end
it 'fills in the assignee username when the assignee has been filtered' do
- send_keys_to_filtered_search('roo')
+ filtered_search.send_keys('roo')
click_assignee(user.name)
expect(page).to have_css(js_dropdown_assignee, visible: false)
- expect(filtered_search.value).to eq("assignee:@#{user.username} ")
+ expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
end
it 'selects `no assignee`' do
find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click
expect(page).to have_css(js_dropdown_assignee, visible: false)
- expect(filtered_search.value).to eq("assignee:none ")
+ expect_tokens([{ name: 'assignee', value: 'none' }])
+ expect_filtered_search_input_empty
end
end
@@ -173,7 +187,7 @@ describe 'Dropdown assignee', js: true, feature: true do
describe 'caching requests' do
it 'caches requests after the first load' do
filtered_search.set('assignee')
- send_keys_to_filtered_search(':')
+ filtered_search.send_keys(':')
initial_size = dropdown_assignee_size
expect(initial_size).to be > 0
@@ -182,7 +196,7 @@ describe 'Dropdown assignee', js: true, feature: true do
project.team << [new_user, :master]
find('.filtered-search-input-container .clear-search').click
filtered_search.set('assignee')
- send_keys_to_filtered_search(':')
+ filtered_search.send_keys(':')
expect(dropdown_assignee_size).to eq(initial_size)
end
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index 59e302f0e2d..19a00618b12 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -1,6 +1,7 @@
require 'rails_helper'
describe 'Dropdown author', js: true, feature: true do
+ include FilteredSearchHelpers
include WaitForAjax
let!(:project) { create(:empty_project) }
@@ -121,14 +122,16 @@ describe 'Dropdown author', js: true, feature: true do
click_author(user_jacob.name)
expect(page).to have_css(js_dropdown_author, visible: false)
- expect(filtered_search.value).to eq("author:@#{user_jacob.username} ")
+ expect_tokens([{ name: 'author', value: "@#{user_jacob.username}" }])
+ expect_filtered_search_input_empty
end
it 'fills in the author username when the author has been filtered' do
click_author(user.name)
expect(page).to have_css(js_dropdown_author, visible: false)
- expect(filtered_search.value).to eq("author:@#{user.username} ")
+ expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
index 04dd54ab459..01b657bcada 100644
--- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
@@ -1,6 +1,7 @@
require 'rails_helper'
describe 'Dropdown hint', js: true, feature: true do
+ include FilteredSearchHelpers
include WaitForAjax
let!(:project) { create(:empty_project) }
@@ -66,7 +67,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-author', visible: true)
- expect(filtered_search.value).to eq('author:')
+ expect_tokens([{ name: 'author' }])
+ expect_filtered_search_input_empty
end
it 'opens the assignee dropdown when you click on assignee' do
@@ -74,7 +76,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-assignee', visible: true)
- expect(filtered_search.value).to eq('assignee:')
+ expect_tokens([{ name: 'assignee' }])
+ expect_filtered_search_input_empty
end
it 'opens the milestone dropdown when you click on milestone' do
@@ -82,7 +85,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-milestone', visible: true)
- expect(filtered_search.value).to eq('milestone:')
+ expect_tokens([{ name: 'milestone' }])
+ expect_filtered_search_input_empty
end
it 'opens the label dropdown when you click on label' do
@@ -90,7 +94,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-label', visible: true)
- expect(filtered_search.value).to eq('label:')
+ expect_tokens([{ name: 'label' }])
+ expect_filtered_search_input_empty
end
end
@@ -101,7 +106,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-author', visible: true)
- expect(filtered_search.value).to eq('author:')
+ expect_tokens([{ name: 'author' }])
+ expect_filtered_search_input_empty
end
it 'opens the assignee dropdown when you click on assignee' do
@@ -110,7 +116,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-assignee', visible: true)
- expect(filtered_search.value).to eq('assignee:')
+ expect_tokens([{ name: 'assignee' }])
+ expect_filtered_search_input_empty
end
it 'opens the milestone dropdown when you click on milestone' do
@@ -119,7 +126,8 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-milestone', visible: true)
- expect(filtered_search.value).to eq('milestone:')
+ expect_tokens([{ name: 'milestone' }])
+ expect_filtered_search_input_empty
end
it 'opens the label dropdown when you click on label' do
@@ -128,7 +136,46 @@ describe 'Dropdown hint', js: true, feature: true do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-label', visible: true)
- expect(filtered_search.value).to eq('label:')
+ expect_tokens([{ name: 'label' }])
+ expect_filtered_search_input_empty
+ end
+ end
+
+ describe 'reselecting from dropdown' do
+ it 'reuses existing author text' do
+ filtered_search.send_keys('author:')
+ filtered_search.send_keys(:backspace)
+ click_hint('author')
+
+ expect_tokens([{ name: 'author' }])
+ expect_filtered_search_input_empty
+ end
+
+ it 'reuses existing assignee text' do
+ filtered_search.send_keys('assignee:')
+ filtered_search.send_keys(:backspace)
+ click_hint('assignee')
+
+ expect_tokens([{ name: 'assignee' }])
+ expect_filtered_search_input_empty
+ end
+
+ it 'reuses existing milestone text' do
+ filtered_search.send_keys('milestone:')
+ filtered_search.send_keys(:backspace)
+ click_hint('milestone')
+
+ expect_tokens([{ name: 'milestone' }])
+ expect_filtered_search_input_empty
+ end
+
+ it 'reuses existing label text' do
+ filtered_search.send_keys('label:')
+ filtered_search.send_keys(:backspace)
+ click_hint('label')
+
+ expect_tokens([{ name: 'label' }])
+ expect_filtered_search_input_empty
end
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index ab3b868fd3a..b192064b693 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -51,7 +51,8 @@ describe 'Dropdown label', js: true, feature: true do
filtered_search.native.send_keys(:down, :down, :enter)
- expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
+ expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }])
+ expect_filtered_search_input_empty
end
end
@@ -92,7 +93,7 @@ describe 'Dropdown label', js: true, feature: true do
end
it 'filters by case-insensitive name with or without symbol' do
- search_for_label('b')
+ filtered_search.send_keys('b')
expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible
expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible
@@ -101,7 +102,7 @@ describe 'Dropdown label', js: true, feature: true do
clear_search_field
init_label_search
- search_for_label('~bu')
+ filtered_search.send_keys('~bu')
expect(filter_dropdown.find('.filter-dropdown-item', text: bug_label.title)).to be_visible
expect(filter_dropdown.find('.filter-dropdown-item', text: uppercase_label.title)).to be_visible
@@ -180,7 +181,8 @@ describe 'Dropdown label', js: true, feature: true do
click_label(bug_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
+ expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }])
+ expect_filtered_search_input_empty
end
it 'fills in the label name when the label is partially filled' do
@@ -188,49 +190,56 @@ describe 'Dropdown label', js: true, feature: true do
click_label(bug_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
+ expect_tokens([{ name: 'label', value: "~#{bug_label.title}" }])
+ expect_filtered_search_input_empty
end
it 'fills in the label name that contains multiple words' do
click_label(two_words_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\" ")
+ expect_tokens([{ name: 'label', value: "\"#{two_words_label.title}\"" }])
+ expect_filtered_search_input_empty
end
it 'fills in the label name that contains multiple words and is very long' do
click_label(long_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect(filtered_search.value).to eq("label:~\"#{long_label.title}\" ")
+ expect_tokens([{ name: 'label', value: "\"#{long_label.title}\"" }])
+ expect_filtered_search_input_empty
end
it 'fills in the label name that contains double quotes' do
click_label(wont_fix_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}' ")
+ expect_tokens([{ name: 'label', value: "~'#{wont_fix_label.title}'" }])
+ expect_filtered_search_input_empty
end
it 'fills in the label name with the correct capitalization' do
click_label(uppercase_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect(filtered_search.value).to eq("label:~#{uppercase_label.title} ")
+ expect_tokens([{ name: 'label', value: "~#{uppercase_label.title}" }])
+ expect_filtered_search_input_empty
end
it 'fills in the label name with special characters' do
click_label(special_label.title)
expect(page).not_to have_css(js_dropdown_label)
- expect(filtered_search.value).to eq("label:~#{special_label.title} ")
+ expect_tokens([{ name: 'label', value: "~#{special_label.title}" }])
+ expect_filtered_search_input_empty
end
it 'selects `no label`' do
find("#{js_dropdown_label} .filter-dropdown-item", text: 'No Label').click
expect(page).not_to have_css(js_dropdown_label)
- expect(filtered_search.value).to eq("label:none ")
+ expect_tokens([{ name: 'label', value: 'none' }])
+ expect_filtered_search_input_empty
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index 0ce16715b86..85ffffe4b6d 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -1,7 +1,7 @@
require 'rails_helper'
-describe 'Dropdown milestone', js: true, feature: true do
- include WaitForAjax
+describe 'Dropdown milestone', :feature, :js do
+ include FilteredSearchHelpers
let!(:project) { create(:empty_project) }
let!(:user) { create(:user) }
@@ -14,18 +14,10 @@ describe 'Dropdown milestone', js: true, feature: true do
let(:filtered_search) { find('.filtered-search') }
let(:js_dropdown_milestone) { '#js-dropdown-milestone' }
-
- def send_keys_to_filtered_search(input)
- input.split("").each do |i|
- filtered_search.send_keys(i)
- sleep 3
- wait_for_ajax
- sleep 3
- end
- end
+ let(:filter_dropdown) { find("#{js_dropdown_milestone} .filter-dropdown") }
def dropdown_milestone_size
- page.all('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item').size
+ filter_dropdown.all('.filter-dropdown-item').size
end
def click_milestone(text)
@@ -64,13 +56,14 @@ describe 'Dropdown milestone', js: true, feature: true do
end
it 'should hide loading indicator when loaded' do
- send_keys_to_filtered_search('milestone:')
+ filtered_search.set('milestone:')
- expect(page).not_to have_css('#js-dropdown-milestone .filter-dropdown-loading')
+ expect(find(js_dropdown_milestone)).to have_css('.filter-dropdown-loading')
+ expect(find(js_dropdown_milestone)).not_to have_css('.filter-dropdown-loading')
end
it 'should load all the milestones when opened' do
- send_keys_to_filtered_search('milestone:')
+ filtered_search.set('milestone:')
expect(dropdown_milestone_size).to be > 0
end
@@ -78,41 +71,48 @@ describe 'Dropdown milestone', js: true, feature: true do
describe 'filtering' do
before do
- filtered_search.set('milestone')
+ filtered_search.set('milestone:')
+
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(milestone.title)
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(uppercase_milestone.title)
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(two_words_milestone.title)
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(wont_fix_milestone.title)
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(special_milestone.title)
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(long_milestone.title)
end
it 'filters by name' do
- send_keys_to_filtered_search(':v1')
+ filtered_search.send_keys('v1')
expect(dropdown_milestone_size).to eq(1)
end
it 'filters by case insensitive name' do
- send_keys_to_filtered_search(':V1')
+ filtered_search.send_keys('V1')
expect(dropdown_milestone_size).to eq(1)
end
it 'filters by name with symbol' do
- send_keys_to_filtered_search(':%v1')
+ filtered_search.send_keys('%v1')
expect(dropdown_milestone_size).to eq(1)
end
it 'filters by case insensitive name with symbol' do
- send_keys_to_filtered_search(':%V1')
+ filtered_search.send_keys('%V1')
expect(dropdown_milestone_size).to eq(1)
end
it 'filters by special characters' do
- send_keys_to_filtered_search(':(+')
+ filtered_search.send_keys('(+')
expect(dropdown_milestone_size).to eq(1)
end
it 'filters by special characters with symbol' do
- send_keys_to_filtered_search(':%(+')
+ filtered_search.send_keys('%(+')
expect(dropdown_milestone_size).to eq(1)
end
@@ -121,70 +121,86 @@ describe 'Dropdown milestone', js: true, feature: true do
describe 'selecting from dropdown' do
before do
filtered_search.set('milestone:')
+
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(milestone.title)
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(uppercase_milestone.title)
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(two_words_milestone.title)
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(wont_fix_milestone.title)
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(special_milestone.title)
+ expect(find("#{js_dropdown_milestone} .filter-dropdown")).to have_content(long_milestone.title)
end
it 'fills in the milestone name when the milestone has not been filled' do
click_milestone(milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%#{milestone.title} ")
+ expect_tokens([{ name: 'milestone', value: "%#{milestone.title}" }])
+ expect_filtered_search_input_empty
end
it 'fills in the milestone name when the milestone is partially filled' do
- send_keys_to_filtered_search('v')
+ filtered_search.send_keys('v')
click_milestone(milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%#{milestone.title} ")
+ expect_tokens([{ name: 'milestone', value: "%#{milestone.title}" }])
+ expect_filtered_search_input_empty
end
it 'fills in the milestone name that contains multiple words' do
click_milestone(two_words_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\" ")
+ expect_tokens([{ name: 'milestone', value: "%\"#{two_words_milestone.title}\"" }])
+ expect_filtered_search_input_empty
end
it 'fills in the milestone name that contains multiple words and is very long' do
click_milestone(long_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\" ")
+ expect_tokens([{ name: 'milestone', value: "%\"#{long_milestone.title}\"" }])
+ expect_filtered_search_input_empty
end
it 'fills in the milestone name that contains double quotes' do
click_milestone(wont_fix_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}' ")
+ expect_tokens([{ name: 'milestone', value: "%'#{wont_fix_milestone.title}'" }])
+ expect_filtered_search_input_empty
end
it 'fills in the milestone name with the correct capitalization' do
click_milestone(uppercase_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title} ")
+ expect_tokens([{ name: 'milestone', value: "%#{uppercase_milestone.title}" }])
+ expect_filtered_search_input_empty
end
it 'fills in the milestone name with special characters' do
click_milestone(special_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:%#{special_milestone.title} ")
+ expect_tokens([{ name: 'milestone', value: "%#{special_milestone.title}" }])
+ expect_filtered_search_input_empty
end
it 'selects `no milestone`' do
click_static_milestone('No Milestone')
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:none ")
+ expect_tokens([{ name: 'milestone', value: 'none' }])
+ expect_filtered_search_input_empty
end
it 'selects `upcoming milestone`' do
click_static_milestone('Upcoming')
expect(page).to have_css(js_dropdown_milestone, visible: false)
- expect(filtered_search.value).to eq("milestone:upcoming ")
+ expect_tokens([{ name: 'milestone', value: 'upcoming' }])
+ expect_filtered_search_input_empty
end
end
@@ -222,16 +238,14 @@ describe 'Dropdown milestone', js: true, feature: true do
describe 'caching requests' do
it 'caches requests after the first load' do
- filtered_search.set('milestone')
- send_keys_to_filtered_search(':')
+ filtered_search.set('milestone:')
initial_size = dropdown_milestone_size
expect(initial_size).to be > 0
create(:milestone, project: project)
find('.filtered-search-input-container .clear-search').click
- filtered_search.set('milestone')
- send_keys_to_filtered_search(':')
+ filtered_search.set('milestone:')
expect(dropdown_milestone_size).to eq(initial_size)
end
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 0420e64d42c..f079a9627e4 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -1,4 +1,4 @@
-require 'rails_helper'
+require 'spec_helper'
describe 'Filter issues', js: true, feature: true do
include FilteredSearchHelpers
@@ -97,7 +97,9 @@ describe 'Filter issues', js: true, feature: true do
it 'filters issues by searched author' do
input_filtered_search("author:@#{user.username}")
+ expect_tokens([{ name: 'author', value: user.username }])
expect_issues_list_count(5)
+ expect_filtered_search_input_empty
end
it 'filters issues by invalid author' do
@@ -110,36 +112,50 @@ describe 'Filter issues', js: true, feature: true do
end
context 'author with other filters' do
+ let(:search_term) { 'issue' }
+
it 'filters issues by searched author and text' do
- search = "author:@#{user.username} issue"
- input_filtered_search(search)
+ input_filtered_search("author:@#{user.username} #{search_term}")
+ expect_tokens([{ name: 'author', value: user.username }])
expect_issues_list_count(3)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched author, assignee and text' do
- search = "author:@#{user.username} assignee:@#{user.username} issue"
- input_filtered_search(search)
+ input_filtered_search("author:@#{user.username} assignee:@#{user.username} #{search_term}")
+ expect_tokens([
+ { name: 'author', value: user.username },
+ { name: 'assignee', value: user.username }
+ ])
expect_issues_list_count(3)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched author, assignee, label, and text' do
- search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} issue"
- input_filtered_search(search)
+ input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} #{search_term}")
+ expect_tokens([
+ { name: 'author', value: user.username },
+ { name: 'assignee', value: user.username },
+ { name: 'label', value: caps_sensitive_label.title }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched author, assignee, label, milestone and text' do
- search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} issue"
- input_filtered_search(search)
+ input_filtered_search("author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}")
+ expect_tokens([
+ { name: 'author', value: user.username },
+ { name: 'assignee', value: user.username },
+ { name: 'label', value: caps_sensitive_label.title },
+ { name: 'milestone', value: milestone.title }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
end
@@ -151,19 +167,19 @@ describe 'Filter issues', js: true, feature: true do
describe 'filter issues by assignee' do
context 'only assignee' do
it 'filters issues by searched assignee' do
- search = "assignee:@#{user.username}"
- input_filtered_search(search)
+ input_filtered_search("assignee:@#{user.username}")
+ expect_tokens([{ name: 'assignee', value: user.username }])
expect_issues_list_count(5)
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
it 'filters issues by no assignee' do
- search = "assignee:none"
- input_filtered_search(search)
+ input_filtered_search('assignee:none')
+ expect_tokens([{ name: 'assignee', value: 'none' }])
expect_issues_list_count(8, 1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
it 'filters issues by invalid assignee' do
@@ -176,36 +192,50 @@ describe 'Filter issues', js: true, feature: true do
end
context 'assignee with other filters' do
+ let(:search_term) { 'searchTerm' }
+
it 'filters issues by searched assignee and text' do
- search = "assignee:@#{user.username} searchTerm"
- input_filtered_search(search)
+ input_filtered_search("assignee:@#{user.username} #{search_term}")
+ expect_tokens([{ name: 'assignee', value: user.username }])
expect_issues_list_count(2)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched assignee, author and text' do
- search = "assignee:@#{user.username} author:@#{user.username} searchTerm"
- input_filtered_search(search)
+ input_filtered_search("assignee:@#{user.username} author:@#{user.username} #{search_term}")
+ expect_tokens([
+ { name: 'assignee', value: user.username },
+ { name: 'author', value: user.username }
+ ])
expect_issues_list_count(2)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched assignee, author, label, text' do
- search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm"
- input_filtered_search(search)
+ input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} #{search_term}")
+ expect_tokens([
+ { name: 'assignee', value: user.username },
+ { name: 'author', value: user.username },
+ { name: 'label', value: caps_sensitive_label.title }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched assignee, author, label, milestone and text' do
- search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm"
- input_filtered_search(search)
+ input_filtered_search("assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} #{search_term}")
+ expect_tokens([
+ { name: 'assignee', value: user.username },
+ { name: 'author', value: user.username },
+ { name: 'label', value: caps_sensitive_label.title },
+ { name: 'milestone', value: milestone.title }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
end
@@ -217,21 +247,23 @@ describe 'Filter issues', js: true, feature: true do
end
describe 'filter issues by label' do
+ let(:search_term) { 'bug' }
+
context 'only label' do
it 'filters issues by searched label' do
- search = "label:~#{bug_label.title}"
- input_filtered_search(search)
+ input_filtered_search("label:~#{bug_label.title}")
+ expect_tokens([{ name: 'label', value: bug_label.title }])
expect_issues_list_count(2)
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
it 'filters issues by no label' do
- search = "label:none"
- input_filtered_search(search)
+ input_filtered_search('label:none')
+ expect_tokens([{ name: 'label', value: 'none' }])
expect_issues_list_count(9, 1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
it 'filters issues by invalid label' do
@@ -239,11 +271,14 @@ describe 'Filter issues', js: true, feature: true do
end
it 'filters issues by multiple labels' do
- search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title}"
- input_filtered_search(search)
+ input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title}")
+ expect_tokens([
+ { name: 'label', value: bug_label.title },
+ { name: 'label', value: caps_sensitive_label.title }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
it 'filters issues by label containing special characters' do
@@ -251,21 +286,20 @@ describe 'Filter issues', js: true, feature: true do
special_issue = create(:issue, title: "Issue with special character label", project: project)
special_issue.labels << special_label
- search = "label:~#{special_label.title}"
- input_filtered_search(search)
-
+ input_filtered_search("label:~#{special_label.title}")
+ expect_tokens([{ name: 'label', value: special_label.title }])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
it 'does not show issues' do
- new_label = create(:label, project: project, title: "new_label")
+ new_label = create(:label, project: project, title: 'new_label')
- search = "label:~#{new_label.title}"
- input_filtered_search(search)
+ input_filtered_search("label:~#{new_label.title}")
+ expect_tokens([{ name: 'label', value: new_label.title }])
expect_no_issues_list()
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
end
@@ -275,29 +309,29 @@ describe 'Filter issues', js: true, feature: true do
special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project)
special_multiple_issue.labels << special_multiple_label
- search = "label:~'#{special_multiple_label.title}'"
- input_filtered_search(search)
+ input_filtered_search("label:~'#{special_multiple_label.title}'")
+ # filtered search defaults quotations to double quotes
+ expect_tokens([{ name: 'label', value: "\"#{special_multiple_label.title}\"" }])
expect_issues_list_count(1)
- # filtered search defaults quotations to double quotes
- expect_filtered_search_input("label:~\"#{special_multiple_label.title}\"")
+ expect_filtered_search_input_empty
end
it 'single quotes' do
- search = "label:~'#{multiple_words_label.title}'"
- input_filtered_search(search)
+ input_filtered_search("label:~'#{multiple_words_label.title}'")
+ expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }])
expect_issues_list_count(1)
- expect_filtered_search_input("label:~\"#{multiple_words_label.title}\"")
+ expect_filtered_search_input_empty
end
it 'double quotes' do
- search = "label:~\"#{multiple_words_label.title}\""
- input_filtered_search(search)
+ input_filtered_search("label:~\"#{multiple_words_label.title}\"")
+ expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
it 'single quotes containing double quotes' do
@@ -305,11 +339,11 @@ describe 'Filter issues', js: true, feature: true do
double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project)
double_quotes_label_issue.labels << double_quotes_label
- search = "label:~'#{double_quotes_label.title}'"
- input_filtered_search(search)
+ input_filtered_search("label:~'#{double_quotes_label.title}'")
+ expect_tokens([{ name: 'label', value: "'#{double_quotes_label.title}'" }])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
it 'double quotes containing single quotes' do
@@ -317,86 +351,115 @@ describe 'Filter issues', js: true, feature: true do
single_quotes_label_issue = create(:issue, title: "Issue with single quotes label", project: project)
single_quotes_label_issue.labels << single_quotes_label
- search = "label:~\"#{single_quotes_label.title}\""
- input_filtered_search(search)
+ input_filtered_search("label:~\"#{single_quotes_label.title}\"")
+ expect_tokens([{ name: 'label', value: "\"#{single_quotes_label.title}\"" }])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
end
context 'label with other filters' do
it 'filters issues by searched label and text' do
- search = "label:~#{caps_sensitive_label.title} bug"
- input_filtered_search(search)
+ input_filtered_search("label:~#{caps_sensitive_label.title} #{search_term}")
+ expect_tokens([{ name: 'label', value: caps_sensitive_label.title }])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched label, author and text' do
- search = "label:~#{caps_sensitive_label.title} author:@#{user.username} bug"
- input_filtered_search(search)
+ input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} #{search_term}")
+ expect_tokens([
+ { name: 'label', value: caps_sensitive_label.title },
+ { name: 'author', value: user.username }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched label, author, assignee and text' do
- search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug"
- input_filtered_search(search)
+ input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} #{search_term}")
+ expect_tokens([
+ { name: 'label', value: caps_sensitive_label.title },
+ { name: 'author', value: user.username },
+ { name: 'assignee', value: user.username }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched label, author, assignee, milestone and text' do
- search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug"
- input_filtered_search(search)
+ input_filtered_search("label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} #{search_term}")
+ expect_tokens([
+ { name: 'label', value: caps_sensitive_label.title },
+ { name: 'author', value: user.username },
+ { name: 'assignee', value: user.username },
+ { name: 'milestone', value: milestone.title }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
end
context 'multiple labels with other filters' do
it 'filters issues by searched label, label2, and text' do
- search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug"
- input_filtered_search(search)
+ input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} #{search_term}")
+ expect_tokens([
+ { name: 'label', value: bug_label.title },
+ { name: 'label', value: caps_sensitive_label.title }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched label, label2, author and text' do
- search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} bug"
- input_filtered_search(search)
+ input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} #{search_term}")
+ expect_tokens([
+ { name: 'label', value: bug_label.title },
+ { name: 'label', value: caps_sensitive_label.title },
+ { name: 'author', value: user.username }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched label, label2, author, assignee and text' do
- search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug"
- input_filtered_search(search)
+ input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} #{search_term}")
+ expect_tokens([
+ { name: 'label', value: bug_label.title },
+ { name: 'label', value: caps_sensitive_label.title },
+ { name: 'author', value: user.username },
+ { name: 'assignee', value: user.username }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched label, label2, author, assignee, milestone and text' do
- search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug"
- input_filtered_search(search)
+ input_filtered_search("label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} #{search_term}")
+ expect_tokens([
+ { name: 'label', value: bug_label.title },
+ { name: 'label', value: caps_sensitive_label.title },
+ { name: 'author', value: user.username },
+ { name: 'assignee', value: user.username },
+ { name: 'milestone', value: milestone.title }
+ ])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
end
context 'issue label clicked' do
before do
find('.issues-list .issue .issue-info a .label', text: multiple_words_label.title).click
- sleep 1
end
it 'filters' do
@@ -404,7 +467,8 @@ describe 'Filter issues', js: true, feature: true do
end
it 'displays in search bar' do
- expect(find('.filtered-search').value).to eq("label:~\"#{multiple_words_label.title}\"")
+ expect_tokens([{ name: 'label', value: "\"#{multiple_words_label.title}\"" }])
+ expect_filtered_search_input_empty
end
end
@@ -420,19 +484,25 @@ describe 'Filter issues', js: true, feature: true do
it 'filters issues by searched milestone' do
input_filtered_search("milestone:%#{milestone.title}")
+ expect_tokens([{ name: 'milestone', value: milestone.title }])
expect_issues_list_count(5)
+ expect_filtered_search_input_empty
end
it 'filters issues by no milestone' do
input_filtered_search("milestone:none")
+ expect_tokens([{ name: 'milestone', value: 'none' }])
expect_issues_list_count(7, 1)
+ expect_filtered_search_input_empty
end
it 'filters issues by upcoming milestones' do
input_filtered_search("milestone:upcoming")
+ expect_tokens([{ name: 'milestone', value: 'upcoming' }])
expect_issues_list_count(1)
+ expect_filtered_search_input_empty
end
it 'filters issues by invalid milestones' do
@@ -447,55 +517,69 @@ describe 'Filter issues', js: true, feature: true do
special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project)
create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone)
- search = "milestone:%#{special_milestone.title}"
- input_filtered_search(search)
+ input_filtered_search("milestone:%#{special_milestone.title}")
+ expect_tokens([{ name: 'milestone', value: special_milestone.title }])
expect_issues_list_count(1)
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
it 'does not show issues' do
new_milestone = create(:milestone, title: "new", project: project)
- search = "milestone:%#{new_milestone.title}"
- input_filtered_search(search)
+ input_filtered_search("milestone:%#{new_milestone.title}")
+ expect_tokens([{ name: 'milestone', value: new_milestone.title }])
expect_no_issues_list()
- expect_filtered_search_input(search)
+ expect_filtered_search_input_empty
end
end
context 'milestone with other filters' do
+ let(:search_term) { 'bug' }
+
it 'filters issues by searched milestone and text' do
- search = "milestone:%#{milestone.title} bug"
- input_filtered_search(search)
+ input_filtered_search("milestone:%#{milestone.title} #{search_term}")
+ expect_tokens([{ name: 'milestone', value: milestone.title }])
expect_issues_list_count(2)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched milestone, author and text' do
- search = "milestone:%#{milestone.title} author:@#{user.username} bug"
- input_filtered_search(search)
+ input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} #{search_term}")
+ expect_tokens([
+ { name: 'milestone', value: milestone.title },
+ { name: 'author', value: user.username }
+ ])
expect_issues_list_count(2)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched milestone, author, assignee and text' do
- search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} bug"
- input_filtered_search(search)
+ input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} #{search_term}")
+ expect_tokens([
+ { name: 'milestone', value: milestone.title },
+ { name: 'author', value: user.username },
+ { name: 'assignee', value: user.username }
+ ])
expect_issues_list_count(2)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
it 'filters issues by searched milestone, author, assignee, label and text' do
- search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug"
- input_filtered_search(search)
-
+ input_filtered_search("milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} #{search_term}")
+
+ expect_tokens([
+ { name: 'milestone', value: milestone.title },
+ { name: 'author', value: user.username },
+ { name: 'assignee', value: user.username },
+ { name: 'label', value: bug_label.title }
+ ])
expect_issues_list_count(2)
- expect_filtered_search_input(search)
+ expect_filtered_search_input(search_term)
end
end
@@ -506,44 +590,6 @@ describe 'Filter issues', js: true, feature: true do
end
end
- describe 'overwrites selected filter' do
- it 'changes author' do
- input_filtered_search("author:@#{user.username}", submit: false)
-
- select_search_at_index(3)
-
- page.within '#js-dropdown-author' do
- click_button user2.username
- end
-
- expect(filtered_search.value).to eq("author:@#{user2.username} ")
- end
-
- it 'changes label' do
- input_filtered_search("author:@#{user.username} label:~#{bug_label.title}", submit: false)
-
- select_search_at_index(27)
-
- page.within '#js-dropdown-label' do
- click_button label.name
- end
-
- expect(filtered_search.value).to eq("author:@#{user.username} label:~#{label.name} ")
- end
-
- it 'changes label correctly space is in previous label' do
- input_filtered_search("label:~\"#{multiple_words_label.title}\"", submit: false)
-
- select_search_at_index(0)
-
- page.within '#js-dropdown-label' do
- click_button label.name
- end
-
- expect(filtered_search.value).to eq("label:~#{label.name} ")
- end
- end
-
describe 'filter issues by text' do
context 'only text' do
it 'filters issues by searched text' do
@@ -605,80 +651,81 @@ describe 'Filter issues', js: true, feature: true do
context 'searched text with other filters' do
it 'filters issues by searched text and author' do
+ # After searching, all search terms are placed at the end
input_filtered_search("bug author:@#{user.username}")
expect_issues_list_count(2)
- expect_filtered_search_input("author:@#{user.username} bug")
+ expect_filtered_search_input('bug')
end
it 'filters issues by searched text, author and more text' do
input_filtered_search("bug author:@#{user.username} report")
expect_issues_list_count(1)
- expect_filtered_search_input("author:@#{user.username} bug report")
+ expect_filtered_search_input('bug report')
end
it 'filters issues by searched text, author and assignee' do
input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}")
expect_issues_list_count(2)
- expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug")
+ expect_filtered_search_input('bug')
end
it 'filters issues by searched text, author, more text and assignee' do
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}")
expect_issues_list_count(1)
- expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report")
+ expect_filtered_search_input('bug report')
end
it 'filters issues by searched text, author, more text, assignee and even more text' do
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with")
expect_issues_list_count(1)
- expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report with")
+ expect_filtered_search_input('bug report with')
end
it 'filters issues by searched text, author, assignee and label' do
input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title}")
expect_issues_list_count(2)
- expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug")
+ expect_filtered_search_input('bug')
end
it 'filters issues by searched text, author, text, assignee, text, label and text' do
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything")
expect_issues_list_count(1)
- expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug report with everything")
+ expect_filtered_search_input('bug report with everything')
end
it 'filters issues by searched text, author, assignee, label and milestone' do
input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title}")
expect_issues_list_count(2)
- expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug")
+ expect_filtered_search_input('bug')
end
it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you")
expect_issues_list_count(1)
- expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug report with everything you")
+ expect_filtered_search_input('bug report with everything you')
end
it 'filters issues by searched text, author, assignee, multiple labels and milestone' do
input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title}")
expect_issues_list_count(1)
- expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug")
+ expect_filtered_search_input('bug')
end
it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do
input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought")
expect_issues_list_count(1)
- expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug report with everything you thought")
+ expect_filtered_search_input('bug report with everything you thought')
end
end
@@ -717,8 +764,8 @@ describe 'Filter issues', js: true, feature: true do
before do
input_filtered_search('bug')
- # Wait for search results to load
- sleep 2
+ # This ensures that the search is performed
+ expect_issues_list_count(4, 1)
end
it 'open state' do
diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb
index 90eb60eb337..59244d65eec 100644
--- a/spec/features/issues/filtered_search/search_bar_spec.rb
+++ b/spec/features/issues/filtered_search/search_bar_spec.rb
@@ -1,6 +1,7 @@
require 'rails_helper'
describe 'Search bar', js: true, feature: true do
+ include FilteredSearchHelpers
include WaitForAjax
let!(:project) { create(:empty_project) }
@@ -32,7 +33,8 @@ describe 'Search bar', js: true, feature: true do
it 'selects item' do
filtered_search.native.send_keys(:down, :down, :enter)
- expect(filtered_search.value).to eq('author:')
+ expect_tokens([{ name: 'author' }])
+ expect_filtered_search_input_empty
end
end
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
new file mode 100644
index 00000000000..96e87c82d2c
--- /dev/null
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -0,0 +1,352 @@
+require 'rails_helper'
+
+describe 'Visual tokens', js: true, feature: true do
+ include FilteredSearchHelpers
+
+ let!(:project) { create(:empty_project) }
+ let!(:user) { create(:user, name: 'administrator', username: 'root') }
+ let!(:user_rock) { create(:user, name: 'The Rock', username: 'rock') }
+ let!(:milestone_nine) { create(:milestone, title: '9.0', project: project) }
+ let!(:milestone_ten) { create(:milestone, title: '10.0', project: project) }
+ let!(:label) { create(:label, project: project, title: 'abc') }
+ let!(:cc_label) { create(:label, project: project, title: 'Community Contribution') }
+
+ let(:filtered_search) { find('.filtered-search') }
+ let(:filter_author_dropdown) { find("#js-dropdown-author .filter-dropdown") }
+ let(:filter_assignee_dropdown) { find("#js-dropdown-assignee .filter-dropdown") }
+ let(:filter_milestone_dropdown) { find("#js-dropdown-milestone .filter-dropdown") }
+ let(:filter_label_dropdown) { find("#js-dropdown-label .filter-dropdown") }
+
+ def is_input_focused
+ page.evaluate_script("document.activeElement.classList.contains('filtered-search')")
+ end
+
+ before do
+ project.add_user(user, :master)
+ project.add_user(user_rock, :master)
+ login_as(user)
+ create(:issue, project: project)
+
+ visit namespace_project_issues_path(project.namespace, project)
+ end
+
+ describe 'editing author token' do
+ before do
+ input_filtered_search('author:@root assignee:none', submit: false)
+ first('.tokens-container .filtered-search-token').double_click
+ end
+
+ it 'opens author dropdown' do
+ expect(page).to have_css('#js-dropdown-author', visible: true)
+ end
+
+ it 'makes value editable' do
+ expect_filtered_search_input('@root')
+ end
+
+ it 'filters value' do
+ filtered_search.send_keys(:backspace)
+
+ expect(page).to have_css('#js-dropdown-author .filter-dropdown .filter-dropdown-item', count: 1)
+ end
+
+ it 'ends editing mode when document is clicked' do
+ find('#content-body').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-author', visible: false)
+ end
+
+ it 'ends editing mode when scroll container is clicked' do
+ find('.scroll-container').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-author', visible: false)
+ end
+
+ describe 'selecting different author from dropdown' do
+ before do
+ filter_author_dropdown.find('.filter-dropdown-item .dropdown-light-content', text: "@#{user_rock.username}").click
+ end
+
+ it 'changes value in visual token' do
+ expect(first('.tokens-container .filtered-search-token .value').text).to eq("@#{user_rock.username}")
+ end
+
+ it 'moves input to the right' do
+ expect(is_input_focused).to eq(true)
+ end
+ end
+ end
+
+ describe 'editing assignee token' do
+ before do
+ input_filtered_search('assignee:@root author:none', submit: false)
+ first('.tokens-container .filtered-search-token').double_click
+ end
+
+ it 'opens assignee dropdown' do
+ expect(page).to have_css('#js-dropdown-assignee', visible: true)
+ end
+
+ it 'makes value editable' do
+ expect_filtered_search_input('@root')
+ end
+
+ it 'filters value' do
+ filtered_search.send_keys(:backspace)
+
+ expect(page).to have_css('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item', count: 1)
+ end
+
+ it 'ends editing mode when document is clicked' do
+ find('#content-body').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-assignee', visible: false)
+ end
+
+ it 'ends editing mode when scroll container is clicked' do
+ find('.scroll-container').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-assignee', visible: false)
+ end
+
+ describe 'selecting static option from dropdown' do
+ before do
+ find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'No Assignee').click
+ end
+
+ it 'changes value in visual token' do
+ expect(first('.tokens-container .filtered-search-token .value').text).to eq('none')
+ end
+
+ it 'moves input to the right' do
+ expect(is_input_focused).to eq(true)
+ end
+ end
+ end
+
+ describe 'editing milestone token' do
+ before do
+ input_filtered_search('milestone:%10.0 author:none', submit: false)
+ first('.tokens-container .filtered-search-token').double_click
+ first('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item')
+ end
+
+ it 'opens milestone dropdown' do
+ expect(filter_milestone_dropdown.find('.filter-dropdown-item', text: milestone_ten.title)).to be_visible
+ expect(filter_milestone_dropdown.find('.filter-dropdown-item', text: milestone_nine.title)).to be_visible
+ expect(page).to have_css('#js-dropdown-milestone', visible: true)
+ end
+
+ it 'selects static option from dropdown' do
+ find("#js-dropdown-milestone").find('.filter-dropdown-item', text: 'Upcoming').click
+
+ expect(first('.tokens-container .filtered-search-token .value').text).to eq('upcoming')
+ expect(is_input_focused).to eq(true)
+ end
+
+ it 'makes value editable' do
+ expect_filtered_search_input('%10.0')
+ end
+
+ it 'filters value' do
+ filtered_search.send_keys(:backspace)
+
+ expect(page).to have_css('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item', count: 1)
+ end
+
+ it 'ends editing mode when document is clicked' do
+ find('#content-body').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-milestone', visible: false)
+ end
+
+ it 'ends editing mode when scroll container is clicked' do
+ find('.scroll-container').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-milestone', visible: false)
+ end
+ end
+
+ describe 'editing label token' do
+ before do
+ input_filtered_search("label:~#{label.title} author:none", submit: false)
+ first('.tokens-container .filtered-search-token').double_click
+ first('#js-dropdown-label .filter-dropdown .filter-dropdown-item')
+ end
+
+ it 'opens label dropdown' do
+ expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
+ expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible
+ expect(page).to have_css('#js-dropdown-label', visible: true)
+ end
+
+ it 'selects option from dropdown' do
+ expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
+ expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible
+
+ find("#js-dropdown-label").find('.filter-dropdown-item', text: cc_label.title).click
+
+ expect(first('.tokens-container .filtered-search-token .value').text).to eq("~\"#{cc_label.title}\"")
+ expect(is_input_focused).to eq(true)
+ end
+
+ it 'makes value editable' do
+ expect_filtered_search_input("~#{label.title}")
+ end
+
+ it 'filters value' do
+ expect(filter_label_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
+ expect(filter_label_dropdown.find('.filter-dropdown-item', text: cc_label.title)).to be_visible
+
+ filtered_search.send_keys(:backspace)
+
+ filter_label_dropdown.find('.filter-dropdown-item')
+
+ expect(page.all('#js-dropdown-label .filter-dropdown .filter-dropdown-item').size).to eq(1)
+ end
+
+ it 'ends editing mode when document is clicked' do
+ find('#content-body').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-label', visible: false)
+ end
+
+ it 'ends editing mode when scroll container is clicked' do
+ find('.scroll-container').click
+
+ expect_filtered_search_input_empty
+ expect(page).to have_css('#js-dropdown-label', visible: false)
+ end
+ end
+
+ describe 'editing multiple tokens' do
+ before do
+ input_filtered_search('author:@root assignee:none', submit: false)
+ first('.tokens-container .filtered-search-token').double_click
+ end
+
+ it 'opens author dropdown' do
+ expect(page).to have_css('#js-dropdown-author', visible: true)
+ end
+
+ it 'opens assignee dropdown' do
+ find('.tokens-container .filtered-search-token', text: 'Assignee').double_click
+ expect(page).to have_css('#js-dropdown-assignee', visible: true)
+ end
+ end
+
+ describe 'editing a search term while editing another filter token' do
+ before do
+ input_filtered_search('author assignee:', submit: false)
+ first('.tokens-container .filtered-search-term').double_click
+ end
+
+ it 'opens hint dropdown' do
+ expect(page).to have_css('#js-dropdown-hint', visible: true)
+ end
+
+ it 'opens author dropdown' do
+ find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: 'author').click
+
+ expect(page).to have_css('#js-dropdown-author', visible: true)
+ end
+ end
+
+ describe 'add new token after editing existing token' do
+ before do
+ input_filtered_search('author:@root assignee:none', submit: false)
+ first('.tokens-container .filtered-search-token').double_click
+ filtered_search.send_keys(' ')
+ end
+
+ describe 'opens dropdowns' do
+ it 'opens hint dropdown' do
+ expect(page).to have_css('#js-dropdown-hint', visible: true)
+ end
+
+ it 'opens author dropdown' do
+ filtered_search.send_keys('author:')
+ expect(page).to have_css('#js-dropdown-author', visible: true)
+ end
+
+ it 'opens assignee dropdown' do
+ filtered_search.send_keys('assignee:')
+ expect(page).to have_css('#js-dropdown-assignee', visible: true)
+ end
+
+ it 'opens milestone dropdown' do
+ filtered_search.send_keys('milestone:')
+ expect(page).to have_css('#js-dropdown-milestone', visible: true)
+ end
+
+ it 'opens label dropdown' do
+ filtered_search.send_keys('label:')
+ expect(page).to have_css('#js-dropdown-label', visible: true)
+ end
+ end
+
+ describe 'creates visual tokens' do
+ it 'creates author token' do
+ filtered_search.send_keys('author:@thomas ')
+ token = page.all('.tokens-container .filtered-search-token')[1]
+
+ expect(token.find('.name').text).to eq('Author')
+ expect(token.find('.value').text).to eq('@thomas')
+ end
+
+ it 'creates assignee token' do
+ filtered_search.send_keys('assignee:@thomas ')
+ token = page.all('.tokens-container .filtered-search-token')[1]
+
+ expect(token.find('.name').text).to eq('Assignee')
+ expect(token.find('.value').text).to eq('@thomas')
+ end
+
+ it 'creates milestone token' do
+ filtered_search.send_keys('milestone:none ')
+ token = page.all('.tokens-container .filtered-search-token')[1]
+
+ expect(token.find('.name').text).to eq('Milestone')
+ expect(token.find('.value').text).to eq('none')
+ end
+
+ it 'creates label token' do
+ filtered_search.send_keys('label:~Backend ')
+ token = page.all('.tokens-container .filtered-search-token')[1]
+
+ expect(token.find('.name').text).to eq('Label')
+ expect(token.find('.value').text).to eq('~Backend')
+ end
+ end
+
+ it 'does not tokenize incomplete token' do
+ filtered_search.send_keys('author:')
+
+ find('#content-body').click
+ token = page.all('.tokens-container .js-visual-token')[1]
+
+ expect_filtered_search_input_empty
+ expect(token.find('.name').text).to eq('Author')
+ end
+ end
+
+ describe 'search using incomplete visual tokens' do
+ before do
+ input_filtered_search('author:@root assignee:none', extra_space: false)
+ end
+
+ it 'tokenizes the search term to complete visual token' do
+ expect_tokens([
+ { name: 'author', value: '@root' },
+ { name: 'assignee', value: 'none' }
+ ])
+ end
+ end
+end
diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb
new file mode 100644
index 00000000000..7df102067d6
--- /dev/null
+++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb
@@ -0,0 +1,136 @@
+require 'spec_helper'
+
+feature 'Diff note avatars', feature: true, js: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") }
+ let(:path) { "files/ruby/popen.rb" }
+ let(:position) do
+ Gitlab::Diff::Position.new(
+ old_path: path,
+ new_path: path,
+ old_line: nil,
+ new_line: 9,
+ diff_refs: merge_request.diff_refs
+ )
+ end
+ let!(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ end
+
+ %w(inline parallel).each do |view|
+ context "#{view} view" do
+ before do
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: view)
+
+ wait_for_ajax
+ end
+
+ it 'shows note avatar' do
+ page.within find("[id='#{position.line_code(project.repository)}']") do
+ find('.diff-notes-collapse').click
+
+ expect(page).to have_selector('img.js-diff-comment-avatar', count: 1)
+ end
+ end
+
+ it 'shows comment on note avatar' do
+ page.within find("[id='#{position.line_code(project.repository)}']") do
+ find('.diff-notes-collapse').click
+
+ expect(first('img.js-diff-comment-avatar')["title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}")
+ end
+ end
+
+ it 'toggles comments when clicking avatar' do
+ page.within find("[id='#{position.line_code(project.repository)}']") do
+ find('.diff-notes-collapse').click
+ end
+
+ expect(page).to have_selector('.notes_holder', visible: false)
+
+ page.within find("[id='#{position.line_code(project.repository)}']") do
+ first('img.js-diff-comment-avatar').click
+ end
+
+ expect(page).to have_selector('.notes_holder')
+ end
+
+ it 'removes avatar when note is deleted' do
+ page.within find(".note-row-#{note.id}") do
+ find('.js-note-delete').click
+ end
+
+ wait_for_ajax
+
+ page.within find("[id='#{position.line_code(project.repository)}']") do
+ expect(page).not_to have_selector('img.js-diff-comment-avatar')
+ end
+ end
+
+ it 'adds avatar when commenting' do
+ click_button 'Reply...'
+
+ page.within '.js-discussion-note-form' do
+ find('.js-note-text').native.send_keys('Test')
+
+ click_button 'Comment'
+
+ wait_for_ajax
+ end
+
+ page.within find("[id='#{position.line_code(project.repository)}']") do
+ find('.diff-notes-collapse').click
+
+ expect(page).to have_selector('img.js-diff-comment-avatar', count: 2)
+ end
+ end
+
+ it 'adds multiple comments' do
+ 3.times do
+ click_button 'Reply...'
+
+ page.within '.js-discussion-note-form' do
+ find('.js-note-text').native.send_keys('Test')
+
+ find('.js-comment-button').trigger 'click'
+
+ wait_for_ajax
+ end
+ end
+
+ page.within find("[id='#{position.line_code(project.repository)}']") do
+ find('.diff-notes-collapse').click
+
+ expect(page).to have_selector('img.js-diff-comment-avatar', count: 3)
+ expect(find('.diff-comments-more-count')).to have_content '+1'
+ end
+ end
+
+ context 'multiple comments' do
+ before do
+ create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
+ create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
+ create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position)
+
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: view)
+
+ wait_for_ajax
+ end
+
+ it 'shows extra comment count' do
+ page.within find("[id='#{position.line_code(project.repository)}']") do
+ find('.diff-notes-collapse').click
+
+ expect(find('.diff-comments-more-count')).to have_content '+1'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index 5608cda28f8..265a0cfc198 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -25,6 +25,9 @@ feature 'Merge Request filtering by Milestone', feature: true do
visit_merge_requests(project)
input_filtered_search('milestone:none')
+ expect_tokens([{ name: 'milestone', value: 'none' }])
+ expect_filtered_search_input_empty
+
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1)
end
diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb
index 6579a88d4ab..70e3997e716 100644
--- a/spec/features/merge_requests/filter_merge_requests_spec.rb
+++ b/spec/features/merge_requests/filter_merge_requests_spec.rb
@@ -24,6 +24,11 @@ describe 'Filter merge requests', feature: true do
describe 'for assignee from mr#index' do
let(:search_query) { "assignee:@#{user.username}" }
+ def expect_assignee_visual_tokens
+ expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
+ end
+
before do
input_filtered_search(search_query)
@@ -32,25 +37,30 @@ describe 'Filter merge requests', feature: true do
context 'assignee', js: true do
it 'updates to current user' do
- expect_filtered_search_input(search_query)
+ expect_assignee_visual_tokens()
end
it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
- expect_filtered_search_input(search_query)
+ expect_assignee_visual_tokens()
end
it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
- expect_filtered_search_input(search_query)
+ expect_assignee_visual_tokens()
end
end
end
describe 'for milestone from mr#index' do
- let(:search_query) { "milestone:%#{milestone.title}" }
+ let(:search_query) { "milestone:%\"#{milestone.title}\"" }
+
+ def expect_milestone_visual_tokens
+ expect_tokens([{ name: 'milestone', value: "%\"#{milestone.title}\"" }])
+ expect_filtered_search_input_empty
+ end
before do
input_filtered_search(search_query)
@@ -60,19 +70,19 @@ describe 'Filter merge requests', feature: true do
context 'milestone', js: true do
it 'updates to current milestone' do
- expect_filtered_search_input(search_query)
+ expect_milestone_visual_tokens()
end
it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
- expect_filtered_search_input(search_query)
+ expect_milestone_visual_tokens()
end
it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
- expect_filtered_search_input(search_query)
+ expect_milestone_visual_tokens()
end
end
end
@@ -82,35 +92,44 @@ describe 'Filter merge requests', feature: true do
input_filtered_search('label:none')
expect_mr_list_count(1)
- expect_filtered_search_input('label:none')
+ expect_tokens([{ name: 'label', value: 'none' }])
+ expect_filtered_search_input_empty
end
it 'filters by a label' do
input_filtered_search("label:~#{label.title}")
expect_mr_list_count(0)
- expect_filtered_search_input("label:~#{label.title}")
+ expect_tokens([{ name: 'label', value: "~#{label.title}" }])
+ expect_filtered_search_input_empty
end
it "filters by `won't fix` and another label" do
input_filtered_search("label:~\"#{wontfix.title}\" label:~#{label.title}")
expect_mr_list_count(0)
- expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}")
+ expect_tokens([
+ { name: 'label', value: "~\"#{wontfix.title}\"" },
+ { name: 'label', value: "~#{label.title}" }
+ ])
+ expect_filtered_search_input_empty
end
it "filters by `won't fix` label followed by another label after page load" do
input_filtered_search("label:~\"#{wontfix.title}\"")
expect_mr_list_count(0)
- expect_filtered_search_input("label:~\"#{wontfix.title}\"")
-
- input_filtered_search_keys(" label:~#{label.title}")
+ expect_tokens([{ name: 'label', value: "~\"#{wontfix.title}\"" }])
+ expect_filtered_search_input_empty
- expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}")
+ input_filtered_search_keys("label:~#{label.title}")
expect_mr_list_count(0)
- expect_filtered_search_input("label:~\"#{wontfix.title}\" label:~#{label.title}")
+ expect_tokens([
+ { name: 'label', value: "~\"#{wontfix.title}\"" },
+ { name: 'label', value: "~#{label.title}" }
+ ])
+ expect_filtered_search_input_empty
end
end
@@ -121,9 +140,10 @@ describe 'Filter merge requests', feature: true do
input_filtered_search("assignee:@#{user.username}")
expect_mr_list_count(1)
- expect_filtered_search_input("assignee:@#{user.username}")
+ expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
- input_filtered_search_keys(" label:~#{label.title}")
+ input_filtered_search_keys("label:~#{label.title} ")
expect_mr_list_count(1)
@@ -131,20 +151,28 @@ describe 'Filter merge requests', feature: true do
end
context 'assignee and label', js: true do
+ def expect_assignee_label_visual_tokens
+ expect_tokens([
+ { name: 'assignee', value: "@#{user.username}" },
+ { name: 'label', value: "~#{label.title}" }
+ ])
+ expect_filtered_search_input_empty
+ end
+
it 'updates to current assignee and label' do
- expect_filtered_search_input(search_query)
+ expect_assignee_label_visual_tokens()
end
it 'does not change when closed link is clicked' do
find('.issues-state-filters a', text: "Closed").click
- expect_filtered_search_input(search_query)
+ expect_assignee_label_visual_tokens()
end
it 'does not change when all link is clicked' do
find('.issues-state-filters a', text: "All").click
- expect_filtered_search_input(search_query)
+ expect_assignee_label_visual_tokens()
end
end
end
@@ -195,6 +223,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys(' label:~bug')
expect_mr_list_count(1)
+ expect_tokens([{ name: 'label', value: '~bug' }])
+ expect_filtered_search_input('Bug')
end
it 'filters by text and milestone' do
@@ -206,6 +236,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys(' milestone:%8')
expect_mr_list_count(1)
+ expect_tokens([{ name: 'milestone', value: '%8' }])
+ expect_filtered_search_input('Bug')
end
it 'filters by text and assignee' do
@@ -217,6 +249,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys(" assignee:@#{user.username}")
expect_mr_list_count(1)
+ expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ expect_filtered_search_input('Bug')
end
it 'filters by text and author' do
@@ -228,6 +262,8 @@ describe 'Filter merge requests', feature: true do
input_filtered_search_keys(" author:@#{user.username}")
expect_mr_list_count(1)
+ expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+ expect_filtered_search_input('Bug')
end
end
end
@@ -266,7 +302,8 @@ describe 'Filter merge requests', feature: true do
it 'filter by current user' do
visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: user.id)
- expect_filtered_search_input("assignee:@#{user.username}")
+ expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
end
it 'filter by new user' do
@@ -275,7 +312,8 @@ describe 'Filter merge requests', feature: true do
visit namespace_project_merge_requests_path(project.namespace, project, assignee_id: new_user.id)
- expect_filtered_search_input("assignee:@#{new_user.username}")
+ expect_tokens([{ name: 'assignee', value: "@#{new_user.username}" }])
+ expect_filtered_search_input_empty
end
end
@@ -283,7 +321,8 @@ describe 'Filter merge requests', feature: true do
it 'filter by current user' do
visit namespace_project_merge_requests_path(project.namespace, project, author_id: user.id)
- expect_filtered_search_input("author:@#{user.username}")
+ expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
end
it 'filter by new user' do
@@ -292,7 +331,8 @@ describe 'Filter merge requests', feature: true do
visit namespace_project_merge_requests_path(project.namespace, project, author_id: new_user.id)
- expect_filtered_search_input("author:@#{new_user.username}")
+ expect_tokens([{ name: 'author', value: "@#{new_user.username}" }])
+ expect_filtered_search_input_empty
end
end
end
diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb
index 58f11499e3f..6fed1568fcf 100644
--- a/spec/features/merge_requests/reset_filters_spec.rb
+++ b/spec/features/merge_requests/reset_filters_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Issues filter reset button', feature: true, js: true do
+feature 'Merge requests filter clear button', feature: true, js: true do
include FilteredSearchHelpers
include MergeRequestHelpers
include WaitForAjax
@@ -24,67 +24,93 @@ feature 'Issues filter reset button', feature: true, js: true do
context 'when a milestone filter has been applied' do
it 'resets the milestone filter' do
visit_merge_requests(project, milestone_title: milestone.title)
+
expect(page).to have_css(merge_request_css, count: 1)
+ expect(get_filtered_search_placeholder).to eq('')
reset_filters
+
expect(page).to have_css(merge_request_css, count: 2)
+ expect(get_filtered_search_placeholder).to eq(default_placeholder)
end
end
context 'when a label filter has been applied' do
it 'resets the label filter' do
visit_merge_requests(project, label_name: bug.name)
+
expect(page).to have_css(merge_request_css, count: 1)
+ expect(get_filtered_search_placeholder).to eq('')
reset_filters
+
expect(page).to have_css(merge_request_css, count: 2)
+ expect(get_filtered_search_placeholder).to eq(default_placeholder)
end
end
context 'when a text search has been conducted' do
it 'resets the text search filter' do
visit_merge_requests(project, search: 'Bug')
+
expect(page).to have_css(merge_request_css, count: 1)
+ expect(get_filtered_search_placeholder).to eq('')
reset_filters
+
expect(page).to have_css(merge_request_css, count: 2)
+ expect(get_filtered_search_placeholder).to eq(default_placeholder)
end
end
context 'when author filter has been applied' do
it 'resets the author filter' do
visit_merge_requests(project, author_username: user.username)
+
expect(page).to have_css(merge_request_css, count: 1)
+ expect(get_filtered_search_placeholder).to eq('')
reset_filters
+
expect(page).to have_css(merge_request_css, count: 2)
+ expect(get_filtered_search_placeholder).to eq(default_placeholder)
end
end
context 'when assignee filter has been applied' do
it 'resets the assignee filter' do
visit_merge_requests(project, assignee_username: user.username)
+
expect(page).to have_css(merge_request_css, count: 1)
+ expect(get_filtered_search_placeholder).to eq('')
reset_filters
+
expect(page).to have_css(merge_request_css, count: 2)
+ expect(get_filtered_search_placeholder).to eq(default_placeholder)
end
end
context 'when all filters have been applied' do
- it 'resets all filters' do
+ it 'clears all filters' do
visit_merge_requests(project, assignee_username: user.username, author_username: user.username, milestone_title: milestone.title, label_name: bug.name, search: 'Bug')
+
expect(page).to have_css(merge_request_css, count: 0)
+ expect(get_filtered_search_placeholder).to eq('')
reset_filters
+
expect(page).to have_css(merge_request_css, count: 2)
+ expect(get_filtered_search_placeholder).to eq(default_placeholder)
end
end
context 'when no filters have been applied' do
- it 'the reset link should not be visible' do
+ it 'the clear button should not be visible' do
visit_merge_requests(project)
+
expect(page).to have_css(merge_request_css, count: 2)
+ expect(get_filtered_search_placeholder).to eq(default_placeholder)
expect(page).not_to have_css(clear_search_css)
end
end
diff --git a/spec/features/projects/environments/environment_metrics_spec.rb b/spec/features/projects/environments/environment_metrics_spec.rb
new file mode 100644
index 00000000000..ee925e811e1
--- /dev/null
+++ b/spec/features/projects/environments/environment_metrics_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+feature 'Environment > Metrics', :feature do
+ include PrometheusHelpers
+
+ given(:user) { create(:user) }
+ given(:project) { create(:prometheus_project) }
+ given(:pipeline) { create(:ci_pipeline, project: project) }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+ given(:environment) { create(:environment, project: project) }
+ given(:current_time) { Time.now.utc }
+
+ background do
+ project.add_developer(user)
+ create(:deployment, environment: environment, deployable: build)
+ stub_all_prometheus_requests(environment.slug)
+
+ login_as(user)
+ visit_environment(environment)
+ end
+
+ around do |example|
+ Timecop.freeze(current_time) { example.run }
+ end
+
+ context 'with deployments and related deployable present' do
+ scenario 'shows metrics' do
+ click_link('See metrics')
+
+ expect(page).to have_css('svg.prometheus-graph')
+ end
+ end
+
+ def visit_environment(environment)
+ visit namespace_project_environment_path(environment.project.namespace,
+ environment.project,
+ environment)
+ end
+end
diff --git a/spec/features/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 65373e3f77d..e2d16e0830a 100644
--- a/spec/features/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -37,13 +37,7 @@ feature 'Environment', :feature do
scenario 'does show deployment SHA' do
expect(page).to have_link(deployment.short_sha)
- end
-
- scenario 'does not show a re-deploy button for deployment without build' do
expect(page).not_to have_link('Re-deploy')
- end
-
- scenario 'does not show terminal button' do
expect(page).not_to have_terminal_button
end
end
@@ -58,13 +52,7 @@ feature 'Environment', :feature do
scenario 'does show build name' do
expect(page).to have_link("#{build.name} (##{build.id})")
- end
-
- scenario 'does show re-deploy button' do
expect(page).to have_link('Re-deploy')
- end
-
- scenario 'does not show terminal button' do
expect(page).not_to have_terminal_button
end
@@ -117,9 +105,6 @@ feature 'Environment', :feature do
it 'displays a web terminal' do
expect(page).to have_selector('#terminal')
- end
-
- it 'displays a link to the environment external url' do
expect(page).to have_link(nil, href: environment.external_url)
end
end
@@ -147,10 +132,6 @@ feature 'Environment', :feature do
on_stop: 'close_app')
end
- scenario 'does show stop button' do
- expect(page).to have_link('Stop')
- end
-
scenario 'does allow to stop environment' do
click_link('Stop')
diff --git a/spec/features/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 25f31b423b8..25f31b423b8 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
index 7414ce21f59..de3c6eceb82 100644
--- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
+++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
@@ -32,7 +32,7 @@ feature 'Issue prioritization', feature: true do
visit namespace_project_issues_path(project.namespace, project, sort: 'priority')
# Ensure we are indicating that issues are sorted by priority
- expect(page).to have_selector('.dropdown-toggle', text: 'Priority')
+ expect(page).to have_selector('.dropdown-toggle', text: 'Label priority')
page.within('.issues-holder') do
issue_titles = all('.issues-list .issue-title-text').map(&:text)
@@ -70,7 +70,7 @@ feature 'Issue prioritization', feature: true do
login_as user
visit namespace_project_issues_path(project.namespace, project, sort: 'priority')
- expect(page).to have_selector('.dropdown-toggle', text: 'Priority')
+ expect(page).to have_selector('.dropdown-toggle', text: 'Label priority')
page.within('.issues-holder') do
issue_titles = all('.issues-list .issue-title-text').map(&:text)
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 0b4dcaa39c6..b64c15e0adc 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -57,6 +57,12 @@ feature 'Projects > Members > User requests access', feature: true do
end
def open_project_settings_menu
- find('#project-settings-button').click
+ page.within('.layout-nav .nav-links') do
+ click_link('Settings')
+ end
+
+ page.within('.page-with-layout-nav .sub-nav') do
+ click_link('Members')
+ end
end
end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index 7da05defa81..a6560a81096 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe "Search", feature: true do
+ include FilteredSearchHelpers
include WaitForAjax
let(:user) { create(:user) }
@@ -170,7 +171,8 @@ describe "Search", feature: true do
sleep 2
expect(page).to have_selector('.filtered-search')
- expect(find('.filtered-search').value).to eq("assignee:@#{user.username}")
+ expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
end
it 'takes user to her issues page when issues authored is clicked' do
@@ -178,7 +180,8 @@ describe "Search", feature: true do
sleep 2
expect(page).to have_selector('.filtered-search')
- expect(find('.filtered-search').value).to eq("author:@#{user.username}")
+ expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
end
it 'takes user to her MR page when MR assigned is clicked' do
@@ -186,7 +189,8 @@ describe "Search", feature: true do
sleep 2
expect(page).to have_selector('.merge-requests-holder')
- expect(find('.filtered-search').value).to eq("assignee:@#{user.username}")
+ expect_tokens([{ name: 'assignee', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
end
it 'takes user to her MR page when MR authored is clicked' do
@@ -194,7 +198,8 @@ describe "Search", feature: true do
sleep 2
expect(page).to have_selector('.merge-requests-holder')
- expect(find('.filtered-search').value).to eq("author:@#{user.username}")
+ expect_tokens([{ name: 'author', value: "@#{user.username}" }])
+ expect_filtered_search_input_empty
end
end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 24af062d763..1a66d1a6a1e 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -110,6 +110,20 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_denied_for(:external) }
end
+ describe "GET /:project_path/settings/repository" do
+ subject { namespace_project_settings_repository_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_denied_for(:developer).of(project) }
+ it { is_expected.to be_denied_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:visitor) }
+ it { is_expected.to be_denied_for(:external) }
+ end
+
describe "GET /:project_path/blob" do
let(:commit) { project.repository.commit }
subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore')) }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index c511dcfa18e..ad3bd60a313 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -110,6 +110,20 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_denied_for(:external) }
end
+ describe "GET /:project_path/settings/repository" do
+ subject { namespace_project_settings_repository_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_denied_for(:developer).of(project) }
+ it { is_expected.to be_denied_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+ end
+
describe "GET /:project_path/blob" do
let(:commit) { project.repository.commit }
subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore'))}
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index d8cc012c27e..e06aab4e0b2 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -110,6 +110,20 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_denied_for(:external) }
end
+ describe "GET /:project_path/settings/repository" do
+ subject { namespace_project_settings_repository_path(project.namespace, project) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_denied_for(:developer).of(project) }
+ it { is_expected.to be_denied_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:visitor) }
+ it { is_expected.to be_denied_for(:external) }
+ end
+
describe "GET /:project_path/pipelines" do
subject { namespace_project_pipelines_path(project.namespace, project) }
diff --git a/spec/features/todos/todos_sorting_spec.rb b/spec/features/todos/todos_sorting_spec.rb
index fec28c55d30..4d5bd476301 100644
--- a/spec/features/todos/todos_sorting_spec.rb
+++ b/spec/features/todos/todos_sorting_spec.rb
@@ -56,8 +56,8 @@ describe "Dashboard > User sorts todos", feature: true do
expect(results_list.all('p')[4]).to have_content("merge_request_1")
end
- it "sorts by priority" do
- click_link "Priority"
+ it "sorts by label priority" do
+ click_link "Label priority"
results_list = page.find('.todos-list')
expect(results_list.all('p')[0]).to have_content("issue_3")
@@ -85,8 +85,8 @@ describe "Dashboard > User sorts todos", feature: true do
visit dashboard_todos_path
end
- it "doesn't mix issues and merge requests priorities" do
- click_link "Priority"
+ it "doesn't mix issues and merge requests label priorities" do
+ click_link "Label priority"
results_list = page.find('.todos-list')
expect(results_list.all('p')[0]).to have_content("issue_1")
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index 8e19cee5440..21c078e0f44 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -11,6 +11,7 @@
"title": { "type": "string" },
"confidential": { "type": "boolean" },
"due_date": { "type": ["date", "null"] },
+ "relative_position": { "type": "integer" },
"labels": {
"type": "array",
"items": {
diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb
index baab30f482f..cf182e6d221 100644
--- a/spec/initializers/6_validations_spec.rb
+++ b/spec/initializers/6_validations_spec.rb
@@ -14,7 +14,7 @@ describe '6_validations', lib: true do
context 'with correct settings' do
before do
- mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/d')
+ mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/d' })
end
it 'passes through' do
@@ -24,7 +24,7 @@ describe '6_validations', lib: true do
context 'with invalid storage names' do
before do
- mock_storages('name with spaces' => 'tmp/tests/paths/a/b/c')
+ mock_storages('name with spaces' => { 'path' => 'tmp/tests/paths/a/b/c' })
end
it 'throws an error' do
@@ -34,7 +34,7 @@ describe '6_validations', lib: true do
context 'with nested storage paths' do
before do
- mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/c/d')
+ mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/c/d' })
end
it 'throws an error' do
@@ -44,7 +44,7 @@ describe '6_validations', lib: true do
context 'with similar but un-nested storage paths' do
before do
- mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/c2')
+ mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c' }, 'bar' => { 'path' => 'tmp/tests/paths/a/b/c2' })
end
it 'passes through' do
@@ -52,6 +52,26 @@ describe '6_validations', lib: true do
end
end
+ context 'with incomplete settings' do
+ before do
+ mock_storages('foo' => {})
+ end
+
+ it 'throws an error suggesting the user to update its settings' do
+ expect { validate_storages }.to raise_error('foo is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example. Please fix this in your gitlab.yml before starting GitLab.')
+ end
+ end
+
+ context 'with deprecated settings structure' do
+ before do
+ mock_storages('foo' => 'tmp/tests/paths/a/b/c')
+ end
+
+ it 'throws an error suggesting the user to update its settings' do
+ expect { validate_storages }.to raise_error("foo is not a valid storage, because it has no `path` key. It may be configured as:\n\nfoo:\n path: tmp/tests/paths/a/b/c\n\nRefer to gitlab.yml.example for an updated example. Please fix this in your gitlab.yml before starting GitLab.")
+ end
+ end
+
def mock_storages(storages)
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
diff --git a/spec/initializers/doorkeeper_spec.rb b/spec/initializers/doorkeeper_spec.rb
new file mode 100644
index 00000000000..74bdbb01166
--- /dev/null
+++ b/spec/initializers/doorkeeper_spec.rb
@@ -0,0 +1,71 @@
+require 'spec_helper'
+require_relative '../../config/initializers/doorkeeper'
+
+describe Doorkeeper.configuration do
+ describe '#default_scopes' do
+ it 'matches Gitlab::Auth::DEFAULT_SCOPES' do
+ expect(subject.default_scopes).to eq Gitlab::Auth::DEFAULT_SCOPES
+ end
+ end
+
+ describe '#optional_scopes' do
+ it 'matches Gitlab::Auth::OPTIONAL_SCOPES' do
+ expect(subject.optional_scopes).to eq Gitlab::Auth::OPTIONAL_SCOPES
+ end
+ end
+
+ describe '#resource_owner_authenticator' do
+ subject { controller.instance_exec(&Doorkeeper.configuration.authenticate_resource_owner) }
+
+ let(:controller) { double }
+
+ before do
+ allow(controller).to receive(:current_user).and_return(current_user)
+ allow(controller).to receive(:session).and_return({})
+ allow(controller).to receive(:request).and_return(OpenStruct.new(fullpath: '/return-path'))
+ allow(controller).to receive(:redirect_to)
+ allow(controller).to receive(:new_user_session_url).and_return('/login')
+ end
+
+ context 'with a user present' do
+ let(:current_user) { create(:user) }
+
+ it 'returns the user' do
+ expect(subject).to eq current_user
+ end
+
+ it 'does not redirect' do
+ expect(controller).not_to receive(:redirect_to)
+
+ subject
+ end
+
+ it 'does not store the return path' do
+ subject
+
+ expect(controller.session).not_to include :user_return_to
+ end
+ end
+
+ context 'without a user present' do
+ let(:current_user) { nil }
+
+ # NOTE: this is required for doorkeeper-openid_connect
+ it 'returns nil' do
+ expect(subject).to eq nil
+ end
+
+ it 'redirects to the login form' do
+ expect(controller).to receive(:redirect_to).with('/login')
+
+ subject
+ end
+
+ it 'stores the return path' do
+ subject
+
+ expect(controller.session[:user_return_to]).to eq '/return-path'
+ end
+ end
+ end
+end
diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb
index ad7f032d1e5..65c97da2efd 100644
--- a/spec/initializers/secret_token_spec.rb
+++ b/spec/initializers/secret_token_spec.rb
@@ -6,6 +6,9 @@ describe 'create_tokens', lib: true do
let(:secrets) { ActiveSupport::OrderedOptions.new }
+ HEX_KEY = /\h{128}/
+ RSA_KEY = /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\Z/m
+
before do
allow(File).to receive(:write)
allow(File).to receive(:delete)
@@ -15,7 +18,7 @@ describe 'create_tokens', lib: true do
allow(self).to receive(:exit)
end
- context 'setting secret_key_base and otp_key_base' do
+ context 'setting secret keys' do
context 'when none of the secrets exist' do
before do
stub_env('SECRET_KEY_BASE', nil)
@@ -24,19 +27,29 @@ describe 'create_tokens', lib: true do
allow(self).to receive(:warn_missing_secret)
end
- it 'generates different secrets for secret_key_base, otp_key_base, and db_key_base' do
+ it 'generates different hashes for secret_key_base, otp_key_base, and db_key_base' do
create_tokens
keys = secrets.values_at(:secret_key_base, :otp_key_base, :db_key_base)
expect(keys.uniq).to eq(keys)
- expect(keys.map(&:length)).to all(eq(128))
+ expect(keys).to all(match(HEX_KEY))
+ end
+
+ it 'generates an RSA key for jws_private_key' do
+ create_tokens
+
+ keys = secrets.values_at(:jws_private_key)
+
+ expect(keys.uniq).to eq(keys)
+ expect(keys).to all(match(RSA_KEY))
end
it 'warns about the secrets to add to secrets.yml' do
expect(self).to receive(:warn_missing_secret).with('secret_key_base')
expect(self).to receive(:warn_missing_secret).with('otp_key_base')
expect(self).to receive(:warn_missing_secret).with('db_key_base')
+ expect(self).to receive(:warn_missing_secret).with('jws_private_key')
create_tokens
end
@@ -48,6 +61,7 @@ describe 'create_tokens', lib: true do
expect(new_secrets['secret_key_base']).to eq(secrets.secret_key_base)
expect(new_secrets['otp_key_base']).to eq(secrets.otp_key_base)
expect(new_secrets['db_key_base']).to eq(secrets.db_key_base)
+ expect(new_secrets['jws_private_key']).to eq(secrets.jws_private_key)
end
create_tokens
@@ -63,6 +77,7 @@ describe 'create_tokens', lib: true do
context 'when the other secrets all exist' do
before do
secrets.db_key_base = 'db_key_base'
+ secrets.jws_private_key = 'jws_private_key'
allow(File).to receive(:exist?).with('.secret').and_return(true)
allow(File).to receive(:read).with('.secret').and_return('file_key')
@@ -73,6 +88,7 @@ describe 'create_tokens', lib: true do
stub_env('SECRET_KEY_BASE', 'env_key')
secrets.secret_key_base = 'secret_key_base'
secrets.otp_key_base = 'otp_key_base'
+ secrets.jws_private_key = 'jws_private_key'
end
it 'does not issue a warning' do
@@ -98,6 +114,7 @@ describe 'create_tokens', lib: true do
before do
secrets.secret_key_base = 'secret_key_base'
secrets.otp_key_base = 'otp_key_base'
+ secrets.jws_private_key = 'jws_private_key'
end
it 'does not write any files' do
@@ -112,6 +129,7 @@ describe 'create_tokens', lib: true do
expect(secrets.secret_key_base).to eq('secret_key_base')
expect(secrets.otp_key_base).to eq('otp_key_base')
expect(secrets.db_key_base).to eq('db_key_base')
+ expect(secrets.jws_private_key).to eq('jws_private_key')
end
it 'deletes the .secret file' do
@@ -135,6 +153,7 @@ describe 'create_tokens', lib: true do
expect(new_secrets['secret_key_base']).to eq('file_key')
expect(new_secrets['otp_key_base']).to eq('file_key')
expect(new_secrets['db_key_base']).to eq('db_key_base')
+ expect(new_secrets['jws_private_key']).to eq('jws_private_key')
end
create_tokens
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index f4b1d777203..d8517e4d3d1 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -46,9 +46,6 @@ const AwardsHandler = require('~/awards_handler');
isEmojiMenuBuilt = true;
resolve();
});
-
- // Fail after 1 second
- setTimeout(reject, 1000);
}
});
};
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
index 9dd741a680b..49a2ca4a78f 100644
--- a/spec/javascripts/boards/boards_store_spec.js
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -5,6 +5,7 @@
/* global Cookies */
/* global listObj */
/* global listObjDuplicate */
+/* global ListIssue */
require('~/lib/utils/url_utility');
require('~/boards/models/issue');
@@ -14,6 +15,7 @@ require('~/boards/models/user');
require('~/boards/services/board_service');
require('~/boards/stores/boards_store');
require('./mock_data');
+require('es6-promise').polyfill();
describe('Store', () => {
beforeEach(() => {
@@ -21,6 +23,10 @@ describe('Store', () => {
gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
gl.issueBoards.BoardsStore.create();
+ spyOn(gl.boardService, 'moveIssue').and.callFake(() => new Promise((resolve) => {
+ resolve();
+ }));
+
Cookies.set('issue_board_welcome_hidden', 'false', {
expires: 365 * 10,
path: ''
@@ -154,5 +160,74 @@ describe('Store', () => {
done();
}, 0);
});
+
+ it('moves issue to top of another list', (done) => {
+ const listOne = gl.issueBoards.BoardsStore.addList(listObj);
+ const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+
+ setTimeout(() => {
+ listOne.issues[0].id = 2;
+
+ expect(listOne.issues.length).toBe(1);
+ expect(listTwo.issues.length).toBe(1);
+
+ gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 0);
+
+ expect(listOne.issues.length).toBe(0);
+ expect(listTwo.issues.length).toBe(2);
+ expect(listTwo.issues[0].id).toBe(2);
+ expect(gl.boardService.moveIssue).toHaveBeenCalledWith(2, listOne.id, listTwo.id, null, 1);
+
+ done();
+ }, 0);
+ });
+
+ it('moves issue to bottom of another list', (done) => {
+ const listOne = gl.issueBoards.BoardsStore.addList(listObj);
+ const listTwo = gl.issueBoards.BoardsStore.addList(listObjDuplicate);
+
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+
+ setTimeout(() => {
+ listOne.issues[0].id = 2;
+
+ expect(listOne.issues.length).toBe(1);
+ expect(listTwo.issues.length).toBe(1);
+
+ gl.issueBoards.BoardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 1);
+
+ expect(listOne.issues.length).toBe(0);
+ expect(listTwo.issues.length).toBe(2);
+ expect(listTwo.issues[1].id).toBe(2);
+ expect(gl.boardService.moveIssue).toHaveBeenCalledWith(2, listOne.id, listTwo.id, 1, null);
+
+ done();
+ }, 0);
+ });
+
+ it('moves issue in list', (done) => {
+ const issue = new ListIssue({
+ title: 'Testing',
+ iid: 2,
+ confidential: false,
+ labels: []
+ });
+ const list = gl.issueBoards.BoardsStore.addList(listObj);
+
+ setTimeout(() => {
+ list.addIssue(issue);
+
+ expect(list.issues.length).toBe(2);
+
+ gl.issueBoards.BoardsStore.moveIssueInList(list, issue, 0, 1, [1, 2]);
+
+ expect(list.issues[0].id).toBe(2);
+ expect(gl.boardService.moveIssue).toHaveBeenCalledWith(2, null, null, 1, null);
+
+ done();
+ });
+ });
});
});
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index aab4d9c501e..c96dfe94a4a 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -79,4 +79,20 @@ describe('Issue model', () => {
issue.removeLabels([issue.labels[0], issue.labels[1]]);
expect(issue.labels.length).toBe(0);
});
+
+ it('sets position to infinity if no position is stored', () => {
+ expect(issue.position).toBe(Infinity);
+ });
+
+ it('sets position', () => {
+ const relativePositionIssue = new ListIssue({
+ title: 'Testing',
+ iid: 1,
+ confidential: false,
+ relative_position: 1,
+ labels: []
+ });
+
+ expect(relativePositionIssue.position).toBe(1);
+ });
});
diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js
index c8a18af7198..d49d3af33d9 100644
--- a/spec/javascripts/boards/list_spec.js
+++ b/spec/javascripts/boards/list_spec.js
@@ -103,6 +103,7 @@ describe('List model', () => {
listDup.updateIssueLabel(list, issue);
- expect(gl.boardService.moveIssue).toHaveBeenCalledWith(issue.id, list.id, listDup.id);
+ expect(gl.boardService.moveIssue)
+ .toHaveBeenCalledWith(issue.id, list.id, listDup.id, undefined, undefined);
});
});
diff --git a/spec/javascripts/diff_comments_store_spec.js b/spec/javascripts/diff_comments_store_spec.js
index f956394ef53..84cf98c930a 100644
--- a/spec/javascripts/diff_comments_store_spec.js
+++ b/spec/javascripts/diff_comments_store_spec.js
@@ -7,7 +7,16 @@ require('~/diff_notes/stores/comments');
(() => {
function createDiscussion(noteId = 1, resolved = true) {
- CommentsStore.create('a', noteId, true, resolved, 'test');
+ CommentsStore.create({
+ discussionId: 'a',
+ noteId,
+ canResolve: true,
+ resolved,
+ resolvedBy: 'test',
+ authorName: 'test',
+ authorAvatar: 'test',
+ noteTruncated: 'test...',
+ });
}
beforeEach(() => {
diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js
index fa9d03c8a9a..c16f77c53a2 100644
--- a/spec/javascripts/filtered_search/dropdown_user_spec.js
+++ b/spec/javascripts/filtered_search/dropdown_user_spec.js
@@ -18,9 +18,7 @@ require('~/filtered_search/dropdown_user');
it('should not return the double quote found in value', () => {
spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
- lastToken: {
- value: '"johnny appleseed',
- },
+ lastToken: '"johnny appleseed',
});
expect(dropdownUser.getSearchInput()).toBe('johnny appleseed');
@@ -28,9 +26,7 @@ require('~/filtered_search/dropdown_user');
it('should not return the single quote found in value', () => {
spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({
- lastToken: {
- value: '\'larry boy',
- },
+ lastToken: '\'larry boy',
});
expect(dropdownUser.getSearchInput()).toBe('larry boy');
diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js
index 1e2d7582d5b..5c65903701b 100644
--- a/spec/javascripts/filtered_search/dropdown_utils_spec.js
+++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js
@@ -45,7 +45,7 @@ require('~/filtered_search/filtered_search_dropdown_manager');
});
it('should filter without symbol', () => {
- input.value = ':roo';
+ input.value = 'roo';
const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
expect(updatedItem.droplab_hidden).toBe(false);
@@ -58,69 +58,62 @@ require('~/filtered_search/filtered_search_dropdown_manager');
expect(updatedItem.droplab_hidden).toBe(false);
});
- it('should filter with colon', () => {
- input.value = 'roo';
-
- const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item);
- expect(updatedItem.droplab_hidden).toBe(false);
- });
-
describe('filters multiple word title', () => {
const multipleWordItem = {
title: 'Community Contributions',
};
it('should filter with double quote', () => {
- input.value = 'label:"';
+ input.value = '"';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false);
});
it('should filter with double quote and symbol', () => {
- input.value = 'label:~"';
+ input.value = '~"';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false);
});
it('should filter with double quote and multiple words', () => {
- input.value = 'label:"community con';
+ input.value = '"community con';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false);
});
it('should filter with double quote, symbol and multiple words', () => {
- input.value = 'label:~"community con';
+ input.value = '~"community con';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false);
});
it('should filter with single quote', () => {
- input.value = 'label:\'';
+ input.value = '\'';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false);
});
it('should filter with single quote and symbol', () => {
- input.value = 'label:~\'';
+ input.value = '~\'';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false);
});
it('should filter with single quote and multiple words', () => {
- input.value = 'label:\'community con';
+ input.value = '\'community con';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false);
});
it('should filter with single quote, symbol and multiple words', () => {
- input.value = 'label:~\'community con';
+ input.value = '~\'community con';
const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem);
expect(updatedItem.droplab_hidden).toBe(false);
diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js
index ed0b0196ec4..a1da3396d7b 100644
--- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js
@@ -1,4 +1,5 @@
require('~/extensions/array');
+require('~/filtered_search/filtered_search_visual_tokens');
require('~/filtered_search/filtered_search_tokenizer');
require('~/filtered_search/filtered_search_dropdown_manager');
@@ -14,24 +15,44 @@ require('~/filtered_search/filtered_search_dropdown_manager');
}
beforeEach(() => {
- const input = document.createElement('input');
- input.classList.add('filtered-search');
- document.body.appendChild(input);
- });
-
- afterEach(() => {
- document.querySelector('.filtered-search').outerHTML = '';
+ setFixtures(`
+ <ul class="tokens-container">
+ <li class="input-token">
+ <input class="filtered-search">
+ </li>
+ </ul>
+ `);
});
describe('input has no existing value', () => {
it('should add just tokenName', () => {
gl.FilteredSearchDropdownManager.addWordToInput('milestone');
- expect(getInputValue()).toBe('milestone:');
+
+ const token = document.querySelector('.tokens-container .js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toBe('milestone');
+ expect(getInputValue()).toBe('');
});
it('should add tokenName and tokenValue', () => {
+ gl.FilteredSearchDropdownManager.addWordToInput('label');
+
+ let token = document.querySelector('.tokens-container .js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toBe('label');
+ expect(getInputValue()).toBe('');
+
gl.FilteredSearchDropdownManager.addWordToInput('label', 'none');
- expect(getInputValue()).toBe('label:none ');
+ // We have to get that reference again
+ // Because gl.FilteredSearchDropdownManager deletes the previous token
+ token = document.querySelector('.tokens-container .js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toBe('label');
+ expect(token.querySelector('.value').innerText).toBe('none');
+ expect(getInputValue()).toBe('');
});
});
@@ -39,19 +60,40 @@ require('~/filtered_search/filtered_search_dropdown_manager');
it('should be able to just add tokenName', () => {
setInputValue('a');
gl.FilteredSearchDropdownManager.addWordToInput('author');
- expect(getInputValue()).toBe('author:');
+
+ const token = document.querySelector('.tokens-container .js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toBe('author');
+ expect(getInputValue()).toBe('');
});
it('should replace tokenValue', () => {
- setInputValue('author:roo');
- gl.FilteredSearchDropdownManager.addWordToInput('author', '@root');
- expect(getInputValue()).toBe('author:@root ');
+ gl.FilteredSearchDropdownManager.addWordToInput('author');
+
+ setInputValue('roo');
+ gl.FilteredSearchDropdownManager.addWordToInput(null, '@root');
+
+ const token = document.querySelector('.tokens-container .js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toBe('author');
+ expect(token.querySelector('.value').innerText).toBe('@root');
+ expect(getInputValue()).toBe('');
});
it('should add tokenValues containing spaces', () => {
- setInputValue('label:~"test');
+ gl.FilteredSearchDropdownManager.addWordToInput('label');
+
+ setInputValue('"test ');
gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\'');
- expect(getInputValue()).toBe('label:~\'"test me"\' ');
+
+ const token = document.querySelector('.tokens-container .js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toBe('label');
+ expect(token.querySelector('.value').innerText).toBe('~\'"test me"\'');
+ expect(getInputValue()).toBe('');
});
});
});
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
index 98959dda242..81c1d81d181 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -4,64 +4,244 @@ require('~/filtered_search/filtered_search_token_keys');
require('~/filtered_search/filtered_search_tokenizer');
require('~/filtered_search/filtered_search_dropdown_manager');
require('~/filtered_search/filtered_search_manager');
+const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper');
(() => {
describe('Filtered Search Manager', () => {
- describe('search', () => {
- let manager;
- const defaultParams = '?scope=all&utf8=✓&state=opened';
+ let input;
+ let manager;
+ let tokensContainer;
+ const placeholder = 'Search or filter results...';
- function getInput() {
- return document.querySelector('.filtered-search');
- }
+ function dispatchBackspaceEvent(element, eventType) {
+ const backspaceKey = 8;
+ const event = new Event(eventType);
+ event.keyCode = backspaceKey;
+ element.dispatchEvent(event);
+ }
- beforeEach(() => {
- setFixtures(`
- <input type='text' class='filtered-search' />
- `);
+ function dispatchDeleteEvent(element, eventType) {
+ const deleteKey = 46;
+ const event = new Event(eventType);
+ event.keyCode = deleteKey;
+ element.dispatchEvent(event);
+ }
- spyOn(gl.FilteredSearchManager.prototype, 'bindEvents').and.callFake(() => {});
- spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {});
- spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
- spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
- spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
+ beforeEach(() => {
+ setFixtures(`
+ <div class="filtered-search-input-container">
+ <form>
+ <ul class="tokens-container list-unstyled">
+ ${FilteredSearchSpecHelper.createInputHTML(placeholder)}
+ </ul>
+ <button class="clear-search" type="button">
+ <i class="fa fa-times"></i>
+ </button>
+ </form>
+ </div>
+ `);
- manager = new gl.FilteredSearchManager();
- });
+ spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {});
+ spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {});
+ spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {});
+ spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {});
+ spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {});
+ spyOn(gl.utils, 'getParameterByName').and.returnValue(null);
+ spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough();
- afterEach(() => {
- getInput().outerHTML = '';
- });
+ input = document.querySelector('.filtered-search');
+ tokensContainer = document.querySelector('.tokens-container');
+ manager = new gl.FilteredSearchManager();
+ });
- it('should search with a single word', () => {
- getInput().value = 'searchTerm';
+ describe('search', () => {
+ const defaultParams = '?scope=all&utf8=✓&state=opened';
+
+ it('should search with a single word', (done) => {
+ input.value = 'searchTerm';
spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=searchTerm`);
+ done();
});
manager.search();
});
- it('should search with multiple words', () => {
- getInput().value = 'awesome search terms';
+ it('should search with multiple words', (done) => {
+ input.value = 'awesome search terms';
spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`);
+ done();
});
manager.search();
});
- it('should search with special characters', () => {
- getInput().value = '~!@#$%^&*()_+{}:<>,.?/';
+ it('should search with special characters', (done) => {
+ input.value = '~!@#$%^&*()_+{}:<>,.?/';
spyOn(gl.utils, 'visitUrl').and.callFake((url) => {
expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`);
+ done();
});
manager.search();
});
});
+
+ describe('handleInputPlaceholder', () => {
+ it('should render placeholder when there is no input', () => {
+ expect(input.placeholder).toEqual(placeholder);
+ });
+
+ it('should not render placeholder when there is input', () => {
+ input.value = 'test words';
+
+ const event = new Event('input');
+ input.dispatchEvent(event);
+
+ expect(input.placeholder).toEqual('');
+ });
+
+ it('should not render placeholder when there are tokens and no input', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
+ );
+
+ const event = new Event('input');
+ input.dispatchEvent(event);
+
+ expect(input.placeholder).toEqual('');
+ });
+ });
+
+ describe('checkForBackspace', () => {
+ describe('tokens and no input', () => {
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
+ );
+ });
+
+ it('removes last token', () => {
+ spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
+ dispatchBackspaceEvent(input, 'keyup');
+
+ expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled();
+ });
+
+ it('sets the input', () => {
+ spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
+ dispatchDeleteEvent(input, 'keyup');
+
+ expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled();
+ expect(input.value).toEqual('~bug');
+ });
+ });
+
+ it('does not remove token or change input when there is existing input', () => {
+ spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough();
+ spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough();
+
+ input.value = 'text';
+ dispatchDeleteEvent(input, 'keyup');
+
+ expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled();
+ expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled();
+ expect(input.value).toEqual('text');
+ });
+ });
+
+ describe('removeSelectedToken', () => {
+ function getVisualTokens() {
+ return tokensContainer.querySelectorAll('.js-visual-token');
+ }
+
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
+ );
+ });
+
+ it('removes selected token when the backspace key is pressed', () => {
+ expect(getVisualTokens().length).toEqual(1);
+
+ dispatchBackspaceEvent(document, 'keydown');
+
+ expect(getVisualTokens().length).toEqual(0);
+ });
+
+ it('removes selected token when the delete key is pressed', () => {
+ expect(getVisualTokens().length).toEqual(1);
+
+ dispatchDeleteEvent(document, 'keydown');
+
+ expect(getVisualTokens().length).toEqual(0);
+ });
+
+ it('updates the input placeholder after removal', () => {
+ manager.handleInputPlaceholder();
+
+ expect(input.placeholder).toEqual('');
+ expect(getVisualTokens().length).toEqual(1);
+
+ dispatchBackspaceEvent(document, 'keydown');
+
+ expect(input.placeholder).not.toEqual('');
+ expect(getVisualTokens().length).toEqual(0);
+ });
+
+ it('updates the clear button after removal', () => {
+ manager.toggleClearSearchButton();
+
+ const clearButton = document.querySelector('.clear-search');
+
+ expect(clearButton.classList.contains('hidden')).toEqual(false);
+ expect(getVisualTokens().length).toEqual(1);
+
+ dispatchBackspaceEvent(document, 'keydown');
+
+ expect(clearButton.classList.contains('hidden')).toEqual(true);
+ expect(getVisualTokens().length).toEqual(0);
+ });
+ });
+
+ describe('unselects token', () => {
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug', true)}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')}
+ `);
+ });
+
+ it('unselects token when input is clicked', () => {
+ const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
+
+ expect(selectedToken.classList.contains('selected')).toEqual(true);
+ expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
+
+ // Click directly on input attached to document
+ // so that the click event will propagate properly
+ document.querySelector('.filtered-search').click();
+
+ expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
+ expect(selectedToken.classList.contains('selected')).toEqual(false);
+ });
+
+ it('unselects token when document.body is clicked', () => {
+ const selectedToken = tokensContainer.querySelector('.js-visual-token .selected');
+
+ expect(selectedToken.classList.contains('selected')).toEqual(true);
+ expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled();
+
+ document.body.click();
+
+ expect(selectedToken.classList.contains('selected')).toEqual(false);
+ expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
+ });
+ });
});
})();
diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
new file mode 100644
index 00000000000..bbda1476fed
--- /dev/null
+++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
@@ -0,0 +1,600 @@
+require('~/filtered_search/filtered_search_visual_tokens');
+const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper');
+
+describe('Filtered Search Visual Tokens', () => {
+ let tokensContainer;
+
+ beforeEach(() => {
+ setFixtures(`
+ <ul class="tokens-container">
+ ${FilteredSearchSpecHelper.createInputHTML()}
+ </ul>
+ `);
+ tokensContainer = document.querySelector('.tokens-container');
+ });
+
+ describe('getLastVisualTokenBeforeInput', () => {
+ it('returns when there are no visual tokens', () => {
+ const { lastVisualToken, isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ expect(lastVisualToken).toEqual(null);
+ expect(isLastVisualTokenValid).toEqual(true);
+ });
+
+ describe('input is the last item in tokensContainer', () => {
+ it('returns when there is one visual token', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
+ );
+
+ const { lastVisualToken, isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
+ expect(isLastVisualTokenValid).toEqual(true);
+ });
+
+ it('returns when there is an incomplete visual token', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('Author'),
+ );
+
+ const { lastVisualToken, isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
+ expect(isLastVisualTokenValid).toEqual(false);
+ });
+
+ it('returns when there are multiple visual tokens', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
+ `);
+
+ const { lastVisualToken, isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ const items = document.querySelectorAll('.tokens-container .js-visual-token');
+
+ expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true);
+ expect(isLastVisualTokenValid).toEqual(true);
+ });
+
+ it('returns when there are multiple visual tokens and an incomplete visual token', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+ ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee')}
+ `);
+
+ const { lastVisualToken, isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ const items = document.querySelectorAll('.tokens-container .js-visual-token');
+
+ expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true);
+ expect(isLastVisualTokenValid).toEqual(false);
+ });
+ });
+
+ describe('input is a middle item in tokensContainer', () => {
+ it('returns last token before input', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${FilteredSearchSpecHelper.createInputHTML()}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
+ `);
+
+ const { lastVisualToken, isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
+ expect(isLastVisualTokenValid).toEqual(true);
+ });
+
+ it('returns last partial token before input', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')}
+ ${FilteredSearchSpecHelper.createInputHTML()}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
+ `);
+
+ const { lastVisualToken, isLastVisualTokenValid }
+ = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+
+ expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
+ expect(isLastVisualTokenValid).toEqual(false);
+ });
+ });
+ });
+
+ describe('unselectTokens', () => {
+ it('does nothing when there are no tokens', () => {
+ const beforeHTML = tokensContainer.innerHTML;
+ gl.FilteredSearchVisualTokens.unselectTokens();
+
+ expect(tokensContainer.innerHTML).toEqual(beforeHTML);
+ });
+
+ it('removes the selected class from buttons', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@author')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '%123', true)}
+ `);
+
+ const selected = tokensContainer.querySelector('.js-visual-token .selected');
+ expect(selected.classList.contains('selected')).toEqual(true);
+
+ gl.FilteredSearchVisualTokens.unselectTokens();
+
+ expect(selected.classList.contains('selected')).toEqual(false);
+ });
+ });
+
+ describe('selectToken', () => {
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')}
+ `);
+ });
+
+ it('removes the selected class if it has selected class', () => {
+ const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable');
+ firstTokenButton.classList.add('selected');
+
+ gl.FilteredSearchVisualTokens.selectToken(firstTokenButton);
+
+ expect(firstTokenButton.classList.contains('selected')).toEqual(false);
+ });
+
+ describe('has no selected class', () => {
+ it('adds selected class', () => {
+ const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable');
+
+ gl.FilteredSearchVisualTokens.selectToken(firstTokenButton);
+
+ expect(firstTokenButton.classList.contains('selected')).toEqual(true);
+ });
+
+ it('removes selected class from other tokens', () => {
+ const tokenButtons = tokensContainer.querySelectorAll('.js-visual-token .selectable');
+ tokenButtons[1].classList.add('selected');
+
+ gl.FilteredSearchVisualTokens.selectToken(tokenButtons[0]);
+
+ expect(tokenButtons[0].classList.contains('selected')).toEqual(true);
+ expect(tokenButtons[1].classList.contains('selected')).toEqual(false);
+ });
+ });
+ });
+
+ describe('removeSelectedToken', () => {
+ it('does not remove when there are no selected tokens', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none'),
+ );
+
+ expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
+
+ gl.FilteredSearchVisualTokens.removeSelectedToken();
+
+ expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
+ });
+
+ it('removes selected token', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none', true),
+ );
+
+ expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
+
+ gl.FilteredSearchVisualTokens.removeSelectedToken();
+
+ expect(tokensContainer.querySelector('.js-visual-token .selectable')).toEqual(null);
+ });
+ });
+
+ describe('createVisualTokenElementHTML', () => {
+ let tokenElement;
+
+ beforeEach(() => {
+ setFixtures(`
+ <div class="test-area">
+ ${gl.FilteredSearchVisualTokens.createVisualTokenElementHTML()}
+ </div>
+ `);
+
+ tokenElement = document.querySelector('.test-area').firstElementChild;
+ });
+
+ it('contains name div', () => {
+ expect(tokenElement.querySelector('.name')).toEqual(jasmine.anything());
+ });
+
+ it('contains value div', () => {
+ expect(tokenElement.querySelector('.value')).toEqual(jasmine.anything());
+ });
+
+ it('contains selectable class', () => {
+ expect(tokenElement.classList.contains('selectable')).toEqual(true);
+ });
+
+ it('contains button role', () => {
+ expect(tokenElement.getAttribute('role')).toEqual('button');
+ });
+ });
+
+ describe('addVisualTokenElement', () => {
+ it('renders search visual tokens', () => {
+ gl.FilteredSearchVisualTokens.addVisualTokenElement('search term', null, true);
+ const token = tokensContainer.querySelector('.js-visual-token');
+
+ expect(token.classList.contains('filtered-search-term')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toEqual('search term');
+ expect(token.querySelector('.value')).toEqual(null);
+ });
+
+ it('renders filter visual token name', () => {
+ gl.FilteredSearchVisualTokens.addVisualTokenElement('milestone');
+ const token = tokensContainer.querySelector('.js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toEqual('milestone');
+ expect(token.querySelector('.value')).toEqual(null);
+ });
+
+ it('renders filter visual token name and value', () => {
+ gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend');
+ const token = tokensContainer.querySelector('.js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toEqual('label');
+ expect(token.querySelector('.value').innerText).toEqual('Frontend');
+ });
+
+ it('inserts visual token before input', () => {
+ tokensContainer.appendChild(FilteredSearchSpecHelper.createFilterVisualToken('assignee', '@root'));
+
+ gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend');
+ const tokens = tokensContainer.querySelectorAll('.js-visual-token');
+ const labelToken = tokens[0];
+ const assigneeToken = tokens[1];
+
+ expect(labelToken.classList.contains('filtered-search-token')).toEqual(true);
+ expect(labelToken.querySelector('.name').innerText).toEqual('label');
+ expect(labelToken.querySelector('.value').innerText).toEqual('Frontend');
+
+ expect(assigneeToken.classList.contains('filtered-search-token')).toEqual(true);
+ expect(assigneeToken.querySelector('.name').innerText).toEqual('assignee');
+ expect(assigneeToken.querySelector('.value').innerText).toEqual('@root');
+ });
+ });
+
+ describe('addValueToPreviousVisualTokenElement', () => {
+ it('does not add when previous visual token element has no value', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root'),
+ );
+
+ const original = tokensContainer.innerHTML;
+ gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
+
+ expect(original).toEqual(tokensContainer.innerHTML);
+ });
+
+ it('does not add when previous visual token element is a search', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+ `);
+
+ const original = tokensContainer.innerHTML;
+ gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
+
+ expect(original).toEqual(tokensContainer.innerHTML);
+ });
+
+ it('adds value to previous visual filter token', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label'),
+ );
+
+ const original = tokensContainer.innerHTML;
+ gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
+ const updatedToken = tokensContainer.querySelector('.js-visual-token');
+
+ expect(updatedToken.querySelector('.name').innerText).toEqual('label');
+ expect(updatedToken.querySelector('.value').innerText).toEqual('value');
+ expect(original).not.toEqual(tokensContainer.innerHTML);
+ });
+ });
+
+ describe('addFilterVisualToken', () => {
+ it('creates visual token with just tokenName', () => {
+ gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone');
+ const token = tokensContainer.querySelector('.js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toEqual('milestone');
+ expect(token.querySelector('.value')).toEqual(null);
+ });
+
+ it('creates visual token with just tokenValue', () => {
+ gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone');
+ gl.FilteredSearchVisualTokens.addFilterVisualToken('%8.17');
+ const token = tokensContainer.querySelector('.js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toEqual('milestone');
+ expect(token.querySelector('.value').innerText).toEqual('%8.17');
+ });
+
+ it('creates full visual token', () => {
+ gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', '@john');
+ const token = tokensContainer.querySelector('.js-visual-token');
+
+ expect(token.classList.contains('filtered-search-token')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toEqual('assignee');
+ expect(token.querySelector('.value').innerText).toEqual('@john');
+ });
+ });
+
+ describe('addSearchVisualToken', () => {
+ it('creates search visual token', () => {
+ gl.FilteredSearchVisualTokens.addSearchVisualToken('search term');
+ const token = tokensContainer.querySelector('.js-visual-token');
+
+ expect(token.classList.contains('filtered-search-term')).toEqual(true);
+ expect(token.querySelector('.name').innerText).toEqual('search term');
+ expect(token.querySelector('.value')).toEqual(null);
+ });
+
+ it('appends to previous search visual token if previous token was a search token', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
+ `);
+
+ gl.FilteredSearchVisualTokens.addSearchVisualToken('append this');
+ const token = tokensContainer.querySelector('.filtered-search-term');
+
+ expect(token.querySelector('.name').innerText).toEqual('search term append this');
+ expect(token.querySelector('.value')).toEqual(null);
+ });
+ });
+
+ describe('getLastTokenPartial', () => {
+ it('should get last token value', () => {
+ const value = '~bug';
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', value),
+ );
+
+ expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(value);
+ });
+
+ it('should get last token name if there is no value', () => {
+ const name = 'assignee';
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createNameFilterVisualTokenHTML(name),
+ );
+
+ expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(name);
+ });
+
+ it('should return empty when there are no tokens', () => {
+ expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual('');
+ });
+ });
+
+ describe('removeLastTokenPartial', () => {
+ it('should remove the last token value if it exists', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~"Community Contribution"'),
+ );
+
+ expect(tokensContainer.querySelector('.js-visual-token .value')).not.toEqual(null);
+
+ gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+
+ expect(tokensContainer.querySelector('.js-visual-token .value')).toEqual(null);
+ });
+
+ it('should remove the last token name if there is no value', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('milestone'),
+ );
+
+ expect(tokensContainer.querySelector('.js-visual-token .name')).not.toEqual(null);
+
+ gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+
+ expect(tokensContainer.querySelector('.js-visual-token .name')).toEqual(null);
+ });
+
+ it('should not remove anything when there are no tokens', () => {
+ const html = tokensContainer.innerHTML;
+ gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+
+ expect(tokensContainer.innerHTML).toEqual(html);
+ });
+ });
+
+ describe('tokenizeInput', () => {
+ it('does not do anything if there is no input', () => {
+ const original = tokensContainer.innerHTML;
+ gl.FilteredSearchVisualTokens.tokenizeInput();
+
+ expect(tokensContainer.innerHTML).toEqual(original);
+ });
+
+ it('adds search visual token if previous visual token is valid', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('assignee', 'none'),
+ );
+
+ const input = document.querySelector('.filtered-search');
+ input.value = 'some value';
+ gl.FilteredSearchVisualTokens.tokenizeInput();
+
+ const newToken = tokensContainer.querySelector('.filtered-search-term');
+
+ expect(input.value).toEqual('');
+ expect(newToken.querySelector('.name').innerText).toEqual('some value');
+ expect(newToken.querySelector('.value')).toEqual(null);
+ });
+
+ it('adds value to previous visual token element if previous visual token is invalid', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee'),
+ );
+
+ const input = document.querySelector('.filtered-search');
+ input.value = '@john';
+ gl.FilteredSearchVisualTokens.tokenizeInput();
+
+ const updatedToken = tokensContainer.querySelector('.filtered-search-token');
+
+ expect(input.value).toEqual('');
+ expect(updatedToken.querySelector('.name').innerText).toEqual('assignee');
+ expect(updatedToken.querySelector('.value').innerText).toEqual('@john');
+ });
+ });
+
+ describe('editToken', () => {
+ let input;
+ let token;
+
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
+ ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search')}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'upcoming')}
+ `);
+
+ input = document.querySelector('.filtered-search');
+ token = document.querySelector('.js-visual-token');
+ });
+
+ it('tokenize\'s existing input', () => {
+ input.value = 'some text';
+ spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callThrough();
+
+ gl.FilteredSearchVisualTokens.editToken(token);
+
+ expect(gl.FilteredSearchVisualTokens.tokenizeInput).toHaveBeenCalled();
+ expect(input.value).not.toEqual('some text');
+ });
+
+ it('moves input to the token position', () => {
+ expect(tokensContainer.children[3].querySelector('.filtered-search')).not.toEqual(null);
+
+ gl.FilteredSearchVisualTokens.editToken(token);
+
+ expect(tokensContainer.children[1].querySelector('.filtered-search')).not.toEqual(null);
+ expect(tokensContainer.children[3].querySelector('.filtered-search')).toEqual(null);
+ });
+
+ it('input contains the visual token value', () => {
+ gl.FilteredSearchVisualTokens.editToken(token);
+
+ expect(input.value).toEqual('none');
+ });
+
+ describe('selected token is a search term token', () => {
+ beforeEach(() => {
+ token = document.querySelector('.filtered-search-term');
+ });
+
+ it('token is removed', () => {
+ expect(tokensContainer.querySelector('.filtered-search-term')).not.toEqual(null);
+
+ gl.FilteredSearchVisualTokens.editToken(token);
+
+ expect(tokensContainer.querySelector('.filtered-search-term')).toEqual(null);
+ });
+
+ it('input has the same value as removed token', () => {
+ expect(input.value).toEqual('');
+
+ gl.FilteredSearchVisualTokens.editToken(token);
+
+ expect(input.value).toEqual('search');
+ });
+ });
+ });
+
+ describe('moveInputTotheRight', () => {
+ it('does nothing if the input is already the right most element', () => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none'),
+ );
+
+ spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callFake(() => {});
+ spyOn(gl.FilteredSearchVisualTokens, 'getLastVisualTokenBeforeInput').and.callThrough();
+
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+
+ expect(gl.FilteredSearchVisualTokens.tokenizeInput).toHaveBeenCalled();
+ expect(gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput).not.toHaveBeenCalled();
+ });
+
+ it('tokenize\'s input', () => {
+ tokensContainer.innerHTML = `
+ ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')}
+ ${FilteredSearchSpecHelper.createInputHTML()}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ `;
+
+ document.querySelector('.filtered-search').value = 'none';
+
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ const value = tokensContainer.querySelector('.js-visual-token .value');
+
+ expect(value.innerText).toEqual('none');
+ });
+
+ it('converts input into search term token if last token is valid', () => {
+ tokensContainer.innerHTML = `
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
+ ${FilteredSearchSpecHelper.createInputHTML()}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ `;
+
+ document.querySelector('.filtered-search').value = 'test';
+
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ const searchValue = tokensContainer.querySelector('.filtered-search-term .name');
+
+ expect(searchValue.innerText).toEqual('test');
+ });
+
+ it('moves the input to the right most element', () => {
+ tokensContainer.innerHTML = `
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
+ ${FilteredSearchSpecHelper.createInputHTML()}
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ `;
+
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+
+ expect(tokensContainer.children[2].querySelector('.filtered-search')).not.toEqual(null);
+ });
+
+ it('tokenizes input even if input is the right most element', () => {
+ tokensContainer.innerHTML = `
+ ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
+ ${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')}
+ ${FilteredSearchSpecHelper.createInputHTML('', '~bug')}
+ `;
+
+ gl.FilteredSearchVisualTokens.moveInputToTheRight();
+
+ const token = tokensContainer.children[1];
+ expect(token.querySelector('.value').innerText).toEqual('~bug');
+ });
+ });
+});
diff --git a/spec/javascripts/fixtures/environments/metrics.html.haml b/spec/javascripts/fixtures/environments/metrics.html.haml
new file mode 100644
index 00000000000..483063fb889
--- /dev/null
+++ b/spec/javascripts/fixtures/environments/metrics.html.haml
@@ -0,0 +1,12 @@
+%div
+ .top-area
+ .row
+ .col-sm-6
+ %h3.page-title
+ Metrics for environment
+ .row
+ .col-sm-12
+ %svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
+ .row
+ .col-sm-12
+ %svg.prometheus-graph{ 'graph-type' => 'memory_values' } \ No newline at end of file
diff --git a/spec/javascripts/helpers/filtered_search_spec_helper.js b/spec/javascripts/helpers/filtered_search_spec_helper.js
new file mode 100644
index 00000000000..ce83a256ddd
--- /dev/null
+++ b/spec/javascripts/helpers/filtered_search_spec_helper.js
@@ -0,0 +1,52 @@
+class FilteredSearchSpecHelper {
+ static createFilterVisualTokenHTML(name, value, isSelected) {
+ return FilteredSearchSpecHelper.createFilterVisualToken(name, value, isSelected).outerHTML;
+ }
+
+ static createFilterVisualToken(name, value, isSelected = false) {
+ const li = document.createElement('li');
+ li.classList.add('js-visual-token', 'filtered-search-token');
+
+ li.innerHTML = `
+ <div class="selectable ${isSelected ? 'selected' : ''}" role="button">
+ <div class="name">${name}</div>
+ <div class="value">${value}</div>
+ </div>
+ `;
+
+ return li;
+ }
+
+ static createNameFilterVisualTokenHTML(name) {
+ return `
+ <li class="js-visual-token filtered-search-token">
+ <div class="name">${name}</div>
+ </li>
+ `;
+ }
+
+ static createSearchVisualTokenHTML(name) {
+ return `
+ <li class="js-visual-token filtered-search-term">
+ <div class="name">${name}</div>
+ </li>
+ `;
+ }
+
+ static createInputHTML(placeholder = '', value = '') {
+ return `
+ <li class="input-token">
+ <input type='text' class='filtered-search' placeholder='${placeholder}' value='${value}'/>
+ </li>
+ `;
+ }
+
+ static createTokensContainerHTML(html, inputPlaceholder) {
+ return `
+ ${html}
+ ${FilteredSearchSpecHelper.createInputHTML(inputPlaceholder)}
+ `;
+ }
+}
+
+module.exports = FilteredSearchSpecHelper;
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index e7530f61385..8d25500b9fd 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,10 +1,9 @@
/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
-/* global Issue */
+import Issue from '~/issue';
require('~/lib/utils/text_utility');
-require('~/issue');
-(function() {
+describe('Issue', function() {
var INVALID_URL = 'http://goesnowhere.nothing/whereami';
var $boxClosed, $boxOpen, $btnClose, $btnReopen;
@@ -59,28 +58,26 @@ require('~/issue');
expect($btnReopen).toHaveText('Reopen issue');
}
- describe('Issue', function() {
- describe('task lists', function() {
- beforeEach(function() {
- loadFixtures('issues/issue-with-task-list.html.raw');
- this.issue = new Issue();
- });
-
- it('modifies the Markdown field', function() {
- spyOn(jQuery, 'ajax').and.stub();
- $('input[type=checkbox]').attr('checked', true).trigger('change');
- expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
- });
+ describe('task lists', function() {
+ beforeEach(function() {
+ loadFixtures('issues/issue-with-task-list.html.raw');
+ this.issue = new Issue();
+ });
- it('submits an ajax request on tasklist:changed', function() {
- spyOn(jQuery, 'ajax').and.callFake(function(req) {
- expect(req.type).toBe('PATCH');
- expect(req.url).toBe(gl.TEST_HOST + '/frontend-fixtures/issues-project/issues/1.json'); // eslint-disable-line prefer-template
- expect(req.data.issue.description).not.toBe(null);
- });
+ it('modifies the Markdown field', function() {
+ spyOn(jQuery, 'ajax').and.stub();
+ $('input[type=checkbox]').attr('checked', true).trigger('change');
+ expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
+ });
- $('.js-task-list-field').trigger('tasklist:changed');
+ it('submits an ajax request on tasklist:changed', function() {
+ spyOn(jQuery, 'ajax').and.callFake(function(req) {
+ expect(req.type).toBe('PATCH');
+ expect(req.url).toBe(gl.TEST_HOST + '/frontend-fixtures/issues-project/issues/1.json'); // eslint-disable-line prefer-template
+ expect(req.data.issue.description).not.toBe(null);
});
+
+ $('.js-task-list-field').trigger('tasklist:changed');
});
});
@@ -165,4 +162,4 @@ require('~/issue');
expect($('.issue_counter')).toHaveText(1);
});
});
-}).call(window);
+});
diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js
new file mode 100644
index 00000000000..823b4bab7fc
--- /dev/null
+++ b/spec/javascripts/monitoring/prometheus_graph_spec.js
@@ -0,0 +1,78 @@
+import 'jquery';
+import es6Promise from 'es6-promise';
+import '~/lib/utils/common_utils';
+import PrometheusGraph from '~/monitoring/prometheus_graph';
+import { prometheusMockData } from './prometheus_mock_data';
+
+es6Promise.polyfill();
+
+describe('PrometheusGraph', () => {
+ const fixtureName = 'static/environments/metrics.html.raw';
+ const prometheusGraphContainer = '.prometheus-graph';
+ const prometheusGraphContents = `${prometheusGraphContainer}[graph-type=cpu_values]`;
+
+ preloadFixtures(fixtureName);
+
+ beforeEach(() => {
+ loadFixtures(fixtureName);
+ this.prometheusGraph = new PrometheusGraph();
+ const self = this;
+ const fakeInit = (metricsResponse) => {
+ self.prometheusGraph.transformData(metricsResponse);
+ self.prometheusGraph.createGraph();
+ };
+ spyOn(this.prometheusGraph, 'init').and.callFake(fakeInit);
+ });
+
+ it('initializes graph properties', () => {
+ // Test for the measurements
+ expect(this.prometheusGraph.margin).toBeDefined();
+ expect(this.prometheusGraph.marginLabelContainer).toBeDefined();
+ expect(this.prometheusGraph.originalWidth).toBeDefined();
+ expect(this.prometheusGraph.originalHeight).toBeDefined();
+ expect(this.prometheusGraph.height).toBeDefined();
+ expect(this.prometheusGraph.width).toBeDefined();
+ expect(this.prometheusGraph.backOffRequestCounter).toBeDefined();
+ // Test for the graph properties (colors, radius, etc.)
+ expect(this.prometheusGraph.graphSpecificProperties).toBeDefined();
+ expect(this.prometheusGraph.commonGraphProperties).toBeDefined();
+ });
+
+ it('transforms the data', () => {
+ this.prometheusGraph.init(prometheusMockData.metrics);
+ expect(this.prometheusGraph.data).toBeDefined();
+ expect(this.prometheusGraph.data.cpu_values.length).toBe(121);
+ expect(this.prometheusGraph.data.memory_values.length).toBe(121);
+ });
+
+ it('creates two graphs', () => {
+ this.prometheusGraph.init(prometheusMockData.metrics);
+ expect($(prometheusGraphContainer).length).toBe(2);
+ });
+
+ describe('Graph contents', () => {
+ beforeEach(() => {
+ this.prometheusGraph.init(prometheusMockData.metrics);
+ });
+
+ it('has axis, an area, a line and a overlay', () => {
+ const $graphContainer = $(prometheusGraphContents).find('.x-axis').parent();
+ expect($graphContainer.find('.x-axis')).toBeDefined();
+ expect($graphContainer.find('.y-axis')).toBeDefined();
+ expect($graphContainer.find('.prometheus-graph-overlay')).toBeDefined();
+ expect($graphContainer.find('.metric-line')).toBeDefined();
+ expect($graphContainer.find('.metric-area')).toBeDefined();
+ });
+
+ it('has legends, labels and an extra axis that labels the metrics', () => {
+ const $prometheusGraphContents = $(prometheusGraphContents);
+ const $axisLabelContainer = $(prometheusGraphContents).find('.label-x-axis-line').parent();
+ expect($prometheusGraphContents.find('.label-x-axis-line')).toBeDefined();
+ expect($prometheusGraphContents.find('.label-y-axis-line')).toBeDefined();
+ expect($prometheusGraphContents.find('.label-axis-text')).toBeDefined();
+ expect($prometheusGraphContents.find('.rect-axis-text')).toBeDefined();
+ expect($axisLabelContainer.find('rect').length).toBe(2);
+ expect($axisLabelContainer.find('text').length).toBe(4);
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/prometheus_mock_data.js b/spec/javascripts/monitoring/prometheus_mock_data.js
new file mode 100644
index 00000000000..1cdc14faaa8
--- /dev/null
+++ b/spec/javascripts/monitoring/prometheus_mock_data.js
@@ -0,0 +1,1014 @@
+/* eslint-disable import/prefer-default-export*/
+export const prometheusMockData = {
+ status: 200,
+ metrics: {
+ success: true,
+ metrics: {
+ memory_values: [
+ {
+ metric: {
+ },
+ values: [
+ [
+ 1488462917.256,
+ '10.12890625',
+ ],
+ [
+ 1488462977.256,
+ '10.140625',
+ ],
+ [
+ 1488463037.256,
+ '10.140625',
+ ],
+ [
+ 1488463097.256,
+ '10.14453125',
+ ],
+ [
+ 1488463157.256,
+ '10.1484375',
+ ],
+ [
+ 1488463217.256,
+ '10.15625',
+ ],
+ [
+ 1488463277.256,
+ '10.15625',
+ ],
+ [
+ 1488463337.256,
+ '10.15625',
+ ],
+ [
+ 1488463397.256,
+ '10.1640625',
+ ],
+ [
+ 1488463457.256,
+ '10.171875',
+ ],
+ [
+ 1488463517.256,
+ '10.171875',
+ ],
+ [
+ 1488463577.256,
+ '10.171875',
+ ],
+ [
+ 1488463637.256,
+ '10.18359375',
+ ],
+ [
+ 1488463697.256,
+ '10.1953125',
+ ],
+ [
+ 1488463757.256,
+ '10.203125',
+ ],
+ [
+ 1488463817.256,
+ '10.20703125',
+ ],
+ [
+ 1488463877.256,
+ '10.20703125',
+ ],
+ [
+ 1488463937.256,
+ '10.20703125',
+ ],
+ [
+ 1488463997.256,
+ '10.20703125',
+ ],
+ [
+ 1488464057.256,
+ '10.2109375',
+ ],
+ [
+ 1488464117.256,
+ '10.2109375',
+ ],
+ [
+ 1488464177.256,
+ '10.2109375',
+ ],
+ [
+ 1488464237.256,
+ '10.2109375',
+ ],
+ [
+ 1488464297.256,
+ '10.21484375',
+ ],
+ [
+ 1488464357.256,
+ '10.22265625',
+ ],
+ [
+ 1488464417.256,
+ '10.22265625',
+ ],
+ [
+ 1488464477.256,
+ '10.2265625',
+ ],
+ [
+ 1488464537.256,
+ '10.23046875',
+ ],
+ [
+ 1488464597.256,
+ '10.23046875',
+ ],
+ [
+ 1488464657.256,
+ '10.234375',
+ ],
+ [
+ 1488464717.256,
+ '10.234375',
+ ],
+ [
+ 1488464777.256,
+ '10.234375',
+ ],
+ [
+ 1488464837.256,
+ '10.234375',
+ ],
+ [
+ 1488464897.256,
+ '10.234375',
+ ],
+ [
+ 1488464957.256,
+ '10.234375',
+ ],
+ [
+ 1488465017.256,
+ '10.23828125',
+ ],
+ [
+ 1488465077.256,
+ '10.23828125',
+ ],
+ [
+ 1488465137.256,
+ '10.2421875',
+ ],
+ [
+ 1488465197.256,
+ '10.2421875',
+ ],
+ [
+ 1488465257.256,
+ '10.2421875',
+ ],
+ [
+ 1488465317.256,
+ '10.2421875',
+ ],
+ [
+ 1488465377.256,
+ '10.2421875',
+ ],
+ [
+ 1488465437.256,
+ '10.2421875',
+ ],
+ [
+ 1488465497.256,
+ '10.2421875',
+ ],
+ [
+ 1488465557.256,
+ '10.2421875',
+ ],
+ [
+ 1488465617.256,
+ '10.2421875',
+ ],
+ [
+ 1488465677.256,
+ '10.2421875',
+ ],
+ [
+ 1488465737.256,
+ '10.2421875',
+ ],
+ [
+ 1488465797.256,
+ '10.24609375',
+ ],
+ [
+ 1488465857.256,
+ '10.25',
+ ],
+ [
+ 1488465917.256,
+ '10.25390625',
+ ],
+ [
+ 1488465977.256,
+ '9.98828125',
+ ],
+ [
+ 1488466037.256,
+ '9.9921875',
+ ],
+ [
+ 1488466097.256,
+ '9.9921875',
+ ],
+ [
+ 1488466157.256,
+ '9.99609375',
+ ],
+ [
+ 1488466217.256,
+ '10',
+ ],
+ [
+ 1488466277.256,
+ '10.00390625',
+ ],
+ [
+ 1488466337.256,
+ '10.0078125',
+ ],
+ [
+ 1488466397.256,
+ '10.01171875',
+ ],
+ [
+ 1488466457.256,
+ '10.0234375',
+ ],
+ [
+ 1488466517.256,
+ '10.02734375',
+ ],
+ [
+ 1488466577.256,
+ '10.02734375',
+ ],
+ [
+ 1488466637.256,
+ '10.03125',
+ ],
+ [
+ 1488466697.256,
+ '10.03125',
+ ],
+ [
+ 1488466757.256,
+ '10.03125',
+ ],
+ [
+ 1488466817.256,
+ '10.03125',
+ ],
+ [
+ 1488466877.256,
+ '10.03125',
+ ],
+ [
+ 1488466937.256,
+ '10.03125',
+ ],
+ [
+ 1488466997.256,
+ '10.03125',
+ ],
+ [
+ 1488467057.256,
+ '10.0390625',
+ ],
+ [
+ 1488467117.256,
+ '10.0390625',
+ ],
+ [
+ 1488467177.256,
+ '10.04296875',
+ ],
+ [
+ 1488467237.256,
+ '10.05078125',
+ ],
+ [
+ 1488467297.256,
+ '10.05859375',
+ ],
+ [
+ 1488467357.256,
+ '10.06640625',
+ ],
+ [
+ 1488467417.256,
+ '10.06640625',
+ ],
+ [
+ 1488467477.256,
+ '10.0703125',
+ ],
+ [
+ 1488467537.256,
+ '10.07421875',
+ ],
+ [
+ 1488467597.256,
+ '10.0859375',
+ ],
+ [
+ 1488467657.256,
+ '10.0859375',
+ ],
+ [
+ 1488467717.256,
+ '10.09765625',
+ ],
+ [
+ 1488467777.256,
+ '10.1015625',
+ ],
+ [
+ 1488467837.256,
+ '10.10546875',
+ ],
+ [
+ 1488467897.256,
+ '10.10546875',
+ ],
+ [
+ 1488467957.256,
+ '10.125',
+ ],
+ [
+ 1488468017.256,
+ '10.13671875',
+ ],
+ [
+ 1488468077.256,
+ '10.1484375',
+ ],
+ [
+ 1488468137.256,
+ '10.15625',
+ ],
+ [
+ 1488468197.256,
+ '10.16796875',
+ ],
+ [
+ 1488468257.256,
+ '10.171875',
+ ],
+ [
+ 1488468317.256,
+ '10.171875',
+ ],
+ [
+ 1488468377.256,
+ '10.171875',
+ ],
+ [
+ 1488468437.256,
+ '10.171875',
+ ],
+ [
+ 1488468497.256,
+ '10.171875',
+ ],
+ [
+ 1488468557.256,
+ '10.171875',
+ ],
+ [
+ 1488468617.256,
+ '10.171875',
+ ],
+ [
+ 1488468677.256,
+ '10.17578125',
+ ],
+ [
+ 1488468737.256,
+ '10.17578125',
+ ],
+ [
+ 1488468797.256,
+ '10.265625',
+ ],
+ [
+ 1488468857.256,
+ '10.19921875',
+ ],
+ [
+ 1488468917.256,
+ '10.19921875',
+ ],
+ [
+ 1488468977.256,
+ '10.19921875',
+ ],
+ [
+ 1488469037.256,
+ '10.19921875',
+ ],
+ [
+ 1488469097.256,
+ '10.19921875',
+ ],
+ [
+ 1488469157.256,
+ '10.203125',
+ ],
+ [
+ 1488469217.256,
+ '10.43359375',
+ ],
+ [
+ 1488469277.256,
+ '10.20703125',
+ ],
+ [
+ 1488469337.256,
+ '10.2109375',
+ ],
+ [
+ 1488469397.256,
+ '10.22265625',
+ ],
+ [
+ 1488469457.256,
+ '10.21484375',
+ ],
+ [
+ 1488469517.256,
+ '10.21484375',
+ ],
+ [
+ 1488469577.256,
+ '10.21484375',
+ ],
+ [
+ 1488469637.256,
+ '10.22265625',
+ ],
+ [
+ 1488469697.256,
+ '10.234375',
+ ],
+ [
+ 1488469757.256,
+ '10.234375',
+ ],
+ [
+ 1488469817.256,
+ '10.234375',
+ ],
+ [
+ 1488469877.256,
+ '10.2421875',
+ ],
+ [
+ 1488469937.256,
+ '10.25',
+ ],
+ [
+ 1488469997.256,
+ '10.25390625',
+ ],
+ [
+ 1488470057.256,
+ '10.26171875',
+ ],
+ [
+ 1488470117.256,
+ '10.2734375',
+ ],
+ ],
+ },
+ ],
+ memory_current: [
+ {
+ metric: {
+ },
+ value: [
+ 1488470117.737,
+ '10.2734375',
+ ],
+ },
+ ],
+ cpu_values: [
+ {
+ metric: {
+ },
+ values: [
+ [
+ 1488462918.15,
+ '0.0002996458625058103',
+ ],
+ [
+ 1488462978.15,
+ '0.0002652382333333314',
+ ],
+ [
+ 1488463038.15,
+ '0.0003485461333333421',
+ ],
+ [
+ 1488463098.15,
+ '0.0003420421999999886',
+ ],
+ [
+ 1488463158.15,
+ '0.00023107150000001297',
+ ],
+ [
+ 1488463218.15,
+ '0.00030463981666664826',
+ ],
+ [
+ 1488463278.15,
+ '0.0002477177833333677',
+ ],
+ [
+ 1488463338.15,
+ '0.00026936656666665115',
+ ],
+ [
+ 1488463398.15,
+ '0.000406264750000022',
+ ],
+ [
+ 1488463458.15,
+ '0.00029592802026561453',
+ ],
+ [
+ 1488463518.15,
+ '0.00023426999683316343',
+ ],
+ [
+ 1488463578.15,
+ '0.0003057080666666915',
+ ],
+ [
+ 1488463638.15,
+ '0.0003408470500000149',
+ ],
+ [
+ 1488463698.15,
+ '0.00025497336666665166',
+ ],
+ [
+ 1488463758.15,
+ '0.0003009282833333534',
+ ],
+ [
+ 1488463818.15,
+ '0.0003119383499999924',
+ ],
+ [
+ 1488463878.15,
+ '0.00028719019999998705',
+ ],
+ [
+ 1488463938.15,
+ '0.000327864749999988',
+ ],
+ [
+ 1488463998.15,
+ '0.0002514917333333422',
+ ],
+ [
+ 1488464058.15,
+ '0.0003614651166666742',
+ ],
+ [
+ 1488464118.15,
+ '0.0003221668000000122',
+ ],
+ [
+ 1488464178.15,
+ '0.00023323083333330884',
+ ],
+ [
+ 1488464238.15,
+ '0.00028531499475009274',
+ ],
+ [
+ 1488464298.15,
+ '0.0002627695294921391',
+ ],
+ [
+ 1488464358.15,
+ '0.00027145463333333453',
+ ],
+ [
+ 1488464418.15,
+ '0.00025669488333335266',
+ ],
+ [
+ 1488464478.15,
+ '0.00022307761666665965',
+ ],
+ [
+ 1488464538.15,
+ '0.0003307265833333517',
+ ],
+ [
+ 1488464598.15,
+ '0.0002817050666666709',
+ ],
+ [
+ 1488464658.15,
+ '0.00022357458333332285',
+ ],
+ [
+ 1488464718.15,
+ '0.00032648590000000275',
+ ],
+ [
+ 1488464778.15,
+ '0.00028410750000000816',
+ ],
+ [
+ 1488464838.15,
+ '0.0003038076999999954',
+ ],
+ [
+ 1488464898.15,
+ '0.00037568226666667335',
+ ],
+ [
+ 1488464958.15,
+ '0.00020160354999999202',
+ ],
+ [
+ 1488465018.15,
+ '0.0003229403333333399',
+ ],
+ [
+ 1488465078.15,
+ '0.00033516069999999236',
+ ],
+ [
+ 1488465138.15,
+ '0.0003365978333333371',
+ ],
+ [
+ 1488465198.15,
+ '0.00020262178333331585',
+ ],
+ [
+ 1488465258.15,
+ '0.00040567498333331876',
+ ],
+ [
+ 1488465318.15,
+ '0.00029114155000001436',
+ ],
+ [
+ 1488465378.15,
+ '0.0002498841000000122',
+ ],
+ [
+ 1488465438.15,
+ '0.00027296763333331715',
+ ],
+ [
+ 1488465498.15,
+ '0.0002958794000000135',
+ ],
+ [
+ 1488465558.15,
+ '0.0002922354666666867',
+ ],
+ [
+ 1488465618.15,
+ '0.00034186624999999653',
+ ],
+ [
+ 1488465678.15,
+ '0.0003397984166666627',
+ ],
+ [
+ 1488465738.15,
+ '0.0002658284166666469',
+ ],
+ [
+ 1488465798.15,
+ '0.00026221139999999346',
+ ],
+ [
+ 1488465858.15,
+ '0.00029467960000001034',
+ ],
+ [
+ 1488465918.15,
+ '0.0002634141333333358',
+ ],
+ [
+ 1488465978.15,
+ '0.0003202958333333209',
+ ],
+ [
+ 1488466038.15,
+ '0.00037890760000000394',
+ ],
+ [
+ 1488466098.15,
+ '0.00023453356666666518',
+ ],
+ [
+ 1488466158.15,
+ '0.0002866827333333433',
+ ],
+ [
+ 1488466218.15,
+ '0.0003335935499999998',
+ ],
+ [
+ 1488466278.15,
+ '0.00022787131666666125',
+ ],
+ [
+ 1488466338.15,
+ '0.00033821938333333064',
+ ],
+ [
+ 1488466398.15,
+ '0.00029233375000001043',
+ ],
+ [
+ 1488466458.15,
+ '0.00026562758333333514',
+ ],
+ [
+ 1488466518.15,
+ '0.0003142600999999819',
+ ],
+ [
+ 1488466578.15,
+ '0.00027392178333333444',
+ ],
+ [
+ 1488466638.15,
+ '0.00028178598333334173',
+ ],
+ [
+ 1488466698.15,
+ '0.0002463400666666911',
+ ],
+ [
+ 1488466758.15,
+ '0.00040234373333332125',
+ ],
+ [
+ 1488466818.15,
+ '0.00023677453333332822',
+ ],
+ [
+ 1488466878.15,
+ '0.00030852703333333523',
+ ],
+ [
+ 1488466938.15,
+ '0.0003582272833333455',
+ ],
+ [
+ 1488466998.15,
+ '0.0002176380833332973',
+ ],
+ [
+ 1488467058.15,
+ '0.00026180203333335447',
+ ],
+ [
+ 1488467118.15,
+ '0.00027862966666667436',
+ ],
+ [
+ 1488467178.15,
+ '0.0002769731166666567',
+ ],
+ [
+ 1488467238.15,
+ '0.0002832899166666477',
+ ],
+ [
+ 1488467298.15,
+ '0.0003446533500000311',
+ ],
+ [
+ 1488467358.15,
+ '0.0002691345999999761',
+ ],
+ [
+ 1488467418.15,
+ '0.000284919933333357',
+ ],
+ [
+ 1488467478.15,
+ '0.0002396026166666528',
+ ],
+ [
+ 1488467538.15,
+ '0.00035625295000002075',
+ ],
+ [
+ 1488467598.15,
+ '0.00036759816666664946',
+ ],
+ [
+ 1488467658.15,
+ '0.00030326608333333855',
+ ],
+ [
+ 1488467718.15,
+ '0.00023584972418043393',
+ ],
+ [
+ 1488467778.15,
+ '0.00025744508892115107',
+ ],
+ [
+ 1488467838.15,
+ '0.00036737541666663395',
+ ],
+ [
+ 1488467898.15,
+ '0.00034325741666666094',
+ ],
+ [
+ 1488467958.15,
+ '0.00026390046666667407',
+ ],
+ [
+ 1488468018.15,
+ '0.0003302534500000102',
+ ],
+ [
+ 1488468078.15,
+ '0.00035243794999999527',
+ ],
+ [
+ 1488468138.15,
+ '0.00020149738333333407',
+ ],
+ [
+ 1488468198.15,
+ '0.0003183469666666679',
+ ],
+ [
+ 1488468258.15,
+ '0.0003835329166666845',
+ ],
+ [
+ 1488468318.15,
+ '0.0002485075333333124',
+ ],
+ [
+ 1488468378.15,
+ '0.0003011457166666768',
+ ],
+ [
+ 1488468438.15,
+ '0.00032242785497684965',
+ ],
+ [
+ 1488468498.15,
+ '0.0002659713747457531',
+ ],
+ [
+ 1488468558.15,
+ '0.0003476860333333202',
+ ],
+ [
+ 1488468618.15,
+ '0.00028336403333334794',
+ ],
+ [
+ 1488468678.15,
+ '0.00017132354999998728',
+ ],
+ [
+ 1488468738.15,
+ '0.0003001915833333276',
+ ],
+ [
+ 1488468798.15,
+ '0.0003025715666666725',
+ ],
+ [
+ 1488468858.15,
+ '0.0003012370166666815',
+ ],
+ [
+ 1488468918.15,
+ '0.00030203619999997025',
+ ],
+ [
+ 1488468978.15,
+ '0.0002804355000000314',
+ ],
+ [
+ 1488469038.15,
+ '0.00033194884999998564',
+ ],
+ [
+ 1488469098.15,
+ '0.00025201496666665455',
+ ],
+ [
+ 1488469158.15,
+ '0.0002777531500000189',
+ ],
+ [
+ 1488469218.15,
+ '0.0003314885833333392',
+ ],
+ [
+ 1488469278.15,
+ '0.0002234891422095589',
+ ],
+ [
+ 1488469338.15,
+ '0.000349117355867791',
+ ],
+ [
+ 1488469398.15,
+ '0.0004036731333333303',
+ ],
+ [
+ 1488469458.15,
+ '0.00024553911666667835',
+ ],
+ [
+ 1488469518.15,
+ '0.0003056456833333184',
+ ],
+ [
+ 1488469578.15,
+ '0.0002618737166666681',
+ ],
+ [
+ 1488469638.15,
+ '0.00022972643333331414',
+ ],
+ [
+ 1488469698.15,
+ '0.0003713522500000307',
+ ],
+ [
+ 1488469758.15,
+ '0.00018322576666666515',
+ ],
+ [
+ 1488469818.15,
+ '0.00034534762753952466',
+ ],
+ [
+ 1488469878.15,
+ '0.00028200510008501677',
+ ],
+ [
+ 1488469938.15,
+ '0.0002773708499999768',
+ ],
+ [
+ 1488469998.15,
+ '0.00027547160000001013',
+ ],
+ [
+ 1488470058.15,
+ '0.00031713610000000023',
+ ],
+ [
+ 1488470118.15,
+ '0.00035276853333332525',
+ ],
+ ],
+ },
+ ],
+ cpu_current: [
+ {
+ metric: {
+ },
+ value: [
+ 1488470118.566,
+ '0.00035276853333332525',
+ ],
+ },
+ ],
+ last_update: '2017-03-02T15:55:18.981Z',
+ },
+ },
+};
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 939e8cb3a56..03c4879ed6f 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -3,6 +3,24 @@ require 'spec_helper'
describe Gitlab::Auth, lib: true do
let(:gl_auth) { described_class }
+ describe 'constants' do
+ it 'API_SCOPES contains all scopes for API access' do
+ expect(subject::API_SCOPES).to eq [:api, :read_user]
+ end
+
+ it 'OPENID_SCOPES contains all scopes for OpenID Connect' do
+ expect(subject::OPENID_SCOPES).to eq [:openid]
+ end
+
+ it 'DEFAULT_SCOPES contains all default scopes' do
+ expect(subject::DEFAULT_SCOPES).to eq [:api]
+ end
+
+ it 'OPTIONAL_SCOPES contains all non-default scopes' do
+ expect(subject::OPTIONAL_SCOPES).to eq [:read_user, :openid]
+ end
+ end
+
describe 'find_for_git_client' do
context 'build token' do
subject { gl_auth.find_for_git_client('gitlab-ci-token', build.token, project: project, ip: 'ip') }
@@ -222,6 +240,18 @@ describe Gitlab::Auth, lib: true do
end
end
+ it "does not find user in blocked state" do
+ user.block
+
+ expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
+ end
+
+ it "does not find user in ldap_blocked state" do
+ user.ldap_block
+
+ expect( gl_auth.find_with_user_password(username, password) ).not_to eql user
+ end
+
context "with ldap enabled" do
before do
allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 3f11f0a4516..bc139d5ef28 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -824,6 +824,32 @@ describe Gitlab::Git::Repository, seed_helper: true do
it { is_expected.to eq(17) }
end
+ describe '#count_commits' do
+ context 'with after timestamp' do
+ it 'returns the number of commits after timestamp' do
+ options = { ref: 'master', limit: nil, after: Time.iso8601('2013-03-03T20:15:01+00:00') }
+
+ expect(repository.count_commits(options)).to eq(25)
+ end
+ end
+
+ context 'with before timestamp' do
+ it 'returns the number of commits after timestamp' do
+ options = { ref: 'feature', limit: nil, before: Time.iso8601('2015-03-03T20:15:01+00:00') }
+
+ expect(repository.count_commits(options)).to eq(9)
+ end
+ end
+
+ context 'with path' do
+ it 'returns the number of commits with path ' do
+ options = { ref: 'master', limit: nil, path: "encoding" }
+
+ expect(repository.count_commits(options)).to eq(2)
+ end
+ end
+ end
+
describe "branch_names_contains" do
subject { repository.branch_names_contains(SeedRepo::LastCommit::ID) }
diff --git a/spec/lib/gitlab/github_import/branch_formatter_spec.rb b/spec/lib/gitlab/github_import/branch_formatter_spec.rb
index 36e7d739f7e..3a31f93efa5 100644
--- a/spec/lib/gitlab/github_import/branch_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/branch_formatter_spec.rb
@@ -6,27 +6,27 @@ describe Gitlab::GithubImport::BranchFormatter, lib: true do
let(:repo) { double }
let(:raw) do
{
- ref: 'feature',
+ ref: 'branch-merged',
repo: repo,
sha: commit.id
}
end
describe '#exists?' do
- it 'returns true when both branch, and commit exists' do
+ it 'returns true when branch exists and commit is part of the branch' do
branch = described_class.new(project, double(raw))
expect(branch.exists?).to eq true
end
- it 'returns false when branch does not exist' do
- branch = described_class.new(project, double(raw.merge(ref: 'removed-branch')))
+ it 'returns false when branch exists and commit is not part of the branch' do
+ branch = described_class.new(project, double(raw.merge(ref: 'feature')))
expect(branch.exists?).to eq false
end
- it 'returns false when commit does not exist' do
- branch = described_class.new(project, double(raw.merge(sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b')))
+ it 'returns false when branch does not exist' do
+ branch = described_class.new(project, double(raw.merge(ref: 'removed-branch')))
expect(branch.exists?).to eq false
end
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
index 33d83d6d2f1..3f080de99dd 100644
--- a/spec/lib/gitlab/github_import/importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -130,7 +130,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
let!(:user) { create(:user, email: octocat.email) }
let(:repository) { double(id: 1, fork: false) }
let(:source_sha) { create(:commit, project: project).id }
- let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) }
+ let(:source_branch) { double(ref: 'branch-merged', repo: repository, sha: source_sha) }
let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) }
let(:pull_request) do
diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
index e46be18aa99..951cbea7857 100644
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -7,10 +7,12 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
let(:repository) { double(id: 1, fork: false) }
let(:source_repo) { repository }
- let(:source_branch) { double(ref: 'feature', repo: source_repo, sha: source_sha) }
+ let(:source_branch) { double(ref: 'branch-merged', repo: source_repo, sha: source_sha) }
+ let(:forked_source_repo) { double(id: 2, fork: true, name: 'otherproject', full_name: 'company/otherproject') }
let(:target_repo) { repository }
let(:target_branch) { double(ref: 'master', repo: target_repo, sha: target_sha) }
let(:removed_branch) { double(ref: 'removed-branch', repo: source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') }
+ let(:forked_branch) { double(ref: 'master', repo: forked_source_repo, sha: '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b') }
let(:octocat) { double(id: 123456, login: 'octocat', email: 'octocat@example.com') }
let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
@@ -49,7 +51,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
title: 'New feature',
description: "*Created by: octocat*\n\nPlease pull these awesome changes",
source_project: project,
- source_branch: 'feature',
+ source_branch: 'branch-merged',
source_branch_sha: source_sha,
target_project: project,
target_branch: 'master',
@@ -75,7 +77,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
title: 'New feature',
description: "*Created by: octocat*\n\nPlease pull these awesome changes",
source_project: project,
- source_branch: 'feature',
+ source_branch: 'branch-merged',
source_branch_sha: source_sha,
target_project: project,
target_branch: 'master',
@@ -102,7 +104,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
title: 'New feature',
description: "*Created by: octocat*\n\nPlease pull these awesome changes",
source_project: project,
- source_branch: 'feature',
+ source_branch: 'branch-merged',
source_branch_sha: source_sha,
target_project: project,
target_branch: 'master',
@@ -194,7 +196,7 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
let(:raw_data) { double(base_data) }
it 'returns branch ref' do
- expect(pull_request.source_branch_name).to eq 'feature'
+ expect(pull_request.source_branch_name).to eq 'branch-merged'
end
end
@@ -205,10 +207,18 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
expect(pull_request.source_branch_name).to eq 'pull/1347/removed-branch'
end
end
+
+ context 'when source branch is from a fork' do
+ let(:raw_data) { double(base_data.merge(head: forked_branch)) }
+
+ it 'prefixes branch name with pull request number and project with namespace to avoid collision' do
+ expect(pull_request.source_branch_name).to eq 'pull/1347/company/otherproject/master'
+ end
+ end
end
shared_examples 'Gitlab::GithubImport::PullRequestFormatter#target_branch_name' do
- context 'when source branch exists' do
+ context 'when target branch exists' do
let(:raw_data) { double(base_data) }
it 'returns branch ref' do
@@ -271,6 +281,24 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
end
+ describe '#cross_project?' do
+ context 'when source and target repositories are different' do
+ let(:raw_data) { double(base_data.merge(head: forked_branch)) }
+
+ it 'returns true' do
+ expect(pull_request.cross_project?).to eq true
+ end
+ end
+
+ context 'when source and target repositories are the same' do
+ let(:raw_data) { double(base_data.merge(head: source_branch)) }
+
+ it 'returns false' do
+ expect(pull_request.cross_project?).to eq false
+ end
+ end
+ end
+
describe '#url' do
let(:raw_data) { double(base_data) }
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 1a1280e5198..e47956a365f 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -136,6 +136,7 @@ project:
- slack_slash_commands_service
- irker_service
- pivotaltracker_service
+- prometheus_service
- hipchat_service
- flowdock_service
- assembla_service
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 3bd1f335a89..c718e792461 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -21,6 +21,7 @@ Issue:
- milestone_id
- weight
- time_estimate
+- relative_position
Event:
- id
- target_type
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
index fd3769d75b5..c2ab015d5cb 100644
--- a/spec/lib/gitlab/middleware/go_spec.rb
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -15,16 +15,93 @@ describe Gitlab::Middleware::Go, lib: true do
end
describe 'when go-get=1' do
- it 'returns a document' do
- env = { 'rack.input' => '',
- 'QUERY_STRING' => 'go-get=1',
- 'PATH_INFO' => '/group/project/path' }
- resp = middleware.call(env)
- expect(resp[0]).to eq(200)
- expect(resp[1]['Content-Type']).to eq('text/html')
- expected_body = "<!DOCTYPE html><html><head><meta content='#{Gitlab.config.gitlab.host}/group/project git http://#{Gitlab.config.gitlab.host}/group/project.git' name='go-import'></head></html>\n"
- expect(resp[2].body).to eq([expected_body])
+ let(:current_user) { nil }
+
+ context 'with simple 2-segment project path' do
+ let!(:project) { create(:project, :private) }
+
+ context 'with subpackages' do
+ let(:path) { "#{project.full_path}/subpackage" }
+
+ it 'returns the full project path' do
+ expect_response_with_path(go, project.full_path)
+ end
+ end
+
+ context 'without subpackages' do
+ let(:path) { project.full_path }
+
+ it 'returns the full project path' do
+ expect_response_with_path(go, project.full_path)
+ end
+ end
+ end
+
+ context 'with a nested project path' do
+ let(:group) { create(:group, :nested) }
+ let!(:project) { create(:project, :public, namespace: group) }
+
+ shared_examples 'a nested project' do
+ context 'when the project is public' do
+ it 'returns the full project path' do
+ expect_response_with_path(go, project.full_path)
+ end
+ end
+
+ context 'when the project is private' do
+ before do
+ project.update_attribute(:visibility_level, Project::PRIVATE)
+ end
+
+ context 'with access to the project' do
+ let(:current_user) { project.creator }
+
+ before do
+ project.team.add_master(current_user)
+ end
+
+ it 'returns the full project path' do
+ expect_response_with_path(go, project.full_path)
+ end
+ end
+
+ context 'without access to the project' do
+ it 'returns the 2-segment group path' do
+ expect_response_with_path(go, group.full_path)
+ end
+ end
+ end
+ end
+
+ context 'with subpackages' do
+ let(:path) { "#{project.full_path}/subpackage" }
+
+ it_behaves_like 'a nested project'
+ end
+
+ context 'without subpackages' do
+ let(:path) { project.full_path }
+
+ it_behaves_like 'a nested project'
+ end
end
end
+
+ def go
+ env = {
+ 'rack.input' => '',
+ 'QUERY_STRING' => 'go-get=1',
+ 'PATH_INFO' => "/#{path}",
+ 'warden' => double(authenticate: current_user)
+ }
+ middleware.call(env)
+ end
+
+ def expect_response_with_path(response, path)
+ expect(response[0]).to eq(200)
+ expect(response[1]['Content-Type']).to eq('text/html')
+ expected_body = "<!DOCTYPE html><html><head><meta content='#{Gitlab.config.gitlab.host}/#{path} git http://#{Gitlab.config.gitlab.host}/#{path}.git' name='go-import'></head></html>\n"
+ expect(response[2].body).to eq([expected_body])
+ end
end
end
diff --git a/spec/lib/gitlab/prometheus_spec.rb b/spec/lib/gitlab/prometheus_spec.rb
new file mode 100644
index 00000000000..280264188e2
--- /dev/null
+++ b/spec/lib/gitlab/prometheus_spec.rb
@@ -0,0 +1,143 @@
+require 'spec_helper'
+
+describe Gitlab::Prometheus, lib: true do
+ include PrometheusHelpers
+
+ subject { described_class.new(api_url: 'https://prometheus.example.com') }
+
+ describe '#ping' do
+ it 'issues a "query" request to the API endpoint' do
+ req_stub = stub_prometheus_request(prometheus_query_url('1'), body: prometheus_value_body('vector'))
+
+ expect(subject.ping).to eq({ "resultType" => "vector", "result" => [{ "metric" => {}, "value" => [1488772511.004, "0.000041021495238095323"] }] })
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ # This shared examples expect:
+ # - query_url: A query URL
+ # - execute_query: A query call
+ shared_examples 'failure response' do
+ context 'when request returns 400 with an error message' do
+ it 'raises a Gitlab::PrometheusError error' do
+ req_stub = stub_prometheus_request(query_url, status: 400, body: { error: 'bar!' })
+
+ expect { execute_query }
+ .to raise_error(Gitlab::PrometheusError, 'bar!')
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ context 'when request returns 400 without an error message' do
+ it 'raises a Gitlab::PrometheusError error' do
+ req_stub = stub_prometheus_request(query_url, status: 400)
+
+ expect { execute_query }
+ .to raise_error(Gitlab::PrometheusError, 'Bad data received')
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ context 'when request returns 500' do
+ it 'raises a Gitlab::PrometheusError error' do
+ req_stub = stub_prometheus_request(query_url, status: 500, body: { message: 'FAIL!' })
+
+ expect { execute_query }
+ .to raise_error(Gitlab::PrometheusError, '500 - {"message":"FAIL!"}')
+ expect(req_stub).to have_been_requested
+ end
+ end
+ end
+
+ describe '#query' do
+ let(:prometheus_query) { prometheus_cpu_query('env-slug') }
+ let(:query_url) { prometheus_query_url(prometheus_query) }
+
+ context 'when request returns vector results' do
+ it 'returns data from the API call' do
+ req_stub = stub_prometheus_request(query_url, body: prometheus_value_body('vector'))
+
+ expect(subject.query(prometheus_query)).to eq [{ "metric" => {}, "value" => [1488772511.004, "0.000041021495238095323"] }]
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ context 'when request returns matrix results' do
+ it 'returns nil' do
+ req_stub = stub_prometheus_request(query_url, body: prometheus_value_body('matrix'))
+
+ expect(subject.query(prometheus_query)).to be_nil
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ context 'when request returns no data' do
+ it 'returns []' do
+ req_stub = stub_prometheus_request(query_url, body: prometheus_empty_body('vector'))
+
+ expect(subject.query(prometheus_query)).to be_empty
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ it_behaves_like 'failure response' do
+ let(:execute_query) { subject.query(prometheus_query) }
+ end
+ end
+
+ describe '#query_range' do
+ let(:prometheus_query) { prometheus_memory_query('env-slug') }
+ let(:query_url) { prometheus_query_range_url(prometheus_query) }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ context 'when a start time is passed' do
+ let(:query_url) { prometheus_query_range_url(prometheus_query, start: 2.hours.ago) }
+
+ it 'passed it in the requested URL' do
+ req_stub = stub_prometheus_request(query_url, body: prometheus_values_body('vector'))
+
+ subject.query_range(prometheus_query, start: 2.hours.ago)
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ context 'when request returns vector results' do
+ it 'returns nil' do
+ req_stub = stub_prometheus_request(query_url, body: prometheus_values_body('vector'))
+
+ expect(subject.query_range(prometheus_query)).to be_nil
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ context 'when request returns matrix results' do
+ it 'returns data from the API call' do
+ req_stub = stub_prometheus_request(query_url, body: prometheus_values_body('matrix'))
+
+ expect(subject.query_range(prometheus_query)).to eq([
+ {
+ "metric" => {},
+ "values" => [[1488758662.506, "0.00002996364761904785"], [1488758722.506, "0.00003090239047619091"]]
+ }
+ ])
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ context 'when request returns no data' do
+ it 'returns []' do
+ req_stub = stub_prometheus_request(query_url, body: prometheus_empty_body('matrix'))
+
+ expect(subject.query_range(prometheus_query)).to be_empty
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ it_behaves_like 'failure response' do
+ let(:execute_query) { subject.query_range(prometheus_query) }
+ end
+ end
+end
diff --git a/spec/models/chat_team_spec.rb b/spec/models/chat_team_spec.rb
index 1aab161ec13..5283561a83f 100644
--- a/spec/models/chat_team_spec.rb
+++ b/spec/models/chat_team_spec.rb
@@ -1,9 +1,14 @@
require 'spec_helper'
describe ChatTeam, type: :model do
+ subject { create(:chat_team) }
+
# Associations
it { is_expected.to belong_to(:namespace) }
+ # Validations
+ it { is_expected.to validate_uniqueness_of(:namespace) }
+
# Fields
it { is_expected.to respond_to(:name) }
it { is_expected.to respond_to(:team_id) }
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 2db42a94077..fd6ea2d6722 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -345,11 +345,11 @@ describe Ci::Build, :models do
describe '#expanded_environment_name' do
subject { build.expanded_environment_name }
- context 'when environment uses $CI_BUILD_REF_NAME' do
+ context 'when environment uses $CI_COMMIT_REF_NAME' do
let(:build) do
create(:ci_build,
ref: 'master',
- environment: 'review/$CI_BUILD_REF_NAME')
+ environment: 'review/$CI_COMMIT_REF_NAME')
end
it { is_expected.to eq('review/master') }
@@ -915,7 +915,7 @@ describe Ci::Build, :models do
end
context 'referenced with a variable' do
- let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_BUILD_REF_NAME") }
+ let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_COMMIT_REF_NAME") }
it { is_expected.to eq(@environment) }
end
@@ -1286,23 +1286,25 @@ describe Ci::Build, :models do
[
{ key: 'CI', value: 'true', public: true },
{ key: 'GITLAB_CI', value: 'true', public: true },
- { key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
- { key: 'CI_BUILD_TOKEN', value: build.token, public: false },
- { key: 'CI_BUILD_REF', value: build.sha, public: true },
- { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true },
- { key: 'CI_BUILD_REF_NAME', value: 'master', public: true },
- { key: 'CI_BUILD_REF_SLUG', value: 'master', public: true },
- { key: 'CI_BUILD_NAME', value: 'test', public: true },
- { key: 'CI_BUILD_STAGE', value: 'test', public: true },
{ key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
{ key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
{ key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
+ { key: 'CI_JOB_ID', value: build.id.to_s, public: true },
+ { key: 'CI_JOB_NAME', value: 'test', public: true },
+ { key: 'CI_JOB_STAGE', value: 'test', public: true },
+ { key: 'CI_JOB_TOKEN', value: build.token, public: false },
+ { key: 'CI_COMMIT_SHA', value: build.sha, public: true },
+ { key: 'CI_COMMIT_REF_NAME', value: build.ref, public: true },
+ { key: 'CI_COMMIT_REF_SLUG', value: build.ref_slug, public: true },
{ key: 'CI_PROJECT_ID', value: project.id.to_s, public: true },
{ key: 'CI_PROJECT_NAME', value: project.path, public: true },
{ key: 'CI_PROJECT_PATH', value: project.full_path, public: true },
{ key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true },
{ key: 'CI_PROJECT_URL', value: project.web_url, public: true },
- { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }
+ { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true },
+ { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true },
+ { key: 'CI_REGISTRY_PASSWORD', value: build.token, public: false },
+ { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false },
]
end
@@ -1317,7 +1319,7 @@ describe Ci::Build, :models do
build.yaml_variables = []
end
- it { is_expected.to eq(predefined_variables) }
+ it { is_expected.to include(*predefined_variables) }
end
context 'when build has user' do
@@ -1355,7 +1357,7 @@ describe Ci::Build, :models do
end
let(:manual_variable) do
- { key: 'CI_BUILD_MANUAL', value: 'true', public: true }
+ { key: 'CI_JOB_MANUAL', value: 'true', public: true }
end
it { is_expected.to include(manual_variable) }
@@ -1363,7 +1365,7 @@ describe Ci::Build, :models do
context 'when build is for tag' do
let(:tag_variable) do
- { key: 'CI_BUILD_TAG', value: 'master', public: true }
+ { key: 'CI_COMMIT_TAG', value: 'master', public: true }
end
before do
@@ -1392,7 +1394,7 @@ describe Ci::Build, :models do
{ key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false }
end
let(:predefined_trigger_variable) do
- { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true }
+ { key: 'CI_PIPELINE_TRIGGERED', value: 'true', public: true }
end
before do
@@ -1416,7 +1418,7 @@ describe Ci::Build, :models do
context 'when config is not found' do
let(:config) { nil }
- it { is_expected.to eq(predefined_variables) }
+ it { is_expected.to include(*predefined_variables) }
end
context 'when config does not have a questioned job' do
@@ -1428,7 +1430,7 @@ describe Ci::Build, :models do
})
end
- it { is_expected.to eq(predefined_variables) }
+ it { is_expected.to include(*predefined_variables) }
end
context 'when config has variables' do
@@ -1446,7 +1448,8 @@ describe Ci::Build, :models do
[{ key: 'KEY', value: 'value', public: true }]
end
- it { is_expected.to eq(predefined_variables + variables) }
+ it { is_expected.to include(*predefined_variables) }
+ it { is_expected.to include(*variables) }
end
end
end
diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb
new file mode 100644
index 00000000000..69906382545
--- /dev/null
+++ b/spec/models/concerns/relative_positioning_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe Issue, 'RelativePositioning' do
+ let(:project) { create(:empty_project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:issue1) { create(:issue, project: project) }
+ let(:new_issue) { create(:issue, project: project) }
+
+ before do
+ [issue, issue1].each do |issue|
+ issue.move_to_end && issue.save
+ end
+ end
+
+ describe '#min_relative_position' do
+ it 'returns maximum position' do
+ expect(issue.min_relative_position).to eq issue.relative_position
+ end
+ end
+
+ describe '#max_relative_position' do
+ it 'returns maximum position' do
+ expect(issue.max_relative_position).to eq issue1.relative_position
+ end
+ end
+
+ describe '#prev_relative_position' do
+ it 'returns previous position if there is an issue above' do
+ expect(issue1.prev_relative_position).to eq issue.relative_position
+ end
+
+ it 'returns minimum position if there is no issue above' do
+ expect(issue.prev_relative_position).to eq RelativePositioning::MIN_POSITION
+ end
+ end
+
+ describe '#next_relative_position' do
+ it 'returns next position if there is an issue below' do
+ expect(issue.next_relative_position).to eq issue1.relative_position
+ end
+
+ it 'returns next position if there is no issue below' do
+ expect(issue1.next_relative_position).to eq RelativePositioning::MAX_POSITION
+ end
+ end
+
+ describe '#move_before' do
+ it 'moves issue before' do
+ [issue1, issue].each(&:move_to_end)
+
+ issue.move_before(issue1)
+
+ expect(issue.relative_position).to be < issue1.relative_position
+ end
+ end
+
+ describe '#move_after' do
+ it 'moves issue after' do
+ [issue, issue1].each(&:move_to_end)
+
+ issue.move_after(issue1)
+
+ expect(issue.relative_position).to be > issue1.relative_position
+ end
+ end
+
+ describe '#move_to_end' do
+ it 'moves issue to the end' do
+ new_issue.move_to_end
+
+ expect(new_issue.relative_position).to be > issue1.relative_position
+ end
+ end
+
+ describe '#move_between' do
+ it 'positions issue between two other' do
+ new_issue.move_between(issue, issue1)
+
+ expect(new_issue.relative_position).to be > issue.relative_position
+ expect(new_issue.relative_position).to be < issue1.relative_position
+ end
+
+ it 'positions issue between on top' do
+ new_issue.move_between(nil, issue)
+
+ expect(new_issue.relative_position).to be < issue.relative_position
+ end
+
+ it 'positions issue between to end' do
+ new_issue.move_between(issue1, nil)
+
+ expect(new_issue.relative_position).to be > issue1.relative_position
+ end
+
+ it 'positions issues even when after and before positions are the same' do
+ issue1.update relative_position: issue.relative_position
+
+ new_issue.move_between(issue, issue1)
+
+ expect(new_issue.relative_position).to be > issue.relative_position
+ expect(issue.relative_position).to be < issue1.relative_position
+ end
+ end
+end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index dce18f008f8..b4305e92812 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -271,7 +271,11 @@ describe Environment, models: true do
context 'when the environment is unavailable' do
let(:project) { create(:kubernetes_project) }
- before { environment.stop }
+
+ before do
+ environment.stop
+ end
+
it { is_expected.to be_falsy }
end
end
@@ -281,20 +285,85 @@ describe Environment, models: true do
subject { environment.terminals }
context 'when the environment has terminals' do
- before { allow(environment).to receive(:has_terminals?).and_return(true) }
+ before do
+ allow(environment).to receive(:has_terminals?).and_return(true)
+ end
it 'returns the terminals from the deployment service' do
- expect(project.deployment_service).
- to receive(:terminals).with(environment).
- and_return(:fake_terminals)
+ expect(project.deployment_service)
+ .to receive(:terminals).with(environment)
+ .and_return(:fake_terminals)
is_expected.to eq(:fake_terminals)
end
end
context 'when the environment does not have terminals' do
- before { allow(environment).to receive(:has_terminals?).and_return(false) }
- it { is_expected.to eq(nil) }
+ before do
+ allow(environment).to receive(:has_terminals?).and_return(false)
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#has_metrics?' do
+ subject { environment.has_metrics? }
+
+ context 'when the enviroment is available' do
+ context 'with a deployment service' do
+ let(:project) { create(:prometheus_project) }
+
+ context 'and a deployment' do
+ let!(:deployment) { create(:deployment, environment: environment) }
+ it { is_expected.to be_truthy }
+ end
+
+ context 'but no deployments' do
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ context 'without a monitoring service' do
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ context 'when the environment is unavailable' do
+ let(:project) { create(:prometheus_project) }
+
+ before do
+ environment.stop
+ end
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '#metrics' do
+ let(:project) { create(:prometheus_project) }
+ subject { environment.metrics }
+
+ context 'when the environment has metrics' do
+ before do
+ allow(environment).to receive(:has_metrics?).and_return(true)
+ end
+
+ it 'returns the metrics from the deployment service' do
+ expect(project.monitoring_service)
+ .to receive(:metrics).with(environment)
+ .and_return(:fake_metrics)
+
+ is_expected.to eq(:fake_metrics)
+ end
+ end
+
+ context 'when the environment does not have metrics' do
+ before do
+ allow(environment).to receive(:has_metrics?).and_return(false)
+ end
+
+ it { is_expected.to be_nil }
end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 7525a1b79ee..757f3921450 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -165,7 +165,7 @@ describe Namespace, models: true do
describe :rm_dir do
let!(:project) { create(:empty_project, namespace: namespace) }
- let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.full_path) }
+ let!(:path) { File.join(Gitlab.config.repositories.storages.default['path'], namespace.full_path) }
it "removes its dirs when deleted" do
namespace.destroy
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index 7c9f4aad836..823623d96fa 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -34,4 +34,28 @@ describe PersonalAccessToken, models: true do
expect(active_personal_access_token).to be_active
end
end
+
+ context "validations" do
+ let(:personal_access_token) { build(:personal_access_token) }
+
+ it "requires at least one scope" do
+ personal_access_token.scopes = []
+
+ expect(personal_access_token).not_to be_valid
+ expect(personal_access_token.errors[:scopes].first).to eq "can't be blank"
+ end
+
+ it "allows creating a token with API scopes" do
+ personal_access_token.scopes = [:api, :read_user]
+
+ expect(personal_access_token).to be_valid
+ end
+
+ it "rejects creating a token with non-API scopes" do
+ personal_access_token.scopes = [:openid, :api]
+
+ expect(personal_access_token).not_to be_valid
+ expect(personal_access_token.errors[:scopes].first).to eq "can only contain API scopes"
+ end
+ end
end
diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb
new file mode 100644
index 00000000000..d15079b686b
--- /dev/null
+++ b/spec/models/project_services/prometheus_service_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe PrometheusService, models: true, caching: true do
+ include PrometheusHelpers
+ include ReactiveCachingHelpers
+
+ let(:project) { create(:prometheus_project) }
+ let(:service) { project.prometheus_service }
+
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ end
+
+ describe 'Validations' do
+ context 'when service is active' do
+ before { subject.active = true }
+
+ it { is_expected.to validate_presence_of(:api_url) }
+ end
+
+ context 'when service is inactive' do
+ before { subject.active = false }
+
+ it { is_expected.not_to validate_presence_of(:api_url) }
+ end
+ end
+
+ describe '#test' do
+ let!(:req_stub) { stub_prometheus_request(prometheus_query_url('1'), body: prometheus_value_body('vector')) }
+
+ context 'success' do
+ it 'reads the discovery endpoint' do
+ expect(service.test[:success]).to be_truthy
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ context 'failure' do
+ let!(:req_stub) { stub_prometheus_request(prometheus_query_url('1'), status: 404) }
+
+ it 'fails to read the discovery endpoint' do
+ expect(service.test[:success]).to be_falsy
+ expect(req_stub).to have_been_requested
+ end
+ end
+ end
+
+ describe '#metrics' do
+ let(:environment) { build_stubbed(:environment, slug: 'env-slug') }
+ subject { service.metrics(environment) }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ context 'with valid data' do
+ before do
+ stub_reactive_cache(service, prometheus_data, 'env-slug')
+ end
+
+ it 'returns reactive data' do
+ is_expected.to eq(prometheus_data)
+ end
+ end
+ end
+
+ describe '#calculate_reactive_cache' do
+ let(:environment) { build_stubbed(:environment, slug: 'env-slug') }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ subject do
+ service.calculate_reactive_cache(environment.slug)
+ end
+
+ context 'when service is inactive' do
+ before do
+ service.active = false
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when Prometheus responds with valid data' do
+ before do
+ stub_all_prometheus_requests(environment.slug)
+ end
+
+ it { expect(subject.to_json).to eq(prometheus_data.to_json) }
+ end
+
+ [404, 500].each do |status|
+ context "when Prometheus responds with #{status}" do
+ before do
+ stub_all_prometheus_requests(environment.slug, status: status, body: 'QUERY FAILED!')
+ end
+
+ it { is_expected.to eq(success: false, result: %(#{status} - "QUERY FAILED!")) }
+ end
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 84bdcbe8e59..e120e21af06 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -179,7 +179,7 @@ describe Project, models: true do
let(:project2) { build(:empty_project, repository_storage: 'missing') }
before do
- storages = { 'custom' => 'tmp/tests/custom_repositories' }
+ storages = { 'custom' => { 'path' => 'tmp/tests/custom_repositories' } }
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
@@ -381,7 +381,7 @@ describe Project, models: true do
before do
FileUtils.mkdir('tmp/tests/custom_repositories')
- storages = { 'custom' => 'tmp/tests/custom_repositories' }
+ storages = { 'custom' => { 'path' => 'tmp/tests/custom_repositories' } }
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
@@ -947,8 +947,8 @@ describe Project, models: true do
before do
storages = {
- 'default' => 'tmp/tests/repositories',
- 'picked' => 'tmp/tests/repositories',
+ 'default' => { 'path' => 'tmp/tests/repositories' },
+ 'picked' => { 'path' => 'tmp/tests/repositories' },
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index eb992e1354e..274e4f00a0a 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1042,7 +1042,7 @@ describe Repository, models: true do
it 'expires the cache for all branches' do
expect(cache).to receive(:expire).
- at_least(repository.branches.length).
+ at_least(repository.branches.length * 2).
times
repository.expire_branch_cache
@@ -1050,14 +1050,14 @@ describe Repository, models: true do
it 'expires the cache for all branches when the root branch is given' do
expect(cache).to receive(:expire).
- at_least(repository.branches.length).
+ at_least(repository.branches.length * 2).
times
repository.expire_branch_cache(repository.root_ref)
end
it 'expires the cache for a specific branch' do
- expect(cache).to receive(:expire).once
+ expect(cache).to receive(:expire).twice
repository.expire_branch_cache('foo')
end
@@ -1742,6 +1742,29 @@ describe Repository, models: true do
end
end
+ describe '#commit_count_for_ref' do
+ let(:project) { create :empty_project }
+
+ context 'with a non-existing repository' do
+ it 'returns 0' do
+ expect(project.repository.commit_count_for_ref('master')).to eq(0)
+ end
+ end
+
+ context 'with empty repository' do
+ it 'returns 0' do
+ project.create_repository
+ expect(project.repository.commit_count_for_ref('master')).to eq(0)
+ end
+ end
+
+ context 'when searching for the root ref' do
+ it 'returns the same count as #commit_count' do
+ expect(repository.commit_count_for_ref(repository.root_ref)).to eq(repository.commit_count)
+ end
+ end
+ end
+
describe '#cache_method_output', caching: true do
context 'with a non-existing repository' do
let(:value) do
diff --git a/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb b/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb
new file mode 100644
index 00000000000..6443f86b6a1
--- /dev/null
+++ b/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Projects::Settings::DeployKeysPresenter do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:deploy_key) { create(:deploy_key, public: true) }
+
+ let!(:deploy_keys_project) do
+ create(:deploy_keys_project, project: project, deploy_key: deploy_key)
+ end
+
+ subject(:presenter) do
+ described_class.new(project, current_user: user)
+ end
+
+ it 'inherits from Gitlab::View::Presenter::Simple' do
+ expect(described_class.superclass).to eq(Gitlab::View::Presenter::Simple)
+ end
+
+ describe '#enabled_keys' do
+ it 'returns currently enabled keys' do
+ expect(presenter.enabled_keys).to eq [deploy_keys_project.deploy_key]
+ end
+
+ it 'does not contain enabled_keys inside available_keys' do
+ expect(presenter.available_keys).not_to include deploy_key
+ end
+
+ it 'returns the enabled_keys size' do
+ expect(presenter.enabled_keys_size).to eq(1)
+ end
+
+ it 'returns true if there is any enabled_keys' do
+ expect(presenter.any_keys_enabled?).to eq(true)
+ end
+ end
+
+ describe '#available_keys/#available_project_keys' do
+ let(:other_deploy_key) { create(:another_deploy_key) }
+
+ before do
+ project_key = create(:deploy_keys_project, deploy_key: other_deploy_key)
+ project_key.project.add_developer(user)
+ end
+
+ it 'returns the current available_keys' do
+ expect(presenter.available_keys).not_to be_empty
+ end
+
+ it 'returns the current available_project_keys' do
+ expect(presenter.available_project_keys).not_to be_empty
+ end
+
+ it 'returns false if any available_project_keys are enabled' do
+ expect(presenter.any_available_project_keys_enabled?).to eq(true)
+ end
+
+ it 'returns the available_project_keys size' do
+ expect(presenter.available_project_keys_size).to eq(1)
+ end
+
+ it 'shows if there is an available key' do
+ expect(presenter.key_available?(deploy_key)).to eq(false)
+ end
+ end
+end
diff --git a/spec/requests/api/api_internal_helpers_spec.rb b/spec/requests/api/api_internal_helpers_spec.rb
index be4bc39ada2..f5265ea60ff 100644
--- a/spec/requests/api/api_internal_helpers_spec.rb
+++ b/spec/requests/api/api_internal_helpers_spec.rb
@@ -21,7 +21,7 @@ describe ::API::Helpers::InternalHelpers do
# Relative and absolute storage paths, with and without trailing /
['.', './', Dir.pwd, Dir.pwd + '/'].each do |storage_path|
context "storage path is #{storage_path}" do
- subject { clean_project_path(project_path, [storage_path]) }
+ subject { clean_project_path(project_path, [{ 'path' => storage_path }]) }
it { is_expected.to eq(expected) }
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 5190fcca2d1..585449e62b6 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -19,6 +19,7 @@ describe API::Commits, api: true do
it "returns project commits" do
commit = project.repository.commit
+
get api("/projects/#{project.id}/repository/commits", user)
expect(response).to have_http_status(200)
@@ -27,6 +28,16 @@ describe API::Commits, api: true do
expect(json_response.first['committer_name']).to eq(commit.committer_name)
expect(json_response.first['committer_email']).to eq(commit.committer_email)
end
+
+ it 'include correct pagination headers' do
+ commit_count = project.repository.count_commits(ref: 'master').to_s
+
+ get api("/projects/#{project.id}/repository/commits", user)
+
+ expect(response).to include_pagination_headers
+ expect(response.headers['X-Total']).to eq(commit_count)
+ expect(response.headers['X-Page']).to eql('1')
+ end
end
context "unauthorized user" do
@@ -39,14 +50,26 @@ describe API::Commits, api: true do
context "since optional parameter" do
it "returns project commits since provided parameter" do
commits = project.repository.commits("master")
- since = commits.second.created_at
+ after = commits.second.created_at
- get api("/projects/#{project.id}/repository/commits?since=#{since.utc.iso8601}", user)
+ get api("/projects/#{project.id}/repository/commits?since=#{after.utc.iso8601}", user)
expect(json_response.size).to eq 2
expect(json_response.first["id"]).to eq(commits.first.id)
expect(json_response.second["id"]).to eq(commits.second.id)
end
+
+ it 'include correct pagination headers' do
+ commits = project.repository.commits("master")
+ after = commits.second.created_at
+ commit_count = project.repository.count_commits(ref: 'master', after: after).to_s
+
+ get api("/projects/#{project.id}/repository/commits?since=#{after.utc.iso8601}", user)
+
+ expect(response).to include_pagination_headers
+ expect(response.headers['X-Total']).to eq(commit_count)
+ expect(response.headers['X-Page']).to eql('1')
+ end
end
context "until optional parameter" do
@@ -65,6 +88,18 @@ describe API::Commits, api: true do
expect(json_response.first["id"]).to eq(commits.second.id)
expect(json_response.second["id"]).to eq(commits.third.id)
end
+
+ it 'include correct pagination headers' do
+ commits = project.repository.commits("master")
+ before = commits.second.created_at
+ commit_count = project.repository.count_commits(ref: 'master', before: before).to_s
+
+ get api("/projects/#{project.id}/repository/commits?until=#{before.utc.iso8601}", user)
+
+ expect(response).to include_pagination_headers
+ expect(response.headers['X-Total']).to eq(commit_count)
+ expect(response.headers['X-Page']).to eql('1')
+ end
end
context "invalid xmlschema date parameters" do
@@ -79,11 +114,66 @@ describe API::Commits, api: true do
context "path optional parameter" do
it "returns project commits matching provided path parameter" do
path = 'files/ruby/popen.rb'
+ commit_count = project.repository.count_commits(ref: 'master', path: path).to_s
get api("/projects/#{project.id}/repository/commits?path=#{path}", user)
expect(json_response.size).to eq(3)
expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d")
+ expect(response).to include_pagination_headers
+ expect(response.headers['X-Total']).to eq(commit_count)
+ end
+
+ it 'include correct pagination headers' do
+ path = 'files/ruby/popen.rb'
+ commit_count = project.repository.count_commits(ref: 'master', path: path).to_s
+
+ get api("/projects/#{project.id}/repository/commits?path=#{path}", user)
+
+ expect(response).to include_pagination_headers
+ expect(response.headers['X-Total']).to eq(commit_count)
+ expect(response.headers['X-Page']).to eql('1')
+ end
+ end
+
+ context 'with pagination params' do
+ let(:page) { 1 }
+ let(:per_page) { 5 }
+ let(:ref_name) { 'master' }
+ let!(:request) do
+ get api("/projects/#{project.id}/repository/commits?page=#{page}&per_page=#{per_page}&ref_name=#{ref_name}", user)
+ end
+
+ it 'returns correct headers' do
+ commit_count = project.repository.count_commits(ref: ref_name).to_s
+
+ expect(response).to include_pagination_headers
+ expect(response.headers['X-Total']).to eq(commit_count)
+ expect(response.headers['X-Page']).to eq('1')
+ expect(response.headers['Link']).to match(/page=1&per_page=5/)
+ expect(response.headers['Link']).to match(/page=2&per_page=5/)
+ end
+
+ context 'viewing the first page' do
+ it 'returns the first 5 commits' do
+ commit = project.repository.commit
+
+ expect(json_response.size).to eq(per_page)
+ expect(json_response.first['id']).to eq(commit.id)
+ expect(response.headers['X-Page']).to eq('1')
+ end
+ end
+
+ context 'viewing the third page' do
+ let(:page) { 3 }
+
+ it 'returns the third 5 commits' do
+ commit = project.repository.commits('HEAD', offset: (page - 1) * per_page).first
+
+ expect(json_response.size).to eq(per_page)
+ expect(json_response.first['id']).to eq(commit.id)
+ expect(response.headers['X-Page']).to eq('3')
+ end
end
end
end
diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb
index 2974875510a..f6fd567eca5 100644
--- a/spec/requests/api/doorkeeper_access_spec.rb
+++ b/spec/requests/api/doorkeeper_access_spec.rb
@@ -39,4 +39,22 @@ describe API::API, api: true do
end
end
end
+
+ describe "when user is blocked" do
+ it "returns authentication error" do
+ user.block
+ get api("/user"), access_token: token.token
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ describe "when user is ldap_blocked" do
+ it "returns authentication error" do
+ user.ldap_block
+ get api("/user"), access_token: token.token
+
+ expect(response).to have_http_status(401)
+ end
+ end
end
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 91f8a35e045..a7fad7f0bdb 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -5,10 +5,9 @@ describe API::Files, api: true do
let(:user) { create(:user) }
let!(:project) { create(:project, :repository, namespace: user.namespace ) }
let(:guest) { create(:user) { |u| project.add_guest(u) } }
- let(:file_path) { 'files/ruby/popen.rb' }
+ let(:file_path) { "files%2Fruby%2Fpopen%2Erb" }
let(:params) do
{
- file_path: file_path,
ref: 'master'
}
end
@@ -30,36 +29,54 @@ describe API::Files, api: true do
before { project.team << [user, :developer] }
- describe "GET /projects/:id/repository/files" do
- let(:route) { "/projects/#{project.id}/repository/files" }
+ def route(file_path = nil)
+ "/projects/#{project.id}/repository/files/#{file_path}"
+ end
+ describe "GET /projects/:id/repository/files/:file_path" do
shared_examples_for 'repository files' do
- it "returns file info" do
- get api(route, current_user), params
+ it 'returns file attributes as json' do
+ get api(route(file_path), current_user), params
expect(response).to have_http_status(200)
- expect(json_response['file_path']).to eq(file_path)
+ expect(json_response['file_path']).to eq(CGI.unescape(file_path))
expect(json_response['file_name']).to eq('popen.rb')
expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
end
- context 'when no params are given' do
+ it 'returns file by commit sha' do
+ # This file is deleted on HEAD
+ file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
+ params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+
+ get api(route(file_path), current_user), params
+
+ expect(response).to have_http_status(200)
+ expect(json_response['file_name']).to eq('commit.js.coffee')
+ expect(Base64.decode64(json_response['content']).lines.first).to eq("class Commit\n")
+ end
+
+ it 'returns raw file info' do
+ url = route(file_path) + "/raw"
+ expect(Gitlab::Workhorse).to receive(:send_git_blob)
+
+ get api(url, current_user), params
+
+ expect(response).to have_http_status(200)
+ end
+
+ context 'when mandatory params are not given' do
it_behaves_like '400 response' do
- let(:request) { get api(route, current_user) }
+ let(:request) { get api(route("any%2Ffile"), current_user) }
end
end
context 'when file_path does not exist' do
- let(:params) do
- {
- file_path: 'app/models/application.rb',
- ref: 'master',
- }
- end
+ let(:params) { { ref: 'master' } }
it_behaves_like '404 response' do
- let(:request) { get api(route, current_user), params }
+ let(:request) { get api(route('app%2Fmodels%2Fapplication%2Erb'), current_user), params }
let(:message) { '404 File Not Found' }
end
end
@@ -68,7 +85,7 @@ describe API::Files, api: true do
include_context 'disabled repository'
it_behaves_like '403 response' do
- let(:request) { get api(route, current_user), params }
+ let(:request) { get api(route(file_path), current_user), params }
end
end
end
@@ -82,7 +99,7 @@ describe API::Files, api: true do
context 'when unauthenticated', 'and project is private' do
it_behaves_like '404 response' do
- let(:request) { get api(route), params }
+ let(:request) { get api(route(file_path)), params }
let(:message) { '404 Project Not Found' }
end
end
@@ -95,33 +112,106 @@ describe API::Files, api: true do
context 'when authenticated', 'as a guest' do
it_behaves_like '403 response' do
- let(:request) { get api(route, guest), params }
+ let(:request) { get api(route(file_path), guest), params }
end
end
end
- describe "POST /projects/:id/repository/files" do
+ describe "GET /projects/:id/repository/files/:file_path/raw" do
+ shared_examples_for 'repository raw files' do
+ it 'returns raw file info' do
+ url = route(file_path) + "/raw"
+ expect(Gitlab::Workhorse).to receive(:send_git_blob)
+
+ get api(url, current_user), params
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns file by commit sha' do
+ # This file is deleted on HEAD
+ file_path = "files%2Fjs%2Fcommit%2Ejs%2Ecoffee"
+ params[:ref] = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9"
+ expect(Gitlab::Workhorse).to receive(:send_git_blob)
+
+ get api(route(file_path) + "/raw", current_user), params
+
+ expect(response).to have_http_status(200)
+ end
+
+ context 'when mandatory params are not given' do
+ it_behaves_like '400 response' do
+ let(:request) { get api(route("any%2Ffile"), current_user) }
+ end
+ end
+
+ context 'when file_path does not exist' do
+ let(:params) { { ref: 'master' } }
+
+ it_behaves_like '404 response' do
+ let(:request) { get api(route('app%2Fmodels%2Fapplication%2Erb'), current_user), params }
+ let(:message) { '404 File Not Found' }
+ end
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route(file_path), current_user), params }
+ end
+ end
+ end
+
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository raw files' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { nil }
+ end
+ end
+
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route(file_path)), params }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository raw files' do
+ let(:current_user) { user }
+ end
+ end
+
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route(file_path), guest), params }
+ end
+ end
+ end
+
+ describe "POST /projects/:id/repository/files/:file_path" do
+ let!(:file_path) { "new_subfolder%2Fnewfile%2Erb" }
let(:valid_params) do
{
- file_path: 'newfile.rb',
- branch: 'master',
- content: 'puts 8',
- commit_message: 'Added newfile'
+ branch: "master",
+ content: "puts 8",
+ commit_message: "Added newfile"
}
end
it "creates a new file in project repo" do
- post api("/projects/#{project.id}/repository/files", user), valid_params
+ post api(route(file_path), user), valid_params
expect(response).to have_http_status(201)
- expect(json_response['file_path']).to eq('newfile.rb')
+ expect(json_response["file_path"]).to eq(CGI.unescape(file_path))
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(user.email)
expect(last_commit.author_name).to eq(user.name)
end
- it "returns a 400 bad request if no params given" do
- post api("/projects/#{project.id}/repository/files", user)
+ it "returns a 400 bad request if no mandatory params given" do
+ post api(route("any%2Etxt"), user)
expect(response).to have_http_status(400)
end
@@ -130,7 +220,7 @@ describe API::Files, api: true do
allow_any_instance_of(Repository).to receive(:create_file).
and_return(false)
- post api("/projects/#{project.id}/repository/files", user), valid_params
+ post api(route("any%2Etxt"), user), valid_params
expect(response).to have_http_status(400)
end
@@ -139,7 +229,7 @@ describe API::Files, api: true do
it "creates a new file with the specified author" do
valid_params.merge!(author_email: author_email, author_name: author_name)
- post api("/projects/#{project.id}/repository/files", user), valid_params
+ post api(route("new_file_with_author%2Etxt"), user), valid_params
expect(response).to have_http_status(201)
last_commit = project.repository.commit.raw
@@ -152,7 +242,7 @@ describe API::Files, api: true do
let!(:project) { create(:project_empty_repo, namespace: user.namespace ) }
it "creates a new file in project repo" do
- post api("/projects/#{project.id}/repository/files", user), valid_params
+ post api(route("newfile%2Erb"), user), valid_params
expect(response).to have_http_status(201)
expect(json_response['file_path']).to eq('newfile.rb')
@@ -166,7 +256,6 @@ describe API::Files, api: true do
describe "PUT /projects/:id/repository/files" do
let(:valid_params) do
{
- file_path: file_path,
branch: 'master',
content: 'puts 8',
commit_message: 'Changed file'
@@ -174,17 +263,17 @@ describe API::Files, api: true do
end
it "updates existing file in project repo" do
- put api("/projects/#{project.id}/repository/files", user), valid_params
+ put api(route(file_path), user), valid_params
expect(response).to have_http_status(200)
- expect(json_response['file_path']).to eq(file_path)
+ expect(json_response['file_path']).to eq(CGI.unescape(file_path))
last_commit = project.repository.commit.raw
expect(last_commit.author_email).to eq(user.email)
expect(last_commit.author_name).to eq(user.name)
end
it "returns a 400 bad request if no params given" do
- put api("/projects/#{project.id}/repository/files", user)
+ put api(route(file_path), user)
expect(response).to have_http_status(400)
end
@@ -193,7 +282,7 @@ describe API::Files, api: true do
it "updates a file with the specified author" do
valid_params.merge!(author_email: author_email, author_name: author_name, content: "New content")
- put api("/projects/#{project.id}/repository/files", user), valid_params
+ put api(route(file_path), user), valid_params
expect(response).to have_http_status(200)
last_commit = project.repository.commit.raw
@@ -206,20 +295,19 @@ describe API::Files, api: true do
describe "DELETE /projects/:id/repository/files" do
let(:valid_params) do
{
- file_path: file_path,
branch: 'master',
commit_message: 'Changed file'
}
end
it "deletes existing file in project repo" do
- delete api("/projects/#{project.id}/repository/files", user), valid_params
+ delete api(route(file_path), user), valid_params
expect(response).to have_http_status(204)
end
it "returns a 400 bad request if no params given" do
- delete api("/projects/#{project.id}/repository/files", user)
+ delete api(route(file_path), user)
expect(response).to have_http_status(400)
end
@@ -227,7 +315,7 @@ describe API::Files, api: true do
it "returns a 400 if fails to create file" do
allow_any_instance_of(Repository).to receive(:delete_file).and_return(false)
- delete api("/projects/#{project.id}/repository/files", user), valid_params
+ delete api(route(file_path), user), valid_params
expect(response).to have_http_status(400)
end
@@ -236,7 +324,7 @@ describe API::Files, api: true do
it "removes a file with the specified author" do
valid_params.merge!(author_email: author_email, author_name: author_name)
- delete api("/projects/#{project.id}/repository/files", user), valid_params
+ delete api(route(file_path), user), valid_params
expect(response).to have_http_status(204)
end
@@ -244,10 +332,9 @@ describe API::Files, api: true do
end
describe "POST /projects/:id/repository/files with binary file" do
- let(:file_path) { 'test.bin' }
+ let(:file_path) { 'test%2Ebin' }
let(:put_params) do
{
- file_path: file_path,
branch: 'master',
content: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=',
commit_message: 'Binary file with a \n should not be touched',
@@ -256,21 +343,20 @@ describe API::Files, api: true do
end
let(:get_params) do
{
- file_path: file_path,
ref: 'master',
}
end
before do
- post api("/projects/#{project.id}/repository/files", user), put_params
+ post api(route(file_path), user), put_params
end
it "remains unchanged" do
- get api("/projects/#{project.id}/repository/files", user), get_params
+ get api(route(file_path), user), get_params
expect(response).to have_http_status(200)
- expect(json_response['file_path']).to eq(file_path)
- expect(json_response['file_name']).to eq(file_path)
+ expect(json_response['file_path']).to eq(CGI.unescape(file_path))
+ expect(json_response['file_name']).to eq(CGI.unescape(file_path))
expect(json_response['content']).to eq(put_params[:content])
end
end
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index a4d27734cc2..9450701064b 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -51,7 +51,7 @@ describe API::Jobs, api: true do
end
context 'filter project with array of scope elements' do
- let(:query) { { 'scope[0]' => 'pending', 'scope[1]' => 'running' } }
+ let(:query) { { scope: %w(pending running) } }
it do
expect(response).to have_http_status(200)
@@ -60,7 +60,7 @@ describe API::Jobs, api: true do
end
context 'respond 400 when scope contains invalid state' do
- let(:query) { { 'scope[0]' => 'unknown', 'scope[1]' => 'running' } }
+ let(:query) { { scope: %w(unknown running) } }
it { expect(response).to have_http_status(400) }
end
@@ -75,6 +75,78 @@ describe API::Jobs, api: true do
end
end
+ describe 'GET /projects/:id/pipelines/:pipeline_id/jobs' do
+ let(:query) { Hash.new }
+
+ before do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query
+ end
+
+ context 'authorized user' do
+ it 'returns pipeline jobs' do
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ end
+
+ it 'returns correct values' do
+ expect(json_response).not_to be_empty
+ expect(json_response.first['commit']['id']).to eq project.commit.id
+ end
+
+ it 'returns pipeline data' do
+ json_build = json_response.first
+
+ expect(json_build['pipeline']).not_to be_empty
+ expect(json_build['pipeline']['id']).to eq build.pipeline.id
+ expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
+ expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
+ expect(json_build['pipeline']['status']).to eq build.pipeline.status
+ end
+
+ context 'filter jobs with one scope element' do
+ let(:query) { { 'scope' => 'pending' } }
+
+ it do
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'filter jobs with array of scope elements' do
+ let(:query) { { scope: %w(pending running) } }
+
+ it do
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'respond 400 when scope contains invalid state' do
+ let(:query) { { scope: %w(unknown running) } }
+
+ it { expect(response).to have_http_status(400) }
+ end
+
+ context 'jobs in different pipelines' do
+ let!(:pipeline2) { create(:ci_empty_pipeline, project: project) }
+ let!(:build2) { create(:ci_build, pipeline: pipeline2) }
+
+ it 'excludes jobs from other pipelines' do
+ json_response.each { |job| expect(job['pipeline']['id']).to eq(pipeline.id) }
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not return jobs' do
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+
describe 'GET /projects/:id/jobs/:job_id' do
before do
get api("/projects/#{project.id}/jobs/#{build.id}", api_user)
diff --git a/spec/requests/api/oauth_tokens_spec.rb b/spec/requests/api/oauth_tokens_spec.rb
index 7e2cc50e591..367225df717 100644
--- a/spec/requests/api/oauth_tokens_spec.rb
+++ b/spec/requests/api/oauth_tokens_spec.rb
@@ -29,5 +29,27 @@ describe API::API, api: true do
expect(json_response['access_token']).not_to be_nil
end
end
+
+ context "when user is blocked" do
+ it "does not create an access token" do
+ user = create(:user)
+ user.block
+
+ request_oauth_token(user)
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when user is ldap_blocked" do
+ it "does not create an access token" do
+ user = create(:user)
+ user.ldap_block
+
+ request_oauth_token(user)
+
+ expect(response).to have_http_status(401)
+ end
+ end
end
end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 7652606a491..4783d011d54 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -30,7 +30,7 @@ describe API::Repositories, api: true do
context 'when ref does not exist' do
it_behaves_like '404 response' do
- let(:request) { get api("#{route}?ref_name=foo", current_user) }
+ let(:request) { get api("#{route}?ref=foo", current_user) }
let(:message) { '404 Tree Not Found' }
end
end
@@ -66,7 +66,7 @@ describe API::Repositories, api: true do
context 'when ref does not exist' do
it_behaves_like '404 response' do
- let(:request) { get api("#{route}?recursive=1&ref_name=foo", current_user) }
+ let(:request) { get api("#{route}?recursive=1&ref=foo", current_user) }
let(:message) { '404 Tree Not Found' }
end
end
@@ -100,82 +100,70 @@ describe API::Repositories, api: true do
end
end
- {
- 'blobs/:sha' => 'blobs/master',
- 'commits/:sha/blob' => 'commits/master/blob'
- }.each do |desc_path, example_path|
- describe "GET /projects/:id/repository/#{desc_path}" do
- let(:route) { "/projects/#{project.id}/repository/#{example_path}?filepath=README.md" }
+ describe "GET /projects/:id/repository/blobs/:sha" do
+ let(:route) { "/projects/#{project.id}/repository/blobs/#{sample_blob.oid}" }
- shared_examples_for 'repository blob' do
- it 'returns the repository blob' do
- get api(route, current_user)
-
- expect(response).to have_http_status(200)
- end
-
- context 'when sha does not exist' do
- it_behaves_like '404 response' do
- let(:request) { get api(route.sub('master', 'invalid_branch_name'), current_user) }
- let(:message) { '404 Commit Not Found' }
- end
- end
+ shared_examples_for 'repository blob' do
+ it 'returns blob attributes as json' do
+ get api(route, current_user)
- context 'when filepath does not exist' do
- it_behaves_like '404 response' do
- let(:request) { get api(route.sub('README.md', 'README.invalid'), current_user) }
- let(:message) { '404 File Not Found' }
- end
- end
+ expect(response).to have_http_status(200)
+ expect(json_response['size']).to eq(111)
+ expect(json_response['encoding']).to eq("base64")
+ expect(Base64.decode64(json_response['content']).lines.first).to eq("class Commit\n")
+ expect(json_response['sha']).to eq(sample_blob.oid)
+ end
- context 'when no filepath is given' do
- it_behaves_like '400 response' do
- let(:request) { get api(route.sub('?filepath=README.md', ''), current_user) }
- end
+ context 'when sha does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route.sub(sample_blob.oid, '123456'), current_user) }
+ let(:message) { '404 Blob Not Found' }
end
+ end
- context 'when repository is disabled' do
- include_context 'disabled repository'
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
- it_behaves_like '403 response' do
- let(:request) { get api(route, current_user) }
- end
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, current_user) }
end
end
+ end
- context 'when unauthenticated', 'and project is public' do
- it_behaves_like 'repository blob' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
- end
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository blob' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
end
+ end
- context 'when unauthenticated', 'and project is private' do
- it_behaves_like '404 response' do
- let(:request) { get api(route) }
- let(:message) { '404 Project Not Found' }
- end
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
end
+ end
- context 'when authenticated', 'as a developer' do
- it_behaves_like 'repository blob' do
- let(:current_user) { user }
- end
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository blob' do
+ let(:current_user) { user }
end
+ end
- context 'when authenticated', 'as a guest' do
- it_behaves_like '403 response' do
- let(:request) { get api(route, guest) }
- end
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, guest) }
end
end
end
- describe "GET /projects/:id/repository/raw_blobs/:sha" do
- let(:route) { "/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}" }
+ describe "GET /projects/:id/repository/blobs/:sha/raw" do
+ let(:route) { "/projects/#{project.id}/repository/blobs/#{sample_blob.oid}/raw" }
shared_examples_for 'repository raw blob' do
it 'returns the repository raw blob' do
+ expect(Gitlab::Workhorse).to receive(:send_git_blob)
+
get api(route, current_user)
expect(response).to have_http_status(200)
diff --git a/spec/requests/api/session_spec.rb b/spec/requests/api/session_spec.rb
index 794e2b5c04d..28fab2011a5 100644
--- a/spec/requests/api/session_spec.rb
+++ b/spec/requests/api/session_spec.rb
@@ -87,5 +87,23 @@ describe API::Session, api: true do
expect(response).to have_http_status(400)
end
end
+
+ context "when user is blocked" do
+ it "returns authentication error" do
+ user.block
+ post api("/session"), email: user.username, password: user.password
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when user is ldap_blocked" do
+ it "returns authentication error" do
+ user.ldap_block
+ post api("/session"), email: user.username, password: user.password
+
+ expect(response).to have_http_status(401)
+ end
+ end
end
end
diff --git a/spec/requests/api/v3/repositories_spec.rb b/spec/requests/api/v3/repositories_spec.rb
index c696721c1c9..fef6fb641fa 100644
--- a/spec/requests/api/v3/repositories_spec.rb
+++ b/spec/requests/api/v3/repositories_spec.rb
@@ -3,6 +3,8 @@ require 'mime/types'
describe API::V3::Repositories, api: true do
include ApiHelpers
+ include RepoHelpers
+ include WorkhorseHelpers
let(:user) { create(:user) }
let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } }
@@ -96,6 +98,226 @@ describe API::V3::Repositories, api: true do
end
end
+ {
+ 'blobs/:sha' => 'blobs/master',
+ 'commits/:sha/blob' => 'commits/master/blob'
+ }.each do |desc_path, example_path|
+ describe "GET /projects/:id/repository/#{desc_path}" do
+ let(:route) { "/projects/#{project.id}/repository/#{example_path}?filepath=README.md" }
+ shared_examples_for 'repository blob' do
+ it 'returns the repository blob' do
+ get v3_api(route, current_user)
+ expect(response).to have_http_status(200)
+ end
+ context 'when sha does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api(route.sub('master', 'invalid_branch_name'), current_user) }
+ let(:message) { '404 Commit Not Found' }
+ end
+ end
+ context 'when filepath does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api(route.sub('README.md', 'README.invalid'), current_user) }
+ let(:message) { '404 File Not Found' }
+ end
+ end
+ context 'when no filepath is given' do
+ it_behaves_like '400 response' do
+ let(:request) { get v3_api(route.sub('?filepath=README.md', ''), current_user) }
+ end
+ end
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+ it_behaves_like '403 response' do
+ let(:request) { get v3_api(route, current_user) }
+ end
+ end
+ end
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository blob' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
+ end
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository blob' do
+ let(:current_user) { user }
+ end
+ end
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get v3_api(route, guest) }
+ end
+ end
+ end
+ end
+ describe "GET /projects/:id/repository/raw_blobs/:sha" do
+ let(:route) { "/projects/#{project.id}/repository/raw_blobs/#{sample_blob.oid}" }
+ shared_examples_for 'repository raw blob' do
+ it 'returns the repository raw blob' do
+ get v3_api(route, current_user)
+ expect(response).to have_http_status(200)
+ end
+ context 'when sha does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api(route.sub(sample_blob.oid, '123456'), current_user) }
+ let(:message) { '404 Blob Not Found' }
+ end
+ end
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+ it_behaves_like '403 response' do
+ let(:request) { get v3_api(route, current_user) }
+ end
+ end
+ end
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository raw blob' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
+ end
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository raw blob' do
+ let(:current_user) { user }
+ end
+ end
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get v3_api(route, guest) }
+ end
+ end
+ end
+ describe "GET /projects/:id/repository/archive(.:format)?:sha" do
+ let(:route) { "/projects/#{project.id}/repository/archive" }
+ shared_examples_for 'repository archive' do
+ it 'returns the repository archive' do
+ get v3_api(route, current_user)
+ expect(response).to have_http_status(200)
+ repo_name = project.repository.name.gsub("\.git", "")
+ type, params = workhorse_send_data
+ expect(type).to eq('git-archive')
+ expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/)
+ end
+ it 'returns the repository archive archive.zip' do
+ get v3_api("/projects/#{project.id}/repository/archive.zip", user)
+ expect(response).to have_http_status(200)
+ repo_name = project.repository.name.gsub("\.git", "")
+ type, params = workhorse_send_data
+ expect(type).to eq('git-archive')
+ expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/)
+ end
+ it 'returns the repository archive archive.tar.bz2' do
+ get v3_api("/projects/#{project.id}/repository/archive.tar.bz2", user)
+ expect(response).to have_http_status(200)
+ repo_name = project.repository.name.gsub("\.git", "")
+ type, params = workhorse_send_data
+ expect(type).to eq('git-archive')
+ expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/)
+ end
+ context 'when sha does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api("#{route}?sha=xxx", current_user) }
+ let(:message) { '404 File Not Found' }
+ end
+ end
+ end
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository archive' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
+ end
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository archive' do
+ let(:current_user) { user }
+ end
+ end
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get v3_api(route, guest) }
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/repository/compare' do
+ let(:route) { "/projects/#{project.id}/repository/compare" }
+ shared_examples_for 'repository compare' do
+ it "compares branches" do
+ get v3_api(route, current_user), from: 'master', to: 'feature'
+ expect(response).to have_http_status(200)
+ expect(json_response['commits']).to be_present
+ expect(json_response['diffs']).to be_present
+ end
+ it "compares tags" do
+ get v3_api(route, current_user), from: 'v1.0.0', to: 'v1.1.0'
+ expect(response).to have_http_status(200)
+ expect(json_response['commits']).to be_present
+ expect(json_response['diffs']).to be_present
+ end
+ it "compares commits" do
+ get v3_api(route, current_user), from: sample_commit.id, to: sample_commit.parent_id
+ expect(response).to have_http_status(200)
+ expect(json_response['commits']).to be_empty
+ expect(json_response['diffs']).to be_empty
+ expect(json_response['compare_same_ref']).to be_falsey
+ end
+ it "compares commits in reverse order" do
+ get v3_api(route, current_user), from: sample_commit.parent_id, to: sample_commit.id
+ expect(response).to have_http_status(200)
+ expect(json_response['commits']).to be_present
+ expect(json_response['diffs']).to be_present
+ end
+ it "compares same refs" do
+ get v3_api(route, current_user), from: 'master', to: 'master'
+ expect(response).to have_http_status(200)
+ expect(json_response['commits']).to be_empty
+ expect(json_response['diffs']).to be_empty
+ expect(json_response['compare_same_ref']).to be_truthy
+ end
+ end
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository compare' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
+ end
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get v3_api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository compare' do
+ let(:current_user) { user }
+ end
+ end
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get v3_api(route, guest) }
+ end
+ end
+ end
+
describe 'GET /projects/:id/repository/contributors' do
let(:route) { "/projects/#{project.id}/repository/contributors" }
diff --git a/spec/requests/api/v3/services_spec.rb b/spec/requests/api/v3/services_spec.rb
index 7e8c8753d02..3a760a8f25c 100644
--- a/spec/requests/api/v3/services_spec.rb
+++ b/spec/requests/api/v3/services_spec.rb
@@ -6,7 +6,9 @@ describe API::V3::Services, api: true do
let(:user) { create(:user) }
let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
- Service.available_services_names.each do |service|
+ available_services = Service.available_services_names
+ available_services.delete('prometheus')
+ available_services.each do |service|
describe "DELETE /projects/:id/services/#{service.dasherize}" do
include_context service
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 87786e85621..006d6a6af1c 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -221,12 +221,20 @@ describe 'Git HTTP requests', lib: true do
end
context "when the user is blocked" do
- it "responds with status 404" do
+ it "responds with status 401" do
user.block
project.team << [user, :master]
download(path, env) do |response|
- expect(response).to have_http_status(404)
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ it "responds with status 401 for unknown projects (no project existence information leak)" do
+ user.block
+
+ download('doesnt/exist.git', env) do |response|
+ expect(response).to have_http_status(401)
end
end
end
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
new file mode 100644
index 00000000000..5206634bca5
--- /dev/null
+++ b/spec/requests/openid_connect_spec.rb
@@ -0,0 +1,134 @@
+require 'spec_helper'
+
+describe 'OpenID Connect requests' do
+ include ApiHelpers
+
+ let(:user) { create :user }
+ let(:access_grant) { create :oauth_access_grant, application: application, resource_owner_id: user.id }
+ let(:access_token) { create :oauth_access_token, application: application, resource_owner_id: user.id }
+
+ def request_access_token
+ login_as user
+
+ post '/oauth/token',
+ grant_type: 'authorization_code',
+ code: access_grant.token,
+ redirect_uri: application.redirect_uri,
+ client_id: application.uid,
+ client_secret: application.secret
+ end
+
+ def request_user_info
+ get '/oauth/userinfo', nil, 'Authorization' => "Bearer #{access_token.token}"
+ end
+
+ def hashed_subject
+ Digest::SHA256.hexdigest("#{user.id}-#{Rails.application.secrets.secret_key_base}")
+ end
+
+ context 'Application without OpenID scope' do
+ let(:application) { create :oauth_application, scopes: 'api' }
+
+ it 'token response does not include an ID token' do
+ request_access_token
+
+ expect(json_response).to include 'access_token'
+ expect(json_response).not_to include 'id_token'
+ end
+
+ it 'userinfo response is unauthorized' do
+ request_user_info
+
+ expect(response).to have_http_status 403
+ expect(response.body).to be_blank
+ end
+ end
+
+ context 'Application with OpenID scope' do
+ let(:application) { create :oauth_application, scopes: 'openid' }
+
+ it 'token response includes an ID token' do
+ request_access_token
+
+ expect(json_response).to include 'id_token'
+ end
+
+ context 'UserInfo payload' do
+ let(:user) do
+ create(
+ :user,
+ name: 'Alice',
+ username: 'alice',
+ emails: [private_email, public_email],
+ email: private_email.email,
+ public_email: public_email.email,
+ website_url: 'https://example.com',
+ avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png"),
+ )
+ end
+
+ let(:public_email) { build :email, email: 'public@example.com' }
+ let(:private_email) { build :email, email: 'private@example.com' }
+
+ it 'includes all user information' do
+ request_user_info
+
+ expect(json_response).to eq({
+ 'sub' => hashed_subject,
+ 'name' => 'Alice',
+ 'nickname' => 'alice',
+ 'email' => 'public@example.com',
+ 'email_verified' => true,
+ 'website' => 'https://example.com',
+ 'profile' => 'http://localhost/alice',
+ 'picture' => "http://localhost/uploads/user/avatar/#{user.id}/dk.png",
+ })
+ end
+ end
+
+ context 'ID token payload' do
+ before do
+ request_access_token
+ @payload = JSON::JWT.decode(json_response['id_token'], :skip_verification)
+ end
+
+ it 'includes the Gitlab root URL' do
+ expect(@payload['iss']).to eq Gitlab.config.gitlab.url
+ end
+
+ it 'includes the hashed user ID' do
+ expect(@payload['sub']).to eq hashed_subject
+ end
+
+ it 'includes the time of the last authentication' do
+ expect(@payload['auth_time']).to eq user.current_sign_in_at.to_i
+ end
+
+ it 'does not include any unknown properties' do
+ expect(@payload.keys).to eq %w[iss sub aud exp iat auth_time]
+ end
+ end
+
+ context 'when user is blocked' do
+ it 'returns authentication error' do
+ access_grant
+ user.block
+
+ expect do
+ request_access_token
+ end.to throw_symbol :warden
+ end
+ end
+
+ context 'when user is ldap_blocked' do
+ it 'returns authentication error' do
+ access_grant
+ user.ldap_block
+
+ expect do
+ request_access_token
+ end.to throw_symbol :warden
+ end
+ end
+ end
+end
diff --git a/spec/routing/openid_connect_spec.rb b/spec/routing/openid_connect_spec.rb
new file mode 100644
index 00000000000..2c3bc08f1a1
--- /dev/null
+++ b/spec/routing/openid_connect_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+# oauth_discovery_keys GET /oauth/discovery/keys(.:format) doorkeeper/openid_connect/discovery#keys
+# oauth_discovery_provider GET /.well-known/openid-configuration(.:format) doorkeeper/openid_connect/discovery#provider
+# oauth_discovery_webfinger GET /.well-known/webfinger(.:format) doorkeeper/openid_connect/discovery#webfinger
+describe Doorkeeper::OpenidConnect::DiscoveryController, 'routing' do
+ it "to #provider" do
+ expect(get('/.well-known/openid-configuration')).to route_to('doorkeeper/openid_connect/discovery#provider')
+ end
+
+ it "to #webfinger" do
+ expect(get('/.well-known/webfinger')).to route_to('doorkeeper/openid_connect/discovery#webfinger')
+ end
+
+ it "to #keys" do
+ expect(get('/oauth/discovery/keys')).to route_to('doorkeeper/openid_connect/discovery#keys')
+ end
+end
+
+# oauth_userinfo GET /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show
+# POST /oauth/userinfo(.:format) doorkeeper/openid_connect/userinfo#show
+describe Doorkeeper::OpenidConnect::UserinfoController, 'routing' do
+ it "to #show" do
+ expect(get('/oauth/userinfo')).to route_to('doorkeeper/openid_connect/userinfo#show')
+ end
+
+ it "to #show" do
+ expect(post('/oauth/userinfo')).to route_to('doorkeeper/openid_connect/userinfo#show')
+ end
+end
diff --git a/spec/rubocop/cop/migration/add_concurrent_index_spec.rb b/spec/rubocop/cop/migration/add_concurrent_index_spec.rb
new file mode 100644
index 00000000000..19a5718b0b1
--- /dev/null
+++ b/spec/rubocop/cop/migration/add_concurrent_index_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/migration/add_concurrent_index'
+
+describe RuboCop::Cop::Migration::AddConcurrentIndex do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'in migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ end
+
+ it 'registers an offense when add_concurrent_index is used inside a change method' do
+ inspect_source(cop, 'def change; add_concurrent_index :table, :column; end')
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ end
+ end
+
+ it 'registers no offense when add_concurrent_index is used inside an up method' do
+ inspect_source(cop, 'def up; add_concurrent_index :table, :column; end')
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+
+ context 'outside of migration' do
+ it 'registers no offense' do
+ inspect_source(cop, 'def change; add_concurrent_index :table, :column; end')
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+end
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index 305278843f5..01baedc4761 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -43,32 +43,6 @@ describe Boards::Issues::ListService, services: true do
described_class.new(project, user, params).execute
end
- context 'sets default order to priority' do
- it 'returns opened issues when list id is missing' do
- params = { board_id: board.id }
-
- issues = described_class.new(project, user, params).execute
-
- 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 }
-
- issues = described_class.new(project, user, params).execute
-
- expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1]
- end
-
- it 'returns opened issues that have label list applied when listing issues from a label list' do
- params = { board_id: board.id, id: list1.id }
-
- issues = described_class.new(project, user, params).execute
-
- expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2]
- end
- end
-
context 'with list that does not belong to the board' do
it 'raises an error' do
list = create(:list)
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
index 77f75167b3d..727ea04ea5c 100644
--- a/spec/services/boards/issues/move_service_spec.rb
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -78,8 +78,10 @@ describe Boards::Issues::MoveService, services: true do
end
context 'when moving to same list' do
- let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
- let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } }
+ let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
+ let(:issue1) { create(:labeled_issue, project: project, labels: [bug, development]) }
+ let(:issue2) { create(:labeled_issue, project: project, labels: [bug, development]) }
+ let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } }
it 'returns false' do
expect(described_class.new(project, user, params).execute(issue)).to eq false
@@ -90,6 +92,18 @@ describe Boards::Issues::MoveService, services: true do
expect(issue.reload.labels).to contain_exactly(bug, development)
end
+
+ it 'sorts issues' do
+ [issue, issue1, issue2].each do |issue|
+ issue.move_to_end && issue.save!
+ end
+
+ params.merge!(move_after_iid: issue1.iid, move_before_iid: issue2.iid)
+
+ described_class.new(project, user, params).execute(issue)
+
+ expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
+ end
end
end
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index d83b09fd32c..fa472f3e2c3 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -58,6 +58,22 @@ describe Issues::UpdateService, services: true do
expect(issue.due_date).to eq Date.tomorrow
end
+ it 'sorts issues as specified by parameters' do
+ issue1 = create(:issue, project: project, assignee_id: user3.id)
+ issue2 = create(:issue, project: project, assignee_id: user3.id)
+
+ [issue, issue1, issue2].each do |issue|
+ issue.move_to_end
+ issue.save
+ end
+
+ opts[:move_between_iids] = [issue1.iid, issue2.iid]
+
+ update_issue(opts)
+
+ expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
+ end
+
context 'when current user cannot admin issues in the project' do
let(:guest) { create(:user) }
before do
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index ff367f54d2a..92729f68e5f 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -58,16 +58,16 @@ describe MergeRequests::RefreshService, services: true do
it 'executes hooks with update action' do
expect(refresh_service).to have_received(:execute_hooks).
with(@merge_request, 'update', @oldrev)
- end
- it { expect(@merge_request.notes).not_to be_empty }
- it { expect(@merge_request).to be_open }
- it { expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey }
- it { expect(@merge_request.diff_head_sha).to eq(@newrev) }
- it { expect(@fork_merge_request).to be_open }
- it { expect(@fork_merge_request.notes).to be_empty }
- it { expect(@build_failed_todo).to be_done }
- it { expect(@fork_build_failed_todo).to be_done }
+ expect(@merge_request.notes).not_to be_empty
+ expect(@merge_request).to be_open
+ expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey
+ expect(@merge_request.diff_head_sha).to eq(@newrev)
+ expect(@fork_merge_request).to be_open
+ expect(@fork_merge_request.notes).to be_empty
+ expect(@build_failed_todo).to be_done
+ expect(@fork_build_failed_todo).to be_done
+ end
end
context 'push to origin repo target branch' do
@@ -76,12 +76,14 @@ describe MergeRequests::RefreshService, services: true do
reload_mrs
end
- it { expect(@merge_request.notes.last.note).to include('merged') }
- it { expect(@merge_request).to be_merged }
- it { expect(@fork_merge_request).to be_merged }
- it { expect(@fork_merge_request.notes.last.note).to include('merged') }
- it { expect(@build_failed_todo).to be_done }
- it { expect(@fork_build_failed_todo).to be_done }
+ it 'updates the merge state' do
+ expect(@merge_request.notes.last.note).to include('merged')
+ expect(@merge_request).to be_merged
+ expect(@fork_merge_request).to be_merged
+ expect(@fork_merge_request.notes.last.note).to include('merged')
+ expect(@build_failed_todo).to be_done
+ expect(@fork_build_failed_todo).to be_done
+ end
end
context 'manual merge of source branch' do
@@ -95,13 +97,15 @@ describe MergeRequests::RefreshService, services: true do
reload_mrs
end
- it { expect(@merge_request.notes.last.note).to include('merged') }
- it { expect(@merge_request).to be_merged }
- it { expect(@merge_request.diffs.size).to be > 0 }
- it { expect(@fork_merge_request).to be_merged }
- it { expect(@fork_merge_request.notes.last.note).to include('merged') }
- it { expect(@build_failed_todo).to be_done }
- it { expect(@fork_build_failed_todo).to be_done }
+ it 'updates the merge state' do
+ expect(@merge_request.notes.last.note).to include('merged')
+ expect(@merge_request).to be_merged
+ expect(@merge_request.diffs.size).to be > 0
+ expect(@fork_merge_request).to be_merged
+ expect(@fork_merge_request.notes.last.note).to include('merged')
+ expect(@build_failed_todo).to be_done
+ expect(@fork_build_failed_todo).to be_done
+ end
end
context 'push to fork repo source branch' do
@@ -117,14 +121,14 @@ describe MergeRequests::RefreshService, services: true do
it 'executes hooks with update action' do
expect(refresh_service).to have_received(:execute_hooks).
with(@fork_merge_request, 'update', @oldrev)
- end
- it { expect(@merge_request.notes).to be_empty }
- it { expect(@merge_request).to be_open }
- it { expect(@fork_merge_request.notes.last.note).to include('added 28 commits') }
- it { expect(@fork_merge_request).to be_open }
- it { expect(@build_failed_todo).to be_pending }
- it { expect(@fork_build_failed_todo).to be_pending }
+ expect(@merge_request.notes).to be_empty
+ expect(@merge_request).to be_open
+ expect(@fork_merge_request.notes.last.note).to include('added 28 commits')
+ expect(@fork_merge_request).to be_open
+ expect(@build_failed_todo).to be_pending
+ expect(@fork_build_failed_todo).to be_pending
+ end
end
context 'closed fork merge request' do
@@ -139,12 +143,14 @@ describe MergeRequests::RefreshService, services: true do
expect(refresh_service).not_to have_received(:execute_hooks)
end
- it { expect(@merge_request.notes).to be_empty }
- it { expect(@merge_request).to be_open }
- it { expect(@fork_merge_request.notes).to be_empty }
- it { expect(@fork_merge_request).to be_closed }
- it { expect(@build_failed_todo).to be_pending }
- it { expect(@fork_build_failed_todo).to be_pending }
+ it 'updates merge request to closed state' do
+ expect(@merge_request.notes).to be_empty
+ expect(@merge_request).to be_open
+ expect(@fork_merge_request.notes).to be_empty
+ expect(@fork_merge_request).to be_closed
+ expect(@build_failed_todo).to be_pending
+ expect(@fork_build_failed_todo).to be_pending
+ end
end
end
@@ -155,12 +161,14 @@ describe MergeRequests::RefreshService, services: true do
reload_mrs
end
- it { expect(@merge_request.notes).to be_empty }
- it { expect(@merge_request).to be_open }
- it { expect(@fork_merge_request.notes).to be_empty }
- it { expect(@fork_merge_request).to be_open }
- it { expect(@build_failed_todo).to be_pending }
- it { expect(@fork_build_failed_todo).to be_pending }
+ it 'updates the merge request state' do
+ expect(@merge_request.notes).to be_empty
+ expect(@merge_request).to be_open
+ expect(@fork_merge_request.notes).to be_empty
+ expect(@fork_merge_request).to be_open
+ expect(@build_failed_todo).to be_pending
+ expect(@fork_build_failed_todo).to be_pending
+ end
end
describe 'merge request diff' do
@@ -179,12 +187,14 @@ describe MergeRequests::RefreshService, services: true do
reload_mrs
end
- it { expect(@merge_request.notes.last.note).to include('merged') }
- it { expect(@merge_request).to be_merged }
- it { expect(@fork_merge_request).to be_open }
- it { expect(@fork_merge_request.notes).to be_empty }
- it { expect(@build_failed_todo).to be_done }
- it { expect(@fork_build_failed_todo).to be_done }
+ it 'updates the merge request state' do
+ expect(@merge_request.notes.last.note).to include('merged')
+ expect(@merge_request).to be_merged
+ expect(@fork_merge_request).to be_open
+ expect(@fork_merge_request.notes).to be_empty
+ expect(@build_failed_todo).to be_done
+ expect(@fork_build_failed_todo).to be_done
+ end
end
context 'push new branch that exists in a merge request' do
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index fb9a8462f84..a8395cb48ea 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -752,7 +752,7 @@ describe TodoService, services: true do
issue = create(:issue, project: project, assignee: john_doe, author: author, description: mentions)
expect(john_doe.todos_pending_count).to eq(0)
- expect(john_doe).to receive(:update_todos_count_cache)
+ expect(john_doe).to receive(:update_todos_count_cache).and_call_original
service.new_issue(issue, author)
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 16d5f2bf0b8..62740ec29fd 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -3,7 +3,7 @@ require 'capybara/rspec'
require 'capybara/poltergeist'
# Give CI some extra time
-timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10
+timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 30 : 10
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/filtered_search_helpers.rb
index 58f6636e680..6b009b132b6 100644
--- a/spec/support/filtered_search_helpers.rb
+++ b/spec/support/filtered_search_helpers.rb
@@ -3,16 +3,25 @@ module FilteredSearchHelpers
page.find('.filtered-search')
end
- def input_filtered_search(search_term, submit: true)
- filtered_search.set(search_term)
+ # Enables input to be set (similar to copy and paste)
+ def input_filtered_search(search_term, submit: true, extra_space: true)
+ search = search_term
+ if extra_space
+ # Add an extra space to engage visual tokens
+ search = "#{search_term} "
+ end
+
+ filtered_search.set(search)
if submit
filtered_search.send_keys(:enter)
end
end
+ # Enables input to be added character by character
def input_filtered_search_keys(search_term)
- filtered_search.send_keys(search_term)
+ # Add an extra space to engage visual tokens
+ filtered_search.send_keys("#{search_term} ")
filtered_search.send_keys(:enter)
end
@@ -34,4 +43,32 @@ module FilteredSearchHelpers
# This ensures the dropdown is shown
expect(find('#js-dropdown-label')).not_to have_css('.filter-dropdown-loading')
end
+
+ def expect_filtered_search_input_empty
+ expect(find('.filtered-search').value).to eq('')
+ end
+
+ # Iterates through each visual token inside
+ # .tokens-container to make sure the correct names and values are rendered
+ def expect_tokens(tokens)
+ page.find '.filtered-search-input-container .tokens-container' do
+ page.all(:css, '.tokens-container li').each_with_index do |el, index|
+ token_name = tokens[index][:name]
+ token_value = tokens[index][:value]
+
+ expect(el.find('.name')).to have_content(token_name)
+ if token_value
+ expect(el.find('.value')).to have_content(token_value)
+ end
+ end
+ end
+ end
+
+ def default_placeholder
+ 'Search or filter results...'
+ end
+
+ def get_filtered_search_placeholder
+ find('.filtered-search')['placeholder']
+ end
end
diff --git a/spec/support/prometheus_helpers.rb b/spec/support/prometheus_helpers.rb
new file mode 100644
index 00000000000..a52d8f37d14
--- /dev/null
+++ b/spec/support/prometheus_helpers.rb
@@ -0,0 +1,117 @@
+module PrometheusHelpers
+ def prometheus_memory_query(environment_slug)
+ %{sum(container_memory_usage_bytes{container_name="app",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]))}
+ end
+
+ def prometheus_query_url(prometheus_query)
+ query = { query: prometheus_query }.to_query
+
+ "https://prometheus.example.com/api/v1/query?#{query}"
+ end
+
+ def prometheus_query_range_url(prometheus_query, start: 8.hours.ago)
+ query = {
+ query: prometheus_query,
+ start: start.to_f,
+ end: Time.now.utc.to_f,
+ step: 1.minute.to_i
+ }.to_query
+
+ "https://prometheus.example.com/api/v1/query_range?#{query}"
+ end
+
+ def stub_prometheus_request(url, body: {}, status: 200)
+ WebMock.stub_request(:get, url)
+ .to_return({
+ status: status,
+ headers: { 'Content-Type' => 'application/json' },
+ body: body.to_json
+ })
+ end
+
+ def stub_all_prometheus_requests(environment_slug, body: nil, status: 200)
+ stub_prometheus_request(
+ prometheus_query_url(prometheus_memory_query(environment_slug)),
+ status: status,
+ body: body || prometheus_value_body
+ )
+ stub_prometheus_request(
+ prometheus_query_range_url(prometheus_memory_query(environment_slug)),
+ status: status,
+ body: body || prometheus_values_body
+ )
+ stub_prometheus_request(
+ prometheus_query_url(prometheus_cpu_query(environment_slug)),
+ status: status,
+ body: body || prometheus_value_body
+ )
+ stub_prometheus_request(
+ prometheus_query_range_url(prometheus_cpu_query(environment_slug)),
+ status: status,
+ body: body || prometheus_values_body
+ )
+ end
+
+ def prometheus_data(last_update: Time.now.utc)
+ {
+ success: true,
+ metrics: {
+ memory_values: prometheus_values_body('matrix').dig(:data, :result),
+ memory_current: prometheus_value_body('vector').dig(:data, :result),
+ cpu_values: prometheus_values_body('matrix').dig(:data, :result),
+ cpu_current: prometheus_value_body('vector').dig(:data, :result)
+ },
+ last_update: last_update
+ }
+ end
+
+ def prometheus_empty_body(type)
+ {
+ "status": "success",
+ "data": {
+ "resultType": type,
+ "result": []
+ }
+ }
+ end
+
+ def prometheus_value_body(type = 'vector')
+ {
+ "status": "success",
+ "data": {
+ "resultType": type,
+ "result": [
+ {
+ "metric": {},
+ "value": [
+ 1488772511.004,
+ "0.000041021495238095323"
+ ]
+ }
+ ]
+ }
+ }
+ end
+
+ def prometheus_values_body(type = 'matrix')
+ {
+ "status": "success",
+ "data": {
+ "resultType": type,
+ "result": [
+ {
+ "metric": {},
+ "values": [
+ [1488758662.506, "0.00002996364761904785"],
+ [1488758722.506, "0.00003090239047619091"]
+ ]
+ }
+ ]
+ }
+ }
+ end
+end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index c3aa3ef44c2..f1d226b6ae3 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -143,7 +143,7 @@ module TestEnv
end
def repos_path
- Gitlab.config.repositories.storages.default
+ Gitlab.config.repositories.storages.default['path']
end
def backup_path
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index dfbfbd05f43..10458966cb9 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -227,8 +227,8 @@ describe 'gitlab:app namespace rake task' do
FileUtils.mkdir('tmp/tests/default_storage')
FileUtils.mkdir('tmp/tests/custom_storage')
storages = {
- 'default' => 'tmp/tests/default_storage',
- 'custom' => 'tmp/tests/custom_storage'
+ 'default' => { 'path' => 'tmp/tests/default_storage' },
+ 'custom' => { 'path' => 'tmp/tests/custom_storage' }
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
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 e741e3cf9b6..f2919f20e85 100644
--- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb
+++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
@@ -3,11 +3,13 @@ require 'spec_helper'
describe 'projects/commit/_commit_box.html.haml' do
include Devise::Test::ControllerHelpers
+ let(:user) { create(:user) }
let(:project) { create(:project) }
before do
assign(:project, project)
assign(:commit, project.commit)
+ allow(view).to receive(:can_collaborate_with_project?).and_return(false)
end
it 'shows the commit SHA' do
@@ -25,4 +27,30 @@ describe 'projects/commit/_commit_box.html.haml' do
expect(rendered).to have_text("Pipeline ##{third_pipeline.id} for #{Commit.truncate_sha(project.commit.sha)} failed")
end
+
+ context 'viewing a commit' do
+ context 'as a developer' do
+ before do
+ expect(view).to receive(:can_collaborate_with_project?).and_return(true)
+ end
+
+ it 'has a link to create a new tag' do
+ render
+
+ expect(rendered).to have_link('Tag')
+ end
+ end
+
+ context 'as a non-developer' do
+ before do
+ expect(view).to receive(:can_collaborate_with_project?).and_return(false)
+ end
+
+ it 'does not have a link to create a new tag' do
+ render
+
+ expect(rendered).not_to have_link('Tag')
+ end
+ end
+ end
end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 5919b99a6ed..7bcb5521202 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -105,6 +105,6 @@ describe PostReceive do
end
def pwd(project)
- File.join(Gitlab.config.repositories.storages.default, project.path_with_namespace)
+ File.join(Gitlab.config.repositories.storages.default['path'], project.path_with_namespace)
end
end
diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore
index a1a65c2d72e..520a86352f7 100644
--- a/vendor/gitignore/Android.gitignore
+++ b/vendor/gitignore/Android.gitignore
@@ -37,6 +37,7 @@ captures/
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
+.idea/dictionaries
.idea/libraries
# Keystore files
@@ -48,7 +49,7 @@ captures/
# Google Services (e.g. APIs or Firebase)
google-services.json
-#Freeline
+# Freeline
freeline.py
freeline/
freeline_project_description.json
diff --git a/vendor/gitignore/Global/Eclipse.gitignore b/vendor/gitignore/Global/Eclipse.gitignore
index 31c9fb31167..4f88399d2d8 100644
--- a/vendor/gitignore/Global/Eclipse.gitignore
+++ b/vendor/gitignore/Global/Eclipse.gitignore
@@ -49,3 +49,8 @@ local.properties
# Code Recommenders
.recommenders/
+
+# Scala IDE specific (Scala & Java development for Eclipse)
+.cache-main
+.scala_dependencies
+.worksheet
diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore
index 401fee15748..ec7e95c6ab5 100644
--- a/vendor/gitignore/Global/JetBrains.gitignore
+++ b/vendor/gitignore/Global/JetBrains.gitignore
@@ -4,6 +4,7 @@
# User-specific stuff:
.idea/**/workspace.xml
.idea/**/tasks.xml
+.idea/dictionaries
# Sensitive or high-churn files:
.idea/**/dataSources/
diff --git a/vendor/gitignore/Global/SBT.gitignore b/vendor/gitignore/Global/SBT.gitignore
index 970d897c75c..5ed6acb6576 100644
--- a/vendor/gitignore/Global/SBT.gitignore
+++ b/vendor/gitignore/Global/SBT.gitignore
@@ -1,9 +1,12 @@
# Simple Build Tool
# http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control
+dist/*
target/
lib_managed/
src_managed/
project/boot/
+project/plugins/project/
.history
.cache
+.lib/
diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore
index dbb4a2dfa1a..6143e53f9e3 100644
--- a/vendor/gitignore/Java.gitignore
+++ b/vendor/gitignore/Java.gitignore
@@ -1,3 +1,4 @@
+# Compiled class file
*.class
# Log file
diff --git a/vendor/gitignore/Maven.gitignore b/vendor/gitignore/Maven.gitignore
index 9af45b175ae..5f2dbe11df9 100644
--- a/vendor/gitignore/Maven.gitignore
+++ b/vendor/gitignore/Maven.gitignore
@@ -8,5 +8,5 @@ dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
-# Exclude maven wrapper
+# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored)
!/.mvn/wrapper/maven-wrapper.jar
diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore
index 38ac77e405e..00cbbdf53f6 100644
--- a/vendor/gitignore/Node.gitignore
+++ b/vendor/gitignore/Node.gitignore
@@ -2,6 +2,8 @@
logs
*.log
npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
# Runtime data
pids
diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore
index af90c007a3f..09dfede4814 100644
--- a/vendor/gitignore/Objective-C.gitignore
+++ b/vendor/gitignore/Objective-C.gitignore
@@ -45,10 +45,10 @@ Carthage/Build
# fastlane
#
-# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
+# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
-# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
diff --git a/vendor/gitignore/PlayFramework.gitignore b/vendor/gitignore/PlayFramework.gitignore
index 6d67f119175..ae5ec9fe1d9 100644
--- a/vendor/gitignore/PlayFramework.gitignore
+++ b/vendor/gitignore/PlayFramework.gitignore
@@ -5,6 +5,7 @@ bin/
/lib/
/logs/
/modules
+/project/project
/project/target
/target
tmp/
diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore
index cf3102d6b00..62c1e736924 100644
--- a/vendor/gitignore/Python.gitignore
+++ b/vendor/gitignore/Python.gitignore
@@ -76,6 +76,9 @@ target/
# celery beat schedule file
celerybeat-schedule
+# SageMath parsed files
+*.sage.py
+
# dotenv
.env
diff --git a/vendor/gitignore/Scala.gitignore b/vendor/gitignore/Scala.gitignore
index 006a7b247fe..9c07d4ae988 100644
--- a/vendor/gitignore/Scala.gitignore
+++ b/vendor/gitignore/Scala.gitignore
@@ -1,23 +1,2 @@
*.class
*.log
-
-# sbt specific
-.cache
-.history
-.lib/
-dist/*
-target/
-lib_managed/
-src_managed/
-project/boot/
-project/plugins/project/
-
-# Scala-IDE specific
-.ensime
-.ensime_cache/
-.scala_dependencies
-.worksheet
-
-# ENSIME specific
-.ensime_cache/
-.ensime
diff --git a/vendor/gitignore/Swift.gitignore b/vendor/gitignore/Swift.gitignore
index 099d22ae2f4..d5340449396 100644
--- a/vendor/gitignore/Swift.gitignore
+++ b/vendor/gitignore/Swift.gitignore
@@ -59,7 +59,7 @@ Carthage/Build
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
# screenshots whenever they are needed.
# For more information about the recommended setup visit:
-# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
diff --git a/vendor/gitignore/Symfony.gitignore b/vendor/gitignore/Symfony.gitignore
index ed4d3c6c28d..6c224e024e9 100644
--- a/vendor/gitignore/Symfony.gitignore
+++ b/vendor/gitignore/Symfony.gitignore
@@ -25,7 +25,6 @@
/bin/*
!bin/console
!bin/symfony_requirements
-/vendor/
# Assets and user uploads
/web/bundles/
@@ -38,8 +37,5 @@
# Build data
/build/
-# Composer PHAR
-/composer.phar
-
# Backup entities generated with doctrine:generate:entities command
**/Entity/*~
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
index 69bfb1eec3e..57ed9f5d972 100644
--- a/vendor/gitignore/TeX.gitignore
+++ b/vendor/gitignore/TeX.gitignore
@@ -28,7 +28,6 @@
*.blg
*-blx.aux
*-blx.bib
-*.brf
*.run.xml
## Build tool auxiliary files:
@@ -77,8 +76,6 @@ acs-*.bib
*.t[1-9]
*.t[1-9][0-9]
*.tfm
-*.[1-9]
-*.[1-9][0-9]
#(r)(e)ledmac/(r)(e)ledpar
*.end
@@ -134,6 +131,9 @@ acs-*.bib
*.mlf
*.mlt
*.mtc[0-9]*
+*.slf[0-9]*
+*.slt[0-9]*
+*.stc[0-9]*
# minted
_minted*
@@ -142,9 +142,6 @@ _minted*
# morewrites
*.mw
-# mylatexformat
-*.fmt
-
# nomencl
*.nlo
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index 8054980d742..a752eacca7d 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -166,7 +166,7 @@ PublishScripts/
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
-# NuGet v3's project.json files produces more ignoreable files
+# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
@@ -276,3 +276,12 @@ __pycache__/
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs \ No newline at end of file
diff --git a/vendor/gitlab-ci-yml/Android.gitlab-ci.yml b/vendor/gitlab-ci-yml/Android.gitlab-ci.yml
new file mode 100644
index 00000000000..5f9d54ff574
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Android.gitlab-ci.yml
@@ -0,0 +1,51 @@
+# Read more about this script on this blog post https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/, by Greyson Parrelli
+image: openjdk:8-jdk
+
+variables:
+ ANDROID_COMPILE_SDK: "25"
+ ANDROID_BUILD_TOOLS: "24.0.0"
+ ANDROID_SDK_TOOLS: "24.4.1"
+
+before_script:
+ - apt-get --quiet update --yes
+ - apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1
+ - wget --quiet --output-document=android-sdk.tgz https://dl.google.com/android/android-sdk_r${ANDROID_SDK_TOOLS}-linux.tgz
+ - tar --extract --gzip --file=android-sdk.tgz
+ - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter android-${ANDROID_COMPILE_SDK}
+ - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter platform-tools
+ - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter build-tools-${ANDROID_BUILD_TOOLS}
+ - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter extra-android-m2repository
+ - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter extra-google-google_play_services
+ - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter extra-google-m2repository
+ - export ANDROID_HOME=$PWD/android-sdk-linux
+ - export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/
+ - chmod +x ./gradlew
+
+stages:
+ - build
+ - test
+
+build:
+ stage: build
+ script:
+ - ./gradlew assembleDebug
+ artifacts:
+ paths:
+ - app/build/outputs/
+
+unitTests:
+ stage: test
+ script:
+ - ./gradlew test
+
+functionalTests:
+ stage: test
+ script:
+ - wget --quiet --output-document=android-wait-for-emulator https://raw.githubusercontent.com/travis-ci/travis-cookbooks/0f497eb71291b52a703143c5cd63a217c8766dc9/community-cookbooks/android-sdk/files/default/android-wait-for-emulator
+ - chmod +x android-wait-for-emulator
+ - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter sys-img-x86-google_apis-${ANDROID_COMPILE_SDK}
+ - echo no | android-sdk-linux/tools/android create avd -n test -t android-${ANDROID_COMPILE_SDK} --abi google_apis/x86
+ - android-sdk-linux/tools/emulator64-x86 -avd test -no-window -no-audio &
+ - ./android-wait-for-emulator
+ - adb shell input keyevent 82
+ - ./gradlew cAT
diff --git a/vendor/gitlab-ci-yml/Bash.gitlab-ci.yml b/vendor/gitlab-ci-yml/Bash.gitlab-ci.yml
new file mode 100644
index 00000000000..27537689b80
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Bash.gitlab-ci.yml
@@ -0,0 +1,35 @@
+# see https://docs.gitlab.com/ce/ci/yaml/README.html for all available options
+
+# you can delete this line if you're not using Docker
+image: busybox:latest
+
+before_script:
+ - echo "Before script section"
+ - echo "For example you might run an update here or install a build dependency"
+ - echo "Or perhaps you might print out some debugging details"
+
+after_script:
+ - echo "After script section"
+ - echo "For example you might do some cleanup here"
+
+build1:
+ stage: build
+ script:
+ - echo "Do your build here"
+
+test1:
+ stage: test
+ script:
+ - echo "Do a test here"
+ - echo "For example run a test suite"
+
+test2:
+ stage: test
+ script:
+ - echo "Do another parallel test here"
+ - echo "For example run a lint test"
+
+deploy1:
+ stage: deploy
+ script:
+ - echo "Do your deploy here" \ No newline at end of file
diff --git a/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml
index e8da49a935e..37e44735f7c 100644
--- a/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Crystal.gitlab-ci.yml
@@ -1,4 +1,3 @@
-# This file is a template, and might need editing before it works on your project.
# Official language image. Look for the different tagged releases at:
# https://hub.docker.com/r/crystallang/crystal/
image: "crystallang/crystal:latest"
diff --git a/vendor/gitlab-ci-yml/Django.gitlab-ci.yml b/vendor/gitlab-ci-yml/Django.gitlab-ci.yml
new file mode 100644
index 00000000000..b3106863cca
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Django.gitlab-ci.yml
@@ -0,0 +1,34 @@
+# Official framework image. Look for the different tagged releases at:
+# https://hub.docker.com/r/library/python
+image: python:latest
+
+# Pick zero or more services to be used on all builds.
+# Only needed when using a docker container to run your tests in.
+# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service
+services:
+ - mysql:latest
+ - postgres:latest
+
+variables:
+ POSTGRES_DB: database_name
+
+# This folder is cached between builds
+# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
+cache:
+ paths:
+ - ~/.cache/pip/
+
+# This is a basic example for a gem or script which doesn't use
+# services such as redis or postgres
+before_script:
+ - python -V # Print out python version for debugging
+ # Uncomment next line if your Django app needs a JS runtime:
+ # - apt-get update -q && apt-get install nodejs -yqq
+ - pip install -r requirements.txt
+
+test:
+ variables:
+ DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB"
+ script:
+ - python manage.py migrate
+ - python manage.py test
diff --git a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
index 98d3039ad06..a65e48a3389 100644
--- a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
@@ -6,6 +6,13 @@
# https://github.com/gradle/gradle
image: java:8
+# Disable the Gradle daemon for Continuous Integration servers as correctness
+# is usually a priority over speed in CI environments. Using a fresh
+# runtime for each build is more reliable since the runtime is completely
+# isolated from any previous builds.
+variables:
+ GRADLE_OPTS: "-Dorg.gradle.daemon=false"
+
# Make the gradle wrapper executable. This essentially downloads a copy of
# Gradle to build the project with.
# https://docs.gradle.org/current/userguide/gradle_wrapper.html
diff --git a/vendor/gitlab-ci-yml/LICENSE b/vendor/gitlab-ci-yml/LICENSE
index 80f7b87b6c0..d6c93c6fcf7 100644
--- a/vendor/gitlab-ci-yml/LICENSE
+++ b/vendor/gitlab-ci-yml/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2016 GitLab.org
+Copyright (c) 2016-2017 GitLab.org
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml b/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml
new file mode 100644
index 00000000000..0d6a6eddc97
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Laravel.gitlab-ci.yml
@@ -0,0 +1,78 @@
+# Official framework image. Look for the different tagged releases at:
+# https://hub.docker.com/r/library/php
+image: php:latest
+
+# Pick zero or more services to be used on all builds.
+# Only needed when using a docker container to run your tests in.
+# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service
+services:
+ - mysql:latest
+
+variables:
+ MYSQL_DATABASE: project_name
+ MYSQL_ROOT_PASSWORD: secret
+
+# This folder is cached between builds
+# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
+cache:
+ paths:
+ - vendor/
+ - node_modules/
+
+# This is a basic example for a gem or script which doesn't use
+# services such as redis or postgres
+before_script:
+ # Update packages
+ - apt-get update -yqq
+
+ # Upgrade to Node 7
+ - curl -sL https://deb.nodesource.com/setup_7.x | bash -
+
+ # Install dependencies
+ - apt-get install git nodejs libcurl4-gnutls-dev libicu-dev libmcrypt-dev libvpx-dev libjpeg-dev libpng-dev libxpm-dev zlib1g-dev libfreetype6-dev libxml2-dev libexpat1-dev libbz2-dev libgmp3-dev libldap2-dev unixodbc-dev libpq-dev libsqlite3-dev libaspell-dev libsnmp-dev libpcre3-dev libtidy-dev -yqq
+
+ # Install php extensions
+ - docker-php-ext-install mbstring mcrypt pdo_mysql curl json intl gd xml zip bz2 opcache
+
+ # Install Composer and project dependencies.
+ - curl -sS https://getcomposer.org/installer | php
+ - php composer.phar install
+
+ # Install Node dependencies.
+ # comment this out if you don't have a node dependency
+ - npm install
+
+ # Copy over testing configuration.
+ # Don't forget to set the database config in .env.testing correctly
+ # DB_HOST=mysql
+ # DB_DATABASE=project_name
+ # DB_USERNAME=root
+ # DB_PASSWORD=secret
+ - cp .env.testing .env
+
+ # Run npm build
+ # comment this out if you don't have a frontend build
+ # you can change this to to your frontend building script like
+ # npm run build
+ - npm run dev
+
+ # Generate an application key. Re-cache.
+ - php artisan key:generate
+ - php artisan config:cache
+
+ # Run database migrations.
+ - php artisan migrate
+
+ # Run database seed
+ - php artisan db:seed
+
+test:
+ script:
+ # run laravel tests
+ - php vendor/bin/phpunit --coverage-text --colors=never
+
+ # run frontend tests
+ # if you have any task for testing frontend
+ # set it in your package.json script
+ # comment this out if you don't have a frontend test
+ - npm test
diff --git a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
index 1678a47f9ac..b75f0665bee 100644
--- a/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Maven.gitlab-ci.yml
@@ -17,16 +17,17 @@
variables:
# This will supress any download for dependencies and plugins or upload messages which would clutter the console log.
# `showDateTime` will show the passed time in milliseconds. You need to specify `--batch-mode` to make this work.
- MAVEN_OPTS: "-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true"
+ MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true"
# As of Maven 3.3.0 instead of this you may define these options in `.mvn/maven.config` so the same config is used
# when running from the command line.
# `installAtEnd` and `deployAtEnd`are only effective with recent version of the corresponding plugins.
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version -DinstallAtEnd=true -DdeployAtEnd=true"
# Cache downloaded dependencies and plugins between builds.
+# To keep cache across branches add 'key: "$CI_BUILD_REF_NAME"'
cache:
paths:
- - /root/.m2/repository/
+ - .m2/repository
# This will only validate and compile stuff and run e.g. maven-enforcer-plugin.
# Because some enforcer rules might check dependency convergence and class duplications
diff --git a/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml b/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml
index 2ba5cad9682..6b6c405a507 100644
--- a/vendor/gitlab-ci-yml/Openshift.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/OpenShift.gitlab-ci.yml
@@ -1,4 +1,3 @@
-# This file is a template, and might need editing before it works on your project.
image: ayufan/openshift-cli
stages:
@@ -6,6 +5,7 @@ stages:
- review
- staging
- production
+ - cleanup
variables:
OPENSHIFT_SERVER: openshift.default.svc.cluster.local
@@ -28,7 +28,7 @@ test2:
.deploy: &deploy
before_script:
- oc login "$OPENSHIFT_SERVER" --token="$OPENSHIFT_TOKEN" --insecure-skip-tls-verify
- - oc project "$CI_PROJECT_NAME" 2> /dev/null || oc new-project "$CI_PROJECT_NAME"
+ - oc project "$CI_PROJECT_NAME-$CI_PROJECT_ID" 2> /dev/null || oc new-project "$CI_PROJECT_NAME-$CI_PROJECT_ID"
script:
- "oc get services $APP 2> /dev/null || oc new-app . --name=$APP --strategy=docker"
- "oc start-build $APP --from-dir=. --follow || sleep 3s || oc start-build $APP --from-dir=. --follow"
@@ -51,7 +51,7 @@ review:
stop-review:
<<: *deploy
- stage: review
+ stage: cleanup
script:
- oc delete all -l "app=$APP"
when: manual
diff --git a/vendor/gitlab-ci-yml/PHP.gitlab-ci.yml b/vendor/gitlab-ci-yml/PHP.gitlab-ci.yml
new file mode 100644
index 00000000000..bb8caa49d6b
--- /dev/null
+++ b/vendor/gitlab-ci-yml/PHP.gitlab-ci.yml
@@ -0,0 +1,33 @@
+# Select image from https://hub.docker.com/_/php/
+image: php:7.1.1
+
+# Select what we should cache between builds
+cache:
+ paths:
+ - vendor/
+
+before_script:
+- apt-get update -yqq
+- apt-get install -yqq git libmcrypt-dev libpq-dev libcurl4-gnutls-dev libicu-dev libvpx-dev libjpeg-dev libpng-dev libxpm-dev zlib1g-dev libfreetype6-dev libxml2-dev libexpat1-dev libbz2-dev libgmp3-dev libldap2-dev unixodbc-dev libsqlite3-dev libaspell-dev libsnmp-dev libpcre3-dev libtidy-dev
+# Install PHP extensions
+- docker-php-ext-install mbstring mcrypt pdo_pgsql curl json intl gd xml zip bz2 opcache
+# Install and run Composer
+- curl -sS https://getcomposer.org/installer | php
+- php composer.phar install
+
+# Bring in any services we need http://docs.gitlab.com/ee/ci/docker/using_docker_images.html#what-is-a-service
+# See http://docs.gitlab.com/ce/ci/services/README.html for examples.
+services:
+ - mysql:5.7
+
+# Set any variables we need
+variables:
+ # Configure mysql environment variables (https://hub.docker.com/r/_/mysql/)
+ MYSQL_DATABASE: mysql_database
+ MYSQL_ROOT_PASSWORD: mysql_strong_password
+
+# Run our tests
+# If Xdebug was installed you can generate a coverage report and see code coverage metrics.
+test:
+ script:
+ - vendor/bin/phpunit --configuration phpunit.xml --coverage-text --colors=never \ No newline at end of file
diff --git a/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml
index 45df6975259..a72b8281401 100644
--- a/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Pages/Hugo.gitlab-ci.yml
@@ -9,3 +9,9 @@ pages:
- public
only:
- master
+
+test:
+ script:
+ - hugo
+ except:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml
index 36918fc005a..d98cf94d635 100644
--- a/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Pages/Jekyll.gitlab-ci.yml
@@ -1,11 +1,15 @@
-# Full project: https://gitlab.com/pages/jekyll
+# Template project: https://gitlab.com/pages/jekyll
+# Docs: https://docs.gitlab.com/ce/pages/
+# Jekyll version: 3.4.0
image: ruby:2.3
+before_script:
+- bundle install
+
test:
stage: test
script:
- - gem install jekyll
- - jekyll build -d test
+ - bundle exec jekyll build -d test
artifacts:
paths:
- test
@@ -15,10 +19,10 @@ test:
pages:
stage: deploy
script:
- - gem install jekyll
- - jekyll build -d public
+ - bundle exec jekyll build -d public
artifacts:
paths:
- public
only:
- master
+ \ No newline at end of file
diff --git a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
index 7298ea73bab..574f9365f14 100644
--- a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml
@@ -12,6 +12,7 @@ stages:
- review
- staging
- production
+ - cleanup
build:
stage: build
@@ -61,7 +62,7 @@ review:
- master
stop_review:
- stage: review
+ stage: cleanup
variables:
GIT_STRATEGY: none
script:
diff --git a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
index 249adbc9f4a..4d6f4e00ebb 100644
--- a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml
@@ -12,6 +12,7 @@ stages:
- review
- staging
- production
+ - cleanup
build:
stage: build
@@ -61,7 +62,7 @@ review:
- master
stop_review:
- stage: review
+ stage: cleanup
variables:
GIT_STRATEGY: none
script: