summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml23
-rw-r--r--CONTRIBUTING.md5
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock4
-rw-r--r--LICENSE6
-rw-r--r--app/assets/javascripts/branches/branches_delete_modal.js8
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue2
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js2
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js2
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight.js61
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_helper.js57
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_options.js12
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js5
-rw-r--r--app/assets/javascripts/fly_out_nav.js3
-rw-r--r--app/assets/javascripts/lib/utils/pretty_time.js107
-rw-r--r--app/assets/javascripts/main.js1
-rw-r--r--app/assets/javascripts/new_sidebar.js14
-rw-r--r--app/assets/javascripts/notes/components/issue_note_actions.vue2
-rw-r--r--app/assets/javascripts/shortcuts.js1
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js5
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js17
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js4
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/buttons.scss18
-rw-r--r--app/assets/stylesheets/framework/feature_highlight.scss94
-rw-r--r--app/assets/stylesheets/framework/media_object.scss4
-rw-r--r--app/assets/stylesheets/framework/nav.scss2
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss6
-rw-r--r--app/assets/stylesheets/new_sidebar.scss17
-rw-r--r--app/assets/stylesheets/pages/admin.scss6
-rw-r--r--app/assets/stylesheets/pages/builds.scss8
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss4
-rw-r--r--app/assets/stylesheets/pages/notes.scss3
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss5
-rw-r--r--app/assets/stylesheets/pages/repo.scss10
-rw-r--r--app/controllers/admin/deploy_keys_controller.rb5
-rw-r--r--app/controllers/admin/labels_controller.rb2
-rw-r--r--app/controllers/autocomplete_controller.rb47
-rw-r--r--app/controllers/profiles/gpg_keys_controller.rb4
-rw-r--r--app/controllers/profiles/keys_controller.rb4
-rw-r--r--app/controllers/profiles/personal_access_tokens_controller.rb2
-rw-r--r--app/controllers/projects/branches_controller.rb10
-rw-r--r--app/controllers/projects/commit_controller.rb7
-rw-r--r--app/controllers/projects/compare_controller.rb6
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb2
-rw-r--r--app/controllers/projects/forks_controller.rb8
-rw-r--r--app/controllers/projects/issues_controller.rb7
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb5
-rw-r--r--app/controllers/projects/merge_requests_controller.rb8
-rw-r--r--app/controllers/projects/network_controller.rb23
-rw-r--r--app/controllers/projects/refs_controller.rb17
-rw-r--r--app/controllers/projects/uploads_controller.rb2
-rw-r--r--app/controllers/root_controller.rb5
-rw-r--r--app/finders/autocomplete_users_finder.rb60
-rw-r--r--app/finders/fork_projects_finder.rb6
-rw-r--r--app/finders/groups_finder.rb2
-rw-r--r--app/finders/issuable_finder.rb2
-rw-r--r--app/helpers/auto_devops_helper.rb21
-rw-r--r--app/helpers/milestones_helper.rb6
-rw-r--r--app/helpers/submodule_helper.rb12
-rw-r--r--app/helpers/tab_helper.rb4
-rw-r--r--app/models/ci/pipeline.rb7
-rw-r--r--app/models/deploy_key.rb6
-rw-r--r--app/models/environment.rb4
-rw-r--r--app/models/gpg_key.rb5
-rw-r--r--app/models/issue.rb2
-rw-r--r--app/models/key.rb5
-rw-r--r--app/models/merge_request.rb9
-rw-r--r--app/models/milestone.rb8
-rw-r--r--app/models/namespace.rb9
-rw-r--r--app/models/network/graph.rb7
-rw-r--r--app/models/personal_access_token.rb2
-rw-r--r--app/models/project.rb23
-rw-r--r--app/models/project_auto_devops.rb4
-rw-r--r--app/models/project_feature.rb2
-rw-r--r--app/models/project_team.rb2
-rw-r--r--app/models/repository.rb152
-rw-r--r--app/policies/group_policy.rb3
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/services/commits/change_service.rb6
-rw-r--r--app/services/delete_merged_branches_service.rb19
-rw-r--r--app/services/deploy_keys/create_service.rb7
-rw-r--r--app/services/gpg_keys/create_service.rb9
-rw-r--r--app/services/issuable_base_service.rb11
-rw-r--r--app/services/issues/close_service.rb1
-rw-r--r--app/services/keys/base_service.rb13
-rw-r--r--app/services/keys/create_service.rb9
-rw-r--r--app/services/merge_requests/close_service.rb1
-rw-r--r--app/services/merge_requests/create_service.rb5
-rw-r--r--app/services/merge_requests/merge_service.rb10
-rw-r--r--app/services/notes/create_service.rb8
-rw-r--r--app/services/projects/count_service.rb7
-rw-r--r--app/services/projects/update_service.rb5
-rw-r--r--app/uploaders/avatar_uploader.rb2
-rw-r--r--app/uploaders/gitlab_uploader.rb2
-rw-r--r--app/views/admin/appearances/_form.html.haml4
-rw-r--r--app/views/admin/dashboard/index.html.haml8
-rw-r--r--app/views/admin/hooks/index.html.haml2
-rw-r--r--app/views/feature_highlight/_issue_boards.svg98
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml24
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml12
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml24
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml32
-rw-r--r--app/views/profiles/accounts/show.html.haml4
-rw-r--r--app/views/projects/branches/_branch.html.haml3
-rw-r--r--app/views/projects/branches/_delete_protected_modal.html.haml9
-rw-r--r--app/views/projects/buttons/_download.html.haml30
-rw-r--r--app/views/projects/compare/_form.html.haml20
-rw-r--r--app/views/projects/compare/index.html.haml18
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--app/views/projects/issues/_issue.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml5
-rw-r--r--app/views/projects/new.html.haml4
-rw-r--r--app/views/projects/pipelines_settings/_show.html.haml24
-rw-r--r--app/views/projects/runners/_form.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/projects/settings/integrations/_project_hook.html.haml2
-rw-r--r--app/views/shared/_group_form.html.haml8
-rw-r--r--app/views/shared/_target_switcher.html.haml20
-rw-r--r--app/views/shared/boards/components/_board.html.haml2
-rw-r--r--app/views/shared/icons/_thumbs_up.svg1
-rw-r--r--app/views/shared/issuable/_filter.html.haml4
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml4
-rw-r--r--app/views/shared/milestones/_issuable.html.haml2
-rw-r--r--changelogs/unreleased/20049-projects-api-forks.yml5
-rw-r--r--changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml5
-rw-r--r--changelogs/unreleased/21331-improve-confusing-compare-page.yml5
-rw-r--r--changelogs/unreleased/24121_extract_yet_another_users_finder.yml5
-rw-r--r--changelogs/unreleased/33287-fix-mr-widget-errors-with-external-services.yml5
-rw-r--r--changelogs/unreleased/34259-project-denial-of-service-via-gitmodules-fix.yml5
-rw-r--r--changelogs/unreleased/35290_allow_public_project_apis.yml4
-rw-r--r--changelogs/unreleased/35917_create_services_for_keys.yml4
-rw-r--r--changelogs/unreleased/36884-gitaly-admin-version.yml5
-rw-r--r--changelogs/unreleased/37259-some-mr-ready-mobile-fixes.yml5
-rw-r--r--changelogs/unreleased/37465-fix-line-resolve-all-green-checkmark-icon.yml6
-rw-r--r--changelogs/unreleased/37590-pipelines-mr.yml5
-rw-r--r--changelogs/unreleased/37890-auto-devops-banner-is-not-shown-when-the-repository-is-empty-new-project.yml5
-rw-r--r--changelogs/unreleased/37894-handle-if-auto-devops-domain-is-not-set.yml5
-rw-r--r--changelogs/unreleased/37999-fix-circuit-breaker.yml5
-rw-r--r--changelogs/unreleased/add_closed_at_attribute.yml5
-rw-r--r--changelogs/unreleased/add_tooltip_for_milestone_in_issues_list.yml5
-rw-r--r--changelogs/unreleased/do-not-perform-disk-check.yml5
-rw-r--r--changelogs/unreleased/docs-17499-documentation-errors-about-creating-a-new-tag.yml5
-rw-r--r--changelogs/unreleased/fix-gb-fix-moving-issue-with-ambiguous-references.yml5
-rw-r--r--changelogs/unreleased/fix-sidebar-with-scrollbars.yml5
-rw-r--r--changelogs/unreleased/issue_32215.yml5
-rw-r--r--changelogs/unreleased/issue_37640.yml6
-rw-r--r--changelogs/unreleased/memoize-the-latest-builds-of-a-pipeline.yml5
-rw-r--r--changelogs/unreleased/milestone-avatar-issuable-link.yml5
-rw-r--r--changelogs/unreleased/mk-clarify-moving-namespaces.yml5
-rw-r--r--changelogs/unreleased/projects-controller-show.yml5
-rw-r--r--changelogs/unreleased/reoganize-deployment-indexes.yml5
-rw-r--r--changelogs/unreleased/replace_project_archived-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_commits_revert-feature.yml5
-rw-r--r--changelogs/unreleased/replace_project_snippets-feature.yml5
-rw-r--r--changelogs/unreleased/replace_search-feature.yml5
-rw-r--r--changelogs/unreleased/sh-optimize-discussion-json.yml5
-rw-r--r--changelogs/unreleased/sh-project-feature-eager-load.yml5
-rw-r--r--changelogs/unreleased/sh-stop-loading-issue-discussions.yml5
-rw-r--r--changelogs/unreleased/winh-protected-branch-modal-merged.yml5
-rw-r--r--config/dependency_decisions.yml36
-rw-r--r--config/initializers/doorkeeper.rb2
-rw-r--r--config/initializers/lograge.rb7
-rw-r--r--config/initializers/postgresql_opclasses_support.rb2
-rw-r--r--config/initializers/static_files.rb2
-rw-r--r--db/fixtures/development/04_project.rb4
-rw-r--r--db/migrate/20170912113435_clean_stages_statuses_migration.rb26
-rw-r--r--db/migrate/20170914135630_add_index_for_recent_push_events.rb40
-rw-r--r--db/migrate/20170918222253_reorganize_deployments_indexes.rb28
-rw-r--r--db/migrate/20170918223303_add_deployments_index_for_last_deployment.rb21
-rw-r--r--db/post_migrate/20170907170235_delete_conflicting_redirect_routes.rb37
-rw-r--r--db/post_migrate/20170913180600_fix_projects_without_project_feature.rb33
-rw-r--r--db/schema.rb9
-rw-r--r--doc/administration/reply_by_email.md31
-rw-r--r--doc/api/issues.md10
-rw-r--r--doc/api/projects.md92
-rw-r--r--doc/api/tags.md2
-rw-r--r--doc/api/wikis.md2
-rw-r--r--doc/ci/environments.md100
-rw-r--r--doc/ci/img/environments_monitoring.pngbin243491 -> 76086 bytes
-rw-r--r--doc/ci/quick_start/README.md8
-rw-r--r--doc/ci/runners/README.md14
-rw-r--r--doc/ci/ssh_keys/README.md2
-rw-r--r--doc/ci/triggers/README.md6
-rw-r--r--doc/ci/variables/README.md18
-rw-r--r--doc/ci/yaml/README.md23
-rw-r--r--doc/development/code_review.md10
-rw-r--r--doc/development/img/manual_build_docs.pngbin14869 -> 14867 bytes
-rw-r--r--doc/development/writing_documentation.md79
-rw-r--r--doc/user/group/index.md21
-rw-r--r--doc/user/permissions.md2
-rw-r--r--doc/user/profile/index.md23
-rw-r--r--doc/user/project/pipelines/settings.md2
-rw-r--r--[-rwxr-xr-x]doc/user/project/repository/img/compare_branches.pngbin35999 -> 206831 bytes
-rw-r--r--features/project/archived.feature30
-rw-r--r--features/project/commits/revert.feature31
-rw-r--r--features/project/snippets.feature35
-rw-r--r--features/search.feature100
-rw-r--r--features/steps/project/archived.rb36
-rw-r--r--features/steps/project/commits/revert.rb42
-rw-r--r--features/steps/project/snippets.rb100
-rw-r--r--features/steps/search.rb116
-rw-r--r--lib/api/branches.rb5
-rw-r--r--lib/api/entities.rb6
-rw-r--r--lib/api/merge_requests.rb3
-rw-r--r--lib/api/projects.rb23
-rw-r--r--lib/api/users.rb2
-rw-r--r--lib/backup/manager.rb2
-rw-r--r--lib/banzai/reference_parser/issue_parser.rb3
-rw-r--r--lib/gitlab/auth.rb24
-rw-r--r--lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb41
-rw-r--r--lib/gitlab/ci/build/policy.rb15
-rw-r--r--lib/gitlab/ci/build/policy/kubernetes.rb19
-rw-r--r--lib/gitlab/ci/build/policy/refs.rb43
-rw-r--r--lib/gitlab/ci/build/policy/specification.rb25
-rw-r--r--lib/gitlab/ci/yaml_processor.rb120
-rw-r--r--lib/gitlab/database/migration_helpers.rb88
-rw-r--r--lib/gitlab/database/read_only_relation.rb16
-rw-r--r--lib/gitlab/diff/file_collection/base.rb5
-rw-r--r--lib/gitlab/ee_compat_check.rb7
-rw-r--r--lib/gitlab/gfm/reference_rewriter.rb6
-rw-r--r--lib/gitlab/git.rb9
-rw-r--r--lib/gitlab/git/commit.rb4
-rw-r--r--lib/gitlab/git/operation_service.rb15
-rw-r--r--lib/gitlab/git/popen.rb8
-rw-r--r--lib/gitlab/git/repository.rb151
-rw-r--r--lib/gitlab/git/rev_list.rb4
-rw-r--r--lib/gitlab/gitaly_client.rb147
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb2
-rw-r--r--lib/gitlab/group_hierarchy.rb15
-rw-r--r--lib/gitlab/health_checks/fs_shards_check.rb2
-rw-r--r--lib/gitlab/i18n.rb3
-rw-r--r--lib/tasks/gitlab/dev.rake5
-rw-r--r--locale/bg/gitlab.po20
-rw-r--r--locale/de/gitlab.po50
-rw-r--r--locale/eo/gitlab.po20
-rw-r--r--locale/es/gitlab.po20
-rw-r--r--locale/fr/gitlab.po20
-rw-r--r--locale/it/gitlab.po20
-rw-r--r--locale/ja/gitlab.po20
-rw-r--r--locale/ko/gitlab.po20
-rw-r--r--locale/nl_NL/gitlab.po1474
-rw-r--r--locale/pt_BR/gitlab.po20
-rw-r--r--locale/ru/gitlab.po20
-rw-r--r--locale/uk/gitlab.po20
-rw-r--r--locale/zh_CN/gitlab.po136
-rw-r--r--locale/zh_HK/gitlab.po20
-rw-r--r--locale/zh_TW/gitlab.po52
-rw-r--r--qa/qa/page/main/menu.rb2
-rw-r--r--scripts/schema_changed.sh10
-rwxr-xr-xscripts/trigger-build-docs45
-rw-r--r--spec/controllers/health_controller_spec.rb1
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb32
-rw-r--r--spec/controllers/projects/pipelines_settings_controller_spec.rb43
-rw-r--r--spec/features/groups/merge_requests_spec.rb2
-rw-r--r--spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb22
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb8
-rw-r--r--spec/features/merge_requests/widget_spec.rb18
-rw-r--r--spec/features/profiles/user_visits_profile_account_page_spec.rb3
-rw-r--r--spec/features/profiles/user_visits_profile_authentication_log_page_spec.rb16
-rw-r--r--spec/features/profiles/user_visits_profile_authentication_log_spec.rb15
-rw-r--r--spec/features/profiles/user_visits_profile_page_spec.rb16
-rw-r--r--spec/features/profiles/user_visits_profile_preferences_page_spec.rb3
-rw-r--r--spec/features/profiles/user_visits_profile_spec.rb15
-rw-r--r--spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb3
-rw-r--r--spec/features/projects/commit/user_reverts_commit_spec.rb56
-rw-r--r--spec/features/projects/snippets/user_comments_on_snippet_spec.rb25
-rw-r--r--spec/features/projects/snippets/user_deletes_snippet_spec.rb20
-rw-r--r--spec/features/projects/snippets/user_updates_snippet_spec.rb25
-rw-r--r--spec/features/projects/snippets/user_views_snippets_spec.rb20
-rw-r--r--spec/features/projects/user_archives_project_spec.rb43
-rw-r--r--spec/features/projects/user_edits_files_spec.rb16
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb67
-rw-r--r--spec/features/search/user_searches_for_comments_spec.rb47
-rw-r--r--spec/features/search/user_searches_for_commits_spec.rb49
-rw-r--r--spec/features/search/user_searches_for_issues_spec.rb76
-rw-r--r--spec/features/search/user_searches_for_merge_requests_spec.rb51
-rw-r--r--spec/features/search/user_searches_for_milestones_spec.rb51
-rw-r--r--spec/features/search/user_searches_for_projects_spec.rb36
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb35
-rw-r--r--spec/features/search/user_uses_header_search_field_spec.rb90
-rw-r--r--spec/features/search/user_uses_search_filters_spec.rb52
-rw-r--r--spec/features/search_spec.rb310
-rw-r--r--spec/finders/autocomplete_users_finder_spec.rb97
-rw-r--r--spec/finders/fork_projects_finder_spec.rb43
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/issues.json1
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/admins.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/basics.json4
-rw-r--r--spec/helpers/submodule_helper_spec.rb6
-rw-r--r--spec/initializers/doorkeeper_spec.rb4
-rw-r--r--spec/javascripts/feature_highlight/feature_highlight_helper_spec.js219
-rw-r--r--spec/javascripts/feature_highlight/feature_highlight_options_spec.js45
-rw-r--r--spec/javascripts/feature_highlight/feature_highlight_spec.js122
-rw-r--r--spec/javascripts/filtered_search/dropdown_user_spec.js2
-rw-r--r--spec/javascripts/fly_out_nav_spec.js9
-rw-r--r--spec/javascripts/pretty_time_spec.js282
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js20
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js60
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js14
-rw-r--r--spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js34
-rw-r--r--spec/lib/banzai/filter/milestone_reference_filter_spec.rb2
-rw-r--r--spec/lib/gitlab/auth_spec.rb10
-rw-r--r--spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb40
-rw-r--r--spec/lib/gitlab/backup/manager_spec.rb46
-rw-r--r--spec/lib/gitlab/checks/force_push_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb30
-rw-r--r--spec/lib/gitlab/ci/build/policy/refs_spec.rb87
-rw-r--r--spec/lib/gitlab/ci/build/policy_spec.rb37
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb251
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb122
-rw-r--r--spec/lib/gitlab/diff/position_tracer_spec.rb10
-rw-r--r--spec/lib/gitlab/gfm/reference_rewriter_spec.rb61
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb2
-rw-r--r--spec/lib/gitlab/git/rev_list_spec.rb4
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb4
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb124
-rw-r--r--spec/lib/gitlab/group_hierarchy_spec.rb15
-rw-r--r--spec/migrations/clean_stages_statuses_migration_spec.rb51
-rw-r--r--spec/migrations/delete_conflicting_redirect_routes_spec.rb58
-rw-r--r--spec/models/ci/pipeline_spec.rb21
-rw-r--r--spec/models/commit_spec.rb7
-rw-r--r--spec/models/gpg_key_spec.rb12
-rw-r--r--spec/models/key_spec.rb12
-rw-r--r--spec/models/milestone_spec.rb15
-rw-r--r--spec/models/project_auto_devops_spec.rb16
-rw-r--r--spec/models/project_spec.rb56
-rw-r--r--spec/models/repository_spec.rb46
-rw-r--r--spec/policies/group_policy_spec.rb18
-rw-r--r--spec/policies/project_policy_spec.rb4
-rw-r--r--spec/requests/api/access_requests_spec.rb12
-rw-r--r--spec/requests/api/award_emoji_spec.rb14
-rw-r--r--spec/requests/api/boards_spec.rb33
-rw-r--r--spec/requests/api/branches_spec.rb12
-rw-r--r--spec/requests/api/broadcast_messages_spec.rb11
-rw-r--r--spec/requests/api/groups_spec.rb3
-rw-r--r--spec/requests/api/issues_spec.rb11
-rw-r--r--spec/requests/api/merge_requests_spec.rb37
-rw-r--r--spec/requests/api/projects_spec.rb53
-rw-r--r--spec/requests/api/users_spec.rb51
-rw-r--r--spec/requests/api/v3/award_emoji_spec.rb14
-rw-r--r--spec/requests/api/v3/boards_spec.rb25
-rw-r--r--spec/requests/api/v3/branches_spec.rb10
-rw-r--r--spec/requests/api/v3/broadcast_messages_spec.rb6
-rw-r--r--spec/requests/api/v3/builds_spec.rb12
-rw-r--r--spec/requests/api/v3/issues_spec.rb19
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb10
-rw-r--r--spec/services/deploy_keys/create_service_spec.rb12
-rw-r--r--spec/services/gpg_keys/create_service_spec.rb21
-rw-r--r--spec/services/issues/close_service_spec.rb2
-rw-r--r--spec/services/issues/create_service_spec.rb2
-rw-r--r--spec/services/issues/update_service_spec.rb7
-rw-r--r--spec/services/keys/create_service_spec.rb21
-rw-r--r--spec/services/merge_requests/close_service_spec.rb2
-rw-r--r--spec/services/merge_requests/create_service_spec.rb2
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb4
-rw-r--r--spec/services/notification_service_spec.rb1
-rw-r--r--spec/services/projects/count_service_spec.rb4
-rw-r--r--spec/services/projects/update_service_spec.rb23
-rw-r--r--spec/services/system_note_service_spec.rb4
-rw-r--r--spec/support/matchers/navigation_matcher.rb6
-rw-r--r--spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb21
-rw-r--r--spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb (renamed from spec/support/project_features_apply_to_issuables_shared_examples.rb)0
-rw-r--r--spec/support/shared_examples/features/search_shared_examples.rb5
-rw-r--r--spec/support/stub_gitlab_calls.rb4
-rw-r--r--spec/views/projects/pipelines_settings/_show.html.haml_spec.rb62
-rw-r--r--spec/views/shared/milestones/_issuable.html.haml.rb19
-rw-r--r--vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml4
-rw-r--r--yarn.lock162
376 files changed, 6791 insertions, 3083 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0520f3b1c37..f8f73e4d4d7 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-phantomjs-2.1-node-7.1-postgresql-9.6"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-phantomjs-2.1-node-8.x-yarn-1.0-postgresql-9.6"
.default-cache: &default-cache
key: "ruby-233-with-yarn"
@@ -128,7 +128,7 @@ stages:
- export CACHE_CLASSES=true
- cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- scripts/gitaly-test-spawn
- - knapsack spinach "-r rerun" || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -r rerun $(cat tmp/spinach-rerun.txt)'
+ - knapsack spinach "-r rerun" -b || retry '[[ -e tmp/spinach-rerun.txt ]] && bundle exec spinach -b -r rerun $(cat tmp/spinach-rerun.txt)'
artifacts:
expire_in: 31d
when: always
@@ -174,7 +174,8 @@ build-package:
# Review docs base
.review-docs: &review-docs
image: ruby:2.4-alpine
- before_script: []
+ before_script:
+ - gem install gitlab --no-doc
services: []
variables:
SETUP_DB: "false"
@@ -193,10 +194,9 @@ review-docs-deploy:
name: review-docs/$CI_COMMIT_REF_NAME
# DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables
# Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693
- url: http://$CI_COMMIT_REF_SLUG-built-from-ce-ee.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
+ url: http://preview-$CI_COMMIT_REF_SLUG.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX
on_stop: review-docs-cleanup
script:
- - gem install gitlab --no-doc
- scripts/trigger-build-docs deploy
# Cleanup remote environment of gitlab-docs
@@ -207,7 +207,6 @@ review-docs-cleanup:
name: review-docs/$CI_COMMIT_REF_NAME
action: stop
script:
- - gem install gitlab --no-doc
- scripts/trigger-build-docs cleanup
# Retrieve knapsack and rspec_flaky reports
@@ -413,12 +412,12 @@ downtime_check:
ee_compat_check:
<<: *rake-exec
- only:
- - branches@gitlab-org/gitlab-ce
except:
- master
- tags
- /^[\d-]+-stable(-ee)?/
+ - branches@gitlab-org/gitlab-ee
+ - branches@gitlab/gitlab-ee
allow_failure: yes
cache:
key: "ee_compat_check_repo"
@@ -517,6 +516,12 @@ db:seed_fu-mysql:
<<: *db-seed_fu
<<: *use-mysql
+db:check-schema-pg:
+ <<: *db-migrate-reset
+ <<: *use-pg
+ script:
+ - source scripts/schema_changed.sh
+
# Frontend-related jobs
gitlab:assets:compile:
<<: *dedicated-runner
@@ -546,7 +551,7 @@ karma:
<<: *dedicated-runner
<<: *except-docs
<<: *pull-cache
- image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-chrome-60.0-node-7.1-postgresql-9.6"
+ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.13-chrome-61.0-node-8.x-yarn-1.0-postgresql-9.6"
stage: test
variables:
BABEL_ENV: "coverage"
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1f25171e8a6..dfb2ce0099a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -286,7 +286,10 @@ might be edited to make them small and simple.
Please submit Feature Proposals using the ['Feature Proposal' issue template](.gitlab/issue_templates/Feature Proposal.md) provided on the issue tracker.
-For changes in the interface, it can be helpful to create a mockup first.
+For changes in the interface, it is helpful to include a mockup. Issues that add to, or change, the interface should
+be given the ~"UX" label. This will allow the UX team to provide input and guidance. You may
+need to ask one of the [core team] members to add the label, if you do not have permissions to do it by yourself.
+
If you want to create something yourself, consider opening an issue first to
discuss whether it is interesting to include this in GitLab.
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 4a36342fcab..fd2a01863fd 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-3.0.0
+3.1.0
diff --git a/Gemfile b/Gemfile
index cc6618d3557..fa25d8ded33 100644
--- a/Gemfile
+++ b/Gemfile
@@ -362,6 +362,7 @@ group :test do
gem 'sham_rack', '~> 1.3.6'
gem 'timecop', '~> 0.8.0'
gem 'concurrent-ruby', '~> 1.0.5'
+ gem 'test-prof', '~> 0.2.5'
end
gem 'octokit', '~> 4.6.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index bcbe6b4f394..90154d98c9c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -357,7 +357,7 @@ GEM
rake
grape_logging (1.7.0)
grape
- grpc (1.4.5)
+ grpc (1.6.0)
google-protobuf (~> 3.1)
googleauth (~> 0.5.1)
haml (4.0.7)
@@ -882,6 +882,7 @@ GEM
ffi
sysexits (1.2.0)
temple (0.7.7)
+ test-prof (0.2.5)
test_after_commit (1.1.0)
activerecord (>= 3.2)
text (1.3.1)
@@ -1163,6 +1164,7 @@ DEPENDENCIES
stackprof (~> 0.2.10)
state_machines-activerecord (~> 0.4.0)
sys-filesystem (~> 1.1.6)
+ test-prof (~> 0.2.5)
test_after_commit (~> 1.1)
thin (~> 1.7.0)
timecop (~> 0.8.0)
diff --git a/LICENSE b/LICENSE
index ad4f2872db5..15c423e1416 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,5 +1,7 @@
Copyright (c) 2011-2017 GitLab B.V.
+With regard to the GitLab Software:
+
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
@@ -17,3 +19,7 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+
+For all third party components incorporated into the GitLab Software, those
+components are licensed under the original license provided by the owner of the
+applicable component. \ No newline at end of file
diff --git a/app/assets/javascripts/branches/branches_delete_modal.js b/app/assets/javascripts/branches/branches_delete_modal.js
index af8bcdc1794..cbc28374b80 100644
--- a/app/assets/javascripts/branches/branches_delete_modal.js
+++ b/app/assets/javascripts/branches/branches_delete_modal.js
@@ -7,6 +7,7 @@ class DeleteModal {
this.$branchName = $('.js-branch-name', this.$modal);
this.$confirmInput = $('.js-delete-branch-input', this.$modal);
this.$deleteBtn = $('.js-delete-branch', this.$modal);
+ this.$notMerged = $('.js-not-merged', this.$modal);
this.bindEvents();
}
@@ -16,8 +17,10 @@ class DeleteModal {
}
setModalData(e) {
- this.branchName = e.currentTarget.dataset.branchName || '';
- this.deletePath = e.currentTarget.dataset.deletePath || '';
+ const branchData = e.currentTarget.dataset;
+ this.branchName = branchData.branchName || '';
+ this.deletePath = branchData.deletePath || '';
+ this.isMerged = !!branchData.isMerged;
this.updateModal();
}
@@ -30,6 +33,7 @@ class DeleteModal {
this.$confirmInput.val('');
this.$deleteBtn.attr('href', this.deletePath);
this.$deleteBtn.attr('disabled', true);
+ this.$notMerged.toggleClass('hidden', this.isMerged);
}
}
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index 904f7f64fa8..b41d464475f 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -73,7 +73,7 @@
</span>
<a
v-if="deployKey.can_edit"
- class="btn btn-small"
+ class="btn btn-sm"
:href="editDeployKeyPath"
>
Edit
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
index 298f737a2bc..497c23f014f 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -4,6 +4,8 @@
import Vue from 'vue';
+import '../mixins/discussion';
+
const JumpToDiscussion = Vue.extend({
mixins: [DiscussionMixins],
props: {
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js b/app/assets/javascripts/diff_notes/components/resolve_count.js
index 96e5a440357..fe7cf8f5fc1 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js
@@ -4,6 +4,8 @@
import Vue from 'vue';
+import '../mixins/discussion';
+
window.ResolveCount = Vue.extend({
mixins: [DiscussionMixins],
props: {
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js
deleted file mode 100644
index 800ca05cd11..00000000000
--- a/app/assets/javascripts/feature_highlight/feature_highlight.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import Cookies from 'js-cookie';
-import _ from 'underscore';
-import {
- getCookieName,
- getSelector,
- hidePopover,
- setupDismissButton,
- mouseenter,
- mouseleave,
-} from './feature_highlight_helper';
-
-export const setupFeatureHighlightPopover = (id, debounceTimeout = 300) => {
- const $selector = $(getSelector(id));
- const $parent = $selector.parent();
- const $popoverContent = $parent.siblings('.feature-highlight-popover-content');
- const hideOnScroll = hidePopover.bind($selector);
- const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout);
-
- $selector
- // Setup popover
- .data('content', $popoverContent.prop('outerHTML'))
- .popover({
- html: true,
- // Override the existing template to add custom CSS classes
- template: `
- <div class="popover feature-highlight-popover" role="tooltip">
- <div class="arrow"></div>
- <div class="popover-content"></div>
- </div>
- `,
- })
- .on('mouseenter', mouseenter)
- .on('mouseleave', debouncedMouseleave)
- .on('inserted.bs.popover', setupDismissButton)
- .on('show.bs.popover', () => {
- window.addEventListener('scroll', hideOnScroll);
- })
- .on('hide.bs.popover', () => {
- window.removeEventListener('scroll', hideOnScroll);
- })
- // Display feature highlight
- .removeAttr('disabled');
-};
-
-export const shouldHighlightFeature = (id) => {
- const element = document.querySelector(getSelector(id));
- const previouslyDismissed = Cookies.get(getCookieName(id)) === 'true';
-
- return element && !previouslyDismissed;
-};
-
-export const highlightFeatures = (highlightOrder) => {
- const featureId = highlightOrder.find(shouldHighlightFeature);
-
- if (featureId) {
- setupFeatureHighlightPopover(featureId);
- return true;
- }
-
- return false;
-};
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
deleted file mode 100644
index 9f741355cd7..00000000000
--- a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import Cookies from 'js-cookie';
-
-export const getCookieName = cookieId => `feature-highlighted-${cookieId}`;
-export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`;
-
-export const showPopover = function showPopover() {
- if (this.hasClass('js-popover-show')) {
- return false;
- }
- this.popover('show');
- this.addClass('disable-animation js-popover-show');
-
- return true;
-};
-
-export const hidePopover = function hidePopover() {
- if (!this.hasClass('js-popover-show')) {
- return false;
- }
- this.popover('hide');
- this.removeClass('disable-animation js-popover-show');
-
- return true;
-};
-
-export const dismiss = function dismiss(cookieId) {
- Cookies.set(getCookieName(cookieId), true);
- hidePopover.call(this);
- this.hide();
-};
-
-export const mouseleave = function mouseleave() {
- if (!$('.popover:hover').length > 0) {
- const $featureHighlight = $(this);
- hidePopover.call($featureHighlight);
- }
-};
-
-export const mouseenter = function mouseenter() {
- const $featureHighlight = $(this);
-
- const showedPopover = showPopover.call($featureHighlight);
- if (showedPopover) {
- $('.popover')
- .on('mouseleave', mouseleave.bind($featureHighlight));
- }
-};
-
-export const setupDismissButton = function setupDismissButton() {
- const popoverId = this.getAttribute('aria-describedby');
- const cookieId = this.dataset.highlight;
- const $popover = $(this);
- const dismissWrapper = dismiss.bind($popover, cookieId);
-
- $(`#${popoverId} .dismiss-feature-highlight`)
- .on('click', dismissWrapper);
-};
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_options.js b/app/assets/javascripts/feature_highlight/feature_highlight_options.js
deleted file mode 100644
index fd48f2e87cc..00000000000
--- a/app/assets/javascripts/feature_highlight/feature_highlight_options.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { highlightFeatures } from './feature_highlight';
-import bp from '../breakpoints';
-
-const highlightOrder = ['issue-boards'];
-
-export default function domContentLoaded(order) {
- if (bp.getBreakpointSize() === 'lg') {
- highlightFeatures(order);
- }
-}
-
-document.addEventListener('DOMContentLoaded', domContentLoaded.bind(this, highlightOrder));
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index 7246ccbb281..720fbc87ea0 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -15,6 +15,7 @@ class DropdownUser extends gl.FilteredSearchDropdown {
params: {
per_page: 20,
active: true,
+ group_id: this.getGroupId(),
project_id: this.getProjectId(),
current_user: true,
},
@@ -47,6 +48,10 @@ class DropdownUser extends gl.FilteredSearchDropdown {
super.renderContent(forceShowList);
}
+ getGroupId() {
+ return this.input.getAttribute('data-group-id');
+ }
+
getProjectId() {
return this.input.getAttribute('data-project-id');
}
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
index ad8254167a2..157280d66e3 100644
--- a/app/assets/javascripts/fly_out_nav.js
+++ b/app/assets/javascripts/fly_out_nav.js
@@ -77,10 +77,11 @@ export const hideMenu = (el) => {
export const moveSubItemsToPosition = (el, subItems) => {
const boundingRect = el.getBoundingClientRect();
const top = calculateTop(boundingRect, subItems.offsetHeight);
+ const left = sidebar ? sidebar.offsetWidth : 50;
const isAbove = top < boundingRect.top;
subItems.classList.add('fly-out-list');
- subItems.style.transform = `translate3d(0, ${Math.floor(top) - headerHeight}px, 0)`; // eslint-disable-line no-param-reassign
+ subItems.style.transform = `translate3d(${left}px, ${Math.floor(top) - headerHeight}px, 0)`; // eslint-disable-line no-param-reassign
const subItemsRect = subItems.getBoundingClientRect();
diff --git a/app/assets/javascripts/lib/utils/pretty_time.js b/app/assets/javascripts/lib/utils/pretty_time.js
index 227bf65b560..b1ffd797f7e 100644
--- a/app/assets/javascripts/lib/utils/pretty_time.js
+++ b/app/assets/javascripts/lib/utils/pretty_time.js
@@ -1,68 +1,61 @@
import _ from 'underscore';
-(() => {
- /*
- * TODO: Make these methods more configurable (e.g. stringifyTime condensed or
- * non-condensed, abbreviateTimelengths)
- * */
-
- const utils = window.gl.utils = gl.utils || {};
- const prettyTime = utils.prettyTime = {
- /*
- * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # }
- * Seconds can be negative or positive, zero or non-zero. Can be configured for any day
- * or week length.
- */
- parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) {
- const DAYS_PER_WEEK = daysPerWeek;
- const HOURS_PER_DAY = hoursPerDay;
- const MINUTES_PER_HOUR = 60;
- const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR;
- const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
-
- const timePeriodConstraints = {
- weeks: MINUTES_PER_WEEK,
- days: MINUTES_PER_DAY,
- hours: MINUTES_PER_HOUR,
- minutes: 1,
- };
+/*
+ * TODO: Make these methods more configurable (e.g. stringifyTime condensed or
+ * non-condensed, abbreviateTimelengths)
+ * */
+
+/*
+ * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # }
+ * Seconds can be negative or positive, zero or non-zero. Can be configured for any day
+ * or week length.
+*/
+
+export function parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) {
+ const DAYS_PER_WEEK = daysPerWeek;
+ const HOURS_PER_DAY = hoursPerDay;
+ const MINUTES_PER_HOUR = 60;
+ const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR;
+ const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
+
+ const timePeriodConstraints = {
+ weeks: MINUTES_PER_WEEK,
+ days: MINUTES_PER_DAY,
+ hours: MINUTES_PER_HOUR,
+ minutes: 1,
+ };
- let unorderedMinutes = prettyTime.secondsToMinutes(seconds);
+ let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR);
- return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => {
- const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
+ return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => {
+ const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
- unorderedMinutes -= (periodCount * minutesPerPeriod);
+ unorderedMinutes -= (periodCount * minutesPerPeriod);
- return periodCount;
- });
- },
+ return periodCount;
+ });
+}
- /*
- * Accepts a timeObject and returns a condensed string representation of it
- * (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
- */
+/*
+* Accepts a timeObject (see parseSeconds) and returns a condensed string representation of it
+* (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
+*/
- stringifyTime(timeObject) {
- const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => {
- const isNonZero = !!unitValue;
- return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
- }, '').trim();
- return reducedTime.length ? reducedTime : '0m';
- },
+export function stringifyTime(timeObject) {
+ const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => {
+ const isNonZero = !!unitValue;
+ return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
+ }, '').trim();
+ return reducedTime.length ? reducedTime : '0m';
+}
- /*
- * Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns
- * the first non-zero unit/value pair.
- */
+/*
+* Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns
+* the first non-zero unit/value pair.
+*/
- abbreviateTime(timeStr) {
- return timeStr.split(' ')
- .filter(unitStr => unitStr.charAt(0) !== '0')[0];
- },
+export function abbreviateTime(timeStr) {
+ return timeStr.split(' ')
+ .filter(unitStr => unitStr.charAt(0) !== '0')[0];
+}
- secondsToMinutes(seconds) {
- return Math.abs(seconds / 60);
- },
- };
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 0f84470828a..c2a104df749 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -101,7 +101,6 @@ import './label_manager';
import './labels';
import './labels_select';
import './layout_nav';
-import './feature_highlight/feature_highlight_options';
import LazyLoader from './lazy_loader';
import './line_highlighter';
import './logo';
diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/new_sidebar.js
index cea4f35096a..997550b37fb 100644
--- a/app/assets/javascripts/new_sidebar.js
+++ b/app/assets/javascripts/new_sidebar.js
@@ -11,11 +11,11 @@ export default class NewNavSidebar {
initDomElements() {
this.$page = $('.page-with-sidebar');
this.$sidebar = $('.nav-sidebar');
+ this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar);
this.$overlay = $('.mobile-overlay');
this.$openSidebar = $('.toggle-mobile-nav');
this.$closeSidebar = $('.close-nav-button');
this.$sidebarToggle = $('.js-toggle-sidebar');
- this.$topLevelLinks = $('.sidebar-top-level-items > li > a');
}
bindEvents() {
@@ -57,9 +57,15 @@ export default class NewNavSidebar {
}
NewNavSidebar.setCollapsedCookie(collapsed);
- this.$topLevelLinks.attr('title', function updateTopLevelTitle() {
- return collapsed ? this.getAttribute('aria-label') : '';
- });
+ this.toggleSidebarOverflow();
+ }
+
+ toggleSidebarOverflow() {
+ if (this.$innerScroll.prop('scrollHeight') > this.$innerScroll.prop('offsetHeight')) {
+ this.$innerScroll.css('overflow-y', 'scroll');
+ } else {
+ this.$innerScroll.css('overflow-y', '');
+ }
}
render() {
diff --git a/app/assets/javascripts/notes/components/issue_note_actions.vue b/app/assets/javascripts/notes/components/issue_note_actions.vue
index 60c172321d1..feb3e73194b 100644
--- a/app/assets/javascripts/notes/components/issue_note_actions.vue
+++ b/app/assets/javascripts/notes/components/issue_note_actions.vue
@@ -86,7 +86,7 @@
<div class="note-actions">
<span
v-if="accessLevel"
- class="note-role">{{accessLevel}}</span>
+ class="note-role note-role-access">{{accessLevel}}</span>
<div
v-if="canAddAwardEmoji"
class="note-actions-item">
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index e3daa8cf949..e754f6c4460 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -1,6 +1,7 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */
/* global Mousetrap */
import Cookies from 'js-cookie';
+import Mousetrap from 'mousetrap';
import findAndFollowLink from './shortcuts_dashboard_navigation';
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js
index 0da265053bd..a9fbc7f1a2f 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.js
@@ -1,6 +1,5 @@
import stopwatchSvg from 'icons/_icon_stopwatch.svg';
-
-import '../../../lib/utils/pretty_time';
+import { abbreviateTime } from '../../../lib/utils/pretty_time';
export default {
name: 'time-tracking-collapsed-state',
@@ -79,7 +78,7 @@ export default {
},
methods: {
abbreviateTime(timeStr) {
- return gl.utils.prettyTime.abbreviateTime(timeStr);
+ return abbreviateTime(timeStr);
},
},
template: `
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js
index 40f5c89c5bb..fd0d4570d68 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js
+++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.js
@@ -1,6 +1,4 @@
-import '../../../lib/utils/pretty_time';
-
-const prettyTime = gl.utils.prettyTime;
+import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time';
export default {
name: 'time-tracking-comparison-pane',
@@ -23,12 +21,12 @@ export default {
},
},
computed: {
- parsedRemaining() {
+ parsedTimeRemaining() {
const diffSeconds = this.timeEstimate - this.timeSpent;
- return prettyTime.parseSeconds(diffSeconds);
+ return parseSeconds(diffSeconds);
},
timeRemainingHumanReadable() {
- return prettyTime.stringifyTime(this.parsedRemaining);
+ return stringifyTime(this.parsedTimeRemaining);
},
timeRemainingTooltip() {
const prefix = this.timeRemainingMinutes < 0 ? 'Over by' : 'Time remaining:';
@@ -44,13 +42,6 @@ export default {
timeRemainingStatusClass() {
return this.timeEstimate >= this.timeSpent ? 'within_estimate' : 'over_estimate';
},
- /* Parsed time values */
- parsedEstimate() {
- return prettyTime.parseSeconds(this.timeEstimate);
- },
- parsedSpent() {
- return prettyTime.parseSeconds(this.timeSpent);
- },
},
template: `
<div class="time-tracking-comparison-pane">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
index aaca42e3ebc..219ff94924e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
@@ -72,12 +72,12 @@ export default {
<a
href="#modal_merge_info"
data-toggle="modal"
- class="btn btn-small inline">
+ class="btn btn-sm inline">
Check out branch
</a>
<span class="dropdown prepend-left-10">
<a
- class="btn btn-small inline dropdown-toggle"
+ class="btn btn-sm inline dropdown-toggle"
data-toggle="dropdown"
aria-label="Download as"
role="button">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
index 6c2e9ba1d30..c79b5c720eb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
@@ -12,6 +12,9 @@ export default {
ciIcon,
},
computed: {
+ hasPipeline() {
+ return this.mr.pipeline && Object.keys(this.mr.pipeline).length > 0;
+ },
hasCIError() {
const { hasCI, ciStatus } = this.mr;
@@ -28,7 +31,9 @@ export default {
},
},
template: `
- <div class="mr-widget-heading">
+ <div
+ v-if="hasPipeline || hasCIError"
+ class="mr-widget-heading">
<div class="ci-widget media">
<template v-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
@@ -40,7 +45,7 @@ export default {
Could not connect to the CI server. Please check your settings and try again
</div>
</template>
- <template v-else>
+ <template v-else-if="hasPipeline">
<div class="ci-status-icon append-right-10">
<a
class="icon-link"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
index b01c923311b..703f3a56a34 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
@@ -27,7 +27,7 @@ export default {
<button
v-if="showDisabledButton"
type="button"
- class="btn btn-success btn-small"
+ class="btn btn-success btn-sm"
disabled="true">
Merge
</button>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js
index 2b16a2d6817..b4e4a6aa161 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js
@@ -11,7 +11,7 @@ export default {
<status-icon status="failed" />
<button
type="button"
- class="btn btn-success btn-small"
+ class="btn btn-success btn-sm"
disabled="true">
Merge
</button>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
index 65187754009..ad709da51ee 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
@@ -29,6 +29,9 @@ export default {
statusIcon,
},
computed: {
+ shouldShowMergeWhenPipelineSucceedsText() {
+ return this.mr.isPipelineActive;
+ },
commitMessageLinkTitle() {
const withDesc = 'Include description in commit message';
const withoutDesc = "Don't include description in commit message";
@@ -36,7 +39,7 @@ export default {
return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
},
mergeButtonClass() {
- const defaultClass = 'btn btn-small btn-success accept-merge-request';
+ const defaultClass = 'btn btn-sm btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`;
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
@@ -56,7 +59,7 @@ export default {
mergeButtonText() {
if (this.isMergingImmediately) {
return 'Merge in progress';
- } else if (this.mr.isPipelineActive) {
+ } else if (this.shouldShowMergeWhenPipelineSucceedsText) {
return 'Merge when pipeline succeeds';
}
@@ -68,7 +71,7 @@ export default {
isMergeButtonDisabled() {
const { commitMessage } = this;
return Boolean(!commitMessage.length
- || !this.isMergeAllowed()
+ || !this.shouldShowMergeControls()
|| this.isMakingRequest
|| this.mr.preventMerge);
},
@@ -82,7 +85,12 @@ export default {
},
methods: {
isMergeAllowed() {
- return !(this.mr.onlyAllowMergeIfPipelineSucceeds && this.mr.isPipelineFailed);
+ return !this.mr.onlyAllowMergeIfPipelineSucceeds ||
+ this.mr.isPipelinePassing ||
+ this.mr.isPipelineSkipped;
+ },
+ shouldShowMergeControls() {
+ return this.isMergeAllowed() || this.shouldShowMergeWhenPipelineSucceedsText;
},
updateCommitMessage() {
const cmwd = this.mr.commitMessageWithDescription;
@@ -202,8 +210,8 @@ export default {
<div class="mr-widget-body media">
<status-icon status="success" />
<div class="media-body">
- <div class="media space-children">
- <span class="btn-group">
+ <div class="mr-widget-body-controls media space-children">
+ <span class="btn-group append-bottom-5">
<button
@click="handleMergeButtonClick()"
:disabled="isMergeButtonDisabled"
@@ -219,7 +227,7 @@ export default {
v-if="shouldShowMergeOptionsDropdown"
:disabled="isMergeButtonDisabled"
type="button"
- class="btn btn-small btn-info dropdown-toggle js-merge-moment"
+ class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
data-toggle="dropdown"
aria-label="Select merge moment">
<i
@@ -260,8 +268,8 @@ export default {
</li>
</ul>
</span>
- <div class="media-body space-children">
- <template v-if="isMergeAllowed()">
+ <div class="media-body-wrap space-children">
+ <template v-if="shouldShowMergeControls()">
<label>
<input
id="remove-source-branch-input"
@@ -286,7 +294,7 @@ export default {
</template>
<template v-else>
<span class="bold">
- The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
+ The pipeline for this merge request has not succeeded yet
</span>
</template>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
index 0042c48816f..2f237262028 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
@@ -57,7 +57,7 @@ export default {
return stateMaps.statesToShowHelpWidget.indexOf(this.mr.state) > -1;
},
shouldRenderPipelines() {
- return Object.keys(this.mr.pipeline).length || this.mr.hasCI;
+ return this.mr.hasCI;
},
shouldRenderRelatedLinks() {
return this.mr.relatedLinks;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index fbea764b739..29464662578 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -85,7 +85,9 @@ export default class MergeRequestStore {
this.ciEnvironmentsStatusPath = data.ci_environments_status_path;
this.hasCI = data.has_ci;
this.ciStatus = data.ci_status;
- this.isPipelineFailed = this.ciStatus ? (this.ciStatus === 'failed' || this.ciStatus === 'canceled') : false;
+ this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
+ this.isPipelinePassing = this.ciStatus === 'success' || this.ciStatus === 'success_with_warnings';
+ this.isPipelineSkipped = this.ciStatus === 'skipped';
this.pipelineDetailedStatus = pipelineStatus;
this.isPipelineActive = data.pipeline ? data.pipeline.active : false;
this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false;
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 35e7a10379f..923d14f2c3d 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -52,4 +52,3 @@
@import "framework/snippets";
@import "framework/memory_graph";
@import "framework/responsive-tables";
-@import "framework/feature_highlight";
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 82350c36df0..d178bc17462 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -46,15 +46,6 @@
}
}
-@mixin btn-svg {
- svg {
- height: 15px;
- width: 15px;
- position: relative;
- top: 2px;
- }
-}
-
@mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) {
background-color: $light;
border-color: $border-light;
@@ -132,7 +123,6 @@
.btn {
@include btn-default;
@include btn-white;
- @include btn-svg;
color: $gl-text-color;
@@ -140,7 +130,6 @@
outline: 0;
}
- &.btn-small,
&.btn-sm {
padding: 4px 10px;
font-size: 13px;
@@ -232,6 +221,13 @@
}
}
+ svg {
+ height: 15px;
+ width: 15px;
+ position: relative;
+ top: 2px;
+ }
+
svg,
.fa {
&:not(:last-child) {
diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss
deleted file mode 100644
index ebae473df50..00000000000
--- a/app/assets/stylesheets/framework/feature_highlight.scss
+++ /dev/null
@@ -1,94 +0,0 @@
-.feature-highlight {
- position: relative;
- margin-left: $gl-padding;
- width: 20px;
- height: 20px;
- cursor: pointer;
-
- &::before {
- content: '';
- display: block;
- position: absolute;
- top: 6px;
- left: 6px;
- width: 8px;
- height: 8px;
- background-color: $blue-500;
- border-radius: 50%;
- box-shadow: 0 0 0 rgba($blue-500, 0.4);
- animation: pulse-highlight 2s infinite;
- }
-
- &:hover::before,
- &.disable-animation::before {
- animation: none;
- }
-
- &[disabled]::before {
- display: none;
- }
-}
-
-.is-showing-fly-out {
- .feature-highlight {
- display: none;
- }
-}
-
-.feature-highlight-popover-content {
- display: none;
-
- hr {
- margin: $gl-padding * 0.5 0;
- }
-
- .btn-link {
- @include btn-svg;
-
- svg path {
- fill: currentColor;
- }
- }
-
- .dismiss-feature-highlight {
- padding: 0;
- }
-
- svg:first-child {
- width: 100%;
- background-color: $indigo-50;
- border-top-left-radius: 2px;
- border-top-right-radius: 2px;
- border-bottom: 1px solid darken($gray-normal, 8%);
- }
-}
-
-.popover .feature-highlight-popover-content {
- display: block;
-}
-
-.feature-highlight-popover {
- padding: 0;
-
- .popover-content {
- padding: 0;
- }
-}
-
-.feature-highlight-popover-sub-content {
- padding: 9px 14px;
-}
-
-@include keyframes(pulse-highlight) {
- 0% {
- box-shadow: 0 0 0 0 rgba($blue-200, 0.4);
- }
-
- 70% {
- box-shadow: 0 0 0 10px transparent;
- }
-
- 100% {
- box-shadow: 0 0 0 0 transparent;
- }
-}
diff --git a/app/assets/stylesheets/framework/media_object.scss b/app/assets/stylesheets/framework/media_object.scss
index b573052c14a..89c561479cc 100644
--- a/app/assets/stylesheets/framework/media_object.scss
+++ b/app/assets/stylesheets/framework/media_object.scss
@@ -6,3 +6,7 @@
.media-body {
flex: 1;
}
+
+.media-body-wrap {
+ flex-grow: 1;
+}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 5ffa67a1220..2f7717760ec 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -328,7 +328,7 @@
border-bottom: 1px solid $border-color;
transition: padding $sidebar-transition-duration;
text-align: center;
- margin-top: $header-height;
+ margin-top: $new-navbar-height;
.container-fluid {
position: relative;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index ef58382ba41..48dc25d343b 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -78,16 +78,16 @@
.right-sidebar {
border-left: 1px solid $border-color;
- height: calc(100% - #{$header-height});
+ height: calc(100% - #{$new-navbar-height});
&.affix {
position: fixed;
- top: $header-height;
+ top: $new-navbar-height;
}
}
.with-performance-bar .right-sidebar.affix {
- top: $header-height + $performance-bar-height;
+ top: $new-navbar-height + $performance-bar-height;
}
@mixin maintain-sidebar-dimensions {
diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss
index 60fe863847c..9c404b7e542 100644
--- a/app/assets/stylesheets/new_sidebar.scss
+++ b/app/assets/stylesheets/new_sidebar.scss
@@ -105,7 +105,8 @@ $new-sidebar-collapsed-width: 50px;
}
&.sidebar-icons-only {
- width: $new-sidebar-collapsed-width;
+ width: auto;
+ min-width: $new-sidebar-collapsed-width;
.nav-sidebar-inner-scroll {
overflow-x: hidden;
@@ -124,6 +125,10 @@ $new-sidebar-collapsed-width: 50px;
.fly-out-top-item {
display: block;
}
+
+ .avatar-container {
+ margin-right: 0;
+ }
}
&.nav-sidebar-expanded {
@@ -188,6 +193,10 @@ $new-sidebar-collapsed-width: 50px;
height: 100%;
width: 100%;
overflow: auto;
+
+ @media (min-width: $screen-sm-min) {
+ overflow: hidden;
+ }
}
.with-performance-bar .nav-sidebar {
@@ -248,7 +257,7 @@ $new-sidebar-collapsed-width: 50px;
@media (min-width: $screen-sm-min) {
position: fixed;
top: 0;
- left: $new-sidebar-width;
+ left: 0;
min-width: 150px;
margin-top: -1px;
padding: 4px 1px;
@@ -386,10 +395,6 @@ $new-sidebar-collapsed-width: 50px;
}
.sidebar-sub-level-items {
- @media (min-width: $screen-sm-min) {
- left: $new-sidebar-collapsed-width;
- }
-
&:not(.flyout-list) {
display: none;
}
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
new file mode 100644
index 00000000000..6c555aee20a
--- /dev/null
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -0,0 +1,6 @@
+.info-well {
+ .admin-well-statistics,
+ .admin-well-features {
+ padding-bottom: 46px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 50ec5110bf1..359dd388d05 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -64,10 +64,10 @@
color: $gl-text-color;
position: sticky;
position: -webkit-sticky;
- top: $header-height;
+ top: $new-navbar-height;
&.affix {
- top: $header-height;
+ top: $new-navbar-height;
}
// with sidebar
@@ -174,10 +174,10 @@
.with-performance-bar .build-page {
.top-bar {
- top: $header-height + $performance-bar-height;
+ top: $new-navbar-height + $performance-bar-height;
&.affix {
- top: $header-height + $performance-bar-height;
+ top: $new-navbar-height + $performance-bar-height;
}
}
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 439636fe026..09a14578dd3 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -356,6 +356,10 @@
}
}
+.mr-widget-body-controls {
+ flex-wrap: wrap;
+}
+
.mr_source_commit,
.mr_target_commit {
margin-bottom: 0;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index e437bad4912..052c005a2e8 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -778,6 +778,7 @@ ul.notes {
background-color: transparent;
border: none;
outline: 0;
+ color: $gray-darkest;
transition: color $general-hover-transition-duration $general-hover-transition-curve;
&.is-disabled {
@@ -801,7 +802,7 @@ ul.notes {
}
svg {
- fill: $gray-darkest;
+ fill: currentColor;
height: 16px;
width: 16px;
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 296b6310552..9d03a042aa3 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -209,6 +209,11 @@
}
.stage-cell {
+ @media (min-width: $screen-md-min) {
+ min-width: 148px;
+ margin-right: -4px;
+ }
+
.mini-pipeline-graph-dropdown-toggle svg {
height: $ci-action-icon-size;
width: $ci-action-icon-size;
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 7dfcf7b7d9c..4d4d92f9494 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -56,7 +56,6 @@
.tree-content-holder {
display: flex;
- max-height: 100vh;
min-height: 300px;
}
@@ -156,7 +155,7 @@
list-style-type: none;
background: $gray-normal;
display: inline-block;
- padding: 10px 18px;
+ padding: #{$gl-padding / 2} $gl-padding;
border-right: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
white-space: nowrap;
@@ -180,10 +179,9 @@
a {
@include str-truncated(100px);
color: $black;
- width: 100px;
- text-align: center;
vertical-align: middle;
text-decoration: none;
+ margin-right: 12px;
&.close {
width: auto;
@@ -193,6 +191,10 @@
}
}
+ .close-icon:hover {
+ color: $hint-color;
+ }
+
.close-icon,
.unsaved-icon {
float: right;
diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb
index e5cba774dcb..a7ab481519d 100644
--- a/app/controllers/admin/deploy_keys_controller.rb
+++ b/app/controllers/admin/deploy_keys_controller.rb
@@ -10,9 +10,8 @@ class Admin::DeployKeysController < Admin::ApplicationController
end
def create
- @deploy_key = deploy_keys.new(create_params.merge(user: current_user))
-
- if @deploy_key.save
+ @deploy_key = DeployKeys::CreateService.new(current_user, create_params.merge(public: true)).execute
+ if @deploy_key.persisted?
redirect_to admin_deploy_keys_path
else
render 'new'
diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb
index cbc7a14ae83..7eb8f758807 100644
--- a/app/controllers/admin/labels_controller.rb
+++ b/app/controllers/admin/labels_controller.rb
@@ -29,7 +29,7 @@ class Admin::LabelsController < Admin::ApplicationController
@label = Labels::UpdateService.new(label_params).execute(@label)
if @label.valid?
- redirect_to admin_labels_path, notice: 'label was successfully updated.'
+ redirect_to admin_labels_path, notice: 'Label was successfully updated.'
else
render :edit
end
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index dfc8bd0ba81..10e8e54f402 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -3,31 +3,10 @@ class AutocompleteController < ApplicationController
skip_before_action :authenticate_user!, only: [:users, :award_emojis]
before_action :load_project, only: [:users]
- before_action :find_users, only: [:users]
+ before_action :load_group, only: [:users]
def users
- @users ||= User.none
- @users = @users.active
- @users = @users.reorder(:name)
- @users = @users.search(params[:search]) if params[:search].present?
- @users = @users.where.not(id: params[:skip_users]) if params[:skip_users].present?
- @users = @users.page(params[:page]).per(params[:per_page])
-
- if params[:todo_filter].present? && current_user
- @users = @users.todo_authors(current_user.id, params[:todo_state_filter])
- end
-
- if params[:search].blank?
- # Include current user if available to filter by "Me"
- if params[:current_user].present? && current_user
- @users = [current_user, *@users].uniq
- end
-
- if params[:author_id].present? && current_user
- author = User.find_by_id(params[:author_id])
- @users = [author, *@users].uniq if author
- end
- end
+ @users = AutocompleteUsersFinder.new(params: params, current_user: current_user, project: @project, group: @group).execute
render json: @users, only: [:name, :username, :id], methods: [:avatar_url]
end
@@ -60,26 +39,14 @@ class AutocompleteController < ApplicationController
private
- def find_users
- @users =
- if @project
- user_ids = @project.team.users.pluck(:id)
-
- if params[:author_id].present?
- user_ids << params[:author_id]
- end
-
- User.where(id: user_ids)
- elsif params[:group_id].present?
+ def load_group
+ @group ||= begin
+ if @project.blank? && params[:group_id].present?
group = Group.find(params[:group_id])
return render_404 unless can?(current_user, :read_group, group)
-
- group.users
- elsif current_user
- User.all
- else
- User.none
+ group
end
+ end
end
def load_project
diff --git a/app/controllers/profiles/gpg_keys_controller.rb b/app/controllers/profiles/gpg_keys_controller.rb
index 6779cc6ddac..689c76059f6 100644
--- a/app/controllers/profiles/gpg_keys_controller.rb
+++ b/app/controllers/profiles/gpg_keys_controller.rb
@@ -7,9 +7,9 @@ class Profiles::GpgKeysController < Profiles::ApplicationController
end
def create
- @gpg_key = current_user.gpg_keys.new(gpg_key_params)
+ @gpg_key = GpgKeys::CreateService.new(current_user, gpg_key_params).execute
- if @gpg_key.save
+ if @gpg_key.persisted?
redirect_to profile_gpg_keys_path
else
@gpg_keys = current_user.gpg_keys.select(&:persisted?)
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index f9f0e8eef83..89d6d7f1b52 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -11,9 +11,9 @@ class Profiles::KeysController < Profiles::ApplicationController
end
def create
- @key = current_user.keys.new(key_params)
+ @key = Keys::CreateService.new(current_user, key_params).execute
- if @key.save
+ if @key.persisted?
redirect_to profile_key_path(@key)
else
@keys = current_user.keys.select(&:persisted?)
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index f748d191ef4..c1cc509a748 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::AVAILABLE_SCOPES
+ @scopes = Gitlab::Auth.available_scopes
@personal_access_token = finder.build
@inactive_personal_access_tokens = finder(state: 'inactive').execute
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 747768eefb1..a9cce578366 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -15,10 +15,14 @@ class Projects::BranchesController < Projects::ApplicationController
respond_to do |format|
format.html do
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37429
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ @max_commits = @branches.reduce(0) do |memo, branch|
+ diverging_commit_counts = repository.diverging_commit_counts(branch)
+ [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
+ end
- @max_commits = @branches.reduce(0) do |memo, branch|
- diverging_commit_counts = repository.diverging_commit_counts(branch)
- [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
+ render
end
end
format.json do
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 1a775def506..a62f05db7db 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -20,7 +20,12 @@ class Projects::CommitController < Projects::ApplicationController
apply_diff_view_cookie!
respond_to do |format|
- format.html
+ format.html do
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37599
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ render
+ end
+ end
format.diff { render text: @commit.to_diff }
format.patch { render text: @commit.to_patch }
end
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 193549663ac..3cb4eb23981 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -17,6 +17,10 @@ class Projects::CompareController < Projects::ApplicationController
def show
apply_diff_view_cookie!
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37430
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ render
+ end
end
def diff_for_path
@@ -27,7 +31,7 @@ class Projects::CompareController < Projects::ApplicationController
def create
if params[:from].blank? || params[:to].blank?
- flash[:alert] = "You must select from and to branches"
+ flash[:alert] = "You must select a Source and a Target revision"
from_to_vars = {
from: params[:from].presence,
to: params[:to].presence
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index c2e621fa190..cf8829ba95b 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -22,7 +22,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def create
- @key = DeployKey.new(create_params.merge(user: current_user))
+ @key = DeployKeys::CreateService.new(current_user, create_params).execute
unless @key.valid? && @project.deploy_keys << @key
flash[:alert] = @key.errors.full_messages.join(', ').html_safe
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index 3f83bef2c79..68978f8fdd1 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -9,14 +9,12 @@ class Projects::ForksController < Projects::ApplicationController
def index
base_query = project.forks.includes(:creator)
- @forks = base_query.merge(ProjectsFinder.new(current_user: current_user).execute)
+ forks = ForkProjectsFinder.new(project, params: params.merge(search: params[:filter_projects]), current_user: current_user).execute
@total_forks_count = base_query.size
- @private_forks_count = @total_forks_count - @forks.size
+ @private_forks_count = @total_forks_count - forks.size
@public_forks_count = @total_forks_count - @private_forks_count
- @sort = params[:sort] || 'id_desc'
- @forks = @forks.search(params[:filter_projects]) if params[:filter_projects].present?
- @forks = @forks.order_by(@sort).page(params[:page])
+ @forks = forks.page(params[:page])
respond_to do |format|
format.html
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 8990c919ca0..a3ec79a56d9 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -71,9 +71,6 @@ class Projects::IssuesController < Projects::ApplicationController
@noteable = @issue
@note = @project.notes.new(noteable: @issue)
- @discussions = @issue.discussions
- @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes), @noteable)
-
respond_to do |format|
format.html
format.json do
@@ -87,9 +84,9 @@ class Projects::IssuesController < Projects::ApplicationController
.inc_relations_for_view
.includes(:noteable)
.fresh
- .reject { |n| n.cross_reference_not_visible_for?(current_user) }
- prepare_notes_for_rendering(notes)
+ notes = prepare_notes_for_rendering(notes)
+ notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
discussions = Discussion.build_collection(notes, @issue)
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index d60a24d3f1d..7d16e77ef66 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -10,7 +10,10 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def show
@environment = @merge_request.environments_for(current_user).last
- render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") }
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37431
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ render json: { html: view_to_html_string("projects/merge_requests/diffs/_diffs") }
+ end
end
def diff_for_path
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 3aa5dadb5ca..c5204080333 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -56,6 +56,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
close_merge_request_without_source_project
check_if_can_be_merged
+ # Return if the response has already been rendered
+ return if response_body
+
respond_to do |format|
format.html do
# Build a note object for comment form
@@ -70,6 +73,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
labels
set_pipeline_variables
+
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37432
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ render
+ end
end
format.json do
diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb
index dfa5e4f7f46..fb68dd771a1 100644
--- a/app/controllers/projects/network_controller.rb
+++ b/app/controllers/projects/network_controller.rb
@@ -8,19 +8,24 @@ class Projects::NetworkController < Projects::ApplicationController
before_action :assign_commit
def show
- @url = project_network_path(@project, @ref, @options.merge(format: :json))
- @commit_url = project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37602
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ @url = project_network_path(@project, @ref, @options.merge(format: :json))
+ @commit_url = project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")
- respond_to do |format|
- format.html do
- if @options[:extended_sha1] && !@commit
- flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist."
+ respond_to do |format|
+ format.html do
+ if @options[:extended_sha1] && !@commit
+ flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist."
+ end
end
- end
- format.json do
- @graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref])
+ format.json do
+ @graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref])
+ end
end
+
+ render
end
end
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 1eb78d8b522..2fd015df688 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -51,13 +51,16 @@ class Projects::RefsController < Projects::ApplicationController
contents.push(*tree.blobs)
contents.push(*tree.submodules)
- @logs = contents[@offset, @limit].to_a.map do |content|
- file = @path ? File.join(@path, content.name) : content.name
- last_commit = @repo.last_commit_for_path(@commit.id, file)
- {
- file_name: content.name,
- commit: last_commit
- }
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37433
+ @logs = Gitlab::GitalyClient.allow_n_plus_1_calls do
+ contents[@offset, @limit].to_a.map do |content|
+ file = @path ? File.join(@path, content.name) : content.name
+ last_commit = @repo.last_commit_for_path(@commit.id, file)
+ {
+ file_name: content.name,
+ commit: last_commit
+ }
+ end
end
offset = (@offset + @limit)
diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
index 6966a7c5fee..4d2fb17a19b 100644
--- a/app/controllers/projects/uploads_controller.rb
+++ b/app/controllers/projects/uploads_controller.rb
@@ -28,7 +28,7 @@ class Projects::UploadsController < Projects::ApplicationController
end
def image_or_video?
- uploader && uploader.file.exists? && uploader.image_or_video?
+ uploader && uploader.exists? && uploader.image_or_video?
end
def uploader_class
diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb
index 1b4545e4a49..19e38993038 100644
--- a/app/controllers/root_controller.rb
+++ b/app/controllers/root_controller.rb
@@ -13,7 +13,10 @@ class RootController < Dashboard::ProjectsController
before_action :redirect_logged_user, if: -> { current_user.present? }
def index
- super
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37434
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ super
+ end
end
private
diff --git a/app/finders/autocomplete_users_finder.rb b/app/finders/autocomplete_users_finder.rb
new file mode 100644
index 00000000000..b8f52e31926
--- /dev/null
+++ b/app/finders/autocomplete_users_finder.rb
@@ -0,0 +1,60 @@
+class AutocompleteUsersFinder
+ attr_reader :current_user, :project, :group, :search, :skip_users,
+ :page, :per_page, :author_id, :params
+
+ def initialize(params:, current_user:, project:, group:)
+ @current_user = current_user
+ @project = project
+ @group = group
+ @search = params[:search]
+ @skip_users = params[:skip_users]
+ @page = params[:page]
+ @per_page = params[:per_page]
+ @author_id = params[:author_id]
+ @params = params
+ end
+
+ def execute
+ items = find_users
+ items = items.active
+ items = items.reorder(:name)
+ items = items.search(search) if search.present?
+ items = items.where.not(id: skip_users) if skip_users.present?
+ items = items.page(page).per(per_page)
+
+ if params[:todo_filter].present? && current_user
+ items = items.todo_authors(current_user.id, params[:todo_state_filter])
+ end
+
+ if search.blank?
+ # Include current user if available to filter by "Me"
+ if params[:current_user].present? && current_user
+ items = [current_user, *items].uniq
+ end
+
+ if author_id.present? && current_user
+ author = User.find_by_id(author_id)
+ items = [author, *items].uniq if author
+ end
+ end
+
+ items
+ end
+
+ private
+
+ def find_users
+ return users_from_project if project
+ return group.users if group
+ return User.all if current_user
+
+ User.none
+ end
+
+ def users_from_project
+ user_ids = project.team.users.pluck(:id)
+ user_ids << author_id if author_id.present?
+
+ User.where(id: user_ids)
+ end
+end
diff --git a/app/finders/fork_projects_finder.rb b/app/finders/fork_projects_finder.rb
new file mode 100644
index 00000000000..28d1b31868e
--- /dev/null
+++ b/app/finders/fork_projects_finder.rb
@@ -0,0 +1,6 @@
+class ForkProjectsFinder < ProjectsFinder
+ def initialize(project, params: {}, current_user: nil)
+ project_ids = project.forks.includes(:creator).select(:id)
+ super(params: params, current_user: current_user, project_ids_relation: project_ids)
+ end
+end
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
index 88d71b0a87b..0c4c4b10fb6 100644
--- a/app/finders/groups_finder.rb
+++ b/app/finders/groups_finder.rb
@@ -57,7 +57,7 @@ class GroupsFinder < UnionFinder
end
def owned_groups
- current_user&.groups || Group.none
+ current_user&.owned_groups || Group.none
end
def include_public_groups?
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 9848497f258..0a2e3c709d9 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -244,6 +244,8 @@ class IssuableFinder
end
def by_scope(items)
+ return items.none if current_user_related? && !current_user
+
case params[:scope]
when 'created-by-me', 'authored'
items.where(author_id: current_user.id)
diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb
index c455d18cff8..483b957decb 100644
--- a/app/helpers/auto_devops_helper.rb
+++ b/app/helpers/auto_devops_helper.rb
@@ -5,6 +5,25 @@ module AutoDevopsHelper
can?(current_user, :admin_pipeline, project) &&
project.has_auto_devops_implicitly_disabled? &&
!project.repository.gitlab_ci_yml &&
- project.ci_services.active.none?
+ !project.ci_service
+ end
+
+ def auto_devops_warning_message(project)
+ missing_domain = !project.auto_devops&.has_domain?
+ missing_service = !project.kubernetes_service&.active?
+
+ if missing_service
+ params = {
+ kubernetes: link_to('Kubernetes service', edit_project_service_path(project, 'kubernetes'))
+ }
+
+ if missing_domain
+ _('Auto Review Apps and Auto Deploy need a domain name and the %{kubernetes} to work correctly.') % params
+ else
+ _('Auto Review Apps and Auto Deploy need the %{kubernetes} to work correctly.') % params
+ end
+ elsif missing_domain
+ _('Auto Review Apps and Auto Deploy need a domain name to work correctly.')
+ end
end
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 446a59030a6..be8cb358de2 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -94,6 +94,12 @@ module MilestonesHelper
end
end
+ def milestone_tooltip_title(milestone)
+ if milestone.due_date
+ [milestone.due_date.to_s(:medium), "(#{milestone_remaining_days(milestone)})"].join(' ')
+ end
+ end
+
def milestone_remaining_days(milestone)
if milestone.expired?
content_tag(:strong, 'Past due')
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index 88f7702db1e..40d69e30188 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -87,10 +87,14 @@ module SubmoduleHelper
namespace = @project.namespace.full_path
end
- [
- namespace_project_path(namespace, base),
- namespace_project_tree_path(namespace, base, commit)
- ]
+ begin
+ [
+ namespace_project_path(namespace, base),
+ namespace_project_tree_path(namespace, base, commit)
+ ]
+ rescue ActionController::UrlGenerationError
+ [nil, nil]
+ end
end
def sanitize_submodule_url(url)
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index 3308ab0c259..ee701076a14 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -119,8 +119,4 @@ module TabHelper
'active' if current_controller?('oauth/applications')
end
-
- def sidebar_link(href, title: nil, css: nil, &block)
- link_to capture(&block), href, title: (title if collapsed_sidebar?), class: css, aria: { label: title }
- end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 476db384bbd..acaa028eaa2 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -31,6 +31,7 @@ module Ci
has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
delegate :id, to: :project, prefix: true
+ delegate :full_path, to: :project, prefix: true
validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create
validates :sha, presence: { unless: :importing? }
@@ -336,7 +337,7 @@ module Ci
return @config_processor if defined?(@config_processor)
@config_processor ||= begin
- Gitlab::Ci::YamlProcessor.new(ci_yaml_file, project.full_path)
+ Gitlab::Ci::YamlProcessor.new(ci_yaml_file)
rescue Gitlab::Ci::YamlProcessor::ValidationError, Psych::SyntaxError => e
self.yaml_errors = e.message
nil
@@ -453,6 +454,10 @@ module Ci
.fabricate!
end
+ def latest_builds_with_artifacts
+ @latest_builds_with_artifacts ||= builds.latest.with_artifacts
+ end
+
private
def ci_yaml_from_repo
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index 51768dd96bc..eae5eee4fee 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -28,10 +28,4 @@ class DeployKey < Key
def can_push_to?(project)
can_push? && has_access_to?(project)
end
-
- private
-
- # we don't want to notify the user for deploy keys
- def notify_user
- end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 44e39e21442..b6868ccbe8f 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -6,9 +6,7 @@ class Environment < ActiveRecord::Base
belongs_to :project, required: true, validate: true
- has_many :deployments,
- -> (env) { where(project_id: env.project_id) },
- dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :deployments, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment'
diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb
index 1633acd4fa9..44deae4234b 100644
--- a/app/models/gpg_key.rb
+++ b/app/models/gpg_key.rb
@@ -36,7 +36,6 @@ class GpgKey < ActiveRecord::Base
before_validation :extract_fingerprint, :extract_primary_keyid
after_commit :update_invalid_gpg_signatures, on: :create
- after_commit :notify_user, on: :create
def primary_keyid
super&.upcase
@@ -107,8 +106,4 @@ class GpgKey < ActiveRecord::Base
# only allows one key
self.primary_keyid = Gitlab::Gpg.primary_keyids_from_key(key).first
end
-
- def notify_user
- NotificationService.new.new_gpg_key(self)
- end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index cd5056aae5e..92a454300af 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -275,8 +275,6 @@ class Issue < ActiveRecord::Base
end
def update_project_counter_caches
- return unless update_project_counter_caches?
-
Projects::OpenIssuesCountService.new(project).refresh_cache
end
diff --git a/app/models/key.rb b/app/models/key.rb
index a6b4dcfec0d..4fa6cac2fd0 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -28,7 +28,6 @@ class Key < ActiveRecord::Base
delegate :name, :email, to: :user, prefix: true
after_commit :add_to_shell, on: :create
- after_commit :notify_user, on: :create
after_create :post_create_hook
after_commit :remove_from_shell, on: :destroy
after_destroy :post_destroy_hook
@@ -118,8 +117,4 @@ class Key < ActiveRecord::Base
"type is forbidden. Must be #{allowed_types}"
end
-
- def notify_user
- NotificationService.new.new_key(self)
- end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 2a56bab48a3..8d9a30397a9 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -415,8 +415,11 @@ class MergeRequest < ActiveRecord::Base
end
def create_merge_request_diff
- merge_request_diffs.create
- reload_merge_request_diff
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37435
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ merge_request_diffs.create
+ reload_merge_request_diff
+ end
end
def reload_merge_request_diff
@@ -955,8 +958,6 @@ class MergeRequest < ActiveRecord::Base
end
def update_project_counter_caches
- return unless update_project_counter_caches?
-
Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index a3070a12b7c..47e6b785c39 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -162,9 +162,7 @@ class Milestone < ActiveRecord::Base
# Milestone.first.to_reference(cross_namespace_project) # => "gitlab-org/gitlab-ce%1"
# Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1"
#
- def to_reference(from_project = nil, format: :iid, full: false)
- return if group_milestone? && format != :name
-
+ def to_reference(from_project = nil, format: :name, full: false)
format_reference = milestone_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
@@ -241,6 +239,10 @@ class Milestone < ActiveRecord::Base
def milestone_format_reference(format = :iid)
raise ArgumentError, 'Unknown format' unless [:iid, :name].include?(format)
+ if group_milestone? && format == :iid
+ raise ArgumentError, 'Cannot refer to a group milestone by an internal id!'
+ end
+
if format == :name && !name.include?('"')
%("#{name}")
else
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 4a9a23fea1f..e279d8dd8c5 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -231,6 +231,13 @@ class Namespace < ActiveRecord::Base
end
def force_share_with_group_lock_on_descendants
- descendants.update_all(share_with_group_lock: true)
+ return unless Group.supports_nested_groups?
+
+ # We can't use `descendants.update_all` since Rails will throw away the WITH
+ # RECURSIVE statement. We also can't use WHERE EXISTS since we can't use
+ # different table aliases, hence we're just using WHERE IN. Since we have a
+ # maximum of 20 nested groups this should be fine.
+ Namespace.where(id: descendants.select(:id))
+ .update_all(share_with_group_lock: true)
end
end
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index 3845e485413..aec7b01e23a 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -61,8 +61,11 @@ module Network
@reserved[i] = []
end
- commits_sort_by_ref.each do |commit|
- place_chain(commit)
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37436
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ commits_sort_by_ref.each do |commit|
+ place_chain(commit)
+ end
end
# find parent spaces for not overlap lines
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index ec0ebe4d353..1f9d712ef84 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -28,7 +28,7 @@ class PersonalAccessToken < ActiveRecord::Base
protected
def validate_scopes
- unless revoked || scopes.all? { |scope| Gitlab::Auth::AVAILABLE_SCOPES.include?(scope.to_sym) }
+ unless revoked || scopes.all? { |scope| Gitlab::Auth.available_scopes.include?(scope.to_sym) }
errors.add :scopes, "can only contain available scopes"
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index ff5638dd155..f7221e4f3b2 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -161,7 +161,7 @@ class Project < ActiveRecord::Base
has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
- has_one :project_feature
+ has_one :project_feature, inverse_of: :project
has_one :statistics, class_name: 'ProjectStatistics'
# Container repositories need to remove data from the container registry,
@@ -190,9 +190,9 @@ class Project < ActiveRecord::Base
has_one :auto_devops, class_name: 'ProjectAutoDevops'
accepts_nested_attributes_for :variables, allow_destroy: true
- accepts_nested_attributes_for :project_feature
+ accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :import_data
- accepts_nested_attributes_for :auto_devops
+ accepts_nested_attributes_for :auto_devops, update_only: true
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
@@ -1163,6 +1163,23 @@ class Project < ActiveRecord::Base
pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
end
+ def latest_successful_pipeline_for_default_branch
+ if defined?(@latest_successful_pipeline_for_default_branch)
+ return @latest_successful_pipeline_for_default_branch
+ end
+
+ @latest_successful_pipeline_for_default_branch =
+ pipelines.latest_successful_for(default_branch)
+ end
+
+ def latest_successful_pipeline_for(ref = nil)
+ if ref && ref != default_branch
+ pipelines.latest_successful_for(ref)
+ else
+ latest_successful_pipeline_for_default_branch
+ end
+ end
+
def enable_ci
project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED)
end
diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb
index 7af3b6870e2..9a52edbff8e 100644
--- a/app/models/project_auto_devops.rb
+++ b/app/models/project_auto_devops.rb
@@ -6,6 +6,10 @@ class ProjectAutoDevops < ActiveRecord::Base
validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
+ def has_domain?
+ domain.present?
+ end
+
def variables
variables = []
variables << { key: 'AUTO_DEVOPS_DOMAIN', value: domain, public: true } if domain.present?
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index fb1db0255aa..bfb8d703ec9 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -41,6 +41,8 @@ class ProjectFeature < ActiveRecord::Base
# http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
belongs_to :project, -> { unscope(where: :pending_delete) }
+ validates :project, presence: true
+
validate :repository_children_level
default_value_for :builds_access_level, value: ENABLED, allows_nil: false
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 09049824ff7..1d35426050e 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -146,7 +146,7 @@ class ProjectTeam
def member?(user, min_access_level = Gitlab::Access::GUEST)
return false unless user
- user.authorized_project?(project, min_access_level)
+ max_member_access(user.id) >= min_access_level
end
def human_max_access(user_id)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index f2b54705e7b..f11cf1b065d 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -768,17 +768,23 @@ class Repository
multi_action(**options)
end
- def with_branch(user, *args)
- result = Gitlab::Git::OperationService.new(user, raw_repository).with_branch(*args) do |start_commit|
- yield start_commit
- end
+ def with_cache_hooks
+ result = yield
- newrev, should_run_after_create, should_run_after_create_branch = result
+ return unless result
- after_create if should_run_after_create
- after_create_branch if should_run_after_create_branch
+ after_create if result.repo_created?
+ after_create_branch if result.branch_created?
+
+ result.newrev
+ end
- newrev
+ def with_branch(user, *args)
+ with_cache_hooks do
+ Gitlab::Git::OperationService.new(user, raw_repository).with_branch(*args) do |start_commit|
+ yield start_commit
+ end
+ end
end
# rubocop:disable Metrics/ParameterLists
@@ -828,10 +834,6 @@ class Repository
}
end
- def user_to_committer(user)
- Gitlab::Git.committer_hash(email: user.email, name: user.name)
- end
-
def can_be_merged?(source_sha, target_branch)
our_commit = rugged.branches[target_branch].target
their_commit = rugged.lookup(source_sha)
@@ -843,81 +845,44 @@ class Repository
end
end
- def merge(user, source, merge_request, options = {})
- with_branch(
- user,
- merge_request.target_branch) do |start_commit|
- our_commit = start_commit.sha
- their_commit = source
-
- raise 'Invalid merge target' unless our_commit
- raise 'Invalid merge source' unless their_commit
-
- merge_index = rugged.merge_commits(our_commit, their_commit)
- break if merge_index.conflicts?
-
- actual_options = options.merge(
- parents: [our_commit, their_commit],
- tree: merge_index.write_tree(rugged)
- )
-
- commit_id = create_commit(actual_options)
- merge_request.update(in_progress_merge_commit_sha: commit_id)
- commit_id
+ def merge(user, source_sha, merge_request, message)
+ with_cache_hooks do
+ raw_repository.merge(user, source_sha, merge_request.target_branch, message) do |commit_id|
+ merge_request.update(in_progress_merge_commit_sha: commit_id)
+ nil # Return value does not matter.
+ end
end
- rescue Gitlab::Git::CommitError # when merge_index.conflicts?
- false
end
def revert(
- user, commit, branch_name,
+ user, commit, branch_name, message,
start_branch_name: nil, start_project: project)
- with_branch(
- user,
- branch_name,
- start_branch_name: start_branch_name,
- start_repository: start_project.repository.raw_repository) do |start_commit|
-
- revert_tree_id = check_revert_content(commit, start_commit.sha)
- unless revert_tree_id
- raise Repository::CreateTreeError.new('Failed to revert commit')
- end
- committer = user_to_committer(user)
-
- create_commit(message: commit.revert_message(user),
- author: committer,
- committer: committer,
- tree: revert_tree_id,
- parents: [start_commit.sha])
+ with_cache_hooks do
+ raw_repository.revert(
+ user: user,
+ commit: commit.raw,
+ branch_name: branch_name,
+ message: message,
+ start_branch_name: start_branch_name,
+ start_repository: start_project.repository.raw_repository
+ )
end
end
def cherry_pick(
- user, commit, branch_name,
+ user, commit, branch_name, message,
start_branch_name: nil, start_project: project)
- with_branch(
- user,
- branch_name,
- start_branch_name: start_branch_name,
- start_repository: start_project.repository.raw_repository) do |start_commit|
-
- cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha)
- unless cherry_pick_tree_id
- raise Repository::CreateTreeError.new('Failed to cherry-pick commit')
- end
-
- committer = user_to_committer(user)
- create_commit(message: commit.cherry_pick_message(user),
- author: {
- email: commit.author_email,
- name: commit.author_name,
- time: commit.authored_date
- },
- committer: committer,
- tree: cherry_pick_tree_id,
- parents: [start_commit.sha])
+ with_cache_hooks do
+ raw_repository.cherry_pick(
+ user: user,
+ commit: commit.raw,
+ branch_name: branch_name,
+ message: message,
+ start_branch_name: start_branch_name,
+ start_repository: start_project.repository.raw_repository
+ )
end
end
@@ -929,36 +894,6 @@ class Repository
end
end
- def check_revert_content(target_commit, source_sha)
- args = [target_commit.sha, source_sha]
- args << { mainline: 1 } if target_commit.merge_commit?
-
- revert_index = rugged.revert_commit(*args)
- return false if revert_index.conflicts?
-
- tree_id = revert_index.write_tree(rugged)
- return false unless diff_exists?(source_sha, tree_id)
-
- tree_id
- end
-
- def check_cherry_pick_content(target_commit, source_sha)
- args = [target_commit.sha, source_sha]
- args << 1 if target_commit.merge_commit?
-
- cherry_pick_index = rugged.cherrypick_commit(*args)
- return false if cherry_pick_index.conflicts?
-
- tree_id = cherry_pick_index.write_tree(rugged)
- return false unless diff_exists?(source_sha, tree_id)
-
- tree_id
- end
-
- def diff_exists?(sha1, sha2)
- rugged.diff(sha1, sha2).size > 0
- end
-
def merged_to_root_ref?(branch_name)
branch_commit = commit(branch_name)
root_ref_commit = commit(root_ref)
@@ -994,6 +929,7 @@ class Repository
def empty_repo?
!exists? || !has_visible_content?
end
+ cache_method :empty_repo?, memoize_only: true
def search_files_by_content(query, ref)
return [] if empty_repo? || query.blank?
@@ -1157,12 +1093,6 @@ class Repository
Gitlab::Metrics.add_event(event, { path: full_path }.merge(tags))
end
- def create_commit(params = {})
- params[:message].delete!("\r")
-
- Rugged::Commit.create(rugged, params)
- end
-
def last_commit_for_path_by_gitaly(sha, path)
c = raw_repository.gitaly_commit_client.last_commit_for_path(sha, path)
commit(c)
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 420991ff6d6..8af9738d75c 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -9,6 +9,7 @@ class GroupPolicy < BasePolicy
condition(:has_access) { access_level != GroupMember::NO_ACCESS }
condition(:guest) { access_level >= GroupMember::GUEST }
+ condition(:developer) { access_level >= GroupMember::DEVELOPER }
condition(:owner) { access_level >= GroupMember::OWNER }
condition(:master) { access_level >= GroupMember::MASTER }
condition(:reporter) { access_level >= GroupMember::REPORTER }
@@ -33,11 +34,11 @@ class GroupPolicy < BasePolicy
rule { admin } .enable :read_group
rule { has_projects } .enable :read_group
+ rule { developer }.enable :admin_milestones
rule { reporter }.enable :admin_label
rule { master }.policy do
enable :create_projects
- enable :admin_milestones
enable :admin_pipeline
enable :admin_build
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index a925fac7d3e..b7b5bd34189 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -155,6 +155,7 @@ class ProjectPolicy < BasePolicy
rule { can?(:developer_access) }.policy do
enable :admin_merge_request
+ enable :admin_milestone
enable :update_merge_request
enable :create_commit_status
enable :update_commit_status
@@ -178,7 +179,6 @@ class ProjectPolicy < BasePolicy
enable :update_project_snippet
enable :update_environment
enable :update_deployment
- enable :admin_milestone
enable :admin_project_snippet
enable :admin_project_member
enable :admin_note
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index 85c2fcf9ea6..b9d0173a2d0 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -12,14 +12,18 @@ module Commits
raise NotImplementedError unless repository.respond_to?(action)
# rubocop:disable GitlabSecurity/PublicSend
+ message = @commit.public_send(:"#{action}_message", current_user)
+
+ # rubocop:disable GitlabSecurity/PublicSend
repository.public_send(
action,
current_user,
@commit,
@branch_name,
+ message,
start_project: @start_project,
start_branch_name: @start_branch)
- rescue Repository::CreateTreeError
+ rescue Gitlab::Git::Repository::CreateTreeError
error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title(current_user)} automatically.
This #{@commit.change_type_title(current_user)} may already have been #{action.to_s.dasherize}ed, or a more recent commit may have updated some of its content."
raise ChangeError, error_msg
diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb
index ff11bd59d29..077268b2388 100644
--- a/app/services/delete_merged_branches_service.rb
+++ b/app/services/delete_merged_branches_service.rb
@@ -6,15 +6,18 @@ class DeleteMergedBranchesService < BaseService
def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :push_code, project)
- branches = project.repository.branch_names
- branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) }
- # Prevent deletion of branches relevant to open merge requests
- branches -= merge_request_branch_names
- # Prevent deletion of protected branches
- branches = branches.reject { |branch| project.protected_for?(branch) }
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37438
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ branches = project.repository.branch_names
+ branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) }
+ # Prevent deletion of branches relevant to open merge requests
+ branches -= merge_request_branch_names
+ # Prevent deletion of protected branches
+ branches = branches.reject { |branch| project.protected_for?(branch) }
- branches.each do |branch|
- DeleteBranchService.new(project, current_user).execute(branch)
+ branches.each do |branch|
+ DeleteBranchService.new(project, current_user).execute(branch)
+ end
end
end
diff --git a/app/services/deploy_keys/create_service.rb b/app/services/deploy_keys/create_service.rb
new file mode 100644
index 00000000000..16de3d08df2
--- /dev/null
+++ b/app/services/deploy_keys/create_service.rb
@@ -0,0 +1,7 @@
+module DeployKeys
+ class CreateService < Keys::BaseService
+ def execute
+ DeployKey.create(params.merge(user: user))
+ end
+ end
+end
diff --git a/app/services/gpg_keys/create_service.rb b/app/services/gpg_keys/create_service.rb
new file mode 100644
index 00000000000..e822a89c4d3
--- /dev/null
+++ b/app/services/gpg_keys/create_service.rb
@@ -0,0 +1,9 @@
+module GpgKeys
+ class CreateService < Keys::BaseService
+ def execute
+ key = user.gpg_keys.create(params)
+ notification_service.new_gpg_key(key) if key.persisted?
+ key
+ end
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 8b967b78052..12604e7eb5d 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -182,6 +182,7 @@ class IssuableBaseService < BaseService
after_create(issuable)
execute_hooks(issuable)
invalidate_cache_counts(issuable, users: issuable.assignees)
+ issuable.update_project_counter_caches
end
issuable
@@ -193,8 +194,6 @@ class IssuableBaseService < BaseService
def after_create(issuable)
# To be overridden by subclasses
-
- issuable.update_project_counter_caches
end
def before_update(issuable)
@@ -203,8 +202,6 @@ class IssuableBaseService < BaseService
def after_update(issuable)
# To be overridden by subclasses
-
- issuable.update_project_counter_caches
end
def update(issuable)
@@ -229,6 +226,10 @@ class IssuableBaseService < BaseService
before_update(issuable)
+ # We have to perform this check before saving the issuable as Rails resets
+ # the changed fields upon calling #save.
+ update_project_counters = issuable.update_project_counter_caches?
+
if issuable.with_transaction_returning_status { issuable.save }
# We do not touch as it will affect a update on updated_at field
ActiveRecord::Base.no_touching do
@@ -249,6 +250,8 @@ class IssuableBaseService < BaseService
after_update(issuable)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
+
+ issuable.update_project_counter_caches if update_project_counters
end
end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 74459c3342c..0c5cf2c62ad 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -29,6 +29,7 @@ module Issues
todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close')
invalidate_cache_counts(issue, users: issue.assignees)
+ issue.update_project_counter_caches
end
issue
diff --git a/app/services/keys/base_service.rb b/app/services/keys/base_service.rb
new file mode 100644
index 00000000000..545832d0bd4
--- /dev/null
+++ b/app/services/keys/base_service.rb
@@ -0,0 +1,13 @@
+module Keys
+ class BaseService
+ attr_accessor :user, :params
+
+ def initialize(user, params)
+ @user, @params = user, params
+ end
+
+ def notification_service
+ NotificationService.new
+ end
+ end
+end
diff --git a/app/services/keys/create_service.rb b/app/services/keys/create_service.rb
new file mode 100644
index 00000000000..e2e5a6c46c5
--- /dev/null
+++ b/app/services/keys/create_service.rb
@@ -0,0 +1,9 @@
+module Keys
+ class CreateService < ::Keys::BaseService
+ def execute
+ key = user.keys.create(params)
+ notification_service.new_key(key) if key.persisted?
+ key
+ end
+ end
+end
diff --git a/app/services/merge_requests/close_service.rb b/app/services/merge_requests/close_service.rb
index c0ce01f7523..40213c99014 100644
--- a/app/services/merge_requests/close_service.rb
+++ b/app/services/merge_requests/close_service.rb
@@ -14,6 +14,7 @@ module MergeRequests
todo_service.close_merge_request(merge_request, current_user)
execute_hooks(merge_request, 'close')
invalidate_cache_counts(merge_request, users: merge_request.assignees)
+ merge_request.update_project_counter_caches
end
merge_request
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 3d53fe0646b..820709583fa 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -13,7 +13,10 @@ module MergeRequests
merge_request.source_branch = params[:source_branch]
merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
- create(merge_request)
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37439
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ create(merge_request)
+ end
end
def before_create(merge_request)
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index b2b6c5627fb..07cbd8f92a9 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -38,15 +38,9 @@ module MergeRequests
private
def commit
- committer = repository.user_to_committer(current_user)
+ message = params[:commit_message] || merge_request.merge_commit_message
- options = {
- message: params[:commit_message] || merge_request.merge_commit_message,
- author: committer,
- committer: committer
- }
-
- commit_id = repository.merge(current_user, source, merge_request, options)
+ commit_id = repository.merge(current_user, source, merge_request, message)
raise MergeError, 'Conflicts detected during merge' unless commit_id
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 06971483992..9ea28733f5f 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -4,7 +4,13 @@ module Notes
merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha)
note = Notes::BuildService.new(project, current_user, params).execute
- return note unless note.valid?
+
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37440
+ note_valid = Gitlab::GitalyClient.allow_n_plus_1_calls do
+ note.valid?
+ end
+
+ return note unless note_valid
# We execute commands (extracted from `params[:note]`) on the noteable
# **before** we save the note because if the note consists of commands
diff --git a/app/services/projects/count_service.rb b/app/services/projects/count_service.rb
index 5e633c37bf8..aa034315280 100644
--- a/app/services/projects/count_service.rb
+++ b/app/services/projects/count_service.rb
@@ -2,6 +2,11 @@ module Projects
# Base class for the various service classes that count project data (e.g.
# issues or forks).
class CountService
+ # The version of the cache format. This should be bumped whenever the
+ # underlying logic changes. This removes the need for explicitly flushing
+ # all caches.
+ VERSION = 1
+
def initialize(project)
@project = project
end
@@ -37,7 +42,7 @@ module Projects
end
def cache_key
- ['projects', @project.id, cache_key_name]
+ ['projects', 'count_service', VERSION, @project.id, cache_key_name]
end
end
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index cb4ffcab778..13e292a18bf 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -24,7 +24,10 @@ module Projects
success
else
- error('Project could not be updated!')
+ model_errors = project.errors.full_messages.to_sentence
+ error_message = model_errors.presence || 'Project could not be updated!'
+
+ error(error_message)
end
end
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index 66d3bcb998a..cbb79376d5f 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -9,7 +9,7 @@ class AvatarUploader < GitlabUploader
end
def exists?
- model.avatar.file && model.avatar.file.exists?
+ model.avatar.file && model.avatar.file.present?
end
# We set move_to_store and move_to_cache to 'false' to prevent stealing
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
index 05a2091633a..7f72b3ce471 100644
--- a/app/uploaders/gitlab_uploader.rb
+++ b/app/uploaders/gitlab_uploader.rb
@@ -51,7 +51,7 @@ class GitlabUploader < CarrierWave::Uploader::Base
end
def exists?
- file.try(:exists?)
+ file.present?
end
# Override this if you don't want to save files by default to the Rails.root directory
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index e403a9da616..935787d1a4a 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -21,7 +21,7 @@
= image_tag @appearance.logo_url, class: 'appearance-logo-preview'
- if @appearance.persisted?
%br
- = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-logo"
+ = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo"
%hr
= f.hidden_field :logo_cache
= f.file_field :logo, class: ""
@@ -38,7 +38,7 @@
= image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview'
- if @appearance.persisted?
%br
- = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-logo"
+ = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo"
%hr
= f.hidden_field :header_logo_cache
= f.file_field :header_logo, class: ""
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 703f4165128..d212c7ca965 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -7,7 +7,7 @@
.row
.col-md-4
.info-well
- .well-segment.admin-well
+ .well-segment.admin-well.admin-well-statistics
%h4 Statistics
%p
Forks
@@ -43,7 +43,7 @@
= number_with_delimiter(User.active.count)
.col-md-4
.info-well
- .well-segment.admin-well
+ .well-segment.admin-well.admin-well-features
%h4 Features
- sign_up = "Sign up"
%p{ "aria-label" => "#{sign_up}: status " + (signup_enabled? ? "on" : "off") }
@@ -111,6 +111,10 @@
GitLab API
%span.pull-right
= API::API::version
+ %p
+ Gitaly
+ %span.pull-right
+ = Gitlab::GitalyClient.expected_server_version
- if Gitlab.config.pages.enabled
%p
GitLab Pages
diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml
index fed6002528d..b6e1df5f3ac 100644
--- a/app/views/admin/hooks/index.html.haml
+++ b/app/views/admin/hooks/index.html.haml
@@ -22,7 +22,7 @@
- @hooks.each do |hook|
%li
.controls
- = render 'shared/web_hooks/test_button', triggers: SystemHook::TRIGGERS, hook: hook, button_class: 'btn-small'
+ = render 'shared/web_hooks/test_button', triggers: SystemHook::TRIGGERS, hook: hook, button_class: 'btn-sm'
= link_to 'Edit', edit_admin_hook_path(hook), class: 'btn btn-sm'
= link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm'
.monospace= hook.url
diff --git a/app/views/feature_highlight/_issue_boards.svg b/app/views/feature_highlight/_issue_boards.svg
deleted file mode 100644
index 1522c9d51c9..00000000000
--- a/app/views/feature_highlight/_issue_boards.svg
+++ /dev/null
@@ -1,98 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="214" height="102" viewBox="0 0 214 102" xmlns:xlink="http://www.w3.org/1999/xlink">
- <defs>
- <path id="b" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,27 C48,28.1045695 47.1045695,29 46,29 L2,29 C0.8954305,29 1.3527075e-16,28.1045695 0,27 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
- <filter id="a" width="102.1%" height="106.9%" x="-1%" y="-1.7%" filterUnits="objectBoundingBox">
- <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
- <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
- </filter>
- <path id="d" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
- <filter id="c" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
- <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
- <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
- </filter>
- <path id="e" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/>
- <path id="h" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
- <filter id="g" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
- <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
- <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
- </filter>
- <path id="j" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
- <filter id="i" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
- <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
- <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
- </filter>
- <path id="l" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
- <filter id="k" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
- <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
- <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
- </filter>
- <path id="n" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
- <filter id="m" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
- <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
- <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
- </filter>
- <path id="p" d="M2,0 L46,0 C47.1045695,-2.02906125e-16 48,0.8954305 48,2 L48,26 C48,27.1045695 47.1045695,28 46,28 L2,28 C0.8954305,28 1.3527075e-16,27.1045695 0,26 L0,2 C-1.3527075e-16,0.8954305 0.8954305,2.02906125e-16 2,0 Z"/>
- <filter id="o" width="102.1%" height="107.1%" x="-1%" y="-1.8%" filterUnits="objectBoundingBox">
- <feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1"/>
- <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0" in="shadowOffsetOuter1"/>
- </filter>
- </defs>
- <g fill="none" fill-rule="evenodd">
- <path fill="#D6D4DE" d="M14,21 L62,21 C64.7614237,21 67,23.2385763 67,26 L67,112 C67,114.761424 64.7614237,117 62,117 L14,117 C11.2385763,117 9,114.761424 9,112 L9,26 C9,23.2385763 11.2385763,21 14,21 Z"/>
- <g transform="translate(11 23)">
- <path fill="#FFFFFF" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/>
- <path fill="#FC6D26" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z"/>
- <g transform="translate(5 10)">
- <use fill="black" filter="url(#a)" xlink:href="#b"/>
- <use fill="#F9F9F9" xlink:href="#b"/>
- </g>
- <g transform="translate(5 42)">
- <use fill="black" filter="url(#c)" xlink:href="#d"/>
- <use fill="#FEF0E8" xlink:href="#d"/>
- <path fill="#FEE1D3" d="M9,8 L33,8 C34.1045695,8 35,8.8954305 35,10 C35,11.1045695 34.1045695,12 33,12 L9,12 C7.8954305,12 7,11.1045695 7,10 C7,8.8954305 7.8954305,8 9,8 Z"/>
- <path fill="#FDC4A8" d="M9,17 L17,17 C18.1045695,17 19,17.8954305 19,19 C19,20.1045695 18.1045695,21 17,21 L9,21 C7.8954305,21 7,20.1045695 7,19 C7,17.8954305 7.8954305,17 9,17 Z"/>
- <path fill="#FC6D26" d="M24,17 L32,17 C33.1045695,17 34,17.8954305 34,19 C34,20.1045695 33.1045695,21 32,21 L24,21 C22.8954305,21 22,20.1045695 22,19 C22,17.8954305 22.8954305,17 24,17 Z"/>
- </g>
- </g>
- <path fill="#D6D4DE" d="M148,26 L196,26 C198.761424,26 201,28.2385763 201,31 L201,117 C201,119.761424 198.761424,122 196,122 L148,122 C145.238576,122 143,119.761424 143,117 L143,31 C143,28.2385763 145.238576,26 148,26 Z"/>
- <g transform="translate(145 28)">
- <mask id="f" fill="white">
- <use xlink:href="#e"/>
- </mask>
- <use fill="#FFFFFF" xlink:href="#e"/>
- <path fill="#FC6D26" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z" mask="url(#f)"/>
- <g transform="translate(5 10)">
- <use fill="black" filter="url(#g)" xlink:href="#h"/>
- <use fill="#F9F9F9" xlink:href="#h"/>
- </g>
- <g transform="translate(5 42)">
- <use fill="black" filter="url(#i)" xlink:href="#j"/>
- <use fill="#FEF0E8" xlink:href="#j"/>
- <path fill="#FEE1D3" d="M9 8L33 8C34.1045695 8 35 8.8954305 35 10 35 11.1045695 34.1045695 12 33 12L9 12C7.8954305 12 7 11.1045695 7 10 7 8.8954305 7.8954305 8 9 8zM9 17L13 17C14.1045695 17 15 17.8954305 15 19 15 20.1045695 14.1045695 21 13 21L9 21C7.8954305 21 7 20.1045695 7 19 7 17.8954305 7.8954305 17 9 17z"/>
- <path fill="#FC6D26" d="M20,17 L24,17 C25.1045695,17 26,17.8954305 26,19 C26,20.1045695 25.1045695,21 24,21 L20,21 C18.8954305,21 18,20.1045695 18,19 C18,17.8954305 18.8954305,17 20,17 Z"/>
- <path fill="#FDC4A8" d="M31,17 L35,17 C36.1045695,17 37,17.8954305 37,19 C37,20.1045695 36.1045695,21 35,21 L31,21 C29.8954305,21 29,20.1045695 29,19 C29,17.8954305 29.8954305,17 31,17 Z"/>
- </g>
- </g>
- <path fill="#D6D4DE" d="M81,14 L129,14 C131.761424,14 134,16.2385763 134,19 L134,105 C134,107.761424 131.761424,110 129,110 L81,110 C78.2385763,110 76,107.761424 76,105 L76,19 C76,16.2385763 78.2385763,14 81,14 Z"/>
- <g transform="translate(78 16)">
- <path fill="#FFFFFF" d="M5,0 L53,0 C55.7614237,-5.07265313e-16 58,2.23857625 58,5 L58,91 C58,93.7614237 55.7614237,96 53,96 L5,96 C2.23857625,96 3.38176876e-16,93.7614237 0,91 L0,5 C-3.38176876e-16,2.23857625 2.23857625,5.07265313e-16 5,0 Z"/>
- <g transform="translate(5 10)">
- <use fill="black" filter="url(#k)" xlink:href="#l"/>
- <use fill="#EFEDF8" xlink:href="#l"/>
- <path fill="#E1DBF1" d="M9,8 L33,8 C34.1045695,8 35,8.8954305 35,10 C35,11.1045695 34.1045695,12 33,12 L9,12 C7.8954305,12 7,11.1045695 7,10 C7,8.8954305 7.8954305,8 9,8 Z"/>
- <path fill="#6B4FBB" d="M9,17 L13,17 C14.1045695,17 15,17.8954305 15,19 C15,20.1045695 14.1045695,21 13,21 L9,21 C7.8954305,21 7,20.1045695 7,19 C7,17.8954305 7.8954305,17 9,17 Z"/>
- <path fill="#C3B8E3" d="M20,17 L28,17 C29.1045695,17 30,17.8954305 30,19 C30,20.1045695 29.1045695,21 28,21 L20,21 C18.8954305,21 18,20.1045695 18,19 C18,17.8954305 18.8954305,17 20,17 Z"/>
- </g>
- <g transform="translate(5 42)">
- <use fill="black" filter="url(#m)" xlink:href="#n"/>
- <use fill="#F9F9F9" xlink:href="#n"/>
- </g>
- <g transform="translate(5 74)">
- <rect width="34" height="4" x="7" y="7" fill="#E1DBF1" rx="2"/>
- <use fill="black" filter="url(#o)" xlink:href="#p"/>
- <use fill="#F9F9F9" xlink:href="#p"/>
- </g>
- <path fill="#6B4FBB" d="M4,0 L54,0 C56.209139,-4.05812251e-16 58,1.790861 58,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 Z"/>
- </g>
- </g>
-</svg>
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index fcebb385a65..615238b94ad 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -7,7 +7,7 @@
.sidebar-context-title Admin Area
%ul.sidebar-top-level-items
= nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts conversational_development_index), html_options: {class: 'home'}) do
- = sidebar_link admin_root_path, title: _('Overview'), css: 'shortcuts-tree' do
+ = link_to admin_root_path, class: 'shortcuts-tree' do
.nav-icon-container
= custom_icon('overview')
%span.nav-item-name
@@ -53,7 +53,7 @@
ConvDev Index
= nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles)) do
- = sidebar_link admin_system_info_path, title: _('Monitoring') do
+ = link_to admin_system_info_path do
.nav-icon-container
= custom_icon('monitoring')
%span.nav-item-name
@@ -87,7 +87,7 @@
Requests Profiles
= nav_link(controller: :broadcast_messages) do
- = sidebar_link admin_broadcast_messages_path, title: _('Messages') do
+ = link_to admin_broadcast_messages_path do
.nav-icon-container
= custom_icon('messages')
%span.nav-item-name
@@ -99,7 +99,7 @@
#{ _('Messages') }
= nav_link(controller: [:hooks, :hook_logs]) do
- = sidebar_link admin_hooks_path, title: _('Hooks') do
+ = link_to admin_hooks_path do
.nav-icon-container
= custom_icon('system_hooks')
%span.nav-item-name
@@ -111,7 +111,7 @@
#{ _('System Hooks') }
= nav_link(controller: :applications) do
- = sidebar_link admin_applications_path, title: _('Applications') do
+ = link_to admin_applications_path do
.nav-icon-container
= custom_icon('applications')
%span.nav-item-name
@@ -123,7 +123,7 @@
#{ _('Applications') }
= nav_link(controller: :abuse_reports) do
- = sidebar_link admin_abuse_reports_path, title: _("Abuse Reports") do
+ = link_to admin_abuse_reports_path do
.nav-icon-container
= custom_icon('abuse_reports')
%span.nav-item-name
@@ -138,7 +138,7 @@
- if akismet_enabled?
= nav_link(controller: :spam_logs) do
- = sidebar_link admin_spam_logs_path, title: _("Spam Logs") do
+ = link_to admin_spam_logs_path do
.nav-icon-container
= custom_icon('spam_logs')
%span.nav-item-name
@@ -150,7 +150,7 @@
#{ _('Spam Logs') }
= nav_link(controller: :deploy_keys) do
- = sidebar_link admin_deploy_keys_path, title: _('Deploy Keys') do
+ = link_to admin_deploy_keys_path do
.nav-icon-container
= custom_icon('key')
%span.nav-item-name
@@ -162,7 +162,7 @@
#{ _('Deploy Keys') }
= nav_link(controller: :services) do
- = sidebar_link admin_application_settings_services_path, title: _('Service Templates') do
+ = link_to admin_application_settings_services_path do
.nav-icon-container
= custom_icon('service_templates')
%span.nav-item-name
@@ -174,7 +174,7 @@
#{ _('Service Templates') }
= nav_link(controller: :labels) do
- = sidebar_link admin_labels_path, title: _('Labels') do
+ = link_to admin_labels_path do
.nav-icon-container
= custom_icon('labels')
%span.nav-item-name
@@ -186,7 +186,7 @@
#{ _('Labels') }
= nav_link(controller: :appearances) do
- = sidebar_link admin_appearances_path, title: _('Appearances') do
+ = link_to admin_appearances_path do
.nav-icon-container
= custom_icon('appearance')
%span.nav-item-name
@@ -198,7 +198,7 @@
#{ _('Appearance') }
= nav_link(controller: :application_settings) do
- = sidebar_link admin_application_settings_path, title: _('Settings') do
+ = link_to admin_application_settings_path do
.nav-icon-container
= custom_icon('settings')
%span.nav-item-name
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index e01dfa7c854..cb44c012f56 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -11,7 +11,7 @@
= @group.name
%ul.sidebar-top-level-items
= nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do
- = sidebar_link group_path(@group), title: _('Group overview') do
+ = link_to group_path(@group) do
.nav-icon-container
= custom_icon('project')
%span.nav-item-name
@@ -34,7 +34,7 @@
Activity
= nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do
- = sidebar_link issues_group_path(@group), title: _('Issues') do
+ = link_to issues_group_path(@group) do
.nav-icon-container
= custom_icon('issues')
%span.nav-item-name
@@ -64,7 +64,7 @@
Milestones
= nav_link(path: 'groups#merge_requests') do
- = sidebar_link merge_requests_group_path(@group), title: _('Merge Requests') do
+ = link_to merge_requests_group_path(@group) do
.nav-icon-container
= custom_icon('mr_bold')
%span.nav-item-name
@@ -77,19 +77,19 @@
#{ _('Merge Requests') }
%span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests.count)
= nav_link(path: 'group_members#index') do
- = sidebar_link group_group_members_path(@group), title: _('Members') do
+ = link_to group_group_members_path(@group) do
.nav-icon-container
= custom_icon('members')
%span.nav-item-name
Members
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: 'group_members#index', html_options: { class: "fly-out-top-item" } ) do
- = link_to merge_requests_group_path(@group) do
+ = link_to group_group_members_path(@group) do
%strong.fly-out-top-item-name
#{ _('Members') }
- if current_user && can?(current_user, :admin_group, @group)
= nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do
- = sidebar_link edit_group_path(@group), title: _('Settings') do
+ = link_to edit_group_path(@group) do
.nav-icon-container
= custom_icon('settings')
%span.nav-item-name
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index 4c26d107ea7..2c402591f62 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -7,7 +7,7 @@
.sidebar-context-title User Settings
%ul.sidebar-top-level-items
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
- = sidebar_link profile_path, title: _('Profile Settings') do
+ = link_to profile_path do
.nav-icon-container
= custom_icon('profile')
%span.nav-item-name
@@ -18,7 +18,7 @@
%strong.fly-out-top-item-name
#{ _('Profile') }
= nav_link(controller: [:accounts, :two_factor_auths]) do
- = sidebar_link profile_account_path, title: _('Account') do
+ = link_to profile_account_path do
.nav-icon-container
= custom_icon('account')
%span.nav-item-name
@@ -30,7 +30,7 @@
#{ _('Account') }
- if current_application_settings.user_oauth_applications?
= nav_link(controller: 'oauth/applications') do
- = sidebar_link applications_profile_path, title: _('Applications') do
+ = link_to applications_profile_path do
.nav-icon-container
= custom_icon('applications')
%span.nav-item-name
@@ -41,7 +41,7 @@
%strong.fly-out-top-item-name
#{ _('Applications') }
= nav_link(controller: :chat_names) do
- = sidebar_link profile_chat_names_path, title: _('Chat') do
+ = link_to profile_chat_names_path do
.nav-icon-container
= custom_icon('chat')
%span.nav-item-name
@@ -52,7 +52,7 @@
%strong.fly-out-top-item-name
#{ _('Chat') }
= nav_link(controller: :personal_access_tokens) do
- = sidebar_link profile_personal_access_tokens_path, title: _('Access Tokens') do
+ = link_to profile_personal_access_tokens_path do
.nav-icon-container
= custom_icon('access_tokens')
%span.nav-item-name
@@ -63,7 +63,7 @@
%strong.fly-out-top-item-name
#{ _('Access Tokens') }
= nav_link(controller: :emails) do
- = sidebar_link profile_emails_path, title: _('Emails') do
+ = link_to profile_emails_path do
.nav-icon-container
= custom_icon('emails')
%span.nav-item-name
@@ -75,7 +75,7 @@
#{ _('Emails') }
- unless current_user.ldap_user?
= nav_link(controller: :passwords) do
- = sidebar_link edit_profile_password_path, title: _('Password') do
+ = link_to edit_profile_password_path do
.nav-icon-container
= custom_icon('lock')
%span.nav-item-name
@@ -86,7 +86,7 @@
%strong.fly-out-top-item-name
#{ _('Password') }
= nav_link(controller: :notifications) do
- = sidebar_link profile_notifications_path, title: _('Notifications') do
+ = link_to profile_notifications_path do
.nav-icon-container
= custom_icon('notifications')
%span.nav-item-name
@@ -97,7 +97,7 @@
%strong.fly-out-top-item-name
#{ _('Notifications') }
= nav_link(controller: :keys) do
- = sidebar_link profile_keys_path, title: _('SSH Keys') do
+ = link_to profile_keys_path do
.nav-icon-container
= custom_icon('key')
%span.nav-item-name
@@ -108,7 +108,7 @@
%strong.fly-out-top-item-name
#{ _('SSH Keys') }
= nav_link(controller: :gpg_keys) do
- = sidebar_link profile_gpg_keys_path, title: _('GPG Keys') do
+ = link_to profile_gpg_keys_path do
.nav-icon-container
= custom_icon('key_2')
%span.nav-item-name
@@ -119,7 +119,7 @@
%strong.fly-out-top-item-name
#{ _('GPG Keys') }
= nav_link(controller: :preferences) do
- = sidebar_link profile_preferences_path, title: _('Preferences') do
+ = link_to profile_preferences_path do
.nav-icon-container
= custom_icon('preferences')
%span.nav-item-name
@@ -130,7 +130,7 @@
%strong.fly-out-top-item-name
#{ _('Preferences') }
= nav_link(path: 'profiles#audit_log') do
- = sidebar_link audit_log_profile_path, title: _('Authentication log') do
+ = link_to audit_log_profile_path do
.nav-icon-container
= custom_icon('authentication_log')
%span.nav-item-name
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 5f7a2d86c0f..8ec2e2c79fc 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -9,7 +9,7 @@
= @project.name
%ul.sidebar-top-level-items
= nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do
- = sidebar_link project_path(@project), title: _('Project overview'), css: 'shortcuts-project' do
+ = link_to project_path(@project), class: 'shortcuts-project' do
.nav-icon-container
= custom_icon('project')
%span.nav-item-name
@@ -36,7 +36,7 @@
- if project_nav_tab? :files
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do
- = sidebar_link project_tree_path(@project), title: _('Repository'), css: 'shortcuts-tree' do
+ = link_to project_tree_path(@project), class: 'shortcuts-tree' do
.nav-icon-container
= custom_icon('doc_text')
%span.nav-item-name
@@ -82,7 +82,7 @@
- if project_nav_tab? :container_registry
= nav_link(controller: %w[projects/registry/repositories]) do
- = sidebar_link project_container_registry_index_path(@project), title: _('Container Registry'), css: 'shortcuts-container-registry' do
+ = link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do
.nav-icon-container
= custom_icon('container_registry')
%span.nav-item-name
@@ -90,7 +90,7 @@
- if project_nav_tab? :issues
= nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
- = sidebar_link project_issues_path(@project), title: _('Issues'), css: 'shortcuts-issues' do
+ = link_to project_issues_path(@project), class: 'shortcuts-issues' do
.nav-icon-container
= custom_icon('issues')
%span.nav-item-name
@@ -117,20 +117,6 @@
= link_to project_boards_path(@project), title: boards_link_text do
%span
= boards_link_text
- .feature-highlight.js-feature-highlight{ disabled: true, data: { trigger: 'manual', container: 'body', toggle: 'popover', placement: 'right', highlight: 'issue-boards' } }
- .feature-highlight-popover-content
- = render 'feature_highlight/issue_boards.svg'
- .feature-highlight-popover-sub-content
- %span= _('Use')
- = link_to 'Issue Boards', project_boards_path(@project)
- %span= _('to create customized software development workflows like')
- %strong= _('Scrum')
- %span= _('or')
- %strong= _('Kanban')
- %hr
- %button.btn-link.dismiss-feature-highlight{ type: 'button' }
- %span= _("Got it! Don't show this again")
- = custom_icon('thumbs_up')
= nav_link(controller: :labels) do
= link_to project_labels_path(@project), title: 'Labels' do
@@ -144,7 +130,7 @@
- if project_nav_tab? :merge_requests
= nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
- = sidebar_link project_merge_requests_path(@project), title: _('Merge Requests'), css: 'shortcuts-merge_requests' do
+ = link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests' do
.nav-icon-container
= custom_icon('mr_bold')
%span.nav-item-name
@@ -161,7 +147,7 @@
- if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do
- = sidebar_link project_pipelines_path(@project), title: _('CI / CD'), css: 'shortcuts-pipelines' do
+ = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do
.nav-icon-container
= custom_icon('pipeline')
%span.nav-item-name
@@ -205,7 +191,7 @@
- if project_nav_tab? :wiki
= nav_link(controller: :wikis) do
- = sidebar_link get_project_wiki_path(@project), title: _('Wiki'), css: 'shortcuts-wiki' do
+ = link_to get_project_wiki_path(@project), class: 'shortcuts-wiki' do
.nav-icon-container
= custom_icon('wiki')
%span.nav-item-name
@@ -218,7 +204,7 @@
- if project_nav_tab? :snippets
= nav_link(controller: :snippets) do
- = sidebar_link project_snippets_path(@project), title: _('Snippets'), css: 'shortcuts-snippets' do
+ = link_to project_snippets_path(@project), class: 'shortcuts-snippets' do
.nav-icon-container
= custom_icon('snippets')
%span.nav-item-name
@@ -231,7 +217,7 @@
- if project_nav_tab? :settings
= nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do
- = sidebar_link edit_project_path(@project), title: _('Settings'), css: 'shortcuts-tree' do
+ = link_to edit_project_path(@project), class: 'shortcuts-tree' do
.nav-icon-container
= custom_icon('settings')
%span.nav-item-name
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 5d778d67ae7..8abbd828032 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -74,7 +74,9 @@
%h4.prepend-top-0.warning-title
Change username
%p
- Changing your username will change path to all personal projects!
+ Changing your username can have unintended side effects.
+ = succeed '.' do
+ = link_to 'Learn more', help_page_path('user/profile/index', anchor: 'changing-your-username'), target: '_blank'
.col-lg-8
= form_for @user, url: update_username_profile_path, method: :put, html: {class: "update-username"} do |f|
.form-group
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 19712a8f1be..05c1d2b383c 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -43,7 +43,8 @@
data: { toggle: "modal",
target: "#modal-delete-branch",
delete_path: project_branch_path(@project, branch.name),
- branch_name: branch.name } }
+ branch_name: branch.name,
+ is_merged: ("true" if @repository.merged_to_root_ref?(branch.name)) } }
= icon("trash-o")
- else
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
diff --git a/app/views/projects/branches/_delete_protected_modal.html.haml b/app/views/projects/branches/_delete_protected_modal.html.haml
index c5888afa54d..f00a0ee6925 100644
--- a/app/views/projects/branches/_delete_protected_modal.html.haml
+++ b/app/views/projects/branches/_delete_protected_modal.html.haml
@@ -6,13 +6,18 @@
%h3.page-title
Delete protected branch
= surround "'", "'?" do
- %span.js-branch-name>[branch name]
+ %span.js-branch-name.ref-name>[branch name]
.modal-body
%p
You’re about to permanently delete the protected branch
= succeed '.' do
- %strong.js-branch-name [branch name]
+ %strong.js-branch-name.ref-name [branch name]
+ %p.js-not-merged
+ - default_branch = capture do
+ %span.ref-name= @repository.root_ref
+ = s_("Branches|This branch hasn’t been merged into %{default_branch}.").html_safe % { default_branch: default_branch }
+ = s_("Branches|To avoid data loss, consider merging this branch before deleting it.")
%p
Once you confirm and press
= succeed ',' do
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 883922dbf04..9d85e027ac9 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,4 +1,4 @@
-- pipeline = local_assigns.fetch(:pipeline) { project.pipelines.latest_successful_for(ref) }
+- pipeline = local_assigns.fetch(:pipeline) { project.latest_successful_pipeline_for(ref) }
- if !project.empty_repo? && can?(current_user, :download_code, project)
.project-action-button.dropdown.inline>
@@ -26,18 +26,16 @@
%i.fa.fa-download
%span= _('Download tar')
- - if pipeline
- - artifacts = pipeline.builds.latest.with_artifacts
- - if artifacts.any?
- %li.dropdown-header Artifacts
- - unless pipeline.latest?
- - latest_pipeline = project.pipeline_for(ref)
- %li
- .unclickable= ci_status_for_statuseable(latest_pipeline)
- %li.dropdown-header Previous Artifacts
- - artifacts.each do |job|
- %li
- = link_to latest_succeeded_project_artifacts_path(project, "#{ref}/download", job: job.name), rel: 'nofollow', download: '' do
- %i.fa.fa-download
- %span
- #{ s_('DownloadArtifacts|Download') } '#{job.name}'
+ - if pipeline && pipeline.latest_builds_with_artifacts.any?
+ %li.dropdown-header Artifacts
+ - unless pipeline.latest?
+ - latest_pipeline = project.pipeline_for(ref)
+ %li
+ .unclickable= ci_status_for_statuseable(latest_pipeline)
+ %li.dropdown-header Previous Artifacts
+ - pipeline.latest_builds_with_artifacts.each do |job|
+ %li
+ = link_to latest_succeeded_project_artifacts_path(project, "#{ref}/download", job: job.name), rel: 'nofollow', download: '' do
+ %i.fa.fa-download
+ %span
+ #{s_('DownloadArtifacts|Download')} '#{job.name}'
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index 94b7db5eb25..a518fced2b4 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -2,22 +2,22 @@
.clearfix
- if params[:to] && params[:from]
.compare-switch-container
- = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Switch base of comparison'}
- .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
- .input-group.inline-input-group
- %span.input-group-addon from
- = hidden_field_tag :from, params[:from]
- = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
- .dropdown-toggle-text.str-truncated= params[:from] || 'Select branch/tag'
- = render 'shared/ref_dropdown'
- .compare-ellipsis.inline ...
+ = link_to icon('exchange'), { from: params[:to], to: params[:from] }, class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Swap revisions'
.form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
.input-group.inline-input-group
- %span.input-group-addon to
+ %span.input-group-addon Source
= hidden_field_tag :to, params[:to]
= button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
.dropdown-toggle-text.str-truncated= params[:to] || 'Select branch/tag'
= render 'shared/ref_dropdown'
+ .compare-ellipsis.inline ...
+ .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
+ .input-group.inline-input-group
+ %span.input-group-addon Target
+ = hidden_field_tag :from, params[:from]
+ = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
+ .dropdown-toggle-text.str-truncated= params[:from] || 'Select branch/tag'
+ = render 'shared/ref_dropdown'
&nbsp;
= button_tag "Compare", class: "btn btn-create commits-compare-btn"
- if @merge_request.present?
diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml
index 2632fea6eba..1ce3ad0c0fd 100644
--- a/app/views/projects/compare/index.html.haml
+++ b/app/views/projects/compare/index.html.haml
@@ -7,13 +7,19 @@
.sub-header-block
Compare Git revisions.
%br
- Fill input field with commit SHA like
- %code.ref-name 4eedf23
- or branch/tag name like
- %code.ref-name master
- and press compare button for the commits list and a code diff.
+ Choose a branch/tag (e.g.
+ = succeed ')' do
+ %code.ref-name master
+ or enter a commit SHA (e.g.
+ = succeed ')' do
+ %code.ref-name 4eedf23
+ to see what's changed or to create a merge request.
%br
- Changes are shown <b>from</b> the version in the first field <b>to</b> the version in the second field.
+ Changes are shown as if the
+ %b source
+ revision was being merged into the
+ %b target
+ revision.
.prepend-top-20
= render "form"
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 5e980314307..d5b83b53ebb 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -27,6 +27,8 @@
- if can?(current_user, :push_code, @project)
%div{ class: container_class }
+ - if show_auto_devops_callout?(@project)
+ = render 'shared/auto_devops_callout'
.prepend-top-20
.empty_wrapper
%h3.page-title-empty
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 7dc35be57a6..64c648f201b 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -24,7 +24,7 @@
- if issue.milestone
%span.issuable-milestone.hidden-xs
&nbsp;
- = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title) do
+ = link_to project_issues_path(issue.project, milestone_title: issue.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_title(issue.milestone) } do
= icon('clock-o')
= issue.milestone.title
- if issue.due_date
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 0a1ebcb8124..2b5e8711b0a 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -23,7 +23,7 @@
- if merge_request.milestone
%span.issuable-milestone.hidden-xs
&nbsp;
- = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title) do
+ = link_to project_merge_requests_path(merge_request.project, milestone_title: merge_request.milestone.title), data: { html: 1, toggle: 'tooltip', title: milestone_tooltip_title(merge_request.milestone) } do
= icon('clock-o')
= merge_request.milestone.title
- if merge_request.target_project.default_branch != merge_request.target_branch
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index c2d16f7e731..d3742f3e4be 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -62,7 +62,10 @@
":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
%span.line-resolve-btn.is-disabled{ type: "button",
":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
- = render "shared/icons/icon_status_success.svg"
+ %template{ 'v-if' => 'resolvedDiscussionCount === discussionCount' }
+ = render 'shared/icons/icon_status_success_solid.svg'
+ %template{ 'v-else' => '' }
+ = render 'shared/icons/icon_resolve_discussion.svg'
%span.line-resolve-text
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
= render "discussions/new_issue_for_all_discussions", merge_request: @merge_request
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 819392b8f0c..cc41b908946 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -16,7 +16,9 @@
New project
- if import_sources_enabled?
%p
- Create or Import your project from popular Git services
+ A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), #{link_to 'among other things', help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank'}.
+ %p
+ All features are enabled when you create a project, but you can disable the ones you don’t need in the project settings.
.col-lg-9.js-toggle-container
= form_for @project, html: { class: 'new_project' } do |f|
.create-project-options
diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml
index 324cd423ede..21d01242c0e 100644
--- a/app/views/projects/pipelines_settings/_show.html.haml
+++ b/app/views/projects/pipelines_settings/_show.html.haml
@@ -3,11 +3,15 @@
= form_for @project, url: project_pipelines_settings_path(@project) do |f|
%fieldset.builds-feature
.form-group
- %p Pipelines need to have Auto DevOps enabled or have a .gitlab-ci.yml configured before you can begin using Continuous Integration and Delivery.
%h5 Auto DevOps (Beta)
%p
- Auto DevOps will automatically build, test, and deploy your application based on a predefined Continious Integration and Delivery configuration.
+ Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.
+ This will happen starting with the next event (e.g.: push) that occurs to the project.
= link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md')
+ - message = auto_devops_warning_message(@project)
+ - if message
+ %p.settings-message.text-center
+ = message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
.radio
= form.label :enabled_true do
@@ -15,26 +19,24 @@
%strong Enable Auto DevOps
%br
%span.descr
- The Auto DevOps pipeline configuration will be used when there is no .gitlab-ci.yml
- in the project.
+ The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project.
.radio
= form.label :enabled_false do
= form.radio_button :enabled, 'false'
%strong Disable Auto DevOps
%br
%span.descr
- A specific .gitlab-ci.yml file needs to be specified before you can begin using Continious Integration and Delivery.
+ An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continious Integration and Delivery.
.radio
- = form.label :enabled do
- = form.radio_button :enabled, nil
- %strong
- Instance default (status: #{current_application_settings.auto_devops_enabled?})
+ = form.label :enabled_nil do
+ = form.radio_button :enabled, ''
+ %strong Instance default (#{current_application_settings.auto_devops_enabled? ? 'enabled' : 'disabled'})
%br
%span.descr
- Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific .gitlab-ci.yml file specified.
+ Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>.
%br
%p
- Define a domain used by Auto DevOps to deploy towards, this is required for deploys to succeed.
+ You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
%hr
diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml
index de85615c672..e660fce652f 100644
--- a/app/views/projects/runners/_form.html.haml
+++ b/app/views/projects/runners/_form.html.haml
@@ -11,7 +11,7 @@
.col-sm-10
.checkbox
= f.check_box :access_level, {}, 'ref_protected', 'not_protected'
- %span.light This runner will only run on pipelines trigged on protected branches
+ %span.light This runner will only run on pipelines triggered on protected branches
.form-group
= label :run_untagged, 'Run untagged jobs', class: 'control-label'
.col-sm-10
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index d4f71d023c6..47c056d097a 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -13,7 +13,7 @@
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
- Update your CI/CD configuration, like job timeout.
+ Update your CI/CD configuration, like job timeout or Auto DevOps.
.settings-content.no-animate{ class: ('expanded' if expanded) }
= render 'projects/pipelines_settings/show'
diff --git a/app/views/projects/settings/integrations/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml
index d5792e95f5a..82516cb4bcf 100644
--- a/app/views/projects/settings/integrations/_project_hook.html.haml
+++ b/app/views/projects/settings/integrations/_project_hook.html.haml
@@ -10,7 +10,7 @@
%span.append-right-10.inline
SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
= link_to 'Edit', edit_project_hook_path(@project, hook), class: 'btn btn-sm'
- = render 'shared/web_hooks/test_button', triggers: ProjectHook::TRIGGERS, hook: hook, button_class: 'btn-small'
+ = render 'shared/web_hooks/test_button', triggers: ProjectHook::TRIGGERS, hook: hook, button_class: 'btn-sm'
= link_to project_hook_path(@project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-transparent' do
%span.sr-only Remove
= icon('trash')
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index 2e1bd5a088c..d0b9e891b82 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -22,11 +22,9 @@
- if @group.persisted?
.alert.alert-warning.prepend-top-10
- %ul
- %li Changing group path can have unintended side effects.
- %li Renaming group path will rename directory for all related projects
- %li It will change web url for access group and group projects.
- %li It will change the git path to repositories under this group.
+ Changing group path can have unintended side effects.
+ = succeed '.' do
+ = link_to 'Learn more', help_page_path('user/group/index', anchor: 'changing-a-groups-path'), target: '_blank'
.form-group.group-name-holder
= f.label :name, class: 'control-label' do
diff --git a/app/views/shared/_target_switcher.html.haml b/app/views/shared/_target_switcher.html.haml
index 9236868652f..bbe9692a7da 100644
--- a/app/views/shared/_target_switcher.html.haml
+++ b/app/views/shared/_target_switcher.html.haml
@@ -1,5 +1,5 @@
- dropdown_toggle_text = @ref || @project.default_branch
-= form_tag nil, method: :get, style: { display: 'none' }, class: "project-refs-target-form" do
+= form_tag nil, method: :get, class: "project-refs-form project-refs-target-form" do
= hidden_field_tag :destination, destination
- if defined?(path)
= hidden_field_tag :path, path
@@ -7,14 +7,10 @@
= hidden_field_tag key, value, id: nil
.dropdown
= dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, find: ['branches']), field_name: 'ref', input_field_name: 'new-branch', submit_form_on_click: true, visit: false }, { toggle_class: "js-project-refs-dropdown" }
- %ul.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
- %li
- = dropdown_title _("Create a new branch")
- %li
- = dropdown_input _("Create a new branch")
- %li
- = dropdown_title _("Select existing branch"), options: {close: false}
- %li
- = dropdown_filter _("Search branches and tags")
- = dropdown_content
- = dropdown_loading
+ .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
+ = dropdown_title _("Create a new branch")
+ = dropdown_input _("Create a new branch")
+ = dropdown_title _("Select existing branch"), options: {close: false}
+ = dropdown_filter _("Search branches and tags")
+ = dropdown_content
+ = dropdown_loading
diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml
index c5a8b32c772..c687e66fd43 100644
--- a/app/views/shared/boards/components/_board.html.haml
+++ b/app/views/shared/boards/components/_board.html.haml
@@ -27,7 +27,7 @@
%span.issue-count-badge-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
{{ list.issuesSize }}
- if can?(current_user, :admin_list, current_board_parent)
- %button.issue-count-badge-add-button.btn.btn-small.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button",
+ %button.issue-count-badge-add-button.btn.btn-sm.btn-default.has-tooltip.js-no-trigger-collapse{ type: "button",
"@click" => "showNewIssueForm",
"v-if" => 'list.type !== "closed"',
"aria-label" => "New issue",
diff --git a/app/views/shared/icons/_thumbs_up.svg b/app/views/shared/icons/_thumbs_up.svg
deleted file mode 100644
index 7267462418e..00000000000
--- a/app/views/shared/icons/_thumbs_up.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.104 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.486.5l.138.137a1 1 0 0 1 .28.87L8.33 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></svg>
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index c4ed7f6e750..d3f0aa2d339 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -11,13 +11,13 @@
- if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id])
= dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit",
- placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user.try(:username), current_user: true, project_id: @project.try(:id), selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
+ placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user&.username, current_user: true, project_id: @project&.id, group_id: @group&.id, selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
.filter-item.inline
- if params[:assignee_id].present?
= hidden_field_tag(:assignee_id, params[:assignee_id])
= dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
- placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user.try(:username), null_user: true, current_user: true, project_id: @project.try(:id), group_id: @group&.id, selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
+ placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user&.username, null_user: true, current_user: true, project_id: @project&.id, group_id: @group&.id, selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
.filter-item.inline.milestone-filter
= render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true, show_started: true
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 9cae3f51825..674f13ddb23 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -26,7 +26,7 @@
= icon('clock-o', 'aria-hidden': 'true')
%span.milestone-title
- if issuable.milestone
- %span.has-tooltip{ title: "#{issuable.milestone.title}<br>#{milestone_remaining_days(issuable.milestone)}", data: { container: 'body', html: 1, placement: 'left' } }
+ %span.has-tooltip{ title: "#{issuable.milestone.title}<br>#{milestone_tooltip_title(issuable.milestone)}", data: { container: 'body', html: 1, placement: 'left' } }
= issuable.milestone.title
- else
None
@@ -37,7 +37,7 @@
= link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link pull-right'
.value.hide-collapsed
- if issuable.milestone
- = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_remaining_days(issuable.milestone), data: { container: "body", html: 1 }
+ = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_title(issuable.milestone), data: { container: "body", html: 1 }
- else
%span.no-value None
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 3739f4c221d..14395bcc661 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -26,6 +26,6 @@
%span.assignee-icon
- assignees.each do |assignee|
- = link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: assignee.id, state: 'all' }),
+ = link_to polymorphic_path(issuable_type_args, { milestone_title: @milestone.title, assignee_id: assignee.id, state: 'all' }),
class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do
- image_tag(avatar_icon(assignee, 16), class: "avatar s16", alt: '')
diff --git a/changelogs/unreleased/20049-projects-api-forks.yml b/changelogs/unreleased/20049-projects-api-forks.yml
new file mode 100644
index 00000000000..c6470620f57
--- /dev/null
+++ b/changelogs/unreleased/20049-projects-api-forks.yml
@@ -0,0 +1,5 @@
+---
+title: Add an API endpoint to determine the forks of a project
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml b/changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml
new file mode 100644
index 00000000000..245b8129de8
--- /dev/null
+++ b/changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml
@@ -0,0 +1,5 @@
+---
+title: Return only group's members in user dropdowns on issuables list pages
+merge_request: 14249
+author:
+type: changed
diff --git a/changelogs/unreleased/21331-improve-confusing-compare-page.yml b/changelogs/unreleased/21331-improve-confusing-compare-page.yml
new file mode 100644
index 00000000000..469cc04930b
--- /dev/null
+++ b/changelogs/unreleased/21331-improve-confusing-compare-page.yml
@@ -0,0 +1,5 @@
+---
+title: Make the labels in the Compare form less confusing
+merge_request: 14225
+author:
+type: changed
diff --git a/changelogs/unreleased/24121_extract_yet_another_users_finder.yml b/changelogs/unreleased/24121_extract_yet_another_users_finder.yml
new file mode 100644
index 00000000000..e43e97303e2
--- /dev/null
+++ b/changelogs/unreleased/24121_extract_yet_another_users_finder.yml
@@ -0,0 +1,5 @@
+---
+title: Extract AutocompleteController#users into finder
+merge_request: 13778
+author: Maxim Rydkin, Mayra Cabrera
+type: other
diff --git a/changelogs/unreleased/33287-fix-mr-widget-errors-with-external-services.yml b/changelogs/unreleased/33287-fix-mr-widget-errors-with-external-services.yml
new file mode 100644
index 00000000000..f0c76060781
--- /dev/null
+++ b/changelogs/unreleased/33287-fix-mr-widget-errors-with-external-services.yml
@@ -0,0 +1,5 @@
+---
+title: Fix errors thrown in merge request widget with external CI service/integration
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/34259-project-denial-of-service-via-gitmodules-fix.yml b/changelogs/unreleased/34259-project-denial-of-service-via-gitmodules-fix.yml
new file mode 100644
index 00000000000..8260f7fa4b2
--- /dev/null
+++ b/changelogs/unreleased/34259-project-denial-of-service-via-gitmodules-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes project denial of service via gitmodules using Extended ASCII.
+merge_request: 14301
+author:
+type: fixed
diff --git a/changelogs/unreleased/35290_allow_public_project_apis.yml b/changelogs/unreleased/35290_allow_public_project_apis.yml
new file mode 100644
index 00000000000..1968eee0a53
--- /dev/null
+++ b/changelogs/unreleased/35290_allow_public_project_apis.yml
@@ -0,0 +1,4 @@
+---
+title: made read-only APIs for public merge requests available without authentication
+merge_request: 13291
+author: haseebeqx
diff --git a/changelogs/unreleased/35917_create_services_for_keys.yml b/changelogs/unreleased/35917_create_services_for_keys.yml
new file mode 100644
index 00000000000..e7cad5a11d5
--- /dev/null
+++ b/changelogs/unreleased/35917_create_services_for_keys.yml
@@ -0,0 +1,4 @@
+---
+title: creation of keys moved to services
+merge_request: 13331
+author: haseebeqx
diff --git a/changelogs/unreleased/36884-gitaly-admin-version.yml b/changelogs/unreleased/36884-gitaly-admin-version.yml
new file mode 100644
index 00000000000..0b3b9a205b5
--- /dev/null
+++ b/changelogs/unreleased/36884-gitaly-admin-version.yml
@@ -0,0 +1,5 @@
+---
+title: Add Gitaly version to Admin Dashboard
+merge_request: 14313
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/37259-some-mr-ready-mobile-fixes.yml b/changelogs/unreleased/37259-some-mr-ready-mobile-fixes.yml
new file mode 100644
index 00000000000..a00a41f567f
--- /dev/null
+++ b/changelogs/unreleased/37259-some-mr-ready-mobile-fixes.yml
@@ -0,0 +1,5 @@
+---
+title: Fix MR ready to merge buttons/controls at mobile breakpoint
+merge_request: 14242
+author:
+type: fixed
diff --git a/changelogs/unreleased/37465-fix-line-resolve-all-green-checkmark-icon.yml b/changelogs/unreleased/37465-fix-line-resolve-all-green-checkmark-icon.yml
new file mode 100644
index 00000000000..24b1d201409
--- /dev/null
+++ b/changelogs/unreleased/37465-fix-line-resolve-all-green-checkmark-icon.yml
@@ -0,0 +1,6 @@
+---
+title: Update x/x discussions resolved checkmark icon to be green when all discussions
+ resolved
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/37590-pipelines-mr.yml b/changelogs/unreleased/37590-pipelines-mr.yml
new file mode 100644
index 00000000000..ee609888155
--- /dev/null
+++ b/changelogs/unreleased/37590-pipelines-mr.yml
@@ -0,0 +1,5 @@
+---
+title: Fix mini graph pipeline breakin in merge request view
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/37890-auto-devops-banner-is-not-shown-when-the-repository-is-empty-new-project.yml b/changelogs/unreleased/37890-auto-devops-banner-is-not-shown-when-the-repository-is-empty-new-project.yml
new file mode 100644
index 00000000000..2dddfa0b882
--- /dev/null
+++ b/changelogs/unreleased/37890-auto-devops-banner-is-not-shown-when-the-repository-is-empty-new-project.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Auto DevOps banner to be shown on empty projects
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/37894-handle-if-auto-devops-domain-is-not-set.yml b/changelogs/unreleased/37894-handle-if-auto-devops-domain-is-not-set.yml
new file mode 100644
index 00000000000..bbb12ff41b1
--- /dev/null
+++ b/changelogs/unreleased/37894-handle-if-auto-devops-domain-is-not-set.yml
@@ -0,0 +1,5 @@
+---
+title: Handle if Auto DevOps domain is not set in project settings
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/37999-fix-circuit-breaker.yml b/changelogs/unreleased/37999-fix-circuit-breaker.yml
new file mode 100644
index 00000000000..a75315c4988
--- /dev/null
+++ b/changelogs/unreleased/37999-fix-circuit-breaker.yml
@@ -0,0 +1,5 @@
+---
+title: Fix the filesystem shard health check to check all configured shards
+merge_request: 14341
+author:
+type: fixed
diff --git a/changelogs/unreleased/add_closed_at_attribute.yml b/changelogs/unreleased/add_closed_at_attribute.yml
new file mode 100644
index 00000000000..3afb75e8915
--- /dev/null
+++ b/changelogs/unreleased/add_closed_at_attribute.yml
@@ -0,0 +1,5 @@
+---
+title: Add 'closed_at' attribute to Issues API
+merge_request: 14316
+author: Vitaliy @blackst0ne Klachkov
+type: added
diff --git a/changelogs/unreleased/add_tooltip_for_milestone_in_issues_list.yml b/changelogs/unreleased/add_tooltip_for_milestone_in_issues_list.yml
new file mode 100644
index 00000000000..0470c6519f4
--- /dev/null
+++ b/changelogs/unreleased/add_tooltip_for_milestone_in_issues_list.yml
@@ -0,0 +1,5 @@
+---
+title: Add tooltip for milestone due date to issue and merge request lists
+merge_request: 14318
+author: Vitaliy @blackst0ne Klachkov
+type: added
diff --git a/changelogs/unreleased/do-not-perform-disk-check.yml b/changelogs/unreleased/do-not-perform-disk-check.yml
new file mode 100644
index 00000000000..cc139ee2c9e
--- /dev/null
+++ b/changelogs/unreleased/do-not-perform-disk-check.yml
@@ -0,0 +1,5 @@
+---
+title: File uploaders do not perform hard check, only soft check
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/docs-17499-documentation-errors-about-creating-a-new-tag.yml b/changelogs/unreleased/docs-17499-documentation-errors-about-creating-a-new-tag.yml
new file mode 100644
index 00000000000..3dfe4114cc9
--- /dev/null
+++ b/changelogs/unreleased/docs-17499-documentation-errors-about-creating-a-new-tag.yml
@@ -0,0 +1,5 @@
+---
+title: Fix docs for lightweight tag creation via API
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/fix-gb-fix-moving-issue-with-ambiguous-references.yml b/changelogs/unreleased/fix-gb-fix-moving-issue-with-ambiguous-references.yml
new file mode 100644
index 00000000000..f90766ef6d2
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-fix-moving-issue-with-ambiguous-references.yml
@@ -0,0 +1,5 @@
+---
+title: Fix errors when moving issue with reference to a group milestone
+merge_request: 14294
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-sidebar-with-scrollbars.yml b/changelogs/unreleased/fix-sidebar-with-scrollbars.yml
new file mode 100644
index 00000000000..e0b3851b97f
--- /dev/null
+++ b/changelogs/unreleased/fix-sidebar-with-scrollbars.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed the sidebar scrollbar overlapping links
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/issue_32215.yml b/changelogs/unreleased/issue_32215.yml
new file mode 100644
index 00000000000..c608eb6dd28
--- /dev/null
+++ b/changelogs/unreleased/issue_32215.yml
@@ -0,0 +1,5 @@
+---
+title: Allow developer role to admin milestones
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/issue_37640.yml b/changelogs/unreleased/issue_37640.yml
new file mode 100644
index 00000000000..d806ed64bed
--- /dev/null
+++ b/changelogs/unreleased/issue_37640.yml
@@ -0,0 +1,6 @@
+---
+title: Fix project feature being deleted when updating project with invalid visibility
+ level
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/memoize-the-latest-builds-of-a-pipeline.yml b/changelogs/unreleased/memoize-the-latest-builds-of-a-pipeline.yml
new file mode 100644
index 00000000000..5a7cd42b888
--- /dev/null
+++ b/changelogs/unreleased/memoize-the-latest-builds-of-a-pipeline.yml
@@ -0,0 +1,5 @@
+---
+title: "Memoize the latest builds of a pipeline on a project's homepage"
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/milestone-avatar-issuable-link.yml b/changelogs/unreleased/milestone-avatar-issuable-link.yml
new file mode 100644
index 00000000000..7915ad60fa8
--- /dev/null
+++ b/changelogs/unreleased/milestone-avatar-issuable-link.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed milestone issuable assignee link URL
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/mk-clarify-moving-namespaces.yml b/changelogs/unreleased/mk-clarify-moving-namespaces.yml
new file mode 100644
index 00000000000..8d89c296f61
--- /dev/null
+++ b/changelogs/unreleased/mk-clarify-moving-namespaces.yml
@@ -0,0 +1,5 @@
+---
+title: Expand docs for changing username or group path
+merge_request: 13914
+author:
+type: other
diff --git a/changelogs/unreleased/projects-controller-show.yml b/changelogs/unreleased/projects-controller-show.yml
new file mode 100644
index 00000000000..25f4a72710b
--- /dev/null
+++ b/changelogs/unreleased/projects-controller-show.yml
@@ -0,0 +1,5 @@
+---
+title: Memoize pipelines for project download buttons
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/reoganize-deployment-indexes.yml b/changelogs/unreleased/reoganize-deployment-indexes.yml
new file mode 100644
index 00000000000..87734b4fe4b
--- /dev/null
+++ b/changelogs/unreleased/reoganize-deployment-indexes.yml
@@ -0,0 +1,5 @@
+---
+title: Reorganize indexes for the "deployments" table
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/replace_project_archived-feature.yml b/changelogs/unreleased/replace_project_archived-feature.yml
new file mode 100644
index 00000000000..d0697347aa0
--- /dev/null
+++ b/changelogs/unreleased/replace_project_archived-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'project/archived.feature' spinach test with an rspec analog
+merge_request: 14322
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_project_commits_revert-feature.yml b/changelogs/unreleased/replace_project_commits_revert-feature.yml
new file mode 100644
index 00000000000..7fc9fcf3580
--- /dev/null
+++ b/changelogs/unreleased/replace_project_commits_revert-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'project/commits/revert.feature' spinach test with an rspec analog
+merge_request: 14325
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_project_snippets-feature.yml b/changelogs/unreleased/replace_project_snippets-feature.yml
new file mode 100644
index 00000000000..4fdee70008a
--- /dev/null
+++ b/changelogs/unreleased/replace_project_snippets-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'project/snippets.feature' spinach test with an rspec analog
+merge_request: 14326
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/replace_search-feature.yml b/changelogs/unreleased/replace_search-feature.yml
new file mode 100644
index 00000000000..487f602ba30
--- /dev/null
+++ b/changelogs/unreleased/replace_search-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the 'search.feature' spinach test with an rspec analog
+merge_request: 14248
+author: Vitaliy @blackst0ne Klachkov
+type: other
diff --git a/changelogs/unreleased/sh-optimize-discussion-json.yml b/changelogs/unreleased/sh-optimize-discussion-json.yml
new file mode 100644
index 00000000000..4be1bc89a91
--- /dev/null
+++ b/changelogs/unreleased/sh-optimize-discussion-json.yml
@@ -0,0 +1,5 @@
+---
+title: Eliminate N+1 queries in loading discussions.json endpoint
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-project-feature-eager-load.yml b/changelogs/unreleased/sh-project-feature-eager-load.yml
new file mode 100644
index 00000000000..406ef119a14
--- /dev/null
+++ b/changelogs/unreleased/sh-project-feature-eager-load.yml
@@ -0,0 +1,5 @@
+---
+title: Eliminate N+1 queries referencing issues
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-stop-loading-issue-discussions.yml b/changelogs/unreleased/sh-stop-loading-issue-discussions.yml
new file mode 100644
index 00000000000..5e7b7387c0d
--- /dev/null
+++ b/changelogs/unreleased/sh-stop-loading-issue-discussions.yml
@@ -0,0 +1,5 @@
+---
+title: Remove unnecessary loading of discussions in `IssuesController#show`
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/winh-protected-branch-modal-merged.yml b/changelogs/unreleased/winh-protected-branch-modal-merged.yml
new file mode 100644
index 00000000000..63f1f424a5d
--- /dev/null
+++ b/changelogs/unreleased/winh-protected-branch-modal-merged.yml
@@ -0,0 +1,5 @@
+---
+title: Display whether branch has been merged when deleting protected branch
+merge_request: 14220
+author:
+type: changed
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index d6c3c84851b..6c5c8cad270 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -416,3 +416,39 @@
:why: https://gitlab.com/gitlab-com/organization/issues/117
:versions: []
:when: 2017-09-04 12:59:51.150798717 Z
+- - :approve
+ - console-browserify
+ - :who: Mike Greiling
+ :why: https://github.com/Raynos/console-browserify/blob/f0a8898487e2a47b8a5dc8734b91059fa2825506/LICENCE
+ :versions: []
+ :when: 2017-09-16 05:13:07.073651000 Z
+- - :approve
+ - duplexer
+ - :who: Mike Greiling
+ :why: https://github.com/Raynos/duplexer/blob/master/LICENCE
+ :versions: []
+ :when: 2017-09-16 05:14:15.774643000 Z
+- - :approve
+ - json3
+ - :who: Mike Greiling
+ :why: https://github.com/bestiejs/json3/blob/v3.3.2/LICENSE
+ :versions: []
+ :when: 2017-09-16 05:15:16.273892000 Z
+- - :approve
+ - mime
+ - :who: Mike Greiling
+ :why: https://github.com/broofa/node-mime/blob/v1.3.4/LICENSE
+ :versions: []
+ :when: 2017-09-16 05:16:21.135542000 Z
+- - :approve
+ - querystring-es3
+ - :who: Mike Greiling
+ :why: https://github.com/mike-spainhower/querystring/blob/v0.2.0/License.md
+ :versions: []
+ :when: 2017-09-16 05:17:20.372089000 Z
+- - :approve
+ - utils-merge
+ - :who: Mike Greiling
+ :why: https://github.com/jaredhanson/utils-merge/blob/v1.0.0/LICENSE
+ :versions: []
+ :when: 2017-09-16 05:18:26.193764000 Z
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index 40e635bf2cf..b89f0419b91 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -58,7 +58,7 @@ Doorkeeper.configure do
# For more information go to
# https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes
default_scopes(*Gitlab::Auth::DEFAULT_SCOPES)
- optional_scopes(*Gitlab::Auth::OPTIONAL_SCOPES)
+ optional_scopes(*Gitlab::Auth.optional_scopes)
# Change the way client credentials are retrieved from the request object.
# By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb
index 21fe8d72459..8560d24526f 100644
--- a/config/initializers/lograge.rb
+++ b/config/initializers/lograge.rb
@@ -12,13 +12,18 @@ unless Sidekiq.server?
config.lograge.logger = ActiveSupport::Logger.new(filename)
# Add request parameters to log output
config.lograge.custom_options = lambda do |event|
- {
+ payload = {
time: event.time.utc.iso8601(3),
params: event.payload[:params].except(*%w(controller action format)),
remote_ip: event.payload[:remote_ip],
user_id: event.payload[:user_id],
username: event.payload[:username]
}
+
+ gitaly_calls = Gitlab::GitalyClient.get_request_count
+ payload[:gitaly_calls] = gitaly_calls if gitaly_calls > 0
+
+ payload
end
end
end
diff --git a/config/initializers/postgresql_opclasses_support.rb b/config/initializers/postgresql_opclasses_support.rb
index 820cc89ef57..c2f3023b330 100644
--- a/config/initializers/postgresql_opclasses_support.rb
+++ b/config/initializers/postgresql_opclasses_support.rb
@@ -127,7 +127,7 @@ module ActiveRecord
orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
where = inddef.scan(/WHERE (.+)$/).flatten[0]
using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
- opclasses = Hash[inddef.scan(/\((.+)\)$/).flatten[0].split(',').map do |column_and_opclass|
+ opclasses = Hash[inddef.scan(/\((.+?)\)(?:$| WHERE )/).flatten[0].split(',').map do |column_and_opclass|
column, opclass = column_and_opclass.split(' ').map(&:strip)
[column, opclass] if opclass
end.compact]
diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb
index 943e01f1496..d3a7a2b9f8b 100644
--- a/config/initializers/static_files.rb
+++ b/config/initializers/static_files.rb
@@ -30,7 +30,7 @@ if app.config.serve_static_files
settings.merge!(
host: Gitlab.config.gitlab.host,
port: Gitlab.config.gitlab.port,
- https: Gitlab.config.gitlab.https
+ https: false
)
app.config.middleware.insert_before(
Gitlab::Middleware::Static,
diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb
index 6553c5d457a..1f8f5cfc82b 100644
--- a/db/fixtures/development/04_project.rb
+++ b/db/fixtures/development/04_project.rb
@@ -4,9 +4,9 @@ Sidekiq::Testing.inline! do
Gitlab::Seeder.quiet do
project_urls = [
'https://gitlab.com/gitlab-org/gitlab-test.git',
- 'https://gitlab.com/gitlab-org/gitlab-ce.git',
- 'https://gitlab.com/gitlab-org/gitlab-ci.git',
'https://gitlab.com/gitlab-org/gitlab-shell.git',
+ 'https://gitlab.com/gnuwget/wget2.git',
+ 'https://gitlab.com/Commit451/LabCoat.git',
'https://github.com/documentcloud/underscore.git',
'https://github.com/twitter/flight.git',
'https://github.com/twitter/typeahead.js.git',
diff --git a/db/migrate/20170912113435_clean_stages_statuses_migration.rb b/db/migrate/20170912113435_clean_stages_statuses_migration.rb
new file mode 100644
index 00000000000..fc091d7894e
--- /dev/null
+++ b/db/migrate/20170912113435_clean_stages_statuses_migration.rb
@@ -0,0 +1,26 @@
+class CleanStagesStatusesMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class Stage < ActiveRecord::Base
+ include ::EachBatch
+ self.table_name = 'ci_stages'
+ end
+
+ def up
+ Gitlab::BackgroundMigration.steal('MigrateStageStatus')
+
+ Stage.where('status IS NULL').each_batch(of: 50) do |batch|
+ range = batch.pluck('MIN(id)', 'MAX(id)').first
+
+ Gitlab::BackgroundMigration::MigrateStageStatus.new.perform(*range)
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/migrate/20170914135630_add_index_for_recent_push_events.rb b/db/migrate/20170914135630_add_index_for_recent_push_events.rb
new file mode 100644
index 00000000000..99f593b0465
--- /dev/null
+++ b/db/migrate/20170914135630_add_index_for_recent_push_events.rb
@@ -0,0 +1,40 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexForRecentPushEvents < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index_if_not_present(
+ :merge_requests,
+ [:source_project_id, :source_branch]
+ )
+
+ remove_concurrent_index_if_present(:merge_requests, :source_project_id)
+ end
+
+ def down
+ add_concurrent_index_if_not_present(:merge_requests, :source_project_id)
+
+ remove_concurrent_index_if_present(
+ :merge_requests,
+ [:source_project_id, :source_branch]
+ )
+ end
+
+ def add_concurrent_index_if_not_present(table, columns)
+ return if index_exists?(table, columns)
+
+ add_concurrent_index(table, columns)
+ end
+
+ def remove_concurrent_index_if_present(table, columns)
+ return unless index_exists?(table, columns)
+
+ remove_concurrent_index(table, columns)
+ end
+end
diff --git a/db/migrate/20170918222253_reorganize_deployments_indexes.rb b/db/migrate/20170918222253_reorganize_deployments_indexes.rb
new file mode 100644
index 00000000000..139427ed2b9
--- /dev/null
+++ b/db/migrate/20170918222253_reorganize_deployments_indexes.rb
@@ -0,0 +1,28 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ReorganizeDeploymentsIndexes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_index_if_not_exists :deployments, [:environment_id, :iid, :project_id]
+ remove_index_if_exists :deployments, [:project_id, :environment_id, :iid]
+ end
+
+ def down
+ add_index_if_not_exists :deployments, [:project_id, :environment_id, :iid]
+ remove_index_if_exists :deployments, [:environment_id, :iid, :project_id]
+ end
+
+ def add_index_if_not_exists(table, columns)
+ add_concurrent_index(table, columns) unless index_exists?(table, columns)
+ end
+
+ def remove_index_if_exists(table, columns)
+ remove_concurrent_index(table, columns) if index_exists?(table, columns)
+ end
+end
diff --git a/db/migrate/20170918223303_add_deployments_index_for_last_deployment.rb b/db/migrate/20170918223303_add_deployments_index_for_last_deployment.rb
new file mode 100644
index 00000000000..b91efb86d98
--- /dev/null
+++ b/db/migrate/20170918223303_add_deployments_index_for_last_deployment.rb
@@ -0,0 +1,21 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddDeploymentsIndexForLastDeployment < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ TO_INDEX = [:deployments, %i[environment_id id]].freeze
+
+ def up
+ add_concurrent_index(*TO_INDEX)
+ end
+
+ def down
+ remove_concurrent_index(*TO_INDEX)
+ end
+end
diff --git a/db/post_migrate/20170907170235_delete_conflicting_redirect_routes.rb b/db/post_migrate/20170907170235_delete_conflicting_redirect_routes.rb
new file mode 100644
index 00000000000..3e84b295be4
--- /dev/null
+++ b/db/post_migrate/20170907170235_delete_conflicting_redirect_routes.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 DeleteConflictingRedirectRoutes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ MIGRATION = 'DeleteConflictingRedirectRoutesRange'.freeze
+ BATCH_SIZE = 200 # At 200, I expect under 20s per batch, which is under our query timeout of 60s.
+ DELAY_INTERVAL = 12.seconds
+
+ disable_ddl_transaction!
+
+ class Route < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'routes'
+ end
+
+ def up
+ say opening_message
+
+ queue_background_migration_jobs_by_range_at_intervals(Route, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
+ end
+
+ def down
+ # nothing
+ end
+
+ def opening_message
+ <<~MSG
+ Clean up redirect routes that conflict with regular routes.
+ See initial bug fix:
+ https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13357
+ MSG
+ end
+end
diff --git a/db/post_migrate/20170913180600_fix_projects_without_project_feature.rb b/db/post_migrate/20170913180600_fix_projects_without_project_feature.rb
new file mode 100644
index 00000000000..bfa9ad80c7d
--- /dev/null
+++ b/db/post_migrate/20170913180600_fix_projects_without_project_feature.rb
@@ -0,0 +1,33 @@
+class FixProjectsWithoutProjectFeature < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ # Deletes corrupted project features
+ sql = "DELETE FROM project_features WHERE project_id IS NULL"
+ execute(sql)
+
+ # Creates missing project features with private visibility
+ sql =
+ %Q{
+ INSERT INTO project_features(project_id, repository_access_level, issues_access_level, merge_requests_access_level, wiki_access_level,
+ builds_access_level, snippets_access_level, created_at, updated_at)
+ SELECT projects.id as project_id,
+ 10 as repository_access_level,
+ 10 as issues_access_level,
+ 10 as merge_requests_access_level,
+ 10 as wiki_access_level,
+ 10 as builds_access_level ,
+ 10 as snippets_access_level,
+ projects.created_at,
+ projects.updated_at
+ FROM projects
+ LEFT OUTER JOIN project_features ON project_features.project_id = projects.id
+ WHERE (project_features.id IS NULL)
+ }
+
+ execute(sql)
+ end
+
+ def down
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index dd0ef04788b..3ec430c0078 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20170913131410) do
+ActiveRecord::Schema.define(version: 20170918223303) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -256,7 +256,7 @@ ActiveRecord::Schema.define(version: 20170913131410) do
add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree
add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree
add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
- add_index "ci_builds", ["id"], name: "index_for_ci_builds_retried_migration", where: "(retried IS NULL)", using: :btree, opclasses: {"id)"=>"WHERE"}
+ add_index "ci_builds", ["id"], name: "index_for_ci_builds_retried_migration", where: "(retried IS NULL)", using: :btree
add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
add_index "ci_builds", ["protected"], name: "index_ci_builds_on_protected", using: :btree
add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
@@ -506,7 +506,8 @@ ActiveRecord::Schema.define(version: 20170913131410) do
end
add_index "deployments", ["created_at"], name: "index_deployments_on_created_at", using: :btree
- add_index "deployments", ["project_id", "environment_id", "iid"], name: "index_deployments_on_project_id_and_environment_id_and_iid", using: :btree
+ add_index "deployments", ["environment_id", "id"], name: "index_deployments_on_environment_id_and_id", using: :btree
+ add_index "deployments", ["environment_id", "iid", "project_id"], name: "index_deployments_on_environment_id_and_iid_and_project_id", using: :btree
add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree
create_table "emails", force: :cascade do |t|
@@ -892,7 +893,7 @@ ActiveRecord::Schema.define(version: 20170913131410) do
add_index "merge_requests", ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree
add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree
add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree
- add_index "merge_requests", ["source_project_id"], name: "index_merge_requests_on_source_project_id", using: :btree
+ add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree
add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md
index e99a7ee29cc..1304476e678 100644
--- a/doc/administration/reply_by_email.md
+++ b/doc/administration/reply_by_email.md
@@ -77,6 +77,33 @@ and use [an application password](https://support.google.com/mail/answer/185833)
To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
[Postfix setup documentation](reply_by_email_postfix_setup.md).
+### Security Concerns
+
+**WARNING:** Be careful when choosing the domain used for receiving incoming
+email.
+
+For the sake of example, suppose your top-level company domain is `hooli.com`.
+All employees in your company have an email address at that domain via Google
+Apps, and your company's private Slack instance requires a valid `@hooli.com`
+email address in order to sign up.
+
+If you also host a public-facing GitLab instance at `hooli.com` and set your
+incoming email domain to `hooli.com`, an attacker could abuse the "Create new
+issue by email" feature by using a project's unique address as the email when
+signing up for Slack, which would send a confirmation email, which would create
+a new issue on the project owned by the attacker, allowing them to click the
+confirmation link and validate their account on your company's private Slack
+instance.
+
+We recommend receiving incoming email on a subdomain, such as
+`incoming.hooli.com`, and ensuring that you do not employ any services that
+authenticate solely based on access to an email domain such as `*.hooli.com.`
+Alternatively, use a dedicated domain for GitLab email communications such as
+`hooli-gitlab.com`.
+
+See GitLab issue [#30366](https://gitlab.com/gitlab-org/gitlab-ce/issues/30366)
+for a real-world example of this exploit.
+
### Omnibus package installations
1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the
@@ -141,7 +168,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
# The IDLE command timeout.
gitlab_rails['incoming_email_idle_timeout'] = 60
```
-
+
```ruby
# Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes mailbox incoming@exchange.example.com
gitlab_rails['incoming_email_enabled'] = true
@@ -253,7 +280,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
# The IDLE command timeout.
idle_timeout: 60
```
-
+
```yaml
# Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes mailbox incoming@exchange.example.com
incoming_email:
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 8ca66049d31..cd2cfe8e430 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -95,6 +95,7 @@ Example response:
"username" : "root"
},
"updated_at" : "2016-01-04T15:31:51.081Z",
+ "closed_at" : null,
"id" : 76,
"title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.",
"created_at" : "2016-01-04T15:31:51.081Z",
@@ -205,6 +206,7 @@ Example response:
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z",
+ "closed_at" : null,
"user_notes_count": 1,
"due_date": null,
"web_url": "http://example.com/example/example/issues/1",
@@ -311,6 +313,7 @@ Example response:
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z",
+ "closed_at" : "2016-01-05T15:31:46.176Z",
"user_notes_count": 1,
"due_date": "2016-07-22",
"web_url": "http://example.com/example/example/issues/1",
@@ -358,7 +361,8 @@ Example response:
"id" : 11,
"title" : "v3.0",
"created_at" : "2016-01-04T15:31:39.788Z",
- "updated_at" : "2016-01-04T15:31:39.788Z"
+ "updated_at" : "2016-01-04T15:31:39.788Z",
+ "closed_at" : "2016-01-05T15:31:46.176Z"
},
"author" : {
"state" : "active",
@@ -465,6 +469,7 @@ Example response:
},
"description" : null,
"updated_at" : "2016-01-07T12:44:33.959Z",
+ "closed_at" : null,
"milestone" : null,
"subscribed" : true,
"user_notes_count": 0,
@@ -533,6 +538,7 @@ Example response:
"project_id" : 4,
"description" : null,
"updated_at" : "2016-01-07T12:55:16.213Z",
+ "closed_at" : "2016-01-08T12:55:16.213Z",
"iid" : 15,
"labels" : [
"bug"
@@ -615,6 +621,7 @@ Example response:
"state": "opened",
"created_at": "2016-04-05T21:41:45.652Z",
"updated_at": "2016-04-07T12:20:17.596Z",
+ "closed_at": null,
"labels": [],
"milestone": null,
"assignees": [{
@@ -692,6 +699,7 @@ Example response:
"state": "opened",
"created_at": "2016-04-05T21:41:45.652Z",
"updated_at": "2016-04-07T12:20:17.596Z",
+ "closed_at": null,
"labels": [],
"milestone": null,
"assignees": [{
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 3144220e588..07331d05231 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -635,6 +635,98 @@ POST /projects/:id/fork
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `namespace` | integer/string | yes | The ID or path of the namespace that the project will be forked to |
+## List Forks of a project
+
+>**Note:** This feature was introduced in GitLab 10.1
+
+List the projects accessible to the calling user that have an established, forked relationship with the specified project
+
+```
+GET /projects/:id/forks
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of projects matching the search criteria |
+| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
+| `owned` | boolean | no | Limit by projects owned by the current user |
+| `membership` | boolean | no | Limit by projects that the current user is a member of |
+| `starred` | boolean | no | Limit by projects starred by the current user |
+| `statistics` | boolean | no | Include project statistics |
+| `with_issues_enabled` | boolean | no | Limit by enabled issues feature |
+| `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/forks"
+```
+
+Example responses:
+
+```json
+[
+ {
+ "id": 3,
+ "description": null,
+ "default_branch": "master",
+ "visibility": "internal",
+ "ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
+ "web_url": "http://example.com/diaspora/diaspora-project-site",
+ "tag_list": [
+ "example",
+ "disapora project"
+ ],
+ "name": "Diaspora Project Site",
+ "name_with_namespace": "Diaspora / Diaspora Project Site",
+ "path": "diaspora-project-site",
+ "path_with_namespace": "diaspora/diaspora-project-site",
+ "issues_enabled": true,
+ "open_issues_count": 1,
+ "merge_requests_enabled": true,
+ "jobs_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": false,
+ "resolve_outdated_diff_discussions": false,
+ "container_registry_enabled": false,
+ "created_at": "2013-09-30T13:46:02Z",
+ "last_activity_at": "2013-09-30T13:46:02Z",
+ "creator_id": 3,
+ "namespace": {
+ "id": 3,
+ "name": "Diaspora",
+ "path": "diaspora",
+ "kind": "group",
+ "full_path": "diaspora"
+ },
+ "import_status": "none",
+ "archived": true,
+ "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
+ "shared_runners_enabled": true,
+ "forks_count": 0,
+ "star_count": 1,
+ "public_jobs": true,
+ "shared_with_groups": [],
+ "only_allow_merge_if_pipeline_succeeds": false,
+ "only_allow_merge_if_all_discussions_are_resolved": false,
+ "request_access_enabled": false,
+ "_links": {
+ "self": "http://example.com/api/v4/projects",
+ "issues": "http://example.com/api/v4/projects/1/issues",
+ "merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
+ "repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
+ "labels": "http://example.com/api/v4/projects/1/labels",
+ "events": "http://example.com/api/v4/projects/1/events",
+ "members": "http://example.com/api/v4/projects/1/members"
+ }
+ }
+]
+```
+
## Star a project
Stars a given project. Returns status code `304` if the project is already starred.
diff --git a/doc/api/tags.md b/doc/api/tags.md
index 32fe5eea692..bebe6536b6e 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -131,7 +131,7 @@ Parameters:
"message": null
}
```
-The message will be `nil` when creating a lightweight tag otherwise
+The message will be `null` when creating a lightweight tag otherwise
it will contain the annotation.
In case of an error,
diff --git a/doc/api/wikis.md b/doc/api/wikis.md
index 10eebc4a3c1..15ce5f96b60 100644
--- a/doc/api/wikis.md
+++ b/doc/api/wikis.md
@@ -155,3 +155,5 @@ curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gi
```
On success the HTTP status code is `204` and no JSON response is expected.
+
+[ce-13372]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13372
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index c1362b7bd5b..acd5682841a 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -240,55 +240,18 @@ Remember that if your environment's name is `production` (all lowercase), then
it will get recorded in [Cycle Analytics](../user/project/cycle_analytics.md).
Double the benefit!
-## Web terminals
-
->**Note:**
-Web terminals were added in GitLab 8.15 and are only available to project
-masters and owners.
-
-If you deploy to your environments with the help of a deployment service (e.g.,
-the [Kubernetes service][kubernetes-service], GitLab can open
-a terminal session to your environment! This is a very powerful feature that
-allows you to debug issues without leaving the comfort of your web browser. To
-enable it, just follow the instructions given in the service documentation.
-
-Once enabled, your environments will gain a "terminal" button:
-
-![Terminal button on environment index](img/environments_terminal_button_on_index.png)
-
-You can also access the terminal button from the page for a specific environment:
-
-![Terminal button for an environment](img/environments_terminal_button_on_show.png)
-
-Wherever you find it, clicking the button will take you to a separate page to
-establish the terminal session:
-
-![Terminal page](img/environments_terminal_page.png)
-
-This works just like any other terminal - you'll be in the container created
-by your deployment, so you can run shell commands and get responses in real
-time, check the logs, try out configuration or code tweaks, etc. You can open
-multiple terminals to the same environment - they each get their own shell
-session - and even a multiplexer like `screen` or `tmux`!
-
->**Note:**
-Container-based deployments often lack basic tools (like an editor), and may
-be stopped or restarted at any time. If this happens, you will lose all your
-changes! Treat this as a debugging tool, not a comprehensive online IDE.
-
----
-
-While this is fine for deploying to some stable environments like staging or
-production, what happens for branches? So far we haven't defined anything
-regarding deployments for branches other than `master`. Dynamic environments
-will help us achieve that.
-
## Dynamic environments
As the name suggests, it is possible to create environments on the fly by just
declaring their names dynamically in `.gitlab-ci.yml`. Dynamic environments is
the basis of [Review apps](review_apps/index.md).
+>**Note:**
+The `name` and `url` parameters can use any of the defined CI variables,
+including predefined, secure variables and `.gitlab-ci.yml`
+[`variables`](yaml/README.md#variables).
+You however cannot use variables defined under `script` or on the Runner's side.
+
GitLab Runner exposes various [environment variables][variables] when a job runs,
and as such, you can use them as environment names. Let's add another job in
our example which will deploy to all branches except `master`:
@@ -434,7 +397,8 @@ Let's briefly see where URL that's defined in the environments is exposed.
## Making use of the environment URL
-The environment URL is exposed in a few places within GitLab.
+The [environment URL](yaml/README.md#environments-url) is exposed in a few
+places within GitLab.
| In a merge request widget as a link | In the Environments view as a button | In the Deployments view as a button |
| -------------------- | ------------ | ----------- |
@@ -598,7 +562,7 @@ exist, you should see something like:
>**Notes:**
>
-- For the monitor dashboard to appear, you need to:
+- For the monitoring dashboard to appear, you need to:
- Have enabled the [Prometheus integration][prom]
- Configured Prometheus to collect at least one [supported metric](../user/project/integrations/prometheus_library/metrics.md)
- With GitLab 9.2, all deployments to an environment are shown directly on the
@@ -608,8 +572,7 @@ If you have enabled [Prometheus for monitoring system and response metrics](http
Once configured, GitLab will attempt to retrieve [supported performance metrics](https://docs.gitlab.com/ee/user/project/integrations/prometheus_library/metrics.html) for any
environment which has had a successful deployment. If monitoring data was
-successfully retrieved, a Monitoring button will appear on the environment's
-detail page.
+successfully retrieved, a Monitoring button will appear for each environment.
![Environment Detail with Metrics](img/prometheus_environment_detail_with_metrics.png)
@@ -623,6 +586,49 @@ version of the app, all without leaving GitLab.
![Monitoring dashboard](img/environments_monitoring.png)
+## Web terminals
+
+>**Note:**
+Web terminals were added in GitLab 8.15 and are only available to project
+masters and owners.
+
+If you deploy to your environments with the help of a deployment service (e.g.,
+the [Kubernetes service][kubernetes-service], GitLab can open
+a terminal session to your environment! This is a very powerful feature that
+allows you to debug issues without leaving the comfort of your web browser. To
+enable it, just follow the instructions given in the service documentation.
+
+Once enabled, your environments will gain a "terminal" button:
+
+![Terminal button on environment index](img/environments_terminal_button_on_index.png)
+
+You can also access the terminal button from the page for a specific environment:
+
+![Terminal button for an environment](img/environments_terminal_button_on_show.png)
+
+Wherever you find it, clicking the button will take you to a separate page to
+establish the terminal session:
+
+![Terminal page](img/environments_terminal_page.png)
+
+This works just like any other terminal - you'll be in the container created
+by your deployment, so you can run shell commands and get responses in real
+time, check the logs, try out configuration or code tweaks, etc. You can open
+multiple terminals to the same environment - they each get their own shell
+session - and even a multiplexer like `screen` or `tmux`!
+
+>**Note:**
+Container-based deployments often lack basic tools (like an editor), and may
+be stopped or restarted at any time. If this happens, you will lose all your
+changes! Treat this as a debugging tool, not a comprehensive online IDE.
+
+---
+
+While this is fine for deploying to some stable environments like staging or
+production, what happens for branches? So far we haven't defined anything
+regarding deployments for branches other than `master`. Dynamic environments
+will help us achieve that.
+
## Checkout deployments locally
Since 8.13, a reference in the git repository is saved for each deployment, so
diff --git a/doc/ci/img/environments_monitoring.png b/doc/ci/img/environments_monitoring.png
index d9c46ea4c95..dcffdd1fdb8 100644
--- a/doc/ci/img/environments_monitoring.png
+++ b/doc/ci/img/environments_monitoring.png
Binary files differ
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 88e53ff40e8..2d56b2540ef 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -106,7 +106,7 @@ What is important is that each job is run independently from each other.
If you want to check whether your `.gitlab-ci.yml` file is valid, there is a
Lint tool under the page `/ci/lint` of your GitLab instance. You can also find
-a "CI Lint" button to go to this page under **Pipelines ➔ Pipelines** and
+a "CI Lint" button to go to this page under **CI/CD ➔ Pipelines** and
**Pipelines ➔ Jobs** in your project.
For more information and a complete `.gitlab-ci.yml` syntax, please read
@@ -155,7 +155,7 @@ Find more information about different Runners in the
[Runners](../runners/README.md) documentation.
You can find whether any Runners are assigned to your project by going to
-**Settings ➔ Pipelines**. Setting up a Runner is easy and straightforward. The
+**Settings ➔ CI/CD**. Setting up a Runner is easy and straightforward. The
official Runner supported by GitLab is written in Go and its documentation
can be found at <https://docs.gitlab.com/runner/>.
@@ -168,7 +168,7 @@ Follow the links above to set up your own Runner or use a Shared Runner as
described in the next section.
Once the Runner has been set up, you should see it on the Runners page of your
-project, following **Settings ➔ Pipelines**.
+project, following **Settings ➔ CI/CD**.
![Activated runners](img/runners_activated.png)
@@ -181,7 +181,7 @@ These are special virtual machines that run on GitLab's infrastructure and can
build any project.
To enable the **Shared Runners** you have to go to your project's
-**Settings ➔ Pipelines** and click **Enable shared runners**.
+**Settings ➔ CI/CD** and click **Enable shared runners**.
[Read more on Shared Runners](../runners/README.md).
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index bac8e972754..8b51d112a2c 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -35,7 +35,7 @@ are:
A Runner that is specific only runs for the specified project(s). A shared Runner
can run jobs for every project that has enabled the option **Allow shared Runners**
-under **Settings ➔ Pipelines**.
+under **Settings ➔ CI/CD**.
Projects with high demand of CI activity can also benefit from using specific
Runners. By having dedicated Runners you are guaranteed that the Runner is not
@@ -61,7 +61,7 @@ You can only register a shared Runner if you are an admin of the GitLab instance
Shared Runners are enabled by default as of GitLab 8.2, but can be disabled
with the **Disable shared Runners** button which is present under each project's
-**Settings ➔ Pipelines** page. Previous versions of GitLab defaulted shared
+**Settings ➔ CI/CD** page. Previous versions of GitLab defaulted shared
Runners to disabled.
## Registering a specific Runner
@@ -76,7 +76,7 @@ Registering a specific can be done in two ways:
To create a specific Runner without having admin rights to the GitLab instance,
visit the project you want to make the Runner work for in GitLab:
-1. Go to **Settings ➔ Pipelines** to obtain the token
+1. Go to **Settings ➔ CI/CD** to obtain the token
1. [Register the Runner][register]
### Making an existing shared Runner specific
@@ -101,7 +101,7 @@ can be changed afterwards under each Runner's settings.
To lock/unlock a Runner:
-1. Visit your project's **Settings ➔ Pipelines**
+1. Visit your project's **Settings ➔ CI/CD**
1. Find the Runner you wish to lock/unlock and make sure it's enabled
1. Click the pencil button
1. Check the **Lock to current projects** option
@@ -115,7 +115,7 @@ you can enable the Runner also on any other project where you have Master permis
To enable/disable a Runner in your project:
-1. Visit your project's **Settings ➔ Pipelines**
+1. Visit your project's **Settings ➔ CI/CD**
1. Find the Runner you wish to enable/disable
1. Click **Enable for this project** or **Disable for this project**
@@ -136,7 +136,7 @@ Whenever a Runner is protected, the Runner picks only jobs created on
To protect/unprotect Runners:
-1. Visit your project's **Settings ➔ Pipelines**
+1. Visit your project's **Settings ➔ CI/CD**
1. Find a Runner you want to protect/unprotect and make sure it's enabled
1. Click the pencil button besides the Runner name
1. Check the **Protected** option
@@ -220,7 +220,7 @@ each Runner's settings.
To make a Runner pick tagged/untagged jobs:
-1. Visit your project's **Settings ➔ Pipelines**
+1. Visit your project's **Settings ➔ CI/CD**
1. Find the Runner you wish and make sure it's enabled
1. Click the pencil button
1. Check the **Run untagged jobs** option
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index cdb9858e179..e5a2bbd1773 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -34,7 +34,7 @@ instructions to [generate an SSH key](../../ssh/README.md). Do not add a
passphrase to the SSH key, or the `before_script` will prompt for it.
Then, create a new **Secret Variable** in your project settings on GitLab
-following **Settings > Pipelines** and look for the "Secret Variables" section.
+following **Settings > CI/CD** and look for the "Secret Variables" section.
As **Key** add the name `SSH_PRIVATE_KEY` and in the **Value** field paste the
content of your _private_ key that you created earlier.
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index 7ec7136d8c6..56a16f77e7f 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -19,7 +19,7 @@ A unique trigger token can be obtained when [adding a new trigger](#adding-a-new
## Adding a new trigger
You can add a new trigger by going to your project's
-**Settings ➔ Pipelines** under **Triggers**. The **Add trigger** button will
+**Settings ➔ CI/CD** under **Triggers**. The **Add trigger** button will
create a new token which you can then use to trigger a rerun of this
particular project's pipeline.
@@ -43,7 +43,7 @@ From now on the trigger will be run as you.
## Revoking a trigger
You can revoke a trigger any time by going at your project's
-**Settings ➔ Pipelines** under **Triggers** and hitting the **Revoke** button.
+**Settings ➔ CI/CD** under **Triggers** and hitting the **Revoke** button.
The action is irreversible.
## Triggering a pipeline
@@ -64,7 +64,7 @@ POST /projects/:id/trigger/pipeline
The required parameters are the [trigger's `token`](#authentication-tokens)
and the Git `ref` on which the trigger will be performed. Valid refs are the
branch and the tag. The `:id` of a project can be found by
-[querying the API](../../api/projects.md) or by visiting the **Pipelines**
+[querying the API](../../api/projects.md) or by visiting the **CI/CD**
settings page which provides self-explanatory examples.
When a rerun of a pipeline is triggered, the information is exposed in GitLab's
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 6513b31826a..ebcb92b5db1 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -158,17 +158,17 @@ script:
settings. Follow the discussion in issue [#13784][ce-13784] for masking the
secret variables.
-GitLab CI allows you to define per-project or per-group **secret variables**
-that are set in the build environment. The secret variables are stored out of
-the repository (`.gitlab-ci.yml`) and are securely passed to GitLab Runner
-making them available in the build environment. It's the recommended method to
-use for storing things like passwords, secret keys and credentials.
+GitLab CI allows you to define per-project or per-group secret variables
+that are set in the pipeline environment. The secret variables are stored out of
+the repository (not in `.gitlab-ci.yml`) and are securely passed to GitLab Runner
+making them available during a pipeline run. It's the recommended method to
+use for storing things like passwords, SSH keys and credentials.
Project-level secret variables can be added by going to your project's
-**Settings ➔ Pipelines**, then finding the section called **Secret variables**.
+**Settings > CI/CD**, then finding the section called **Secret variables**.
Likewise, group-level secret variables can be added by going to your group's
-**Settings ➔ Pipelines**, then finding the section called **Secret variables**.
+**Settings > CI/CD**, then finding the section called **Secret variables**.
Any variables of [subgroups] will be inherited recursively.
Once you set them, they will be available for all subsequent pipelines. You can also
@@ -185,8 +185,8 @@ protected, it would only be securely passed to pipelines running on the
protected variables.
Protected variables can be added by going to your project's
-**Settings ➔ Pipelines**, then finding the section called
-**Secret variables**, and check *Protected*.
+**Settings > CI/CD**, then finding the section called
+**Secret variables**, and check "Protected".
Once you set them, they will be available for all subsequent pipelines.
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 78733b9cc4b..aad81843299 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -252,6 +252,8 @@ The `cache:key` variable can use any of the [predefined variables](../variables/
The default key is **default** across the project, therefore everything is
shared between each pipelines and jobs by default, starting from GitLab 9.0.
+>**Note:** The `cache:key` variable cannot contain the `/` character.
+
---
**Example configurations**
@@ -276,7 +278,7 @@ To enable per-job and per-branch caching:
```yaml
cache:
- key: "$CI_JOB_NAME/$CI_COMMIT_REF_NAME"
+ key: "$CI_JOB_NAME-$CI_COMMIT_REF_NAME"
untracked: true
```
@@ -284,7 +286,7 @@ To enable per-branch and per-stage caching:
```yaml
cache:
- key: "$CI_JOB_STAGE/$CI_COMMIT_REF_NAME"
+ key: "$CI_JOB_STAGE-$CI_COMMIT_REF_NAME"
untracked: true
```
@@ -293,7 +295,7 @@ If you use **Windows Batch** to run your shell scripts you need to replace
```yaml
cache:
- key: "%CI_JOB_STAGE%/%CI_COMMIT_REF_NAME%"
+ key: "%CI_JOB_STAGE%-%CI_COMMIT_REF_NAME%"
untracked: true
```
@@ -302,7 +304,7 @@ If you use **Windows PowerShell** to run your shell scripts you need to replace
```yaml
cache:
- key: "$env:CI_JOB_STAGE/$env:CI_COMMIT_REF_NAME"
+ key: "$env:CI_JOB_STAGE-$env:CI_COMMIT_REF_NAME"
untracked: true
```
@@ -727,6 +729,9 @@ deployment to the `production` environment.
- Before GitLab 8.11, the name of an environment could be defined as a string like
`environment: production`. The recommended way now is to define it under the
`name` keyword.
+- The `name` parameter can use any of the defined CI variables,
+ including predefined, secure variables and `.gitlab-ci.yml` [`variables`](#variables).
+ You however cannot use variables defined under `script`.
The `environment` name can contain:
@@ -762,6 +767,9 @@ deploy to production:
- Introduced in GitLab 8.11.
- Before GitLab 8.11, the URL could be added only in GitLab's UI. The
recommended way now is to define it in `.gitlab-ci.yml`.
+- The `url` parameter can use any of the defined CI variables,
+ including predefined, secure variables and `.gitlab-ci.yml` [`variables`](#variables).
+ You however cannot use variables defined under `script`.
This is an optional value that when set, it exposes buttons in various places
in GitLab which when clicked take you to the defined URL.
@@ -841,10 +849,9 @@ The `stop_review_app` job is **required** to have the following keywords defined
**Notes:**
- [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6.
- The `$CI_ENVIRONMENT_SLUG` was [introduced][ce-7983] in GitLab 8.15.
-
-`environment` can also represent a configuration hash with `name` and `url`.
-These parameters can use any of the defined [CI variables](#variables)
-(including predefined, secure variables and `.gitlab-ci.yml` variables).
+- The `name` and `url` parameters can use any of the defined CI variables,
+ including predefined, secure variables and `.gitlab-ci.yml` [`variables`](#variables).
+ You however cannot use variables defined under `script`.
For example:
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 64a89976300..7165b8062a7 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -9,8 +9,18 @@ There are a few rules to get your merge request accepted:
**approved by a [backend maintainer][projects]**.
1. If your merge request includes only frontend changes [^1], it must be
**approved by a [frontend maintainer][projects]**.
+ 1. If your merge request includes UX changes [^1], it must
+ be **approved by a [UX team member][team]**.
+ 1. If your merge request includes adding a new JavaScript library [^1], it must be
+ **approved by a [frontend lead][team]**.
+ 1. If your merge request includes adding a new UI/UX paradigm [^1], it must be
+ **approved by a [UX lead][team]**.
1. If your merge request includes frontend and backend changes [^1], it must
be **approved by a [frontend and a backend maintainer][projects]**.
+ 1. If your merge request includes UX and frontend changes [^1], it must
+ be **approved by a [UX team member and a frontend maintainer][team]**.
+ 1. If your merge request includes UX, frontend and backend changes [^1], it must
+ be **approved by a [UX team member, a frontend and a backend maintainer][team]**.
1. If your merge request includes a new dependency or a filesystem change, it must
be **approved by a [Build team member][team]**. See [how to work with the Build team][build handbook] for more details.
1. To lower the amount of merge requests maintainers need to review, you can
diff --git a/doc/development/img/manual_build_docs.png b/doc/development/img/manual_build_docs.png
index fef767c2a79..615facabb5f 100644
--- a/doc/development/img/manual_build_docs.png
+++ b/doc/development/img/manual_build_docs.png
Binary files differ
diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md
index 479258f743e..b1eb020a592 100644
--- a/doc/development/writing_documentation.md
+++ b/doc/development/writing_documentation.md
@@ -106,21 +106,84 @@ CE and EE.
## Previewing the changes live
-If you want to preview your changes live, you can use the manual `build-docs`
-job in your merge request.
+If you want to preview the doc changes of your merge request live, you can use
+the manual `review-docs-deploy` job in your merge request.
+
+TIP: **Tip:**
+If your branch contains only documentation changes, you can use
+[special branch names](#testing) to avoid long running pipelines.
![Manual trigger a docs build](img/manual_build_docs.png)
This job will:
1. Create a new branch in the [gitlab-docs](https://gitlab.com/gitlab-com/gitlab-docs)
- project named after the scheme: `<CE/EE-branch-slug>-built-from-ce-ee`
-1. Trigger a pipeline and build the docs site with your changes
-
-Look for the docs URL at the output of the `build-docs` job.
-
->**Note:**
+ project named after the scheme: `preview-<branch-slug>`
+1. Trigger a cross project pipeline and build the docs site with your changes
+
+After a few minutes, the Review App will be deployed and you will be able to
+preview the changes. The docs URL can be found in two places:
+
+- In the merge request widget
+- In the output of the `review-docs-deploy` job, which also includes the
+ triggered pipeline so that you can investigate whether something went wrong
+
+In case the Review App URL returns 404, follow these steps to debug:
+
+1. **Did you follow the URL from the merge request widget?** If yes, then check if
+ the link is the same as the one in the job output. It can happen that if the
+ branch name slug is longer than 35 characters, it is automatically
+ truncated. That means that the merge request widget will not show the proper
+ URL due to a limitation of how `environment: url` works, but you can find the
+ real URL from the output of the `review-docs-deploy` job.
+1. **Did you follow the URL from the job output?** If yes, then it means that
+ either the site is not yet deployed or something went wrong with the remote
+ pipeline. Give it a few minutes and it should appear online, otherwise you
+ can check the status of the remote pipeline from the link in the job output.
+ If the pipeline failed or got stuck, drop a line in the `#docs` chat channel.
+
+TIP: **Tip:**
+Someone that has no merge rights to the CE/EE projects (think of forks from
+contributors) will not be able to run the manual job. In that case, you can
+ask someone from the GitLab team who has the permissions to do that for you.
+
+NOTE: **Note:**
Make sure that you always delete the branch of the merge request you were
working on. If you don't, the remote docs branch won't be removed either,
and the server where the Review Apps are hosted will eventually be out of
disk space.
+
+### Behind the scenes
+
+If you want to know the hot details, here's what's really happening:
+
+1. You manually run the `review-docs-deploy` job in a CE/EE merge request.
+1. The job runs the [`scirpts/trigger-build-docs`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/trigger-build-docs)
+ script with the `deploy` flag, which in turn:
+ 1. Takes your branch name and applies the following:
+ - The slug of the branch name is used to avoid special characters since
+ ultimately this will be used by NGINX.
+ - The `preview-` prefix is added to avoid conflicts if there's a remote branch
+ with the same name that you created in the merge request.
+ - The final branch name is truncated to 42 characters to avoid filesystem
+ limitations with long branch names (> 63 chars).
+ 1. The remote branch is then created if it doesn't exist (meaning you can
+ re-run the manual job as many times as you want and this step will be skipped).
+ 1. A new cross-project pipeline is triggered in the docs project.
+ 1. The preview URL is shown both at the job output and in the merge request
+ widget. You also get the link to the remote pipeline.
+1. In the docs project, the pipeline is created and it
+ [skips the test jobs](https://gitlab.com/gitlab-com/gitlab-docs/blob/8d5d5c750c602a835614b02f9db42ead1c4b2f5e/.gitlab-ci.yml#L50-55)
+ to lower the build time.
+1. Once the docs site is built, the HTML files are uploaded as artifacts.
+1. A specific Runner tied only to the docs project, runs the Review App job
+ that downloads the artifacts and uses `rsync` to transfer the files over
+ to a location where NGINX serves them.
+
+The following GitLab features are used among others:
+
+- [Manual actions](../ci/yaml/README.md#manual-actions)
+- [Multi project pipelines](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html)
+- [Review Apps](../ci/review_apps/index.md)
+- [Artifacts](../ci/yaml/README.md#artifacts)
+- [Specific Runner](../ci/runners/README.md#locking-a-specific-runner-from-being-enabled-for-other-projects)
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index db0242f1324..a1671f9dd91 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -188,6 +188,27 @@ Besides giving you the option to edit any settings you've previously
set when [creating the group](#create-a-new-group), you can also
access further configurations for your group.
+#### Changing a group's path
+
+> **Note:** If you want to retain ownership over the original namespace and
+protect the URL redirects, then instead of changing a group's path or renaming a
+username, you can create a new group and transfer projects to it.
+
+Changing a group's path can have unintended side effects.
+
+* Existing web URLs for the group and anything under it (i.e. projects) will
+redirect to the new URLs
+* Existing Git remote URLs for projects under the group will no longer work, but
+Git responses will show an error with the new remote URL
+* The original namespace can be claimed again by any group or user, which will
+destroy web redirects and Git remote warnings
+* If you are vacating the path so it can be claimed by another group or user,
+you may need to rename the group name as well since both names and paths must be
+unique
+
+> It is currently not possible to rename a namespace if it contains a
+project with container registry tags, because the project cannot be moved.
+
#### Enforce 2FA to group members
Add a security layer to your group by
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 0c17905aa8c..44ee994a26b 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -253,7 +253,7 @@ only.
[^1]: On public and internal projects, all users are able to perform this action.
[^2]: Guest users can only view the confidential issues they created themselves
-[^3]: If **Public pipelines** is enabled in **Project Settings > Pipelines**
+[^3]: If **Public pipelines** is enabled in **Project Settings > CI/CD**
[^4]: Not allowed for Guest, Reporter, Developer, Master, or Owner
[^5]: Only if user is not external one.
[^6]: Only if user is a member of the project.
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 7d25970fcb1..5ebb88bf324 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -8,10 +8,27 @@ experience according to the best approach to their cases.
Your `username` is a unique [`namespace`](../group/index.md#namespaces)
related to your user ID.
+### Changing your username
+
You can change your `username` from your
-[profile settings](#profile-settings). To avoid breaking
-paths when you change your `username`, we suggest you follow
-[this procedure from the GitLab Team Handbook](https://about.gitlab.com/handbook/tools-and-tips/#how-to-change-your-username-at-gitlabcom).
+[profile settings](#profile-settings).
+
+> **Note:** If you want to retain ownership over the original namespace and
+protect the URL redirects, then instead of changing your username, you can
+create a new group and transfer projects to it.
+Alternatively, you can follow [this detailed procedure from the GitLab Team Handbook](https://about.gitlab.com/handbook/tools-and-tips/#how-to-change-your-username-at-gitlabcom).
+
+Changing your username can have unintended side effects.
+
+* Existing web URLs for the user and anything under it (i.e. projects) will
+redirect to the new URLs
+* Existing Git remote URLs for projects under the user will no longer work, but
+Git responses will show an error with the new remote URL
+* The original namespace can be claimed again by any group or user, which will
+destroy any web redirects and Git remote warnings
+
+> It is currently not possible to rename a namespace if it contains a
+project with container registry tags, because the project cannot be moved.
## User profile
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
index dbc1305101f..56f58fd755a 100644
--- a/doc/user/project/pipelines/settings.md
+++ b/doc/user/project/pipelines/settings.md
@@ -1,7 +1,7 @@
# Pipelines settings
To reach the pipelines settings navigate to your project's
-**Settings ➔ Pipelines**.
+**Settings ➔ CI/CD**.
The following settings can be configured per project.
diff --git a/doc/user/project/repository/img/compare_branches.png b/doc/user/project/repository/img/compare_branches.png
index 353bd72ef4e..d7ab587f030 100755..100644
--- a/doc/user/project/repository/img/compare_branches.png
+++ b/doc/user/project/repository/img/compare_branches.png
Binary files differ
diff --git a/features/project/archived.feature b/features/project/archived.feature
deleted file mode 100644
index ad466f4f307..00000000000
--- a/features/project/archived.feature
+++ /dev/null
@@ -1,30 +0,0 @@
-Feature: Project Archived
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And I own project "Forum"
-
- Scenario: I should not see archived on project page of not-archive project
- And project "Forum" is archived
- And I visit project "Shop" page
- Then I should not see "Archived"
-
- Scenario: I should see archived on project page of archive project
- And project "Forum" is archived
- And I visit project "Forum" page
- Then I should see "Archived"
-
- Scenario: I archive project
- When project "Shop" has push event
- And I visit project "Shop" page
- And I visit edit project "Shop" page
- And I set project archived
- Then I should see "Archived"
-
- Scenario: I unarchive project
- When project "Shop" has push event
- And project "Shop" is archived
- And I visit project "Shop" page
- And I visit edit project "Shop" page
- And I set project unarchived
- Then I should not see "Archived"
diff --git a/features/project/commits/revert.feature b/features/project/commits/revert.feature
deleted file mode 100644
index 7ee1d717d80..00000000000
--- a/features/project/commits/revert.feature
+++ /dev/null
@@ -1,31 +0,0 @@
-@project_commits
-Feature: Revert Commits
- Background:
- Given I sign in as a user
- And I own a project
- And I visit my project's commits page
-
- @javascript
- Scenario: I revert a commit
- Given I click on commit link
- And I click on the revert button
- And I revert the changes directly
- Then I should see the revert commit notice
-
- @javascript
- Scenario: I revert a commit that was previously reverted
- Given I click on commit link
- And I click on the revert button
- And I revert the changes directly
- And I visit my project's commits page
- And I click on commit link
- And I click on the revert button
- And I revert the changes directly
- Then I should see a revert error
-
- @javascript
- Scenario: I revert a commit in a new merge request
- Given I click on commit link
- And I click on the revert button
- And I revert the changes in a new merge request
- Then I should see the new merge request notice
diff --git a/features/project/snippets.feature b/features/project/snippets.feature
deleted file mode 100644
index 50bc4c93df3..00000000000
--- a/features/project/snippets.feature
+++ /dev/null
@@ -1,35 +0,0 @@
-Feature: Project Snippets
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And project "Shop" have "Snippet one" snippet
- And project "Shop" have no "Snippet two" snippet
- And I visit project "Shop" snippets page
-
- Scenario: I should see snippets
- Given I visit project "Shop" snippets page
- Then I should see "Snippet one" in snippets
- And I should not see "Snippet two" in snippets
-
- @javascript
- Scenario: I create new project snippet
- Given I click link "New snippet"
- And I submit new snippet "Snippet three"
- Then I should see snippet "Snippet three"
-
- @javascript
- Scenario: I comment on a snippet "Snippet one"
- Given I visit snippet page "Snippet one"
- And I leave a comment like "Good snippet!"
- Then I should see comment "Good snippet!"
-
- Scenario: I update "Snippet one"
- Given I visit snippet page "Snippet one"
- And I click link "Edit"
- And I submit new title "Snippet new title"
- Then I should see "Snippet new title"
-
- Scenario: I destroy "Snippet one"
- Given I visit snippet page "Snippet one"
- And I click link "Delete"
- Then I should not see "Snippet one" in snippets
diff --git a/features/search.feature b/features/search.feature
deleted file mode 100644
index f894b6b84a1..00000000000
--- a/features/search.feature
+++ /dev/null
@@ -1,100 +0,0 @@
-@dashboard
-Feature: Search
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And I visit dashboard search page
-
- Scenario: I should see project I am looking for
- Given I search for "Sho"
- Then I should see "Shop" project link
-
- @javascript
- Scenario: I should see issues I am looking for
- And project has issues
- When I search for "Foo"
- And I click "Issues" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- @javascript
- Scenario: I should see merge requests I am looking for
- And project has merge requests
- When I search for "Foo"
- When I click "Merge requests" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- @javascript
- Scenario: I should see milestones I am looking for
- And project has milestones
- When I search for "Foo"
- When I click "Milestones" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- @javascript
- Scenario: I should see project code I am looking for
- When I click project "Shop" link
- And I search for "rspec"
- Then I should see code results for project "Shop"
-
- @javascript
- Scenario: I should see project issues
- And project has issues
- When I click project "Shop" link
- And I search for "Foo"
- And I click "Issues" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- @javascript
- Scenario: I should see project merge requests
- And project has merge requests
- When I click project "Shop" link
- And I search for "Foo"
- And I click "Merge requests" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- @javascript
- Scenario: I should see project milestones
- And project has milestones
- When I click project "Shop" link
- And I search for "Foo"
- And I click "Milestones" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- @javascript
- Scenario: I should see Wiki blobs
- And project has Wiki content
- When I click project "Shop" link
- And I search for "Wiki content"
- And I click "Wiki" link
- Then I should see "test_wiki" link in the search results
-
- Scenario: I logout and should see project I am looking for
- Given project "Shop" is public
- And I logout directly
- And I visit dashboard search page
- And I search for "Sho"
- Then I should see "Shop" project link
-
- @javascript
- Scenario: I logout and should see issues I am looking for
- Given project "Shop" is public
- And I logout directly
- And I visit dashboard search page
- And project has issues
- When I search for "Foo"
- And I click "Issues" link
- Then I should see "Foo" link in the search results
- And I should not see "Bar" link in the search results
-
- Scenario: I logout and should see project code I am looking for
- Given project "Shop" is public
- And I logout directly
- When I visit project "Shop" page
- And I search for "rspec" on project page
- Then I should see code results for project "Shop"
diff --git a/features/steps/project/archived.rb b/features/steps/project/archived.rb
deleted file mode 100644
index e4847180be9..00000000000
--- a/features/steps/project/archived.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-class Spinach::Features::ProjectArchived < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
-
- When 'project "Forum" is archived' do
- project = Project.find_by(name: "Forum")
- project.update_attribute(:archived, true)
- end
-
- When 'project "Shop" is archived' do
- project = Project.find_by(name: "Shop")
- project.update_attribute(:archived, true)
- end
-
- When 'I visit project "Forum" page' do
- project = Project.find_by(name: "Forum")
- visit project_path(project)
- end
-
- step 'I should not see "Archived"' do
- expect(page).not_to have_content "Archived"
- end
-
- step 'I should see "Archived"' do
- expect(page).to have_content "Archived"
- end
-
- When 'I set project archived' do
- click_link "Archive"
- end
-
- When 'I set project unarchived' do
- click_link "Unarchive"
- end
-end
diff --git a/features/steps/project/commits/revert.rb b/features/steps/project/commits/revert.rb
deleted file mode 100644
index ebfa7a878bb..00000000000
--- a/features/steps/project/commits/revert.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-class Spinach::Features::RevertCommits < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedPaths
- include SharedDiffNote
- include RepoHelpers
-
- step 'I click on commit link' do
- visit project_commit_path(@project, sample_commit.id)
- end
-
- step 'I click on the revert button' do
- find(".header-action-buttons .dropdown").click
- find("a[href='#modal-revert-commit']").click
- end
-
- step 'I revert the changes directly' do
- page.within('#modal-revert-commit') do
- uncheck 'create_merge_request'
- click_button 'Revert'
- end
- end
-
- step 'I should see the revert commit notice' do
- page.should have_content('The commit has been successfully reverted.')
- end
-
- step 'I should see a revert error' do
- page.should have_content('Sorry, we cannot revert this commit automatically.')
- end
-
- step 'I revert the changes in a new merge request' do
- page.within('#modal-revert-commit') do
- click_button 'Revert'
- end
- end
-
- step 'I should see the new merge request notice' do
- page.should have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.')
- page.should have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master")
- end
-end
diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb
deleted file mode 100644
index 2a1e6b2bce8..00000000000
--- a/features/steps/project/snippets.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedProject
- include SharedNote
- include SharedPaths
- include WaitForRequests
-
- step 'project "Shop" have "Snippet one" snippet' do
- create(:project_snippet,
- title: "Snippet one",
- content: "Test content",
- file_name: "snippet.rb",
- project: project,
- author: project.users.first)
- end
-
- step 'project "Shop" have no "Snippet two" snippet' do
- create(:snippet,
- title: "Snippet two",
- content: "Test content",
- file_name: "snippet.rb",
- author: project.users.first)
- end
-
- step 'I click link "New snippet"' do
- page.within '.nav-controls' do
- first(:link, "New snippet").click
- end
- end
-
- step 'I click link "Snippet one"' do
- click_link "Snippet one"
- end
-
- step 'I should see "Snippet one" in snippets' do
- expect(page).to have_content "Snippet one"
- end
-
- step 'I should not see "Snippet two" in snippets' do
- expect(page).not_to have_content "Snippet two"
- end
-
- step 'I should not see "Snippet one" in snippets' do
- expect(page).not_to have_content "Snippet one"
- end
-
- step 'I click link "Edit"' do
- page.within ".detail-page-header" do
- first(:link, "Edit").click
- end
- end
-
- step 'I click link "Delete"' do
- first(:link, "Delete").click
- end
-
- step 'I submit new snippet "Snippet three"' do
- fill_in "project_snippet_title", with: "Snippet three"
- fill_in "project_snippet_file_name", with: "my_snippet.rb"
- page.within('.file-editor') do
- find('.ace_editor').native.send_keys 'Content of snippet three'
- end
- click_button "Create snippet"
- wait_for_requests
- end
-
- step 'I should see snippet "Snippet three"' do
- expect(page).to have_content "Snippet three"
- expect(page).to have_content "Content of snippet three"
- end
-
- step 'I submit new title "Snippet new title"' do
- fill_in "project_snippet_title", with: "Snippet new title"
- click_button "Save"
- end
-
- step 'I should see "Snippet new title"' do
- expect(page).to have_content "Snippet new title"
- end
-
- step 'I leave a comment like "Good snippet!"' do
- page.within('.js-main-target-form') do
- fill_in "note_note", with: "Good snippet!"
- click_button "Comment"
- end
- wait_for_requests
- end
-
- step 'I should see comment "Good snippet!"' do
- expect(page).to have_content "Good snippet!"
- end
-
- step 'I visit snippet page "Snippet one"' do
- visit project_snippet_path(project, project_snippet)
- end
-
- def project_snippet
- @project_snippet ||= ProjectSnippet.find_by!(title: "Snippet one")
- end
-end
diff --git a/features/steps/search.rb b/features/steps/search.rb
deleted file mode 100644
index 16c4a5ab2e4..00000000000
--- a/features/steps/search.rb
+++ /dev/null
@@ -1,116 +0,0 @@
-class Spinach::Features::Search < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedProject
-
- step 'I search for "Sho"' do
- fill_in "dashboard_search", with: "Sho"
- click_button "Search"
- end
-
- step 'I search for "Foo"' do
- fill_in "dashboard_search", with: "Foo"
- find('.btn-search').trigger('click')
- end
-
- step 'I search for "rspec"' do
- fill_in "dashboard_search", with: "rspec"
- find('.btn-search').trigger('click')
- end
-
- step 'I search for "rspec" on project page' do
- fill_in "search", with: "rspec"
- click_button "Go"
- end
-
- step 'I search for "Wiki content"' do
- fill_in "dashboard_search", with: "content"
- find('.btn-search').trigger('click')
- end
-
- step 'I click "Issues" link' do
- page.within '.search-filter' do
- click_link 'Issues'
- end
- end
-
- step 'I click project "Shop" link' do
- find('.js-search-project-dropdown').trigger('click')
- page.within '.project-filter' do
- click_link project.name_with_namespace
- end
- end
-
- step 'I click "Merge requests" link' do
- page.within '.search-filter' do
- click_link 'Merge requests'
- end
- end
-
- step 'I click "Milestones" link' do
- page.within '.search-filter' do
- click_link 'Milestones'
- end
- end
-
- step 'I click "Wiki" link' do
- page.within '.search-filter' do
- click_link 'Wiki'
- end
- end
-
- step 'I should see "Shop" project link' do
- expect(page).to have_link "Shop"
- end
-
- step 'I should see code results for project "Shop"' do
- page.within('.results') do
- page.should have_content 'Update capybara, rspec-rails, poltergeist to recent versions'
- end
- end
-
- step 'I search for "Contibuting"' do
- fill_in "dashboard_search", with: "Contibuting"
- click_button "Search"
- end
-
- step 'project has issues' do
- create(:issue, title: "Foo", project: project)
- create(:issue, title: "Bar", project: project)
- end
-
- step 'project has merge requests' do
- create(:merge_request, title: "Foo", source_project: project, target_project: project)
- create(:merge_request, :simple, title: "Bar", source_project: project, target_project: project)
- end
-
- step 'project has milestones' do
- create(:milestone, title: "Foo", project: project)
- create(:milestone, title: "Bar", project: project)
- end
-
- step 'I should see "Foo" link in the search results' do
- page.within('.results') do
- find(:css, '.search-results').should have_link 'Foo'
- end
- end
-
- step 'I should not see "Bar" link in the search results' do
- expect(find(:css, '.search-results')).not_to have_link 'Bar'
- end
-
- step 'I should see "test_wiki" link in the search results' do
- page.within('.results') do
- expect(find(:css, '.search-results')).to have_link 'test_wiki'
- end
- end
-
- step 'project has Wiki content' do
- @wiki = ::ProjectWiki.new(project, current_user)
- @wiki.create_page("test_wiki", "Some Wiki content", :markdown, "first commit")
- end
-
- step 'project "Shop" is public' do
- project.update_attributes(visibility_level: Project::PUBLIC)
- end
-end
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 642c1140fcc..643c8e6fb8e 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -21,7 +21,10 @@ module API
get ':id/repository/branches' do
branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name))
- present paginate(branches), with: Entities::RepoBranch, project: user_project
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37442
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ present paginate(branches), with: Entities::RepoBranch, project: user_project
+ end
end
resource ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 52c49e5caa9..71253f72533 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -244,7 +244,10 @@ module API
end
expose :merged do |repo_branch, options|
- options[:project].repository.merged_to_root_ref?(repo_branch.name)
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37442
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ options[:project].repository.merged_to_root_ref?(repo_branch.name)
+ end
end
expose :protected do |repo_branch, options|
@@ -332,6 +335,7 @@ module API
end
class IssueBasic < ProjectEntity
+ expose :closed_at
expose :labels do |issue, options|
# Avoids an N+1 query since labels are preloaded
issue.labels.map(&:title).sort
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 56d72d511da..8aa1e0216ee 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -2,7 +2,7 @@ module API
class MergeRequests < Grape::API
include PaginationParams
- before { authenticate! }
+ before { authenticate_non_get! }
helpers ::Gitlab::IssuableMetadata
@@ -55,6 +55,7 @@ module API
desc: 'Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`'
end
get do
+ authenticate! unless params[:scope] == 'all'
merge_requests = find_merge_requests
options = { with: Entities::MergeRequestBasic,
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 7dc19788462..aab7a6c3f93 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -70,8 +70,11 @@ module API
optional :import_url, type: String, desc: 'URL from which the project is imported'
end
- def present_projects(options = {})
- projects = ProjectsFinder.new(current_user: current_user, params: project_finder_params).execute
+ def load_projects
+ ProjectsFinder.new(current_user: current_user, params: project_finder_params).execute
+ end
+
+ def present_projects(projects, options = {})
projects = reorder_projects(projects)
projects = projects.with_statistics if params[:statistics]
projects = projects.with_issues_enabled if params[:with_issues_enabled]
@@ -111,7 +114,7 @@ module API
params[:user] = user
- present_projects
+ present_projects load_projects
end
end
@@ -124,7 +127,7 @@ module API
use :statistics_params
end
get do
- present_projects
+ present_projects load_projects
end
desc 'Create new project' do
@@ -229,6 +232,18 @@ module API
end
end
+ desc 'List forks of this project' do
+ success Entities::Project
+ end
+ params do
+ use :collection_params
+ end
+ get ':id/forks' do
+ forks = ForkProjectsFinder.new(user_project, params: project_finder_params, current_user: current_user).execute
+
+ present_projects forks
+ end
+
desc 'Update an existing project' do
success Entities::Project
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 1825c90a23b..bdebda58d3f 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -88,7 +88,7 @@ module API
user = User.find_by(id: params[:id])
not_found!('User') unless user && can?(current_user, :read_user, user)
- opts = current_user&.admin? ? { with: Entities::UserWithAdmin } : {}
+ opts = current_user&.admin? ? { with: Entities::UserWithAdmin } : { with: Entities::User }
present user, opts
end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index b9a573d3542..f4051c95959 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -79,7 +79,7 @@ module Backup
# - 1495527122_gitlab_backup.tar
# - 1495527068_2017_05_23_gitlab_backup.tar
# - 1495527097_2017_05_23_9.3.0-pre_gitlab_backup.tar
- next unless file =~ /(\d+)(?:_\d{4}_\d{2}_\d{2}(_\d+\.\d+\.\d+.*)?)?_gitlab_backup\.tar$/
+ next unless file =~ /^(\d{10})(?:_\d{4}_\d{2}_\d{2}(_\d+\.\d+\.\d+((-|\.)(pre|rc\d))?)?)?_gitlab_backup\.tar$/
timestamp = $1.to_i
diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb
index a65bbe23958..e0a8ca653cb 100644
--- a/lib/banzai/reference_parser/issue_parser.rb
+++ b/lib/banzai/reference_parser/issue_parser.rb
@@ -34,7 +34,8 @@ module Banzai
{ namespace: :owner },
{ group: [:owners, :group_members] },
:invited_groups,
- :project_members
+ :project_members,
+ :project_feature
]
}
),
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 11ace83c15c..87aeb76b66a 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -2,7 +2,7 @@ module Gitlab
module Auth
MissingPersonalTokenError = Class.new(StandardError)
- REGISTRY_SCOPES = Gitlab.config.registry.enabled ? [:read_registry].freeze : [].freeze
+ REGISTRY_SCOPES = [:read_registry].freeze
# Scopes used for GitLab API access
API_SCOPES = [:api, :read_user].freeze
@@ -13,11 +13,6 @@ module Gitlab
# Default scopes for OAuth applications that don't define their own
DEFAULT_SCOPES = [:api].freeze
- AVAILABLE_SCOPES = (API_SCOPES + REGISTRY_SCOPES).freeze
-
- # Other available scopes
- OPTIONAL_SCOPES = (AVAILABLE_SCOPES + OPENID_SCOPES - DEFAULT_SCOPES).freeze
-
class << self
include Gitlab::CurrentSettings
@@ -132,7 +127,7 @@ module Gitlab
token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password)
- if token && valid_scoped_token?(token, AVAILABLE_SCOPES)
+ if token && valid_scoped_token?(token, available_scopes)
Gitlab::Auth::Result.new(token.user, nil, :personal_token, abilities_for_scope(token.scopes))
end
end
@@ -230,6 +225,21 @@ module Gitlab
def read_user_scope_authentication_abilities
[]
end
+
+ def available_scopes
+ API_SCOPES + registry_scopes
+ end
+
+ # Other available scopes
+ def optional_scopes
+ available_scopes + OPENID_SCOPES - DEFAULT_SCOPES
+ end
+
+ def registry_scopes
+ return [] unless Gitlab.config.registry.enabled
+
+ REGISTRY_SCOPES
+ end
end
end
end
diff --git a/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb b/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb
new file mode 100644
index 00000000000..b1411be3016
--- /dev/null
+++ b/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range.rb
@@ -0,0 +1,41 @@
+module Gitlab
+ module BackgroundMigration
+ class DeleteConflictingRedirectRoutesRange
+ class Route < ActiveRecord::Base
+ self.table_name = 'routes'
+ end
+
+ class RedirectRoute < ActiveRecord::Base
+ self.table_name = 'redirect_routes'
+ end
+
+ # start_id - The start ID of the range of events to process
+ # end_id - The end ID of the range to process.
+ def perform(start_id, end_id)
+ return unless migrate?
+
+ conflicts = RedirectRoute.where(routes_match_redirects_clause(start_id, end_id))
+ num_rows = conflicts.delete_all
+
+ Rails.logger.info("Gitlab::BackgroundMigration::DeleteConflictingRedirectRoutesRange [#{start_id}, #{end_id}] - Deleted #{num_rows} redirect routes that were conflicting with routes.")
+ end
+
+ def migrate?
+ Route.table_exists? && RedirectRoute.table_exists?
+ end
+
+ def routes_match_redirects_clause(start_id, end_id)
+ <<~ROUTES_MATCH_REDIRECTS
+ EXISTS (
+ SELECT 1 FROM routes
+ WHERE (
+ LOWER(redirect_routes.path) = LOWER(routes.path)
+ OR LOWER(redirect_routes.path) LIKE LOWER(CONCAT(routes.path, '/%'))
+ )
+ AND routes.id BETWEEN #{start_id} AND #{end_id}
+ )
+ ROUTES_MATCH_REDIRECTS
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/policy.rb b/lib/gitlab/ci/build/policy.rb
new file mode 100644
index 00000000000..d10cc7802d4
--- /dev/null
+++ b/lib/gitlab/ci/build/policy.rb
@@ -0,0 +1,15 @@
+module Gitlab
+ module Ci
+ module Build
+ module Policy
+ def self.fabricate(specs)
+ specifications = specs.to_h.map do |spec, value|
+ self.const_get(spec.to_s.camelize).new(value)
+ end
+
+ specifications.compact
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/policy/kubernetes.rb b/lib/gitlab/ci/build/policy/kubernetes.rb
new file mode 100644
index 00000000000..b20d374288f
--- /dev/null
+++ b/lib/gitlab/ci/build/policy/kubernetes.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Ci
+ module Build
+ module Policy
+ class Kubernetes < Policy::Specification
+ def initialize(spec)
+ unless spec.to_sym == :active
+ raise UnknownPolicyError
+ end
+ end
+
+ def satisfied_by?(pipeline)
+ pipeline.has_kubernetes_active?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/policy/refs.rb b/lib/gitlab/ci/build/policy/refs.rb
new file mode 100644
index 00000000000..eadc0948d2f
--- /dev/null
+++ b/lib/gitlab/ci/build/policy/refs.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module Ci
+ module Build
+ module Policy
+ class Refs < Policy::Specification
+ def initialize(refs)
+ @patterns = Array(refs)
+ end
+
+ def satisfied_by?(pipeline)
+ @patterns.any? do |pattern|
+ pattern, path = pattern.split('@', 2)
+
+ matches_path?(path, pipeline) &&
+ matches_pattern?(pattern, pipeline)
+ end
+ end
+
+ private
+
+ def matches_path?(path, pipeline)
+ return true unless path
+
+ pipeline.project_full_path == path
+ end
+
+ def matches_pattern?(pattern, pipeline)
+ return true if pipeline.tag? && pattern == 'tags'
+ return true if pipeline.branch? && pattern == 'branches'
+ return true if pipeline.source == pattern
+ return true if pipeline.source&.pluralize == pattern
+
+ if pattern.first == "/" && pattern.last == "/"
+ Regexp.new(pattern[1...-1]) =~ pipeline.ref
+ else
+ pattern == pipeline.ref
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/policy/specification.rb b/lib/gitlab/ci/build/policy/specification.rb
new file mode 100644
index 00000000000..c317291f29d
--- /dev/null
+++ b/lib/gitlab/ci/build/policy/specification.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module Ci
+ module Build
+ module Policy
+ ##
+ # Abstract class that defines an interface of job policy
+ # specification.
+ #
+ # Used for job's only/except policy configuration.
+ #
+ class Specification
+ UnknownPolicyError = Class.new(StandardError)
+
+ def initialize(spec)
+ @spec = spec
+ end
+
+ def satisfied_by?(pipeline)
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index 7582964b24e..0bd78b03448 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -5,12 +5,11 @@ module Gitlab
include Gitlab::Ci::Config::Entry::LegacyValidationHelpers
- attr_reader :path, :cache, :stages, :jobs
+ attr_reader :cache, :stages, :jobs
- def initialize(config, path = nil)
+ def initialize(config)
@ci_config = Gitlab::Ci::Config.new(config)
@config = @ci_config.to_hash
- @path = path
unless @ci_config.valid?
raise ValidationError, @ci_config.errors.first
@@ -21,28 +20,12 @@ module Gitlab
raise ValidationError, e.message
end
- def builds_for_stage_and_ref(stage, ref, tag = false, source = nil)
- jobs_for_stage_and_ref(stage, ref, tag, source).map do |name, _|
- build_attributes(name)
- end
- end
-
def builds
@jobs.map do |name, _|
build_attributes(name)
end
end
- def stage_seeds(pipeline)
- seeds = @stages.uniq.map do |stage|
- builds = pipeline_stage_builds(stage, pipeline)
-
- Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any?
- end
-
- seeds.compact
- end
-
def build_attributes(name)
job = @jobs[name.to_sym] || {}
@@ -70,6 +53,32 @@ module Gitlab
}.compact }
end
+ def pipeline_stage_builds(stage, pipeline)
+ selected_jobs = @jobs.select do |_, job|
+ next unless job[:stage] == stage
+
+ only_specs = Gitlab::Ci::Build::Policy
+ .fabricate(job.fetch(:only, {}))
+ except_specs = Gitlab::Ci::Build::Policy
+ .fabricate(job.fetch(:except, {}))
+
+ only_specs.all? { |spec| spec.satisfied_by?(pipeline) } &&
+ except_specs.none? { |spec| spec.satisfied_by?(pipeline) }
+ end
+
+ selected_jobs.map { |_, job| build_attributes(job[:name]) }
+ end
+
+ def stage_seeds(pipeline)
+ seeds = @stages.uniq.map do |stage|
+ builds = pipeline_stage_builds(stage, pipeline)
+
+ Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any?
+ end
+
+ seeds.compact
+ end
+
def self.validation_message(content)
return 'Please provide content of .gitlab-ci.yml' if content.blank?
@@ -83,34 +92,6 @@ module Gitlab
private
- def pipeline_stage_builds(stage, pipeline)
- builds = builds_for_stage_and_ref(
- stage, pipeline.ref, pipeline.tag?, pipeline.source)
-
- builds.select do |build|
- job = @jobs[build.fetch(:name).to_sym]
- has_kubernetes = pipeline.has_kubernetes_active?
- only_kubernetes = job.dig(:only, :kubernetes)
- except_kubernetes = job.dig(:except, :kubernetes)
-
- [!only_kubernetes && !except_kubernetes,
- only_kubernetes && has_kubernetes,
- except_kubernetes && !has_kubernetes].any?
- end
- end
-
- def jobs_for_ref(ref, tag = false, source = nil)
- @jobs.select do |_, job|
- process?(job.dig(:only, :refs), job.dig(:except, :refs), ref, tag, source)
- end
- end
-
- def jobs_for_stage_and_ref(stage, ref, tag = false, source = nil)
- jobs_for_ref(ref, tag, source).select do |_, job|
- job[:stage] == stage
- end
- end
-
def initial_parsing
##
# Global config
@@ -203,51 +184,6 @@ module Gitlab
raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined"
end
end
-
- def process?(only_params, except_params, ref, tag, source)
- if only_params.present?
- return false unless matching?(only_params, ref, tag, source)
- end
-
- if except_params.present?
- return false if matching?(except_params, ref, tag, source)
- end
-
- true
- end
-
- def matching?(patterns, ref, tag, source)
- patterns.any? do |pattern|
- pattern, path = pattern.split('@', 2)
- matches_path?(path) && matches_pattern?(pattern, ref, tag, source)
- end
- end
-
- def matches_path?(path)
- return true unless path
-
- path == self.path
- end
-
- def matches_pattern?(pattern, ref, tag, source)
- return true if tag && pattern == 'tags'
- return true if !tag && pattern == 'branches'
- return true if source_to_pattern(source) == pattern
-
- if pattern.first == "/" && pattern.last == "/"
- Regexp.new(pattern[1...-1]) =~ ref
- else
- pattern == ref
- end
- end
-
- def source_to_pattern(source)
- if %w[api external web].include?(source)
- source
- else
- source&.pluralize
- end
- end
end
end
end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index fb14798efe6..2c35da8f1aa 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -1,6 +1,9 @@
module Gitlab
module Database
module MigrationHelpers
+ BACKGROUND_MIGRATION_BATCH_SIZE = 1000 # Number of rows to process per job
+ BACKGROUND_MIGRATION_JOB_BUFFER_SIZE = 1000 # Number of jobs to bulk queue at a time
+
# Adds `created_at` and `updated_at` columns with timezone information.
#
# This method is an improved version of Rails' built-in method `add_timestamps`.
@@ -653,6 +656,91 @@ into similar problems in the future (e.g. when new tables are created).
EOF
end
end
+
+ # Bulk queues background migration jobs for an entire table, batched by ID range.
+ # "Bulk" meaning many jobs will be pushed at a time for efficiency.
+ # If you need a delay interval per job, then use `queue_background_migration_jobs_by_range_at_intervals`.
+ #
+ # model_class - The table being iterated over
+ # job_class_name - The background migration job class as a string
+ # batch_size - The maximum number of rows per job
+ #
+ # Example:
+ #
+ # class Route < ActiveRecord::Base
+ # include EachBatch
+ # self.table_name = 'routes'
+ # end
+ #
+ # bulk_queue_background_migration_jobs_by_range(Route, 'ProcessRoutes')
+ #
+ # Where the model_class includes EachBatch, and the background migration exists:
+ #
+ # class Gitlab::BackgroundMigration::ProcessRoutes
+ # def perform(start_id, end_id)
+ # # do something
+ # end
+ # end
+ def bulk_queue_background_migration_jobs_by_range(model_class, job_class_name, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE)
+ raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id')
+
+ jobs = []
+
+ model_class.each_batch(of: batch_size) do |relation|
+ start_id, end_id = relation.pluck('MIN(id), MAX(id)').first
+
+ if jobs.length >= BACKGROUND_MIGRATION_JOB_BUFFER_SIZE
+ # Note: This code path generally only helps with many millions of rows
+ # We push multiple jobs at a time to reduce the time spent in
+ # Sidekiq/Redis operations. We're using this buffer based approach so we
+ # don't need to run additional queries for every range.
+ BackgroundMigrationWorker.perform_bulk(jobs)
+ jobs.clear
+ end
+
+ jobs << [job_class_name, [start_id, end_id]]
+ end
+
+ BackgroundMigrationWorker.perform_bulk(jobs) unless jobs.empty?
+ end
+
+ # Queues background migration jobs for an entire table, batched by ID range.
+ # Each job is scheduled with a `delay_interval` in between.
+ # If you use a small interval, then some jobs may run at the same time.
+ #
+ # model_class - The table being iterated over
+ # job_class_name - The background migration job class as a string
+ # delay_interval - The duration between each job's scheduled time (must respond to `to_f`)
+ # batch_size - The maximum number of rows per job
+ #
+ # Example:
+ #
+ # class Route < ActiveRecord::Base
+ # include EachBatch
+ # self.table_name = 'routes'
+ # end
+ #
+ # queue_background_migration_jobs_by_range_at_intervals(Route, 'ProcessRoutes', 1.minute)
+ #
+ # Where the model_class includes EachBatch, and the background migration exists:
+ #
+ # class Gitlab::BackgroundMigration::ProcessRoutes
+ # def perform(start_id, end_id)
+ # # do something
+ # end
+ # end
+ def queue_background_migration_jobs_by_range_at_intervals(model_class, job_class_name, delay_interval, batch_size: BACKGROUND_MIGRATION_BATCH_SIZE)
+ raise "#{model_class} does not have an ID to use for batch ranges" unless model_class.column_names.include?('id')
+
+ model_class.each_batch(of: batch_size) do |relation, index|
+ start_id, end_id = relation.pluck('MIN(id), MAX(id)').first
+
+ # `BackgroundMigrationWorker.bulk_perform_in` schedules all jobs for
+ # the same time, which is not helpful in most cases where we wish to
+ # spread the work over time.
+ BackgroundMigrationWorker.perform_in(delay_interval * index, job_class_name, [start_id, end_id])
+ end
+ end
end
end
end
diff --git a/lib/gitlab/database/read_only_relation.rb b/lib/gitlab/database/read_only_relation.rb
new file mode 100644
index 00000000000..4571ad122ce
--- /dev/null
+++ b/lib/gitlab/database/read_only_relation.rb
@@ -0,0 +1,16 @@
+module Gitlab
+ module Database
+ # Module that can be injected into a ActiveRecord::Relation to make it
+ # read-only.
+ module ReadOnlyRelation
+ [:delete, :delete_all, :update, :update_all].each do |method|
+ define_method(method) do |*args|
+ raise(
+ ActiveRecord::ReadOnlyRecord,
+ "This relation is marked as read-only"
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb
index a6007ebf531..88ae65cb468 100644
--- a/lib/gitlab/diff/file_collection/base.rb
+++ b/lib/gitlab/diff/file_collection/base.rb
@@ -22,7 +22,10 @@ module Gitlab
end
def diff_files
- @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) }
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37445
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) }
+ end
end
def diff_file_with_old_path(old_path)
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index abd401224d8..c5a8ea12245 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -284,13 +284,18 @@ module Gitlab
EE/master, and no `#{ee_branch_prefix}` or `#{ee_branch_suffix}` branch
was found in the EE repository.
+ If you're a community contributor, don't worry, someone from
+ GitLab Inc. will take care of this, and you don't have to do anything.
+ If you're willing to help, and are ok to contribute to EE as well,
+ you're welcome to help. You could follow the instructions below.
+
#{conflicting_files_msg}
We advise you to create a `#{ee_branch_prefix}` or `#{ee_branch_suffix}`
branch that includes changes from `#{ce_branch}` but also specific changes
than can be applied cleanly to EE/master. In some cases, the conflicts
are trivial and you can ignore the warning from this job. As always,
- use your best judgment!
+ use your best judgement!
There are different ways to create such branch:
diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb
index b984492d369..455814a9159 100644
--- a/lib/gitlab/gfm/reference_rewriter.rb
+++ b/lib/gitlab/gfm/reference_rewriter.rb
@@ -29,6 +29,8 @@ module Gitlab
# http://gitlab.com/some/link/#1234, and code `puts #1234`'
#
class ReferenceRewriter
+ RewriteError = Class.new(StandardError)
+
def initialize(text, source_project, current_user)
@text = text
@source_project = source_project
@@ -61,6 +63,10 @@ module Gitlab
cross_reference = build_cross_reference(referable, target_project)
return reference if reference == cross_reference
+ if cross_reference.nil?
+ raise RewriteError, "Unspecified reference detected for #{referable.class.name}"
+ end
+
new_text = before + cross_reference + after
substitution_valid?(new_text) ? cross_reference : reference
end
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index b4b6326cfdd..c78fe63f9b5 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -57,6 +57,15 @@ module Gitlab
def version
Gitlab::VersionInfo.parse(Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --version)).first)
end
+
+ def check_namespace!(*objects)
+ expected_namespace = self.name + '::'
+ objects.each do |object|
+ unless object.class.name.start_with?(expected_namespace)
+ raise ArgumentError, "expected object in #{expected_namespace}, got #{object}"
+ end
+ end
+ end
end
end
end
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 1f370686186..1957c254c28 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -413,6 +413,10 @@ module Gitlab
end
end
+ def merge_commit?
+ parent_ids.size > 1
+ end
+
private
def init_from_hash(hash)
diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb
index 347e3b5165e..786e2e7e8dc 100644
--- a/lib/gitlab/git/operation_service.rb
+++ b/lib/gitlab/git/operation_service.rb
@@ -1,6 +1,13 @@
module Gitlab
module Git
class OperationService
+ include Gitlab::Git::Popen
+
+ WithBranchResult = Struct.new(:newrev, :repo_created, :branch_created) do
+ alias_method :repo_created?, :repo_created
+ alias_method :branch_created?, :branch_created
+ end
+
attr_reader :user, :repository
def initialize(user, new_repository)
@@ -10,9 +17,7 @@ module Gitlab
end
# Refactoring aid
- unless new_repository.is_a?(Gitlab::Git::Repository)
- raise "expected a Gitlab::Git::Repository, got #{new_repository}"
- end
+ Gitlab::Git.check_namespace!(new_repository)
@repository = new_repository
end
@@ -107,7 +112,7 @@ module Gitlab
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
update_ref_in_hooks(ref, newrev, oldrev)
- [newrev, was_empty, was_empty || Gitlab::Git.blank_ref?(oldrev)]
+ WithBranchResult.new(newrev, was_empty, was_empty || Gitlab::Git.blank_ref?(oldrev))
end
def find_oldrev_from_branch(newrev, branch)
@@ -147,7 +152,7 @@ module Gitlab
# (and have!) accidentally reset the ref to an earlier state, clobbering
# commits. See also https://github.com/libgit2/libgit2/issues/1534.
command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z]
- _, status = Gitlab::Popen.popen(
+ _, status = popen(
command,
repository.path) do |stdin|
stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00")
diff --git a/lib/gitlab/git/popen.rb b/lib/gitlab/git/popen.rb
index 25fa62ce4bd..3d2fc471d28 100644
--- a/lib/gitlab/git/popen.rb
+++ b/lib/gitlab/git/popen.rb
@@ -5,17 +5,21 @@ require 'open3'
module Gitlab
module Git
module Popen
- def popen(cmd, path)
+ def popen(cmd, path, vars = {})
unless cmd.is_a?(Array)
raise "System commands must be given as an array of strings"
end
- vars = { "PWD" => path }
+ path ||= Dir.pwd
+ vars['PWD'] = path
options = { chdir: path }
@cmd_output = ""
@cmd_status = 0
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
+ yield(stdin) if block_given?
+ stdin.close
+
@cmd_output << stdout.read
@cmd_output << stderr.read
@cmd_status = wait_thr.value.exitstatus
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 32a265b15f2..0be35034d24 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -19,6 +19,7 @@ module Gitlab
InvalidRef = Class.new(StandardError)
GitError = Class.new(StandardError)
DeleteBranchError = Class.new(StandardError)
+ CreateTreeError = Class.new(StandardError)
class << self
# Unlike `new`, `create` takes the storage path, not the storage name
@@ -489,7 +490,7 @@ module Gitlab
# Not found -> ["", 0]
# Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0]
- Gitlab::Popen.popen(args, @path).first.split.last
+ popen(args, @path).first.split.last
end
end
end
@@ -653,6 +654,125 @@ module Gitlab
tags.find { |tag| tag.name == name }
end
+ def merge(user, source_sha, target_branch, message)
+ committer = Gitlab::Git.committer_hash(email: user.email, name: user.name)
+
+ OperationService.new(user, self).with_branch(target_branch) do |start_commit|
+ our_commit = start_commit.sha
+ their_commit = source_sha
+
+ raise 'Invalid merge target' unless our_commit
+ raise 'Invalid merge source' unless their_commit
+
+ merge_index = rugged.merge_commits(our_commit, their_commit)
+ break if merge_index.conflicts?
+
+ options = {
+ parents: [our_commit, their_commit],
+ tree: merge_index.write_tree(rugged),
+ message: message,
+ author: committer,
+ committer: committer
+ }
+
+ commit_id = create_commit(options)
+
+ yield commit_id
+
+ commit_id
+ end
+ rescue Gitlab::Git::CommitError # when merge_index.conflicts?
+ nil
+ end
+
+ def revert(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
+ OperationService.new(user, self).with_branch(
+ branch_name,
+ start_branch_name: start_branch_name,
+ start_repository: start_repository
+ ) do |start_commit|
+
+ Gitlab::Git.check_namespace!(commit, start_repository)
+
+ revert_tree_id = check_revert_content(commit, start_commit.sha)
+ raise CreateTreeError unless revert_tree_id
+
+ committer = user_to_committer(user)
+
+ create_commit(message: message,
+ author: committer,
+ committer: committer,
+ tree: revert_tree_id,
+ parents: [start_commit.sha])
+ end
+ end
+
+ def check_revert_content(target_commit, source_sha)
+ args = [target_commit.sha, source_sha]
+ args << { mainline: 1 } if target_commit.merge_commit?
+
+ revert_index = rugged.revert_commit(*args)
+ return false if revert_index.conflicts?
+
+ tree_id = revert_index.write_tree(rugged)
+ return false unless diff_exists?(source_sha, tree_id)
+
+ tree_id
+ end
+
+ def cherry_pick(user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
+ OperationService.new(user, self).with_branch(
+ branch_name,
+ start_branch_name: start_branch_name,
+ start_repository: start_repository
+ ) do |start_commit|
+
+ Gitlab::Git.check_namespace!(commit, start_repository)
+
+ cherry_pick_tree_id = check_cherry_pick_content(commit, start_commit.sha)
+ raise CreateTreeError unless cherry_pick_tree_id
+
+ committer = user_to_committer(user)
+
+ create_commit(message: message,
+ author: {
+ email: commit.author_email,
+ name: commit.author_name,
+ time: commit.authored_date
+ },
+ committer: committer,
+ tree: cherry_pick_tree_id,
+ parents: [start_commit.sha])
+ end
+ end
+
+ def check_cherry_pick_content(target_commit, source_sha)
+ args = [target_commit.sha, source_sha]
+ args << 1 if target_commit.merge_commit?
+
+ cherry_pick_index = rugged.cherrypick_commit(*args)
+ return false if cherry_pick_index.conflicts?
+
+ tree_id = cherry_pick_index.write_tree(rugged)
+ return false unless diff_exists?(source_sha, tree_id)
+
+ tree_id
+ end
+
+ def diff_exists?(sha1, sha2)
+ rugged.diff(sha1, sha2).size > 0
+ end
+
+ def user_to_committer(user)
+ Gitlab::Git.committer_hash(email: user.email, name: user.name)
+ end
+
+ def create_commit(params = {})
+ params[:message].delete!("\r")
+
+ Rugged::Commit.create(rugged, params)
+ end
+
# Delete the specified branch from the repository
def delete_branch(branch_name)
gitaly_migrate(:delete_branch) do |is_enabled|
@@ -672,9 +792,7 @@ module Gitlab
end
command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z]
- message, status = Gitlab::Popen.popen(
- command,
- path) do |stdin|
+ message, status = popen(command, path) do |stdin|
stdin.write(instructions.join)
end
@@ -798,7 +916,7 @@ module Gitlab
end
def with_repo_branch_commit(start_repository, start_branch_name)
- raise "expected Gitlab::Git::Repository, got #{start_repository}" unless start_repository.is_a?(Gitlab::Git::Repository)
+ Gitlab::Git.check_namespace!(start_repository)
return yield nil if start_repository.empty_repo?
@@ -913,8 +1031,8 @@ module Gitlab
@gitaly_repository_client ||= Gitlab::GitalyClient::RepositoryService.new(self)
end
- def gitaly_migrate(method, &block)
- Gitlab::GitalyClient.migrate(method, &block)
+ def gitaly_migrate(method, status: Gitlab::GitalyClient::MigrationStatus::OPT_IN, &block)
+ Gitlab::GitalyClient.migrate(method, status: status, &block)
rescue GRPC::NotFound => e
raise NoRepository.new(e)
rescue GRPC::BadStatus => e
@@ -925,14 +1043,17 @@ module Gitlab
# Gitaly note: JV: Trying to get rid of the 'filter' option so we can implement this with 'git'.
def branches_filter(filter: nil, sort_by: nil)
- branches = rugged.branches.each(filter).map do |rugged_ref|
- begin
- target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
- Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
- rescue Rugged::ReferenceError
- # Omit invalid branch
- end
- end.compact
+ # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37464
+ branches = Gitlab::GitalyClient.allow_n_plus_1_calls do
+ rugged.branches.each(filter).map do |rugged_ref|
+ begin
+ target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target)
+ Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit)
+ rescue Rugged::ReferenceError
+ # Omit invalid branch
+ end
+ end.compact
+ end
sort_branches(branches, sort_by)
end
diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb
index 2b5785a1f08..e0943d3a3eb 100644
--- a/lib/gitlab/git/rev_list.rb
+++ b/lib/gitlab/git/rev_list.rb
@@ -3,6 +3,8 @@
module Gitlab
module Git
class RevList
+ include Gitlab::Git::Popen
+
attr_reader :oldrev, :newrev, :path_to_repo
def initialize(path_to_repo:, newrev:, oldrev: nil)
@@ -26,7 +28,7 @@ module Gitlab
private
def execute(args)
- output, status = Gitlab::Popen.popen(args, nil, Gitlab::Git::Env.all.stringify_keys)
+ output, status = popen(args, nil, Gitlab::Git::Env.all.stringify_keys)
unless status.zero?
raise "Got a non-zero exit code while calling out `#{args.join(' ')}`."
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index a3dc2cd0b60..cbd9ff406de 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -10,7 +10,24 @@ module Gitlab
OPT_OUT = 3
end
+ class TooManyInvocationsError < StandardError
+ attr_reader :call_site, :invocation_count, :max_call_stack
+
+ def initialize(call_site, invocation_count, max_call_stack, most_invoked_stack)
+ @call_site = call_site
+ @invocation_count = invocation_count
+ @max_call_stack = max_call_stack
+ stacks = most_invoked_stack.join('\n') if most_invoked_stack
+
+ msg = "GitalyClient##{call_site} called #{invocation_count} times from single request. Potential n+1?"
+ msg << "\nThe following call site called into Gitaly #{max_call_stack} times:\n#{stacks}\n" if stacks
+
+ super(msg)
+ end
+ end
+
SERVER_VERSION_FILE = 'GITALY_SERVER_VERSION'.freeze
+ MAXIMUM_GITALY_CALLS = 30
MUTEX = Mutex.new
private_constant :MUTEX
@@ -53,6 +70,8 @@ module Gitlab
# All Gitaly RPC call sites should use GitalyClient.call. This method
# makes sure that per-request authentication headers are set.
def self.call(storage, service, rpc, request)
+ enforce_gitaly_request_limits(:call)
+
metadata = request_metadata(storage)
metadata = yield(metadata) if block_given?
stub(service, storage).__send__(rpc, request, metadata) # rubocop:disable GitlabSecurity/PublicSend
@@ -107,12 +126,100 @@ module Gitlab
private_class_method :opt_into_all_features?
def self.migrate(feature, status: MigrationStatus::OPT_IN)
+ # Enforce limits at both the `migrate` and `call` sites to ensure that
+ # problems are not hidden by a feature being disabled
+ enforce_gitaly_request_limits(:migrate)
+
is_enabled = feature_enabled?(feature, status: status)
metric_name = feature.to_s
metric_name += "_gitaly" if is_enabled
Gitlab::Metrics.measure(metric_name) do
- yield is_enabled
+ # Some migrate calls wrap other migrate calls
+ allow_n_plus_1_calls do
+ yield is_enabled
+ end
+ end
+ end
+
+ # Ensures that Gitaly is not being abuse through n+1 misuse etc
+ def self.enforce_gitaly_request_limits(call_site)
+ # Only count limits in request-response environments (not sidekiq for example)
+ return unless RequestStore.active?
+
+ # This is this actual number of times this call was made. Used for information purposes only
+ actual_call_count = increment_call_count("gitaly_#{call_site}_actual")
+
+ # Do no enforce limits in production
+ return if Rails.env.production?
+
+ # Check if this call is nested within a allow_n_plus_1_calls
+ # block and skip check if it is
+ return if get_call_count(:gitaly_call_count_exception_block_depth) > 0
+
+ # This is the count of calls outside of a `allow_n_plus_1_calls` block
+ # It is used for enforcement but not statistics
+ permitted_call_count = increment_call_count("gitaly_#{call_site}_permitted")
+
+ count_stack
+
+ return if permitted_call_count <= MAXIMUM_GITALY_CALLS
+
+ raise TooManyInvocationsError.new(call_site, actual_call_count, max_call_count, max_stacks)
+ end
+
+ def self.allow_n_plus_1_calls
+ return yield unless RequestStore.active?
+
+ begin
+ increment_call_count(:gitaly_call_count_exception_block_depth)
+ yield
+ ensure
+ decrement_call_count(:gitaly_call_count_exception_block_depth)
+ end
+ end
+
+ def self.get_call_count(key)
+ RequestStore.store[key] || 0
+ end
+ private_class_method :get_call_count
+
+ def self.increment_call_count(key)
+ RequestStore.store[key] ||= 0
+ RequestStore.store[key] += 1
+ end
+ private_class_method :increment_call_count
+
+ def self.decrement_call_count(key)
+ RequestStore.store[key] -= 1
+ end
+ private_class_method :decrement_call_count
+
+ # Returns an estimate of the number of Gitaly calls made for this
+ # request
+ def self.get_request_count
+ return 0 unless RequestStore.active?
+
+ gitaly_migrate_count = get_call_count("gitaly_migrate_actual")
+ gitaly_call_count = get_call_count("gitaly_call_actual")
+
+ # Using the maximum of migrate and call_count will provide an
+ # indicator of how many Gitaly calls will be made, even
+ # before a feature is enabled. This provides us with a single
+ # metric, but not an exact number, but this tradeoff is acceptable
+ if gitaly_migrate_count > gitaly_call_count
+ gitaly_migrate_count
+ else
+ gitaly_call_count
+ end
+ end
+
+ def self.reset_counts
+ return unless RequestStore.active?
+
+ %w[migrate call].each do |call_site|
+ RequestStore.store["gitaly_#{call_site}_actual"] = 0
+ RequestStore.store["gitaly_#{call_site}_permitted"] = 0
end
end
@@ -124,5 +231,43 @@ module Gitlab
def self.encode(s)
s.dup.force_encoding(Encoding::ASCII_8BIT)
end
+
+ # Count a stack. Used for n+1 detection
+ def self.count_stack
+ return unless RequestStore.active?
+
+ stack_string = caller.drop(1).join("\n")
+
+ RequestStore.store[:stack_counter] ||= Hash.new
+
+ count = RequestStore.store[:stack_counter][stack_string] || 0
+ RequestStore.store[:stack_counter][stack_string] = count + 1
+ end
+ private_class_method :count_stack
+
+ # Returns a count for the stack which called Gitaly the most times. Used for n+1 detection
+ def self.max_call_count
+ return 0 unless RequestStore.active?
+
+ stack_counter = RequestStore.store[:stack_counter]
+ return 0 unless stack_counter
+
+ stack_counter.values.max
+ end
+ private_class_method :max_call_count
+
+ # Returns the stacks that calls Gitaly the most times. Used for n+1 detection
+ def self.max_stacks
+ return nil unless RequestStore.active?
+
+ stack_counter = RequestStore.store[:stack_counter]
+ return nil unless stack_counter
+
+ max = max_call_count
+ return nil if max.zero?
+
+ stack_counter.select { |_, v| v == max }.keys
+ end
+ private_class_method :max_stacks
end
end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 1ba1a7830a4..b536eb1868c 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -221,7 +221,7 @@ module Gitlab
repository: @gitaly_repo,
left_commit_id: parent_id,
right_commit_id: commit.id,
- paths: options.fetch(:paths, [])
+ paths: options.fetch(:paths, []).map { |path| GitalyClient.encode(path) }
}
end
diff --git a/lib/gitlab/group_hierarchy.rb b/lib/gitlab/group_hierarchy.rb
index 5a31e56cb30..635f52131f9 100644
--- a/lib/gitlab/group_hierarchy.rb
+++ b/lib/gitlab/group_hierarchy.rb
@@ -22,7 +22,7 @@ module Gitlab
def base_and_ancestors
return ancestors_base unless Group.supports_nested_groups?
- base_and_ancestors_cte.apply_to(model.all)
+ read_only(base_and_ancestors_cte.apply_to(model.all))
end
# Returns a relation that includes the descendants_base set of groups
@@ -30,7 +30,7 @@ module Gitlab
def base_and_descendants
return descendants_base unless Group.supports_nested_groups?
- base_and_descendants_cte.apply_to(model.all)
+ read_only(base_and_descendants_cte.apply_to(model.all))
end
# Returns a relation that includes the base groups, their ancestors,
@@ -67,11 +67,13 @@ module Gitlab
union = SQL::Union.new([model.unscoped.from(ancestors_table),
model.unscoped.from(descendants_table)])
- model
+ relation = model
.unscoped
.with
.recursive(ancestors.to_arel, descendants.to_arel)
.from("(#{union.to_sql}) #{model.table_name}")
+
+ read_only(relation)
end
private
@@ -107,5 +109,12 @@ module Gitlab
def groups_table
model.arel_table
end
+
+ def read_only(relation)
+ # relations using a CTE are not safe to use with update_all as it will
+ # throw away the CTE, hence we mark them as read-only.
+ relation.extend(Gitlab::Database::ReadOnlyRelation)
+ relation
+ end
end
end
diff --git a/lib/gitlab/health_checks/fs_shards_check.rb b/lib/gitlab/health_checks/fs_shards_check.rb
index eef97f54962..a533d4364ef 100644
--- a/lib/gitlab/health_checks/fs_shards_check.rb
+++ b/lib/gitlab/health_checks/fs_shards_check.rb
@@ -58,7 +58,7 @@ module Gitlab
end
def repository_storages
- @repository_storage ||= Gitlab::CurrentSettings.current_application_settings.repository_storages
+ @repository_storage ||= storages_paths.keys
end
def storages_paths
diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb
index 5d106b5c075..bdc0f04b56b 100644
--- a/lib/gitlab/i18n.rb
+++ b/lib/gitlab/i18n.rb
@@ -17,7 +17,8 @@ module Gitlab
'it' => 'Italiano',
'uk' => 'Українська',
'ja' => '日本語',
- 'ko' => '한국어'
+ 'ko' => '한국어',
+ 'nl_NL' => 'Nederlands'
}.freeze
def available_locales
diff --git a/lib/tasks/gitlab/dev.rake b/lib/tasks/gitlab/dev.rake
index 7ccda04a35f..3eade7bf553 100644
--- a/lib/tasks/gitlab/dev.rake
+++ b/lib/tasks/gitlab/dev.rake
@@ -13,7 +13,10 @@ namespace :gitlab do
args
end
- if Gitlab::EeCompatCheck.new(opts || {}).check
+ if File.basename(Rails.root) == 'gitlab-ee'
+ puts "Skipping EE projects"
+ exit 0
+ elsif Gitlab::EeCompatCheck.new(opts || {}).check
exit 0
else
exit 1
diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po
index fcd4aa29834..9d90f4ed5b1 100644
--- a/locale/bg/gitlab.po
+++ b/locale/bg/gitlab.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"PO-Revision-Date: 2017-09-15 05:22-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Bulgarian\n"
"Language: bg_BG\n"
@@ -986,6 +986,24 @@ msgstr "Графика"
msgid "Push Rules"
msgstr ""
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push events"
msgstr ""
diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po
index 86deb620f0b..19961043ede 100644
--- a/locale/de/gitlab.po
+++ b/locale/de/gitlab.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"PO-Revision-Date: 2017-09-15 05:22-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: German\n"
"Language: de_DE\n"
@@ -70,7 +70,7 @@ msgid "Account"
msgstr ""
msgid "Active"
-msgstr ""
+msgstr "Aktiv"
msgid "Activity"
msgstr ""
@@ -82,7 +82,7 @@ msgid "Add Contribution guide"
msgstr "Mitarbeitsanleitung hinzufügen"
msgid "Add License"
-msgstr ""
+msgstr "Lizenz hinzufügen"
msgid "Add an SSH key to your profile to pull or push via SSH."
msgstr "Füge einen SSH Schlüssel zu deinem Profil hinzu, um mittels SSH zu übertragen (push) oder abzurufen (pull)."
@@ -94,10 +94,10 @@ msgid "All"
msgstr "Alle"
msgid "Appearances"
-msgstr ""
+msgstr "Erscheinungsbild"
msgid "Applications"
-msgstr ""
+msgstr "Anwendungen"
msgid "Archived project! Repository is read-only"
msgstr "Archiviertes Projekt! Repository ist nicht änderbar."
@@ -213,10 +213,10 @@ msgid "CI / CD"
msgstr ""
msgid "CI configuration"
-msgstr ""
+msgstr "CI-Konfiguration"
msgid "Cancel"
-msgstr ""
+msgstr "Abbrechen"
msgid "Cancel edit"
msgstr "Bearbeitung abbrechen"
@@ -487,7 +487,7 @@ msgid "Edit Pipeline Schedule %{id}"
msgstr "Pipeline Zeitplan bearbeiten %{id}"
msgid "Emails"
-msgstr ""
+msgstr "E-Mails"
msgid "EventFilterBy|Filter by all"
msgstr "Filtere alle"
@@ -573,7 +573,7 @@ msgid "GoToYourFork|Fork"
msgstr "Ableger"
msgid "Group overview"
-msgstr ""
+msgstr "Gruppen-Übersicht"
msgid "Health Check"
msgstr "Systemzustand"
@@ -662,7 +662,7 @@ msgid "Leave project"
msgstr "Verlasse das Projekt"
msgid "License"
-msgstr ""
+msgstr "Lizenz"
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
@@ -670,13 +670,13 @@ msgstr[0] "Limitiere die Anzeige auf höchstens %d Ereignis"
msgstr[1] "Limitiere die Anzeige auf höchstens %d Ereignisse"
msgid "Locked Files"
-msgstr ""
+msgstr "Gesperrte Dateien"
msgid "Median"
-msgstr ""
+msgstr "Median"
msgid "Members"
-msgstr ""
+msgstr "Mitglieder"
msgid "Merge Requests"
msgstr ""
@@ -795,7 +795,7 @@ msgid "NotificationLevel|Watch"
msgstr "Beobachten"
msgid "Notifications"
-msgstr ""
+msgstr "Benachrichtigungen"
msgid "OfSearchInADropdown|Filter"
msgstr "Filter"
@@ -807,13 +807,13 @@ msgid "Options"
msgstr "Optionen"
msgid "Overview"
-msgstr ""
+msgstr "Übersicht"
msgid "Owner"
msgstr "Besitzer"
msgid "Password"
-msgstr ""
+msgstr "Passwort"
msgid "Pipeline"
msgstr ""
@@ -986,6 +986,24 @@ msgstr "Diagramm"
msgid "Push Rules"
msgstr ""
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push events"
msgstr "Übertragungsereignisse"
diff --git a/locale/eo/gitlab.po b/locale/eo/gitlab.po
index 8f25c893ecd..f9f61a109f6 100644
--- a/locale/eo/gitlab.po
+++ b/locale/eo/gitlab.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:21-0400\n"
+"PO-Revision-Date: 2017-09-15 05:22-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Esperanto\n"
"Language: eo_UY\n"
@@ -986,6 +986,24 @@ msgstr "Grafeo"
msgid "Push Rules"
msgstr ""
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push events"
msgstr ""
diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po
index eee720d5ba2..ccf4b0abf9f 100644
--- a/locale/es/gitlab.po
+++ b/locale/es/gitlab.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"PO-Revision-Date: 2017-09-15 05:19-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Spanish\n"
"Language: es_ES\n"
@@ -986,6 +986,24 @@ msgstr "Historial gráfico"
msgid "Push Rules"
msgstr ""
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push events"
msgstr ""
diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po
index 43e66d8dea4..c98156e026e 100644
--- a/locale/fr/gitlab.po
+++ b/locale/fr/gitlab.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"PO-Revision-Date: 2017-09-15 05:22-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: French\n"
"Language: fr_FR\n"
@@ -986,6 +986,24 @@ msgstr "Graphique "
msgid "Push Rules"
msgstr ""
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push events"
msgstr ""
diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po
index 46b3e12f97c..0249c4fe9eb 100644
--- a/locale/it/gitlab.po
+++ b/locale/it/gitlab.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"PO-Revision-Date: 2017-09-15 05:20-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@@ -986,6 +986,24 @@ msgstr "Grafico"
msgid "Push Rules"
msgstr ""
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push events"
msgstr ""
diff --git a/locale/ja/gitlab.po b/locale/ja/gitlab.po
index bc25b69c80a..c66dd3c1b6b 100644
--- a/locale/ja/gitlab.po
+++ b/locale/ja/gitlab.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"PO-Revision-Date: 2017-09-15 05:20-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Japanese\n"
"Language: ja_JP\n"
@@ -975,6 +975,24 @@ msgstr "ネットワークグラフ"
msgid "Push Rules"
msgstr ""
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push events"
msgstr ""
diff --git a/locale/ko/gitlab.po b/locale/ko/gitlab.po
index 4baefdb9a3e..bbf4aa15cd7 100644
--- a/locale/ko/gitlab.po
+++ b/locale/ko/gitlab.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"PO-Revision-Date: 2017-09-15 05:19-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Korean\n"
"Language: ko_KR\n"
@@ -975,6 +975,24 @@ msgstr "그래프"
msgid "Push Rules"
msgstr ""
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push events"
msgstr "푸쉬 이벤트"
diff --git a/locale/nl_NL/gitlab.po b/locale/nl_NL/gitlab.po
new file mode 100644
index 00000000000..250d3bd413c
--- /dev/null
+++ b/locale/nl_NL/gitlab.po
@@ -0,0 +1,1474 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: gitlab-ee\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2017-09-06 08:32+0200\n"
+"PO-Revision-Date: 2017-09-15 05:20-0400\n"
+"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
+"Language-Team: Dutch\n"
+"Language: nl_NL\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: crowdin.com\n"
+"X-Crowdin-Project: gitlab-ee\n"
+"X-Crowdin-Language: nl\n"
+"X-Crowdin-File: /master/locale/gitlab.pot\n"
+
+msgid "%d commit"
+msgid_plural "%d commits"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%s additional commit has been omitted to prevent performance issues."
+msgid_plural "%s additional commits have been omitted to prevent performance issues."
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "%{commit_author_link} committed %{commit_timeago}"
+msgstr ""
+
+msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will allow access on the next attempt."
+msgstr ""
+
+msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will block access for %{number_of_seconds} seconds."
+msgstr ""
+
+msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved."
+msgstr ""
+
+msgid "%{storage_name}: failed storage access attempt on host:"
+msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "(checkout the %{link} for information on how to install it)."
+msgstr ""
+
+msgid "1 pipeline"
+msgid_plural "%d pipelines"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "A collection of graphs regarding Continuous Integration"
+msgstr ""
+
+msgid "About auto deploy"
+msgstr ""
+
+msgid "Abuse Reports"
+msgstr ""
+
+msgid "Access Tokens"
+msgstr ""
+
+msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
+msgstr ""
+
+msgid "Account"
+msgstr ""
+
+msgid "Active"
+msgstr ""
+
+msgid "Activity"
+msgstr ""
+
+msgid "Add Changelog"
+msgstr ""
+
+msgid "Add Contribution guide"
+msgstr ""
+
+msgid "Add License"
+msgstr ""
+
+msgid "Add an SSH key to your profile to pull or push via SSH."
+msgstr ""
+
+msgid "Add new directory"
+msgstr ""
+
+msgid "All"
+msgstr ""
+
+msgid "Appearances"
+msgstr ""
+
+msgid "Applications"
+msgstr ""
+
+msgid "Archived project! Repository is read-only"
+msgstr ""
+
+msgid "Are you sure you want to delete this pipeline schedule?"
+msgstr ""
+
+msgid "Are you sure you want to discard your changes?"
+msgstr ""
+
+msgid "Are you sure you want to reset registration token?"
+msgstr ""
+
+msgid "Are you sure you want to reset the health check token?"
+msgstr ""
+
+msgid "Are you sure?"
+msgstr ""
+
+msgid "Attach a file by drag &amp; drop or %{upload_link}"
+msgstr ""
+
+msgid "Authentication log"
+msgstr ""
+
+msgid "Billing"
+msgstr ""
+
+msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
+msgstr ""
+
+msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
+msgstr ""
+
+msgid "BillingPlans|Current plan"
+msgstr ""
+
+msgid "BillingPlans|Customer Support"
+msgstr ""
+
+msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
+msgstr ""
+
+msgid "BillingPlans|Manage plan"
+msgstr ""
+
+msgid "BillingPlans|Please contact %{customer_support_link} in that case."
+msgstr ""
+
+msgid "BillingPlans|See all %{plan_name} features"
+msgstr ""
+
+msgid "BillingPlans|This group uses the plan associated with its parent group."
+msgstr ""
+
+msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
+msgstr ""
+
+msgid "BillingPlans|Upgrade"
+msgstr ""
+
+msgid "BillingPlans|You are currently on the %{plan_link} plan."
+msgstr ""
+
+msgid "BillingPlans|frequently asked questions"
+msgstr ""
+
+msgid "BillingPlans|monthly"
+msgstr ""
+
+msgid "BillingPlans|paid annually at %{price_per_year}"
+msgstr ""
+
+msgid "BillingPlans|per user"
+msgstr ""
+
+msgid "Billinglans|Downgrade"
+msgstr ""
+
+msgid "Branch"
+msgid_plural "Branches"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
+msgstr ""
+
+msgid "BranchSwitcherPlaceholder|Search branches"
+msgstr ""
+
+msgid "BranchSwitcherTitle|Switch branch"
+msgstr ""
+
+msgid "Branches"
+msgstr ""
+
+msgid "Browse Directory"
+msgstr ""
+
+msgid "Browse File"
+msgstr ""
+
+msgid "Browse Files"
+msgstr ""
+
+msgid "Browse files"
+msgstr ""
+
+msgid "ByAuthor|by"
+msgstr ""
+
+msgid "CI / CD"
+msgstr ""
+
+msgid "CI configuration"
+msgstr ""
+
+msgid "Cancel"
+msgstr ""
+
+msgid "Cancel edit"
+msgstr ""
+
+msgid "ChangeTypeActionLabel|Pick into branch"
+msgstr ""
+
+msgid "ChangeTypeActionLabel|Revert in branch"
+msgstr ""
+
+msgid "ChangeTypeAction|Cherry-pick"
+msgstr ""
+
+msgid "ChangeTypeAction|Revert"
+msgstr ""
+
+msgid "Changelog"
+msgstr ""
+
+msgid "Charts"
+msgstr ""
+
+msgid "Chat"
+msgstr ""
+
+msgid "Cherry-pick this commit"
+msgstr ""
+
+msgid "Cherry-pick this merge request"
+msgstr ""
+
+msgid "CiStatusLabel|canceled"
+msgstr ""
+
+msgid "CiStatusLabel|created"
+msgstr ""
+
+msgid "CiStatusLabel|failed"
+msgstr ""
+
+msgid "CiStatusLabel|manual action"
+msgstr ""
+
+msgid "CiStatusLabel|passed"
+msgstr ""
+
+msgid "CiStatusLabel|passed with warnings"
+msgstr ""
+
+msgid "CiStatusLabel|pending"
+msgstr ""
+
+msgid "CiStatusLabel|skipped"
+msgstr ""
+
+msgid "CiStatusLabel|waiting for manual action"
+msgstr ""
+
+msgid "CiStatusText|blocked"
+msgstr ""
+
+msgid "CiStatusText|canceled"
+msgstr ""
+
+msgid "CiStatusText|created"
+msgstr ""
+
+msgid "CiStatusText|failed"
+msgstr ""
+
+msgid "CiStatusText|manual"
+msgstr ""
+
+msgid "CiStatusText|passed"
+msgstr ""
+
+msgid "CiStatusText|pending"
+msgstr ""
+
+msgid "CiStatusText|skipped"
+msgstr ""
+
+msgid "CiStatus|running"
+msgstr ""
+
+msgid "Comments"
+msgstr ""
+
+msgid "Commit"
+msgid_plural "Commits"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Commit duration in minutes for last 30 commits"
+msgstr ""
+
+msgid "Commit message"
+msgstr ""
+
+msgid "CommitBoxTitle|Commit"
+msgstr ""
+
+msgid "CommitMessage|Add %{file_name}"
+msgstr ""
+
+msgid "Commits"
+msgstr ""
+
+msgid "Commits feed"
+msgstr ""
+
+msgid "Commits|History"
+msgstr ""
+
+msgid "Committed by"
+msgstr ""
+
+msgid "Compare"
+msgstr ""
+
+msgid "Container Registry"
+msgstr ""
+
+msgid "Contribution guide"
+msgstr ""
+
+msgid "Contributors"
+msgstr ""
+
+msgid "Copy SSH public key to clipboard"
+msgstr ""
+
+msgid "Copy URL to clipboard"
+msgstr ""
+
+msgid "Copy commit SHA to clipboard"
+msgstr ""
+
+msgid "Create New Directory"
+msgstr ""
+
+msgid "Create a new branch"
+msgstr ""
+
+msgid "Create a personal access token on your account to pull or push via %{protocol}."
+msgstr ""
+
+msgid "Create directory"
+msgstr ""
+
+msgid "Create empty bare repository"
+msgstr ""
+
+msgid "Create merge request"
+msgstr ""
+
+msgid "Create new..."
+msgstr ""
+
+msgid "CreateNewFork|Fork"
+msgstr ""
+
+msgid "CreateTag|Tag"
+msgstr ""
+
+msgid "CreateTokenToCloneLink|create a personal access token"
+msgstr ""
+
+msgid "Cron Timezone"
+msgstr ""
+
+msgid "Cron syntax"
+msgstr ""
+
+msgid "Custom notification events"
+msgstr ""
+
+msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
+msgstr ""
+
+msgid "Cycle Analytics"
+msgstr ""
+
+msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
+msgstr ""
+
+msgid "CycleAnalyticsStage|Code"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Issue"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Plan"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Production"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Review"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Staging"
+msgstr ""
+
+msgid "CycleAnalyticsStage|Test"
+msgstr ""
+
+msgid "Define a custom pattern with cron syntax"
+msgstr ""
+
+msgid "Delete"
+msgstr ""
+
+msgid "Deploy"
+msgid_plural "Deploys"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Deploy Keys"
+msgstr ""
+
+msgid "Description"
+msgstr ""
+
+msgid "Details"
+msgstr ""
+
+msgid "Directory name"
+msgstr ""
+
+msgid "Discard changes"
+msgstr ""
+
+msgid "Don't show again"
+msgstr ""
+
+msgid "Download"
+msgstr ""
+
+msgid "Download tar"
+msgstr ""
+
+msgid "Download tar.bz2"
+msgstr ""
+
+msgid "Download tar.gz"
+msgstr ""
+
+msgid "Download zip"
+msgstr "Download zip"
+
+msgid "DownloadArtifacts|Download"
+msgstr ""
+
+msgid "DownloadCommit|Email Patches"
+msgstr ""
+
+msgid "DownloadCommit|Plain Diff"
+msgstr ""
+
+msgid "DownloadSource|Download"
+msgstr ""
+
+msgid "Edit"
+msgstr ""
+
+msgid "Edit Pipeline Schedule %{id}"
+msgstr ""
+
+msgid "Emails"
+msgstr ""
+
+msgid "EventFilterBy|Filter by all"
+msgstr ""
+
+msgid "EventFilterBy|Filter by comments"
+msgstr ""
+
+msgid "EventFilterBy|Filter by issue events"
+msgstr ""
+
+msgid "EventFilterBy|Filter by merge events"
+msgstr ""
+
+msgid "EventFilterBy|Filter by push events"
+msgstr ""
+
+msgid "EventFilterBy|Filter by team"
+msgstr ""
+
+msgid "Every day (at 4:00am)"
+msgstr ""
+
+msgid "Every month (on the 1st at 4:00am)"
+msgstr ""
+
+msgid "Every week (Sundays at 4:00am)"
+msgstr ""
+
+msgid "Failed to change the owner"
+msgstr ""
+
+msgid "Failed to remove the pipeline schedule"
+msgstr ""
+
+msgid "Files"
+msgstr ""
+
+msgid "Filter by commit message"
+msgstr ""
+
+msgid "Find by path"
+msgstr ""
+
+msgid "Find file"
+msgstr ""
+
+msgid "FirstPushedBy|First"
+msgstr ""
+
+msgid "FirstPushedBy|pushed by"
+msgstr ""
+
+msgid "Fork"
+msgid_plural "Forks"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "ForkedFromProjectPath|Forked from"
+msgstr ""
+
+msgid "From issue creation until deploy to production"
+msgstr ""
+
+msgid "From merge request merge until deploy to production"
+msgstr ""
+
+msgid "GPG Keys"
+msgstr ""
+
+msgid "Geo Nodes"
+msgstr ""
+
+msgid "Git storage health information has been reset"
+msgstr ""
+
+msgid "GitLab Runner section"
+msgstr ""
+
+msgid "Go to your fork"
+msgstr ""
+
+msgid "GoToYourFork|Fork"
+msgstr ""
+
+msgid "Group overview"
+msgstr ""
+
+msgid "Health Check"
+msgstr ""
+
+msgid "Health information can be retrieved from the following endpoints. More information is available"
+msgstr ""
+
+msgid "HealthCheck|Access token is"
+msgstr ""
+
+msgid "HealthCheck|Healthy"
+msgstr ""
+
+msgid "HealthCheck|No Health Problems Detected"
+msgstr ""
+
+msgid "HealthCheck|Unhealthy"
+msgstr ""
+
+msgid "Home"
+msgstr ""
+
+msgid "Hooks"
+msgstr ""
+
+msgid "Housekeeping successfully started"
+msgstr ""
+
+msgid "Import repository"
+msgstr ""
+
+msgid "Install a Runner compatible with GitLab CI"
+msgstr ""
+
+msgid "Interval Pattern"
+msgstr ""
+
+msgid "Introducing Cycle Analytics"
+msgstr ""
+
+msgid "Issue events"
+msgstr ""
+
+msgid "Issues"
+msgstr ""
+
+msgid "LFSStatus|Disabled"
+msgstr ""
+
+msgid "LFSStatus|Enabled"
+msgstr ""
+
+msgid "Labels"
+msgstr ""
+
+msgid "Last %d day"
+msgid_plural "Last %d days"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Last Pipeline"
+msgstr ""
+
+msgid "Last Update"
+msgstr ""
+
+msgid "Last commit"
+msgstr ""
+
+msgid "LastPushEvent|You pushed to"
+msgstr ""
+
+msgid "LastPushEvent|at"
+msgstr ""
+
+msgid "Learn more in the"
+msgstr ""
+
+msgid "Learn more in the|pipeline schedules documentation"
+msgstr ""
+
+msgid "Leave group"
+msgstr ""
+
+msgid "Leave project"
+msgstr ""
+
+msgid "License"
+msgstr ""
+
+msgid "Limited to showing %d event at most"
+msgid_plural "Limited to showing %d events at most"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Locked Files"
+msgstr ""
+
+msgid "Median"
+msgstr ""
+
+msgid "Members"
+msgstr ""
+
+msgid "Merge Requests"
+msgstr ""
+
+msgid "Merge events"
+msgstr ""
+
+msgid "Messages"
+msgstr ""
+
+msgid "MissingSSHKeyWarningLink|add an SSH key"
+msgstr ""
+
+msgid "Monitoring"
+msgstr ""
+
+msgid "More information is available|here"
+msgstr ""
+
+msgid "New Issue"
+msgid_plural "New Issues"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "New Pipeline Schedule"
+msgstr ""
+
+msgid "New branch"
+msgstr ""
+
+msgid "New directory"
+msgstr ""
+
+msgid "New file"
+msgstr ""
+
+msgid "New issue"
+msgstr ""
+
+msgid "New merge request"
+msgstr ""
+
+msgid "New schedule"
+msgstr ""
+
+msgid "New snippet"
+msgstr ""
+
+msgid "New tag"
+msgstr ""
+
+msgid "No repository"
+msgstr ""
+
+msgid "No schedules"
+msgstr ""
+
+msgid "Not available"
+msgstr ""
+
+msgid "Not enough data"
+msgstr ""
+
+msgid "Notification events"
+msgstr ""
+
+msgid "NotificationEvent|Close issue"
+msgstr ""
+
+msgid "NotificationEvent|Close merge request"
+msgstr ""
+
+msgid "NotificationEvent|Failed pipeline"
+msgstr ""
+
+msgid "NotificationEvent|Merge merge request"
+msgstr ""
+
+msgid "NotificationEvent|New issue"
+msgstr ""
+
+msgid "NotificationEvent|New merge request"
+msgstr ""
+
+msgid "NotificationEvent|New note"
+msgstr ""
+
+msgid "NotificationEvent|Reassign issue"
+msgstr ""
+
+msgid "NotificationEvent|Reassign merge request"
+msgstr ""
+
+msgid "NotificationEvent|Reopen issue"
+msgstr ""
+
+msgid "NotificationEvent|Successful pipeline"
+msgstr ""
+
+msgid "NotificationLevel|Custom"
+msgstr ""
+
+msgid "NotificationLevel|Disabled"
+msgstr ""
+
+msgid "NotificationLevel|Global"
+msgstr ""
+
+msgid "NotificationLevel|On mention"
+msgstr ""
+
+msgid "NotificationLevel|Participate"
+msgstr ""
+
+msgid "NotificationLevel|Watch"
+msgstr ""
+
+msgid "Notifications"
+msgstr ""
+
+msgid "OfSearchInADropdown|Filter"
+msgstr ""
+
+msgid "OpenedNDaysAgo|Opened"
+msgstr ""
+
+msgid "Options"
+msgstr ""
+
+msgid "Overview"
+msgstr ""
+
+msgid "Owner"
+msgstr ""
+
+msgid "Password"
+msgstr ""
+
+msgid "Pipeline"
+msgstr ""
+
+msgid "Pipeline Health"
+msgstr ""
+
+msgid "Pipeline Schedule"
+msgstr ""
+
+msgid "Pipeline Schedules"
+msgstr ""
+
+msgid "Pipeline quota"
+msgstr ""
+
+msgid "PipelineCharts|Failed:"
+msgstr ""
+
+msgid "PipelineCharts|Overall statistics"
+msgstr ""
+
+msgid "PipelineCharts|Success ratio:"
+msgstr ""
+
+msgid "PipelineCharts|Successful:"
+msgstr ""
+
+msgid "PipelineCharts|Total:"
+msgstr ""
+
+msgid "PipelineSchedules|Activated"
+msgstr ""
+
+msgid "PipelineSchedules|Active"
+msgstr ""
+
+msgid "PipelineSchedules|All"
+msgstr ""
+
+msgid "PipelineSchedules|Inactive"
+msgstr ""
+
+msgid "PipelineSchedules|Input variable key"
+msgstr ""
+
+msgid "PipelineSchedules|Input variable value"
+msgstr ""
+
+msgid "PipelineSchedules|Next Run"
+msgstr ""
+
+msgid "PipelineSchedules|None"
+msgstr ""
+
+msgid "PipelineSchedules|Provide a short description for this pipeline"
+msgstr ""
+
+msgid "PipelineSchedules|Remove variable row"
+msgstr ""
+
+msgid "PipelineSchedules|Take ownership"
+msgstr ""
+
+msgid "PipelineSchedules|Target"
+msgstr ""
+
+msgid "PipelineSchedules|Variables"
+msgstr ""
+
+msgid "PipelineSheduleIntervalPattern|Custom"
+msgstr ""
+
+msgid "Pipelines"
+msgstr ""
+
+msgid "Pipelines charts"
+msgstr ""
+
+msgid "Pipelines for last month"
+msgstr ""
+
+msgid "Pipelines for last week"
+msgstr ""
+
+msgid "Pipelines for last year"
+msgstr ""
+
+msgid "Pipeline|all"
+msgstr ""
+
+msgid "Pipeline|success"
+msgstr ""
+
+msgid "Pipeline|with stage"
+msgstr ""
+
+msgid "Pipeline|with stages"
+msgstr ""
+
+msgid "Preferences"
+msgstr ""
+
+msgid "Profile Settings"
+msgstr ""
+
+msgid "Project"
+msgstr ""
+
+msgid "Project '%{project_name}' queued for deletion."
+msgstr ""
+
+msgid "Project '%{project_name}' was successfully created."
+msgstr ""
+
+msgid "Project '%{project_name}' was successfully updated."
+msgstr ""
+
+msgid "Project '%{project_name}' will be deleted."
+msgstr ""
+
+msgid "Project access must be granted explicitly to each user."
+msgstr ""
+
+msgid "Project details"
+msgstr ""
+
+msgid "Project export could not be deleted."
+msgstr ""
+
+msgid "Project export has been deleted."
+msgstr ""
+
+msgid "Project export link has expired. Please generate a new export from your project settings."
+msgstr ""
+
+msgid "Project export started. A download link will be sent by email."
+msgstr ""
+
+msgid "Project home"
+msgstr ""
+
+msgid "Project overview"
+msgstr ""
+
+msgid "ProjectActivityRSS|Subscribe"
+msgstr ""
+
+msgid "ProjectFeature|Disabled"
+msgstr ""
+
+msgid "ProjectFeature|Everyone with access"
+msgstr ""
+
+msgid "ProjectFeature|Only team members"
+msgstr ""
+
+msgid "ProjectFileTree|Name"
+msgstr ""
+
+msgid "ProjectLastActivity|Never"
+msgstr ""
+
+msgid "ProjectLifecycle|Stage"
+msgstr ""
+
+msgid "ProjectNetworkGraph|Graph"
+msgstr ""
+
+msgid "Push Rules"
+msgstr ""
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
+msgid "Push events"
+msgstr ""
+
+msgid "Read more"
+msgstr ""
+
+msgid "Readme"
+msgstr ""
+
+msgid "RefSwitcher|Branches"
+msgstr ""
+
+msgid "RefSwitcher|Tags"
+msgstr ""
+
+msgid "Related Commits"
+msgstr ""
+
+msgid "Related Deployed Jobs"
+msgstr ""
+
+msgid "Related Issues"
+msgstr ""
+
+msgid "Related Jobs"
+msgstr ""
+
+msgid "Related Merge Requests"
+msgstr ""
+
+msgid "Related Merged Requests"
+msgstr ""
+
+msgid "Remind later"
+msgstr ""
+
+msgid "Remove project"
+msgstr ""
+
+msgid "Repository"
+msgstr ""
+
+msgid "Request Access"
+msgstr ""
+
+msgid "Reset git storage health information"
+msgstr ""
+
+msgid "Reset health check access token"
+msgstr ""
+
+msgid "Reset runners registration token"
+msgstr ""
+
+msgid "Revert this commit"
+msgstr ""
+
+msgid "Revert this merge request"
+msgstr ""
+
+msgid "SSH Keys"
+msgstr ""
+
+msgid "Save pipeline schedule"
+msgstr ""
+
+msgid "Schedule a new pipeline"
+msgstr ""
+
+msgid "Scheduling Pipelines"
+msgstr ""
+
+msgid "Search branches and tags"
+msgstr ""
+
+msgid "Select Archive Format"
+msgstr ""
+
+msgid "Select a timezone"
+msgstr ""
+
+msgid "Select existing branch"
+msgstr ""
+
+msgid "Select target branch"
+msgstr ""
+
+msgid "Service Templates"
+msgstr ""
+
+msgid "Set a password on your account to pull or push via %{protocol}."
+msgstr ""
+
+msgid "Set up CI"
+msgstr ""
+
+msgid "Set up Koding"
+msgstr ""
+
+msgid "Set up auto deploy"
+msgstr ""
+
+msgid "SetPasswordToCloneLink|set a password"
+msgstr ""
+
+msgid "Settings"
+msgstr ""
+
+msgid "Showing %d event"
+msgid_plural "Showing %d events"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Snippets"
+msgstr ""
+
+msgid "Source code"
+msgstr ""
+
+msgid "Spam Logs"
+msgstr ""
+
+msgid "Specify the following URL during the Runner setup:"
+msgstr ""
+
+msgid "StarProject|Star"
+msgstr ""
+
+msgid "Start a %{new_merge_request} with these changes"
+msgstr ""
+
+msgid "Start the Runner!"
+msgstr ""
+
+msgid "Switch branch/tag"
+msgstr ""
+
+msgid "Tag"
+msgid_plural "Tags"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Tags"
+msgstr ""
+
+msgid "Target Branch"
+msgstr ""
+
+msgid "Team"
+msgstr ""
+
+msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
+msgstr ""
+
+msgid "The collection of events added to the data gathered for that stage."
+msgstr ""
+
+msgid "The fork relationship has been removed."
+msgstr ""
+
+msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
+msgstr ""
+
+msgid "The phase of the development lifecycle."
+msgstr ""
+
+msgid "The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user."
+msgstr ""
+
+msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
+msgstr ""
+
+msgid "The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle."
+msgstr ""
+
+msgid "The project can be accessed by any logged in user."
+msgstr ""
+
+msgid "The project can be accessed without any authentication."
+msgstr ""
+
+msgid "The repository for this project does not exist."
+msgstr ""
+
+msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
+msgstr ""
+
+msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
+msgstr ""
+
+msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
+msgstr ""
+
+msgid "The time taken by each data entry gathered by that stage."
+msgstr ""
+
+msgid "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."
+msgstr ""
+
+msgid "There are problems accessing Git storage: "
+msgstr ""
+
+msgid "This means you can not push code until you create an empty repository or import existing one."
+msgstr ""
+
+msgid "Time before an issue gets scheduled"
+msgstr ""
+
+msgid "Time before an issue starts implementation"
+msgstr ""
+
+msgid "Time between merge request creation and merge/close"
+msgstr ""
+
+msgid "Time until first merge request"
+msgstr ""
+
+msgid "Timeago|%s days ago"
+msgstr ""
+
+msgid "Timeago|%s days remaining"
+msgstr ""
+
+msgid "Timeago|%s hours remaining"
+msgstr ""
+
+msgid "Timeago|%s minutes ago"
+msgstr ""
+
+msgid "Timeago|%s minutes remaining"
+msgstr ""
+
+msgid "Timeago|%s months ago"
+msgstr ""
+
+msgid "Timeago|%s months remaining"
+msgstr ""
+
+msgid "Timeago|%s seconds remaining"
+msgstr ""
+
+msgid "Timeago|%s weeks ago"
+msgstr ""
+
+msgid "Timeago|%s weeks remaining"
+msgstr ""
+
+msgid "Timeago|%s years ago"
+msgstr ""
+
+msgid "Timeago|%s years remaining"
+msgstr ""
+
+msgid "Timeago|1 day remaining"
+msgstr ""
+
+msgid "Timeago|1 hour remaining"
+msgstr ""
+
+msgid "Timeago|1 minute remaining"
+msgstr ""
+
+msgid "Timeago|1 month remaining"
+msgstr ""
+
+msgid "Timeago|1 week remaining"
+msgstr ""
+
+msgid "Timeago|1 year remaining"
+msgstr ""
+
+msgid "Timeago|Past due"
+msgstr ""
+
+msgid "Timeago|a day ago"
+msgstr ""
+
+msgid "Timeago|a month ago"
+msgstr ""
+
+msgid "Timeago|a week ago"
+msgstr ""
+
+msgid "Timeago|a while"
+msgstr ""
+
+msgid "Timeago|a year ago"
+msgstr ""
+
+msgid "Timeago|about %s hours ago"
+msgstr ""
+
+msgid "Timeago|about a minute ago"
+msgstr ""
+
+msgid "Timeago|about an hour ago"
+msgstr ""
+
+msgid "Timeago|in %s days"
+msgstr ""
+
+msgid "Timeago|in %s hours"
+msgstr ""
+
+msgid "Timeago|in %s minutes"
+msgstr ""
+
+msgid "Timeago|in %s months"
+msgstr ""
+
+msgid "Timeago|in %s seconds"
+msgstr ""
+
+msgid "Timeago|in %s weeks"
+msgstr ""
+
+msgid "Timeago|in %s years"
+msgstr ""
+
+msgid "Timeago|in 1 day"
+msgstr ""
+
+msgid "Timeago|in 1 hour"
+msgstr ""
+
+msgid "Timeago|in 1 minute"
+msgstr ""
+
+msgid "Timeago|in 1 month"
+msgstr ""
+
+msgid "Timeago|in 1 week"
+msgstr ""
+
+msgid "Timeago|in 1 year"
+msgstr ""
+
+msgid "Timeago|less than a minute ago"
+msgstr ""
+
+msgid "Time|hr"
+msgid_plural "Time|hrs"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Time|min"
+msgid_plural "Time|mins"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "Time|s"
+msgstr ""
+
+msgid "Total Time"
+msgstr ""
+
+msgid "Total test time for all commits/merges"
+msgstr ""
+
+msgid "Unstar"
+msgstr ""
+
+msgid "Upload New File"
+msgstr ""
+
+msgid "Upload file"
+msgstr ""
+
+msgid "UploadLink|click to upload"
+msgstr ""
+
+msgid "Use the following registration token during setup:"
+msgstr ""
+
+msgid "Use your global notification setting"
+msgstr ""
+
+msgid "View open merge request"
+msgstr ""
+
+msgid "VisibilityLevel|Internal"
+msgstr ""
+
+msgid "VisibilityLevel|Private"
+msgstr ""
+
+msgid "VisibilityLevel|Public"
+msgstr ""
+
+msgid "VisibilityLevel|Unknown"
+msgstr ""
+
+msgid "Want to see the data? Please ask an administrator for access."
+msgstr ""
+
+msgid "We don't have enough data to show this stage."
+msgstr ""
+
+msgid "Wiki"
+msgstr ""
+
+msgid "Withdraw Access Request"
+msgstr ""
+
+msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr ""
+
+msgid "You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?"
+msgstr ""
+
+msgid "You are going to remove the fork relationship to source project %{forked_from_project}. Are you ABSOLUTELY sure?"
+msgstr ""
+
+msgid "You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?"
+msgstr ""
+
+msgid "You can only add files when you are on a branch"
+msgstr ""
+
+msgid "You have reached your project limit"
+msgstr ""
+
+msgid "You must sign in to star a project"
+msgstr ""
+
+msgid "You need permission."
+msgstr ""
+
+msgid "You will not get any notifications via email"
+msgstr ""
+
+msgid "You will only receive notifications for the events you choose"
+msgstr ""
+
+msgid "You will only receive notifications for threads you have participated in"
+msgstr ""
+
+msgid "You will receive notifications for any activity"
+msgstr ""
+
+msgid "You will receive notifications only for comments in which you were @mentioned"
+msgstr ""
+
+msgid "You won't be able to pull or push project code via %{protocol} until you %{set_password_link} on your account"
+msgstr ""
+
+msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
+msgstr ""
+
+msgid "Your name"
+msgstr ""
+
+msgid "day"
+msgid_plural "days"
+msgstr[0] ""
+msgstr[1] ""
+
+msgid "new merge request"
+msgstr ""
+
+msgid "notification emails"
+msgstr ""
+
+msgid "parent"
+msgid_plural "parents"
+msgstr[0] ""
+msgstr[1] ""
+
diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po
index 88ca25dbb3b..5469f77d950 100644
--- a/locale/pt_BR/gitlab.po
+++ b/locale/pt_BR/gitlab.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"PO-Revision-Date: 2017-09-15 05:18-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Portuguese, Brazilian\n"
"Language: pt_BR\n"
@@ -986,6 +986,24 @@ msgstr "Árvore"
msgid "Push Rules"
msgstr ""
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push events"
msgstr ""
diff --git a/locale/ru/gitlab.po b/locale/ru/gitlab.po
index 96e6c8a8d3f..808bc9dedce 100644
--- a/locale/ru/gitlab.po
+++ b/locale/ru/gitlab.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"PO-Revision-Date: 2017-09-15 05:19-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@@ -997,6 +997,24 @@ msgstr "Граф"
msgid "Push Rules"
msgstr ""
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push events"
msgstr ""
diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po
index 4d24140f3dc..1dc42901daf 100644
--- a/locale/uk/gitlab.po
+++ b/locale/uk/gitlab.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"PO-Revision-Date: 2017-09-15 05:20-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Ukrainian\n"
"Language: uk_UA\n"
@@ -997,6 +997,24 @@ msgstr "Історія"
msgid "Push Rules"
msgstr ""
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push events"
msgstr ""
diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po
index 47de28209df..d6f756e813f 100644
--- a/locale/zh_CN/gitlab.po
+++ b/locale/zh_CN/gitlab.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"PO-Revision-Date: 2017-09-15 05:21-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Chinese Simplified\n"
"Language: zh_CN\n"
@@ -54,16 +54,16 @@ msgid "About auto deploy"
msgstr "关于自动部署"
msgid "Abuse Reports"
-msgstr ""
+msgstr "滥用报告"
msgid "Access Tokens"
-msgstr ""
+msgstr "访问令牌"
msgid "Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again."
msgstr "为方便修复挂载问题,访问故障存储已被暂时禁用。在问题解决后请重置存储健康信息,以允许再次访问。"
msgid "Account"
-msgstr ""
+msgstr "帐号"
msgid "Active"
msgstr "启用"
@@ -90,10 +90,10 @@ msgid "All"
msgstr "全部"
msgid "Appearances"
-msgstr ""
+msgstr "外观样式"
msgid "Applications"
-msgstr ""
+msgstr "应用程序"
msgid "Archived project! Repository is read-only"
msgstr "项目已归档!存储库为只读状态"
@@ -117,61 +117,61 @@ msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "拖放文件到此处或者 %{upload_link}"
msgid "Authentication log"
-msgstr ""
+msgstr "认证日志"
msgid "Billing"
-msgstr ""
+msgstr "账单"
msgid "BillingPlans|%{group_name} is currently on the %{plan_link} plan."
-msgstr ""
+msgstr "%{group_name} 目前正在使用 %{plan_link} 方案。"
msgid "BillingPlans|Automatic downgrade and upgrade to some plans is currently not available."
-msgstr ""
+msgstr "当某些方案当前不可用时自动降级和升级。"
msgid "BillingPlans|Current plan"
-msgstr ""
+msgstr "当前方案"
msgid "BillingPlans|Customer Support"
-msgstr ""
+msgstr "客户支持"
msgid "BillingPlans|Learn more about each plan by reading our %{faq_link}."
-msgstr ""
+msgstr "通过阅读%{faq_link} 了解关于每个方案的更多信息。"
msgid "BillingPlans|Manage plan"
-msgstr ""
+msgstr "管理方案"
msgid "BillingPlans|Please contact %{customer_support_link} in that case."
-msgstr ""
+msgstr "在这种情况下,请联系 %{customer_support_link}。"
msgid "BillingPlans|See all %{plan_name} features"
-msgstr ""
+msgstr "查看 %{plan_name} 的所有功能"
msgid "BillingPlans|This group uses the plan associated with its parent group."
-msgstr ""
+msgstr "该群组使用与它的父团队相关联的计划。"
msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}."
-msgstr ""
+msgstr "请访问 %{parent_billing_page_link} 的计费方案部分来管理该团队的计费方案,。"
msgid "BillingPlans|Upgrade"
-msgstr ""
+msgstr "升级"
msgid "BillingPlans|You are currently on the %{plan_link} plan."
-msgstr ""
+msgstr "你目前正在使用 %{plan_link} 方案。"
msgid "BillingPlans|frequently asked questions"
-msgstr ""
+msgstr "常见问题"
msgid "BillingPlans|monthly"
-msgstr ""
+msgstr "每月"
msgid "BillingPlans|paid annually at %{price_per_year}"
-msgstr ""
+msgstr "每年支付 %{price_per_year}"
msgid "BillingPlans|per user"
-msgstr ""
+msgstr "每个用户"
msgid "Billinglans|Downgrade"
-msgstr ""
+msgstr "降级"
msgid "Branch"
msgid_plural "Branches"
@@ -205,7 +205,7 @@ msgid "ByAuthor|by"
msgstr "作者:"
msgid "CI / CD"
-msgstr ""
+msgstr "CI / CD"
msgid "CI configuration"
msgstr "CI 配置"
@@ -235,7 +235,7 @@ msgid "Charts"
msgstr "统计图"
msgid "Chat"
-msgstr ""
+msgstr "交流"
msgid "Cherry-pick this commit"
msgstr "优选此提交"
@@ -332,7 +332,7 @@ msgid "Compare"
msgstr "比较"
msgid "Container Registry"
-msgstr ""
+msgstr "容器注册表"
msgid "Contribution guide"
msgstr "贡献指南"
@@ -341,7 +341,7 @@ msgid "Contributors"
msgstr "贡献者"
msgid "Copy SSH public key to clipboard"
-msgstr ""
+msgstr "将 SSH 公钥复制到剪贴板"
msgid "Copy URL to clipboard"
msgstr "复制 URL 到剪贴板"
@@ -429,7 +429,7 @@ msgid_plural "Deploys"
msgstr[0] "部署"
msgid "Deploy Keys"
-msgstr ""
+msgstr "部署密钥"
msgid "Description"
msgstr "描述"
@@ -480,7 +480,7 @@ msgid "Edit Pipeline Schedule %{id}"
msgstr "编辑 %{id} 流水线计划"
msgid "Emails"
-msgstr ""
+msgstr "电子邮件"
msgid "EventFilterBy|Filter by all"
msgstr "全部"
@@ -547,10 +547,10 @@ msgid "From merge request merge until deploy to production"
msgstr "从合并请求被合并后到部署至生产环境"
msgid "GPG Keys"
-msgstr ""
+msgstr "GPG 密钥"
msgid "Geo Nodes"
-msgstr ""
+msgstr "Geo 节点"
msgid "Git storage health information has been reset"
msgstr "Git 存储健康信息已重置"
@@ -565,7 +565,7 @@ msgid "GoToYourFork|Fork"
msgstr "跳转到派生项目"
msgid "Group overview"
-msgstr ""
+msgstr "群组概览"
msgid "Health Check"
msgstr "健康检查"
@@ -589,7 +589,7 @@ msgid "Home"
msgstr "首页"
msgid "Hooks"
-msgstr ""
+msgstr "钩子"
msgid "Housekeeping successfully started"
msgstr "已开始维护"
@@ -610,7 +610,7 @@ msgid "Issue events"
msgstr "议题事件"
msgid "Issues"
-msgstr ""
+msgstr "议题"
msgid "LFSStatus|Disabled"
msgstr "停用"
@@ -619,7 +619,7 @@ msgid "LFSStatus|Enabled"
msgstr "启用"
msgid "Labels"
-msgstr ""
+msgstr "标签"
msgid "Last %d day"
msgid_plural "Last %d days"
@@ -653,35 +653,35 @@ msgid "Leave project"
msgstr "退出项目"
msgid "License"
-msgstr ""
+msgstr "许可"
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] "最多显示 %d 个事件"
msgid "Locked Files"
-msgstr ""
+msgstr "锁定的文件"
msgid "Median"
msgstr "中位数"
msgid "Members"
-msgstr ""
+msgstr "成员"
msgid "Merge Requests"
-msgstr ""
+msgstr "合并请求"
msgid "Merge events"
msgstr "合并事件"
msgid "Messages"
-msgstr ""
+msgstr "消息"
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "新建 SSH 公钥"
msgid "Monitoring"
-msgstr ""
+msgstr "监控"
msgid "More information is available|here"
msgstr "帮助文档"
@@ -784,7 +784,7 @@ msgid "NotificationLevel|Watch"
msgstr "关注"
msgid "Notifications"
-msgstr ""
+msgstr "通知"
msgid "OfSearchInADropdown|Filter"
msgstr "筛选"
@@ -796,13 +796,13 @@ msgid "Options"
msgstr "操作"
msgid "Overview"
-msgstr ""
+msgstr "概览"
msgid "Owner"
msgstr "所有者"
msgid "Password"
-msgstr ""
+msgstr "密码"
msgid "Pipeline"
msgstr "流水线"
@@ -817,7 +817,7 @@ msgid "Pipeline Schedules"
msgstr "流水线计划"
msgid "Pipeline quota"
-msgstr ""
+msgstr "流水线配额"
msgid "PipelineCharts|Failed:"
msgstr "失败:"
@@ -883,13 +883,13 @@ msgid "Pipelines charts"
msgstr "流水线统计图"
msgid "Pipelines for last month"
-msgstr ""
+msgstr "上个月的流水线"
msgid "Pipelines for last week"
-msgstr ""
+msgstr "上周的流水线"
msgid "Pipelines for last year"
-msgstr ""
+msgstr "去年的流水线"
msgid "Pipeline|all"
msgstr "所有"
@@ -904,10 +904,10 @@ msgid "Pipeline|with stages"
msgstr "于阶段"
msgid "Preferences"
-msgstr ""
+msgstr "偏好设置"
msgid "Profile Settings"
-msgstr ""
+msgstr "账户设置"
msgid "Project"
msgstr "项目"
@@ -946,7 +946,7 @@ msgid "Project home"
msgstr "项目首页"
msgid "Project overview"
-msgstr ""
+msgstr "项目概览"
msgid "ProjectActivityRSS|Subscribe"
msgstr "订阅"
@@ -973,6 +973,24 @@ msgid "ProjectNetworkGraph|Graph"
msgstr "分支图"
msgid "Push Rules"
+msgstr "推送规则"
+
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
msgid "Push events"
@@ -1036,7 +1054,7 @@ msgid "Revert this merge request"
msgstr "还原此合并请求"
msgid "SSH Keys"
-msgstr ""
+msgstr "SSH 密钥"
msgid "Save pipeline schedule"
msgstr "保存流水线计划"
@@ -1063,7 +1081,7 @@ msgid "Select target branch"
msgstr "选择目标分支"
msgid "Service Templates"
-msgstr ""
+msgstr "服务模板"
msgid "Set a password on your account to pull or push via %{protocol}."
msgstr "为账号创建一个用于推送或拉取的 %{protocol} 密码。"
@@ -1081,20 +1099,20 @@ msgid "SetPasswordToCloneLink|set a password"
msgstr "设置密码"
msgid "Settings"
-msgstr ""
+msgstr "设置"
msgid "Showing %d event"
msgid_plural "Showing %d events"
msgstr[0] "显示 %d 个事件"
msgid "Snippets"
-msgstr ""
+msgstr "代码片段"
msgid "Source code"
msgstr "源代码"
msgid "Spam Logs"
-msgstr ""
+msgstr "垃圾信息日志"
msgid "Specify the following URL during the Runner setup:"
msgstr "在 Runner 设置时指定以下 URL:"
@@ -1370,7 +1388,7 @@ msgid "We don't have enough data to show this stage."
msgstr "该阶段的数据不足,无法显示。"
msgid "Wiki"
-msgstr ""
+msgstr "Wiki"
msgid "Withdraw Access Request"
msgstr "取消权限申请"
diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po
index fee0d661c7a..48b86508d1e 100644
--- a/locale/zh_HK/gitlab.po
+++ b/locale/zh_HK/gitlab.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:21-0400\n"
+"PO-Revision-Date: 2017-09-15 05:21-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Chinese Traditional, Hong Kong\n"
"Language: zh_HK\n"
@@ -975,6 +975,24 @@ msgstr "分支圖"
msgid "Push Rules"
msgstr ""
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push events"
msgstr "推送事件 (push event) "
diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po
index 09c07a83d34..da6a98bdb5c 100644
--- a/locale/zh_TW/gitlab.po
+++ b/locale/zh_TW/gitlab.po
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: gitlab-ee\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-09-06 08:32+0200\n"
-"PO-Revision-Date: 2017-09-06 06:20-0400\n"
+"PO-Revision-Date: 2017-09-15 05:21-0400\n"
"Last-Translator: gitlab <mbartlett+crowdin@gitlab.com>\n"
"Language-Team: Chinese Traditional\n"
"Language: zh_TW\n"
@@ -34,7 +34,7 @@ msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will block
msgstr "已失敗 %{number_of_failures} 次,在失敗 %{maximum_failures} 次前 GitLab 會在 %{number_of_seconds} 秒後重試。"
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved."
-msgstr ""
+msgstr "已失敗 %{number_of_failures} / %{maximum_failures} 次,GitLab 將不再自動重試。請在確認問題解決後手動重置儲存空間資訊。"
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
@@ -90,10 +90,10 @@ msgid "All"
msgstr "全部"
msgid "Appearances"
-msgstr ""
+msgstr "外觀"
msgid "Applications"
-msgstr ""
+msgstr "應用程式"
msgid "Archived project! Repository is read-only"
msgstr "此專案已封存!檔案庫 (repository) 為唯讀狀態"
@@ -480,7 +480,7 @@ msgid "Edit Pipeline Schedule %{id}"
msgstr "編輯 %{id} 流水線 (pipeline) 排程"
msgid "Emails"
-msgstr ""
+msgstr "電子郵件"
msgid "EventFilterBy|Filter by all"
msgstr "顯示全部"
@@ -565,7 +565,7 @@ msgid "GoToYourFork|Fork"
msgstr "前往您的分支 (fork) "
msgid "Group overview"
-msgstr ""
+msgstr "群組總覽"
msgid "Health Check"
msgstr "健康檢查"
@@ -610,7 +610,7 @@ msgid "Issue events"
msgstr "議題 (issue) 事件"
msgid "Issues"
-msgstr ""
+msgstr "議題"
msgid "LFSStatus|Disabled"
msgstr "停用"
@@ -619,7 +619,7 @@ msgid "LFSStatus|Enabled"
msgstr "啟用"
msgid "Labels"
-msgstr ""
+msgstr "標籤"
msgid "Last %d day"
msgid_plural "Last %d days"
@@ -666,7 +666,7 @@ msgid "Median"
msgstr "中位數"
msgid "Members"
-msgstr ""
+msgstr "成員"
msgid "Merge Requests"
msgstr ""
@@ -675,13 +675,13 @@ msgid "Merge events"
msgstr "合併 (merge) 事件"
msgid "Messages"
-msgstr ""
+msgstr "訊息"
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr "新增 SSH 金鑰"
msgid "Monitoring"
-msgstr ""
+msgstr "監控"
msgid "More information is available|here"
msgstr "健康檢查"
@@ -784,7 +784,7 @@ msgid "NotificationLevel|Watch"
msgstr "關注"
msgid "Notifications"
-msgstr ""
+msgstr "通知"
msgid "OfSearchInADropdown|Filter"
msgstr "篩選"
@@ -796,13 +796,13 @@ msgid "Options"
msgstr "選項"
msgid "Overview"
-msgstr ""
+msgstr "總覽"
msgid "Owner"
msgstr "所有權"
msgid "Password"
-msgstr ""
+msgstr "密碼"
msgid "Pipeline"
msgstr "流水線 (pipeline) "
@@ -946,7 +946,7 @@ msgid "Project home"
msgstr "專案首頁"
msgid "Project overview"
-msgstr ""
+msgstr "專案總覽"
msgid "ProjectActivityRSS|Subscribe"
msgstr "訂閱"
@@ -975,6 +975,24 @@ msgstr "分支圖"
msgid "Push Rules"
msgstr ""
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push events"
msgstr "推送 (push) 事件"
@@ -1063,7 +1081,7 @@ msgid "Select target branch"
msgstr "選擇目標分支 (branch) "
msgid "Service Templates"
-msgstr ""
+msgstr "服務範本"
msgid "Set a password on your account to pull or push via %{protocol}."
msgstr "請先設定密碼,才能使用 %{protocol} 來上傳 (push) 或下載 (pull) 。"
@@ -1081,7 +1099,7 @@ msgid "SetPasswordToCloneLink|set a password"
msgstr "設定密碼"
msgid "Settings"
-msgstr ""
+msgstr "設定"
msgid "Showing %d event"
msgid_plural "Showing %d events"
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
index 74e53d86266..178c5ea6930 100644
--- a/qa/qa/page/main/menu.rb
+++ b/qa/qa/page/main/menu.rb
@@ -11,7 +11,7 @@ module QA
end
def go_to_admin_area
- within_top_menu { click_link 'Admin area' }
+ within_top_menu { find('.admin-icon').click }
end
def sign_out
diff --git a/scripts/schema_changed.sh b/scripts/schema_changed.sh
new file mode 100644
index 00000000000..5de2b35571d
--- /dev/null
+++ b/scripts/schema_changed.sh
@@ -0,0 +1,10 @@
+function schema_changed() {
+ if [[ ! -z `git diff --name-only -- db/schema.rb` ]]; then
+ echo "db/schema.rb after rake db:migrate:reset is different from one in the repository"
+ exit 1
+ else
+ echo "db/schema.rb after rake db:migrate:reset matches one in the repository"
+ fi
+}
+
+schema_changed
diff --git a/scripts/trigger-build-docs b/scripts/trigger-build-docs
index 44f832ed3e6..d3a9f5ff4ea 100755
--- a/scripts/trigger-build-docs
+++ b/scripts/trigger-build-docs
@@ -3,13 +3,6 @@
require 'gitlab'
#
-# Give the remote branch a different name than the current one
-# in order to avoid conflicts
-#
-@docs_branch = "#{ENV["CI_COMMIT_REF_SLUG"]}-built-from-ce-ee"
-GITLAB_DOCS_REPO = 'gitlab-com/gitlab-docs'.freeze
-
-#
# Configure credentials to be used with gitlab gem
#
Gitlab.configure do |config|
@@ -18,6 +11,26 @@ Gitlab.configure do |config|
end
#
+# The remote docs project
+#
+GITLAB_DOCS_REPO = 'gitlab-com/gitlab-docs'.freeze
+
+#
+# Truncate the remote docs branch name if it's more than 63 characters
+# otherwise we hit the filesystem limit and the directory name where
+# NGINX serves the site won't match the branch name.
+#
+def docs_branch
+ # The maximum string length a file can have on a filesystem (ext4)
+ # is 63 characters. Let's use something smaller to be 100% sure.
+ max = 42
+ # Prefix the remote branch with 'preview-' in order to avoid
+ # name conflicts in the rare case the branch name already
+ # exists in the docs repo and truncate to max length.
+ "preview-#{ENV["CI_COMMIT_REF_SLUG"]}"[0...max]
+end
+
+#
# Dummy way to find out in which repo we are, CE or EE
#
def ee?
@@ -28,18 +41,18 @@ end
# Create a remote branch in gitlab-docs
#
def create_remote_branch
- Gitlab.create_branch(GITLAB_DOCS_REPO, @docs_branch, 'master')
- puts "Remote branch '#{@docs_branch}' created"
+ Gitlab.create_branch(GITLAB_DOCS_REPO, docs_branch, 'master')
+ puts "Remote branch '#{docs_branch}' created"
rescue Gitlab::Error::BadRequest
- puts "Remote branch '#{@docs_branch}' already exists"
+ puts "Remote branch '#{docs_branch}' already exists"
end
#
# Remove a remote branch in gitlab-docs
#
def remove_remote_branch
- Gitlab.delete_branch(GITLAB_DOCS_REPO, @docs_branch)
- puts "Remote branch '#{@docs_branch}' deleted"
+ Gitlab.delete_branch(GITLAB_DOCS_REPO, docs_branch)
+ puts "Remote branch '#{docs_branch}' deleted"
end
#
@@ -50,11 +63,11 @@ def trigger_pipeline
param_name = ee? ? 'BRANCH_EE' : 'BRANCH_CE'
# The review app URL
- app_url = "http://#{@docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{ee? ? 'ee' : 'ce'}"
+ app_url = "http://#{docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{ee? ? 'ee' : 'ce'}"
# Create the pipeline
puts "=> Triggering a pipeline..."
- pipeline = Gitlab.run_trigger(GITLAB_DOCS_REPO, ENV["DOCS_TRIGGER_TOKEN"], @docs_branch, { param_name => ENV["CI_COMMIT_REF_NAME"] })
+ pipeline = Gitlab.run_trigger(GITLAB_DOCS_REPO, ENV["CI_JOB_TOKEN"], docs_branch, { param_name => ENV["CI_COMMIT_REF_NAME"] })
puts "=> Pipeline created:"
puts ""
@@ -77,4 +90,8 @@ when 'deploy'
trigger_pipeline
when 'cleanup'
remove_remote_branch
+else
+ puts "Please provide a valid option:
+ deploy - Creates the remote branch and triggers a pipeline
+ cleanup - Deletes the remote branch and stops the Review App"
end
diff --git a/spec/controllers/health_controller_spec.rb b/spec/controllers/health_controller_spec.rb
index cc389e554ad..9e9cf4f2c1f 100644
--- a/spec/controllers/health_controller_spec.rb
+++ b/spec/controllers/health_controller_spec.rb
@@ -10,6 +10,7 @@ describe HealthController do
before do
allow(Settings.monitoring).to receive(:ip_whitelist).and_return([whitelisted_ip])
+ stub_storage_settings({}) # Hide the broken storage
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 5d9403c23ac..b4a22a46b51 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -900,5 +900,37 @@ describe Projects::IssuesController do
expect(JSON.parse(response.body).first.keys).to match_array(%w[id reply_id expanded notes individual_note])
end
+
+ context 'with cross-reference system note', :request_store do
+ let(:new_issue) { create(:issue) }
+ let(:cross_reference) { "mentioned in #{new_issue.to_reference(issue.project)}" }
+
+ before do
+ create(:discussion_note_on_issue, :system, noteable: issue, project: issue.project, note: cross_reference)
+ end
+
+ it 'filters notes that the user should not see' do
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
+
+ expect(JSON.parse(response.body).count).to eq(1)
+ end
+
+ it 'does not result in N+1 queries' do
+ # Instantiate the controller variables to ensure QueryRecorder has an accurate base count
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
+
+ RequestStore.clear!
+
+ control_count = ActiveRecord::QueryRecorder.new do
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
+ end.count
+
+ RequestStore.clear!
+
+ create_list(:discussion_note_on_issue, 2, :system, noteable: issue, project: issue.project, note: cross_reference)
+
+ expect { get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid }.not_to exceed_query_limit(control_count)
+ end
+ end
end
end
diff --git a/spec/controllers/projects/pipelines_settings_controller_spec.rb b/spec/controllers/projects/pipelines_settings_controller_spec.rb
new file mode 100644
index 00000000000..ee46ad00947
--- /dev/null
+++ b/spec/controllers/projects/pipelines_settings_controller_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Projects::PipelinesSettingsController do
+ set(:user) { create(:user) }
+ set(:project_auto_devops) { create(:project_auto_devops) }
+ let(:project) { project_auto_devops.project }
+
+ before do
+ project.add_master(user)
+
+ sign_in(user)
+ end
+
+ describe 'PATCH update' do
+ before do
+ patch :update,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ project: {
+ auto_devops_attributes: params
+ }
+ end
+
+ context 'when updating the auto_devops settings' do
+ let(:params) { { enabled: '', domain: 'mepmep.md' } }
+
+ it 'redirects to the settings page' do
+ expect(response).to have_http_status(302)
+ expect(flash[:notice]).to eq("Pipelines settings for '#{project.name}' were successfully updated.")
+ end
+
+ context 'following the instance default' do
+ let(:params) { { enabled: '' } }
+
+ it 'allows enabled to be set to nil' do
+ project_auto_devops.reload
+
+ expect(project_auto_devops.enabled).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 2577d98df6f..7ce6a61d50c 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -25,7 +25,7 @@ feature 'Group merge requests page' do
end
it 'ignores archived merge request count badges in navbar' do
- expect( page.find('[aria-label="Merge Requests"] span.badge.count').text).to eq("1")
+ expect(first(:link, text: 'Merge Requests').find('.badge').text).to eq("1")
end
it 'ignores archived merge request count badges in state-filters' do
diff --git a/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb b/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb
new file mode 100644
index 00000000000..5ed4f3ad2bc
--- /dev/null
+++ b/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+feature 'Groups > User sees users dropdowns in issuables list' do
+ let(:entity) { create(:group) }
+ let(:user_in_dropdown) { create(:user) }
+ let!(:user_not_in_dropdown) { create(:user) }
+ let!(:project) { create(:project, group: entity) }
+
+ before do
+ entity.add_developer(user_in_dropdown)
+ end
+
+ it_behaves_like 'issuable user dropdown behaviors' do
+ let(:issuable) { create(:issue, project: project) }
+ let(:issuables_path) { issues_group_path(entity) }
+ end
+
+ it_behaves_like 'issuable user dropdown behaviors' do
+ let(:issuable) { create(:merge_request, source_project: project) }
+ let(:issuables_path) { merge_requests_group_path(entity) }
+ end
+end
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index 9261acda9dc..7437c469a72 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -159,7 +159,7 @@ feature 'Issues > User uses quick actions', js: true do
describe 'move the issue to another project' do
let(:issue) { create(:issue, project: project) }
- context 'when the project is valid', js: true do
+ context 'when the project is valid' do
let(:target_project) { create(:project, :public) }
before do
@@ -180,7 +180,7 @@ feature 'Issues > User uses quick actions', js: true do
end
end
- context 'when the project is valid but the user not authorized', js: true do
+ context 'when the project is valid but the user not authorized' do
let(:project_unauthorized) {create(:project, :public)}
before do
@@ -196,7 +196,7 @@ feature 'Issues > User uses quick actions', js: true do
end
end
- context 'when the project is invalid', js: true do
+ context 'when the project is invalid' do
before do
sign_in(user)
visit project_issue_path(project, issue)
@@ -210,7 +210,7 @@ feature 'Issues > User uses quick actions', js: true do
end
end
- context 'when the user issues multiple commands', js: true do
+ context 'when the user issues multiple commands' do
let(:target_project) { create(:project, :public) }
let(:milestone) { create(:milestone, title: '1.0', project: project) }
let(:target_milestone) { create(:milestone, title: '1.0', project: target_project) }
diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb
index fd991293ee9..443b596b3c6 100644
--- a/spec/features/merge_requests/widget_spec.rb
+++ b/spec/features/merge_requests/widget_spec.rb
@@ -142,6 +142,24 @@ describe 'Merge request', :js do
end
end
+ context 'view merge request where project has CI setup but no CI status' do
+ before do
+ pipeline = create(:ci_pipeline, project: project,
+ sha: merge_request.diff_head_sha,
+ ref: merge_request.source_branch)
+ create(:ci_build, pipeline: pipeline)
+
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'has pipeline error text' do
+ # Wait for the `ci_status` and `merge_check` requests
+ wait_for_requests
+
+ expect(page).to have_text('Could not connect to the CI server. Please check your settings and try again')
+ end
+ end
+
context 'view merge request with MWPS enabled but automatically merge fails' do
before do
merge_request.update(
diff --git a/spec/features/profiles/user_visits_profile_account_page_spec.rb b/spec/features/profiles/user_visits_profile_account_page_spec.rb
index 8c7233c77ad..a8c08a680d7 100644
--- a/spec/features/profiles/user_visits_profile_account_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_account_page_spec.rb
@@ -10,7 +10,6 @@ describe 'User visits the profile account page' do
end
it 'shows correct menu item' do
- expect(find('.sidebar-top-level-items > li.active')).to have_content('Account')
- expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
+ expect(page).to have_active_navigation('Account')
end
end
diff --git a/spec/features/profiles/user_visits_profile_authentication_log_page_spec.rb b/spec/features/profiles/user_visits_profile_authentication_log_page_spec.rb
deleted file mode 100644
index ffb504cc573..00000000000
--- a/spec/features/profiles/user_visits_profile_authentication_log_page_spec.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require 'spec_helper'
-
-describe 'User visits the authentication log page' do
- let(:user) { create(:user) }
-
- before do
- sign_in(user)
-
- visit(audit_log_profile_path)
- end
-
- it 'shows correct menu item' do
- expect(find('.sidebar-top-level-items > li.active')).to have_content('Authentication log')
- expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
- end
-end
diff --git a/spec/features/profiles/user_visits_profile_authentication_log_spec.rb b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb
new file mode 100644
index 00000000000..a50ebb29e01
--- /dev/null
+++ b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'User visits the authentication log' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ visit(audit_log_profile_path)
+ end
+
+ it 'shows correct menu item' do
+ expect(page).to have_active_navigation('Authentication log')
+ end
+end
diff --git a/spec/features/profiles/user_visits_profile_page_spec.rb b/spec/features/profiles/user_visits_profile_page_spec.rb
deleted file mode 100644
index 3bf6d718bc7..00000000000
--- a/spec/features/profiles/user_visits_profile_page_spec.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require 'spec_helper'
-
-describe 'User visits the profile page' do
- let(:user) { create(:user) }
-
- before do
- sign_in(user)
-
- visit(profile_path)
- end
-
- it 'shows correct menu item' do
- expect(find('.sidebar-top-level-items > li.active')).to have_content('Profile')
- expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
- end
-end
diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
index d1776b3d7c2..924ee0e4174 100644
--- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
@@ -10,8 +10,7 @@ describe 'User visits the profile preferences page' do
end
it 'shows correct menu item' do
- expect(find('.sidebar-top-level-items > li.active')).to have_content('Preferences')
- expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
+ expect(page).to have_active_navigation('Preferences')
end
describe 'User changes their syntax highlighting theme', :js do
diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb
new file mode 100644
index 00000000000..6601d3039ed
--- /dev/null
+++ b/spec/features/profiles/user_visits_profile_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'User visits their profile' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ visit(profile_path)
+ end
+
+ it 'shows correct menu item' do
+ expect(page).to have_active_navigation('Profile')
+ end
+end
diff --git a/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb b/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb
index 0b7a63b54b4..685bf44619d 100644
--- a/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb
@@ -10,7 +10,6 @@ describe 'User visits the profile SSH keys page' do
end
it 'shows correct menu item' do
- expect(find('.sidebar-top-level-items > li.active')).to have_content('SSH Keys')
- expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
+ expect(page).to have_active_navigation('SSH Keys')
end
end
diff --git a/spec/features/projects/commit/user_reverts_commit_spec.rb b/spec/features/projects/commit/user_reverts_commit_spec.rb
new file mode 100644
index 00000000000..221f1d7757e
--- /dev/null
+++ b/spec/features/projects/commit/user_reverts_commit_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe 'User reverts a commit', :js do
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ visit(project_commit_path(project, sample_commit.id))
+
+ find('.header-action-buttons .dropdown').click
+ find('a[href="#modal-revert-commit"]').click
+ end
+
+ context 'without creating a new merge request' do
+ before do
+ page.within('#modal-revert-commit') do
+ uncheck('create_merge_request')
+ click_button('Revert')
+ end
+ end
+
+ it 'reverts a commit' do
+ expect(page).to have_content('The commit has been successfully reverted.')
+ end
+
+ it 'does not revert a previously reverted commit' do
+ # Visit the comment again once it was reverted.
+ visit project_commit_path(project, sample_commit.id)
+
+ find('.header-action-buttons .dropdown').click
+ find('a[href="#modal-revert-commit"]').click
+
+ page.within('#modal-revert-commit') do
+ uncheck('create_merge_request')
+ click_button('Revert')
+ end
+
+ expect(page).to have_content('Sorry, we cannot revert this commit automatically.')
+ end
+ end
+
+ context 'with creating a new merge request' do
+ it 'reverts a commit' do
+ page.within('#modal-revert-commit') do
+ click_button('Revert')
+ end
+
+ expect(page).to have_content('The commit has been successfully reverted. You can now submit a merge request to get this change into the original branch.')
+ expect(page).to have_content("From revert-#{Commit.truncate_sha(sample_commit.id)} into master")
+ end
+ end
+end
diff --git a/spec/features/projects/snippets/user_comments_on_snippet_spec.rb b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
new file mode 100644
index 00000000000..1bd2098af6d
--- /dev/null
+++ b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe 'User comments on a snippet', :js do
+ let(:project) { create(:project) }
+ let!(:snippet) { create(:project_snippet, project: project, author: user) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_snippet_path(project, snippet))
+ end
+
+ it 'leaves a comment on a snippet' do
+ page.within('.js-main-target-form') do
+ fill_in('note_note', with: 'Good snippet!')
+ click_button('Comment')
+ end
+
+ wait_for_requests
+
+ expect(page).to have_content('Good snippet!')
+ end
+end
diff --git a/spec/features/projects/snippets/user_deletes_snippet_spec.rb b/spec/features/projects/snippets/user_deletes_snippet_spec.rb
new file mode 100644
index 00000000000..ca5f7981c33
--- /dev/null
+++ b/spec/features/projects/snippets/user_deletes_snippet_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe 'User deletes a snippet' do
+ let(:project) { create(:project) }
+ let!(:snippet) { create(:project_snippet, project: project, author: user) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_snippet_path(project, snippet))
+ end
+
+ it 'deletes a snippet' do
+ first(:link, 'Delete').click
+
+ expect(page).not_to have_content(snippet.title)
+ end
+end
diff --git a/spec/features/projects/snippets/user_updates_snippet_spec.rb b/spec/features/projects/snippets/user_updates_snippet_spec.rb
new file mode 100644
index 00000000000..09a390443cf
--- /dev/null
+++ b/spec/features/projects/snippets/user_updates_snippet_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe 'User updates a snippet' do
+ let(:project) { create(:project) }
+ let!(:snippet) { create(:project_snippet, project: project, author: user) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_snippet_path(project, snippet))
+ end
+
+ it 'updates a snippet' do
+ page.within('.detail-page-header') do
+ first(:link, 'Edit').click
+ end
+
+ fill_in('project_snippet_title', with: 'Snippet new title')
+ click_button('Save')
+
+ expect(page).to have_content('Snippet new title')
+ end
+end
diff --git a/spec/features/projects/snippets/user_views_snippets_spec.rb b/spec/features/projects/snippets/user_views_snippets_spec.rb
new file mode 100644
index 00000000000..e9992e00ca8
--- /dev/null
+++ b/spec/features/projects/snippets/user_views_snippets_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe 'User views snippets' do
+ let(:project) { create(:project) }
+ let!(:project_snippet) { create(:project_snippet, project: project, author: user) }
+ let!(:snippet) { create(:snippet, author: user) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_snippets_path(project))
+ end
+
+ it 'shows snippets' do
+ expect(page).to have_content(project_snippet.title)
+ expect(page).not_to have_content(snippet.title)
+ end
+end
diff --git a/spec/features/projects/user_archives_project_spec.rb b/spec/features/projects/user_archives_project_spec.rb
new file mode 100644
index 00000000000..72063d13c2a
--- /dev/null
+++ b/spec/features/projects/user_archives_project_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe 'User archives a project' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+
+ sign_in(user)
+ end
+
+ context 'when a project is archived' do
+ let(:project) { create(:project, :archived, namespace: user.namespace) }
+
+ before do
+ visit(edit_project_path(project))
+ end
+
+ it 'unarchives a project' do
+ expect(page).to have_content('Unarchive project')
+
+ click_link('Unarchive')
+
+ expect(page).not_to have_content('Archived project')
+ end
+ end
+
+ context 'when a project is unarchived' do
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
+
+ before do
+ visit(edit_project_path(project))
+ end
+
+ it 'archives a project' do
+ expect(page).to have_content('Archive project')
+
+ click_link('Archive')
+
+ expect(page).to have_content('Archived')
+ end
+ end
+end
diff --git a/spec/features/projects/user_edits_files_spec.rb b/spec/features/projects/user_edits_files_spec.rb
index 3129aad8473..19954313c23 100644
--- a/spec/features/projects/user_edits_files_spec.rb
+++ b/spec/features/projects/user_edits_files_spec.rb
@@ -20,8 +20,7 @@ describe 'User edits files' do
it 'inserts a content of a file', js: true do
click_link('.gitignore')
find('.js-edit-blob').click
-
- wait_for_requests
+ find('.file-editor', match: :first)
execute_script("ace.edit('editor').setValue('*.rbca')")
@@ -38,8 +37,7 @@ describe 'User edits files' do
it 'commits an edited file', js: true do
click_link('.gitignore')
find('.js-edit-blob').click
-
- wait_for_requests
+ find('.file-editor', match: :first)
execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
@@ -56,7 +54,7 @@ describe 'User edits files' do
click_link('.gitignore')
find('.js-edit-blob').click
- wait_for_requests
+ find('.file-editor', match: :first)
execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
@@ -67,15 +65,13 @@ describe 'User edits files' do
click_link('Changes')
- wait_for_requests
expect(page).to have_content('*.rbca')
end
it 'shows the diff of an edited file', js: true do
click_link('.gitignore')
find('.js-edit-blob').click
-
- wait_for_requests
+ find('.file-editor', match: :first)
execute_script("ace.edit('editor').setValue('*.rbca')")
click_link('Preview changes')
@@ -104,7 +100,7 @@ describe 'User edits files' do
"A fork of this project has been created that you can make changes in, so you can submit a merge request."
)
- wait_for_requests
+ find('.file-editor', match: :first)
execute_script("ace.edit('editor').setValue('*.rbca')")
@@ -120,7 +116,7 @@ describe 'User edits files' do
click_link('Fork')
- wait_for_requests
+ find('.file-editor', match: :first)
execute_script("ace.edit('editor').setValue('*.rbca')")
fill_in(:commit_message, with: 'New commit message', visible: true)
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
new file mode 100644
index 00000000000..0ed797a62ea
--- /dev/null
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe 'User searches for code' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository, namespace: user.namespace) }
+
+ context 'when signed in' do
+ before do
+ project.add_master(user)
+ sign_in(user)
+ end
+
+ it 'finds a file' do
+ visit(project_path(project))
+
+ page.within('.search') do
+ fill_in('search', with: 'application.js')
+ click_button('Go')
+ end
+
+ click_link('Code')
+
+ expect(page).to have_selector('.file-content .code')
+ expect(page).to have_selector("span.line[lang='javascript']")
+ end
+
+ context 'when on a project page', :js do
+ before do
+ visit(search_path)
+ end
+
+ include_examples 'top right search form'
+
+ it 'finds code' do
+ find('.js-search-project-dropdown').trigger('click')
+
+ page.within('.project-filter') do
+ click_link(project.name_with_namespace)
+ end
+
+ fill_in('dashboard_search', with: 'rspec')
+ find('.btn-search').trigger('click')
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_content('Update capybara, rspec-rails, poltergeist to recent versions')
+ end
+ end
+ end
+ end
+
+ context 'when signed out' do
+ let(:project) { create(:project, :public, :repository) }
+
+ before do
+ visit(project_path(project))
+ end
+
+ it 'finds code' do
+ fill_in('search', with: 'rspec')
+ click_button('Go')
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_content('Update capybara, rspec-rails, poltergeist to recent versions')
+ end
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_comments_spec.rb b/spec/features/search/user_searches_for_comments_spec.rb
new file mode 100644
index 00000000000..c7c469a262c
--- /dev/null
+++ b/spec/features/search/user_searches_for_comments_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe 'User searches for comments' do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_reporter(user)
+ sign_in(user)
+
+ visit(project_path(project))
+ end
+
+ context 'when a comment is in commits' do
+ context 'when comment belongs to an invalid commit' do
+ let(:comment) { create(:note_on_commit, author: user, project: project, commit_id: 12345678, note: 'Bug here') }
+
+ it 'finds a commit' do
+ page.within('.search') do
+ fill_in('search', with: comment.note)
+ click_button('Go')
+ end
+
+ click_link('Comments')
+
+ expect(page).to have_text('Commit deleted')
+ expect(page).to have_text('12345678')
+ end
+ end
+ end
+
+ context 'when a comment is in a snippet' do
+ let(:snippet) { create(:project_snippet, :private, project: project, author: user, title: 'Some title') }
+ let(:comment) { create(:note, noteable: snippet, author: user, note: 'Supercalifragilisticexpialidocious', project: project) }
+
+ it 'finds a snippet' do
+ page.within('.search') do
+ fill_in('search', with: comment.note)
+ click_button('Go')
+ end
+
+ click_link('Comments')
+
+ expect(page).to have_link(snippet.title)
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_commits_spec.rb b/spec/features/search/user_searches_for_commits_spec.rb
new file mode 100644
index 00000000000..28cae444588
--- /dev/null
+++ b/spec/features/search/user_searches_for_commits_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe 'User searches for commits' do
+ let(:project) { create(:project, :repository) }
+ let(:sha) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_reporter(user)
+ sign_in(user)
+
+ visit(search_path(project_id: project.id))
+ end
+
+ context 'when searching by SHA' do
+ it 'finds a commit and redirects to its page' do
+ fill_in('search', with: sha)
+ click_button('Search')
+
+ expect(page).to have_current_path(project_commit_path(project, sha))
+ end
+
+ it 'finds a commit in uppercase and redirects to its page' do
+ fill_in('search', with: sha.upcase)
+ click_button('Search')
+
+ expect(page).to have_current_path(project_commit_path(project, sha))
+ end
+ end
+
+ context 'when searching by message' do
+ it 'finds a commit and holds on /search page' do
+ create_commit('Message referencing another sha: "deadbeef"', project, user, 'master')
+
+ fill_in('search', with: 'deadbeef')
+ click_button('Search')
+
+ expect(page).to have_current_path('/search', only_path: true)
+ end
+
+ it 'finds multiple commits' do
+ fill_in('search', with: 'See merge request')
+ click_button('Search')
+ click_link('Commits')
+
+ expect(page).to have_selector('.commit-row-description', count: 9)
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb
new file mode 100644
index 00000000000..630a81b1c5e
--- /dev/null
+++ b/spec/features/search/user_searches_for_issues_spec.rb
@@ -0,0 +1,76 @@
+require 'spec_helper'
+
+describe 'User searches for issues', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let!(:issue1) { create(:issue, title: 'Foo', project: project) }
+ let!(:issue2) { create(:issue, title: 'Bar', project: project) }
+
+ context 'when signed in' do
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(search_path)
+ end
+
+ include_examples 'top right search form'
+
+ it 'finds an issue' do
+ fill_in('dashboard_search', with: issue1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Issues')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(issue1.title).and have_no_link(issue2.title)
+ end
+ end
+
+ context 'when on a project page' do
+ it 'finds an issue' do
+ find('.js-search-project-dropdown').trigger('click')
+
+ page.within('.project-filter') do
+ click_link(project.name_with_namespace)
+ end
+
+ fill_in('dashboard_search', with: issue1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Issues')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(issue1.title).and have_no_link(issue2.title)
+ end
+ end
+ end
+ end
+
+ context 'when signed out' do
+ let(:project) { create(:project, :public) }
+
+ before do
+ visit(search_path)
+ end
+
+ include_examples 'top right search form'
+
+ it 'finds an issue' do
+ fill_in('dashboard_search', with: issue1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Issues')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(issue1.title).and have_no_link(issue2.title)
+ end
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb
new file mode 100644
index 00000000000..116256682f4
--- /dev/null
+++ b/spec/features/search/user_searches_for_merge_requests_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe 'User searches for merge requests', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let!(:merge_request1) { create(:merge_request, title: 'Foo', source_project: project, target_project: project) }
+ let!(:merge_request2) { create(:merge_request, :simple, title: 'Bar', source_project: project, target_project: project) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(search_path)
+ end
+
+ include_examples 'top right search form'
+
+ it 'finds a merge request' do
+ fill_in('dashboard_search', with: merge_request1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Merge requests')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(merge_request1.title).and have_no_link(merge_request2.title)
+ end
+ end
+
+ context 'when on a project page' do
+ it 'finds a merge request' do
+ find('.js-search-project-dropdown').trigger('click')
+
+ page.within('.project-filter') do
+ click_link(project.name_with_namespace)
+ end
+
+ fill_in('dashboard_search', with: merge_request1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Merge requests')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(merge_request1.title).and have_no_link(merge_request2.title)
+ end
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb
new file mode 100644
index 00000000000..4fa9fe9ce8c
--- /dev/null
+++ b/spec/features/search/user_searches_for_milestones_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe 'User searches for milestones', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let!(:milestone1) { create(:milestone, title: 'Foo', project: project) }
+ let!(:milestone2) { create(:milestone, title: 'Bar', project: project) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(search_path)
+ end
+
+ include_examples 'top right search form'
+
+ it 'finds a milestone' do
+ fill_in('dashboard_search', with: milestone1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Milestones')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(milestone1.title).and have_no_link(milestone2.title)
+ end
+ end
+
+ context 'when on a project page' do
+ it 'finds a milestone' do
+ find('.js-search-project-dropdown').trigger('click')
+
+ page.within('.project-filter') do
+ click_link(project.name_with_namespace)
+ end
+
+ fill_in('dashboard_search', with: milestone1.title)
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Milestones')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(milestone1.title).and have_no_link(milestone2.title)
+ end
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_projects_spec.rb b/spec/features/search/user_searches_for_projects_spec.rb
new file mode 100644
index 00000000000..242e437e41c
--- /dev/null
+++ b/spec/features/search/user_searches_for_projects_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe 'User searches for projects' do
+ let!(:project) { create(:project, :public, name: 'Shop') }
+
+ context 'when signed out' do
+ include_examples 'top right search form'
+
+ it 'finds a project' do
+ visit(search_path)
+
+ fill_in('dashboard_search', with: project.name[0..3])
+ click_button('Search')
+
+ expect(page).to have_link(project.name)
+ end
+
+ it 'preserves the group being searched in' do
+ visit(search_path(group_id: project.namespace.id))
+
+ fill_in('search', with: 'foo')
+ click_button('Search')
+
+ expect(find('#group_id', visible: false).value).to eq(project.namespace.id.to_s)
+ end
+
+ it 'preserves the project being searched in' do
+ visit(search_path(project_id: project.id))
+
+ fill_in('search', with: 'foo')
+ click_button('Search')
+
+ expect(find('#project_id', visible: false).value).to eq(project.id.to_s)
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb
new file mode 100644
index 00000000000..1ea56479ecc
--- /dev/null
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe 'User searches for wiki pages', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let!(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'test_wiki', content: 'Some Wiki content' }) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(search_path)
+ end
+
+ include_examples 'top right search form'
+
+ it 'finds a page' do
+ find('.js-search-project-dropdown').trigger('click')
+
+ page.within('.project-filter') do
+ click_link(project.name_with_namespace)
+ end
+
+ fill_in('dashboard_search', with: 'content')
+ find('.btn-search').trigger('click')
+
+ page.within('.search-filter') do
+ click_link('Wiki')
+ end
+
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(wiki_page.title)
+ end
+ end
+end
diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb
new file mode 100644
index 00000000000..5ddea36add5
--- /dev/null
+++ b/spec/features/search/user_uses_header_search_field_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+describe 'User uses header search field' do
+ include FilteredSearchHelpers
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_reporter(user)
+ sign_in(user)
+
+ visit(project_path(project))
+ end
+
+ it 'starts searching by pressing the enter key', :js do
+ fill_in('search', with: 'gitlab')
+ find('#search').native.send_keys(:enter)
+
+ page.within('.breadcrumbs-sub-title') do
+ expect(page).to have_content('Search')
+ end
+ end
+
+ it 'contains location badge' do
+ expect(page).to have_selector('.has-location-badge')
+ end
+
+ context 'when clicking the search field', :js do
+ before do
+ page.find('#search').click
+ end
+
+ it 'shows category search dropdown' do
+ expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i)
+ end
+
+ context 'when clicking issues' do
+ let!(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
+
+ it 'shows assigned issues' do
+ find('.dropdown-menu').click_link('Issues assigned to me')
+
+ expect(page).to have_selector('.filtered-search')
+ expect_tokens([assignee_token(user.name)])
+ expect_filtered_search_input_empty
+ end
+
+ it 'shows created issues' do
+ find('.dropdown-menu').click_link("Issues I've created")
+
+ expect(page).to have_selector('.filtered-search')
+ expect_tokens([author_token(user.name)])
+ expect_filtered_search_input_empty
+ end
+ end
+
+ context 'when clicking merge requests' do
+ let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) }
+
+ it 'shows assigned merge requests' do
+ find('.dropdown-menu').click_link('Merge requests assigned to me')
+
+ expect(page).to have_selector('.merge-requests-holder')
+ expect_tokens([assignee_token(user.name)])
+ expect_filtered_search_input_empty
+ end
+
+ it 'shows created merge requests' do
+ find('.dropdown-menu').click_link("Merge requests I've created")
+
+ expect(page).to have_selector('.merge-requests-holder')
+ expect_tokens([author_token(user.name)])
+ expect_filtered_search_input_empty
+ end
+ end
+ end
+
+ context 'when entering text into the search field', :js do
+ before do
+ page.within('.search-input-wrap') do
+ fill_in('search', with: project.name[0..3])
+ end
+ end
+
+ it 'does not display the category search dropdown' do
+ expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i)
+ end
+ end
+end
diff --git a/spec/features/search/user_uses_search_filters_spec.rb b/spec/features/search/user_uses_search_filters_spec.rb
new file mode 100644
index 00000000000..95f3eb5e805
--- /dev/null
+++ b/spec/features/search/user_uses_search_filters_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe 'User uses search filters', :js do
+ let(:group) { create(:group) }
+ let!(:group_project) { create(:project, group: group) }
+ let(:project) { create(:project, namespace: user.namespace) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_reporter(user)
+ group.add_owner(user)
+ sign_in(user)
+
+ visit(search_path)
+ end
+
+ context' when filtering by group' do
+ it 'shows group projects' do
+ find('.js-search-group-dropdown').trigger('click')
+
+ wait_for_requests
+
+ page.within('.search-holder') do
+ click_link(group.name)
+ end
+
+ expect(find('.js-search-group-dropdown')).to have_content(group.name)
+
+ page.within('.project-filter') do
+ find('.js-search-project-dropdown').trigger('click')
+
+ wait_for_requests
+
+ expect(page).to have_link(group_project.name_with_namespace)
+ end
+ end
+ end
+
+ context' when filtering by project' do
+ it 'shows a project' do
+ page.within('.project-filter') do
+ find('.js-search-project-dropdown').trigger('click')
+
+ wait_for_requests
+
+ click_link(project.name_with_namespace)
+ end
+
+ expect(find('.js-search-project-dropdown')).to have_content(project.name_with_namespace)
+ end
+ end
+end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
deleted file mode 100644
index 8f6d0bb9d1b..00000000000
--- a/spec/features/search_spec.rb
+++ /dev/null
@@ -1,310 +0,0 @@
-require 'spec_helper'
-
-describe "Search" do
- include FilteredSearchHelpers
-
- let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
- let!(:issue) { create(:issue, project: project, assignees: [user]) }
- let!(:issue2) { create(:issue, project: project, author: user) }
-
- before do
- sign_in(user)
- project.team << [user, :reporter]
- visit search_path
- end
-
- it 'does not show top right search form' do
- expect(page).not_to have_selector('.search')
- end
-
- context 'search filters', js: true do
- let(:group) { create(:group) }
- let!(:group_project) { create(:project, group: group) }
-
- before do
- group.add_owner(user)
- end
-
- it 'shows group name after filtering' do
- find('.js-search-group-dropdown').trigger('click')
- wait_for_requests
-
- page.within '.search-holder' do
- click_link group.name
- end
-
- expect(find('.js-search-group-dropdown')).to have_content(group.name)
- end
-
- it 'filters by group projects after filtering by group' do
- find('.js-search-group-dropdown').trigger('click')
- wait_for_requests
-
- page.within '.search-holder' do
- click_link group.name
- end
-
- expect(find('.js-search-group-dropdown')).to have_content(group.name)
-
- page.within('.project-filter') do
- find('.js-search-project-dropdown').trigger('click')
- wait_for_requests
-
- expect(page).to have_link(group_project.name_with_namespace)
- end
- end
-
- it 'shows project name after filtering' do
- page.within('.project-filter') do
- find('.js-search-project-dropdown').trigger('click')
- wait_for_requests
-
- click_link project.name_with_namespace
- end
-
- expect(find('.js-search-project-dropdown')).to have_content(project.name_with_namespace)
- end
- end
-
- describe 'searching for Projects' do
- it 'finds a project' do
- page.within '.search-holder' do
- fill_in "search", with: project.name[0..3]
- click_button "Search"
- end
-
- expect(page).to have_content project.name
- end
- end
-
- context 'search for comments' do
- context 'when comment belongs to a invalid commit' do
- let(:project) { create(:project, :repository) }
- let(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'Bug here') }
-
- before do
- note.update_attributes(commit_id: 12345678)
- end
-
- it 'finds comment' do
- visit project_path(project)
-
- page.within '.search' do
- fill_in 'search', with: note.note
- click_button 'Go'
- end
-
- click_link 'Comments'
-
- expect(page).to have_text("Commit deleted")
- expect(page).to have_text("12345678")
- end
- end
-
- it 'finds a snippet' do
- snippet = create(:project_snippet, :private, project: project, author: user, title: 'Some title')
- note = create(:note,
- noteable: snippet,
- author: user,
- note: 'Supercalifragilisticexpialidocious',
- project: project)
- # Must visit project dashboard since global search won't search
- # everything (e.g. comments, snippets, etc.)
- visit project_path(project)
-
- page.within '.search' do
- fill_in 'search', with: note.note
- click_button 'Go'
- end
-
- click_link 'Comments'
-
- expect(page).to have_link(snippet.title)
- end
-
- it 'finds a commit' do
- project = create(:project, :repository) { |p| p.add_reporter(user) }
- visit project_path(project)
-
- page.within '.search' do
- fill_in 'search', with: 'add'
- click_button 'Go'
- end
-
- click_link "Commits"
-
- expect(page).to have_selector('.commit-row-description')
- end
-
- it 'finds a code' do
- project = create(:project, :repository) { |p| p.add_reporter(user) }
- visit project_path(project)
-
- page.within '.search' do
- fill_in 'search', with: 'application.js'
- click_button 'Go'
- end
-
- click_link "Code"
-
- expect(page).to have_selector('.file-content .code')
-
- expect(page).to have_selector("span.line[lang='javascript']")
- end
- end
-
- describe 'Right header search field' do
- it 'allows enter key to search', js: true do
- visit project_path(project)
- fill_in 'search', with: 'gitlab'
- find('#search').native.send_keys(:enter)
-
- page.within '.breadcrumbs-sub-title' do
- expect(page).to have_content 'Search'
- end
- end
-
- describe 'Search in project page' do
- before do
- visit project_path(project)
- end
-
- it 'shows top right search form' do
- expect(page).to have_selector('#search')
- end
-
- it 'contains location badge in top right search form' do
- expect(page).to have_selector('.has-location-badge')
- end
-
- context 'clicking the search field', js: true do
- it 'shows category search dropdown' do
- page.find('#search').click
-
- expect(page).to have_selector('.dropdown-header', text: /#{project.name}/i)
- end
- end
-
- context 'click the links in the category search dropdown', js: true do
- let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) }
-
- before do
- page.find('#search').click
- end
-
- it 'takes user to her issues page when issues assigned is clicked' do
- find('.dropdown-menu').click_link 'Issues assigned to me'
-
- expect(page).to have_selector('.filtered-search')
- expect_tokens([assignee_token(user.name)])
- expect_filtered_search_input_empty
- end
-
- it 'takes user to her issues page when issues authored is clicked' do
- find('.dropdown-menu').click_link "Issues I've created"
-
- expect(page).to have_selector('.filtered-search')
- expect_tokens([author_token(user.name)])
- expect_filtered_search_input_empty
- end
-
- it 'takes user to her MR page when MR assigned is clicked' do
- find('.dropdown-menu').click_link 'Merge requests assigned to me'
-
- expect(page).to have_selector('.merge-requests-holder')
- expect_tokens([assignee_token(user.name)])
- expect_filtered_search_input_empty
- end
-
- it 'takes user to her MR page when MR authored is clicked' do
- find('.dropdown-menu').click_link "Merge requests I've created"
-
- expect(page).to have_selector('.merge-requests-holder')
- expect_tokens([author_token(user.name)])
- expect_filtered_search_input_empty
- end
- end
-
- context 'entering text into the search field', js: true do
- before do
- page.within '.search-input-wrap' do
- fill_in "search", with: project.name[0..3]
- end
- end
-
- it 'does not display the category search dropdown' do
- expect(page).not_to have_selector('.dropdown-header', text: /#{project.name}/i)
- end
- end
- end
- end
-
- describe 'search for commits' do
- let(:project) { create(:project, :repository) }
-
- before do
- visit search_path(project_id: project.id)
- end
-
- it 'redirects to commit page when search by sha and only commit found' do
- fill_in 'search', with: '6d394385cf567f80a8fd85055db1ab4c5295806f'
-
- click_button 'Search'
-
- expect(page).to have_current_path(project_commit_path(project, '6d394385cf567f80a8fd85055db1ab4c5295806f'))
- end
-
- it 'redirects to single commit regardless of query case' do
- fill_in 'search', with: '6D394385cf'
-
- click_button 'Search'
-
- expect(page).to have_current_path(project_commit_path(project, '6d394385cf567f80a8fd85055db1ab4c5295806f'))
- end
-
- it 'holds on /search page when the only commit is found by message' do
- create_commit('Message referencing another sha: "deadbeef" ', project, user, 'master')
-
- fill_in 'search', with: 'deadbeef'
- click_button 'Search'
-
- expect(page).to have_current_path('/search', only_path: true)
- end
-
- it 'shows multiple matching commits' do
- fill_in 'search', with: 'See merge request'
-
- click_button 'Search'
- click_link 'Commits'
-
- expect(page).to have_selector('.commit-row-description', count: 9)
- end
- end
-
- context 'anonymous user' do
- let(:project) { create(:project, :public) }
-
- before do
- sign_out(user)
- end
-
- it 'preserves the group being searched in' do
- visit search_path(group_id: project.namespace.id)
-
- fill_in 'search', with: 'foo'
- click_button 'Search'
-
- expect(find('#group_id', visible: false).value).to eq(project.namespace.id.to_s)
- end
-
- it 'preserves the project being searched in' do
- visit search_path(project_id: project.id)
-
- fill_in 'search', with: 'foo'
- click_button 'Search'
-
- expect(find('#project_id', visible: false).value).to eq(project.id.to_s)
- end
- end
-end
diff --git a/spec/finders/autocomplete_users_finder_spec.rb b/spec/finders/autocomplete_users_finder_spec.rb
new file mode 100644
index 00000000000..684af74d750
--- /dev/null
+++ b/spec/finders/autocomplete_users_finder_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe AutocompleteUsersFinder do
+ describe '#execute' do
+ let!(:user1) { create(:user, username: 'johndoe') }
+ let!(:user2) { create(:user, :blocked, username: 'notsorandom') }
+ let!(:external_user) { create(:user, :external) }
+ let!(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
+ let(:current_user) { create(:user) }
+ let(:params) { {} }
+
+ let(:project) { nil }
+ let(:group) { nil }
+
+ subject { described_class.new(params: params, current_user: current_user, project: project, group: group).execute.to_a }
+
+ context 'when current_user not passed or nil' do
+ let(:current_user) { nil }
+
+ it { is_expected.to match_array([]) }
+ end
+
+ context 'when project passed' do
+ let(:project) { create(:project) }
+
+ it { is_expected.to match_array([project.owner]) }
+
+ context 'when author_id passed' do
+ let(:params) { { author_id: user2.id } }
+
+ it { is_expected.to match_array([project.owner, user2]) }
+ end
+ end
+
+ context 'when group passed and project not passed' do
+ let(:group) { create(:group, :public) }
+
+ before do
+ group.add_users([user1], GroupMember::DEVELOPER)
+ end
+
+ it { is_expected.to match_array([user1]) }
+ end
+
+ it { is_expected.to match_array([user1, external_user, omniauth_user, current_user]) }
+
+ context 'when filtered by search' do
+ let(:params) { { search: 'johndoe' } }
+
+ it { is_expected.to match_array([user1]) }
+ end
+
+ context 'when filtered by skip_users' do
+ let(:params) { { skip_users: [omniauth_user.id, current_user.id] } }
+
+ it { is_expected.to match_array([user1, external_user]) }
+ end
+
+ context 'when todos exist' do
+ let!(:pending_todo1) { create(:todo, user: current_user, author: user1, state: :pending) }
+ let!(:pending_todo2) { create(:todo, user: external_user, author: omniauth_user, state: :pending) }
+ let!(:done_todo1) { create(:todo, user: current_user, author: external_user, state: :done) }
+ let!(:done_todo2) { create(:todo, user: user1, author: external_user, state: :done) }
+
+ context 'when filtered by todo_filter without todo_state_filter' do
+ let(:params) { { todo_filter: true } }
+
+ it { is_expected.to match_array([]) }
+ end
+
+ context 'when filtered by todo_filter with pending todo_state_filter' do
+ let(:params) { { todo_filter: true, todo_state_filter: 'pending' } }
+
+ it { is_expected.to match_array([user1]) }
+ end
+
+ context 'when filtered by todo_filter with done todo_state_filter' do
+ let(:params) { { todo_filter: true, todo_state_filter: 'done' } }
+
+ it { is_expected.to match_array([external_user]) }
+ end
+ end
+
+ context 'when filtered by current_user' do
+ let(:current_user) { user2 }
+ let(:params) { { current_user: true } }
+
+ it { is_expected.to match_array([user2, user1, external_user, omniauth_user]) }
+ end
+
+ context 'when filtered by author_id' do
+ let(:params) { { author_id: user2.id } }
+
+ it { is_expected.to match_array([user2, user1, external_user, omniauth_user, current_user]) }
+ end
+ end
+end
diff --git a/spec/finders/fork_projects_finder_spec.rb b/spec/finders/fork_projects_finder_spec.rb
new file mode 100644
index 00000000000..f0cef7ea406
--- /dev/null
+++ b/spec/finders/fork_projects_finder_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe ForkProjectsFinder do
+ let(:source_project) { create(:project, :empty_repo) }
+ let(:private_fork) { create(:project, :private, :empty_repo, name: 'A') }
+ let(:internal_fork) { create(:project, :internal, :empty_repo, name: 'B') }
+ let(:public_fork) { create(:project, :public, :empty_repo, name: 'C') }
+
+ let(:non_member) { create(:user) }
+ let(:private_fork_member) { create(:user) }
+
+ before do
+ private_fork.add_developer(private_fork_member)
+
+ source_project.forks << private_fork
+ source_project.forks << internal_fork
+ source_project.forks << public_fork
+ end
+
+ describe '#execute' do
+ let(:finder) { described_class.new(source_project, params: {}, current_user: current_user) }
+
+ subject { finder.execute }
+
+ describe 'without a user' do
+ let(:current_user) { nil }
+
+ it { is_expected.to eq([public_fork]) }
+ end
+
+ describe 'with a user' do
+ let(:current_user) { non_member }
+
+ it { is_expected.to eq([public_fork, internal_fork]) }
+ end
+
+ describe 'with a member' do
+ let(:current_user) { private_fork_member }
+
+ it { is_expected.to eq([public_fork, internal_fork, private_fork]) }
+ end
+ end
+end
diff --git a/spec/fixtures/api/schemas/public_api/v4/issues.json b/spec/fixtures/api/schemas/public_api/v4/issues.json
index 8acd9488215..03c422ab023 100644
--- a/spec/fixtures/api/schemas/public_api/v4/issues.json
+++ b/spec/fixtures/api/schemas/public_api/v4/issues.json
@@ -9,6 +9,7 @@
"title": { "type": "string" },
"description": { "type": ["string", "null"] },
"state": { "type": "string" },
+ "closed_at": { "type": "date" },
"created_at": { "type": "date" },
"updated_at": { "type": "date" },
"labels": {
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/admins.json b/spec/fixtures/api/schemas/public_api/v4/user/admins.json
new file mode 100644
index 00000000000..4a107f0ddbe
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/user/admins.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "admin.json" }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/basics.json b/spec/fixtures/api/schemas/public_api/v4/user/basics.json
new file mode 100644
index 00000000000..6f7cf42229d
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/user/basics.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "basic.json" }
+}
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index c4f4e0d21dc..5a2e4b34069 100644
--- a/spec/helpers/submodule_helper_spec.rb
+++ b/spec/helpers/submodule_helper_spec.rb
@@ -147,6 +147,12 @@ describe SubmoduleHelper do
expect(helper.submodule_links(submodule_item)).to eq([nil, nil])
end
+ it 'sanitizes invalid URL with extended ASCII' do
+ stub_url('é')
+
+ expect(helper.submodule_links(submodule_item)).to eq([nil, nil])
+ end
+
it 'returns original' do
stub_url('http://mygitserver.com/gitlab-org/gitlab-ce')
expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
diff --git a/spec/initializers/doorkeeper_spec.rb b/spec/initializers/doorkeeper_spec.rb
index 37cc08b3038..1a78196e33d 100644
--- a/spec/initializers/doorkeeper_spec.rb
+++ b/spec/initializers/doorkeeper_spec.rb
@@ -9,8 +9,8 @@ describe Doorkeeper.configuration do
end
describe '#optional_scopes' do
- it 'matches Gitlab::Auth::OPTIONAL_SCOPES' do
- expect(subject.optional_scopes).to eq Gitlab::Auth::OPTIONAL_SCOPES - Gitlab::Auth::REGISTRY_SCOPES
+ it 'matches Gitlab::Auth.optional_scopes' do
+ expect(subject.optional_scopes).to eq Gitlab::Auth.optional_scopes - Gitlab::Auth::REGISTRY_SCOPES
end
end
diff --git a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js
deleted file mode 100644
index 114d282e48a..00000000000
--- a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js
+++ /dev/null
@@ -1,219 +0,0 @@
-import Cookies from 'js-cookie';
-import {
- getCookieName,
- getSelector,
- showPopover,
- hidePopover,
- dismiss,
- mouseleave,
- mouseenter,
- setupDismissButton,
-} from '~/feature_highlight/feature_highlight_helper';
-
-describe('feature highlight helper', () => {
- describe('getCookieName', () => {
- it('returns `feature-highlighted-` prefix', () => {
- const cookieId = 'cookieId';
- expect(getCookieName(cookieId)).toEqual(`feature-highlighted-${cookieId}`);
- });
- });
-
- describe('getSelector', () => {
- it('returns js-feature-highlight selector', () => {
- const highlightId = 'highlightId';
- expect(getSelector(highlightId)).toEqual(`.js-feature-highlight[data-highlight=${highlightId}]`);
- });
- });
-
- describe('showPopover', () => {
- it('returns true when popover is shown', () => {
- const context = {
- hasClass: () => false,
- popover: () => {},
- addClass: () => {},
- };
-
- expect(showPopover.call(context)).toEqual(true);
- });
-
- it('returns false when popover is already shown', () => {
- const context = {
- hasClass: () => true,
- };
-
- expect(showPopover.call(context)).toEqual(false);
- });
-
- it('shows popover', (done) => {
- const context = {
- hasClass: () => false,
- popover: () => {},
- addClass: () => {},
- };
-
- spyOn(context, 'popover').and.callFake((method) => {
- expect(method).toEqual('show');
- done();
- });
-
- showPopover.call(context);
- });
-
- it('adds disable-animation and js-popover-show class', (done) => {
- const context = {
- hasClass: () => false,
- popover: () => {},
- addClass: () => {},
- };
-
- spyOn(context, 'addClass').and.callFake((classNames) => {
- expect(classNames).toEqual('disable-animation js-popover-show');
- done();
- });
-
- showPopover.call(context);
- });
- });
-
- describe('hidePopover', () => {
- it('returns true when popover is hidden', () => {
- const context = {
- hasClass: () => true,
- popover: () => {},
- removeClass: () => {},
- };
-
- expect(hidePopover.call(context)).toEqual(true);
- });
-
- it('returns false when popover is already hidden', () => {
- const context = {
- hasClass: () => false,
- };
-
- expect(hidePopover.call(context)).toEqual(false);
- });
-
- it('hides popover', (done) => {
- const context = {
- hasClass: () => true,
- popover: () => {},
- removeClass: () => {},
- };
-
- spyOn(context, 'popover').and.callFake((method) => {
- expect(method).toEqual('hide');
- done();
- });
-
- hidePopover.call(context);
- });
-
- it('removes disable-animation and js-popover-show class', (done) => {
- const context = {
- hasClass: () => true,
- popover: () => {},
- removeClass: () => {},
- };
-
- spyOn(context, 'removeClass').and.callFake((classNames) => {
- expect(classNames).toEqual('disable-animation js-popover-show');
- done();
- });
-
- hidePopover.call(context);
- });
- });
-
- describe('dismiss', () => {
- const context = {
- hide: () => {},
- };
-
- beforeEach(() => {
- spyOn(Cookies, 'set').and.callFake(() => {});
- spyOn(hidePopover, 'call').and.callFake(() => {});
- spyOn(context, 'hide').and.callFake(() => {});
- dismiss.call(context);
- });
-
- it('sets cookie to true', () => {
- expect(Cookies.set).toHaveBeenCalled();
- });
-
- it('calls hide popover', () => {
- expect(hidePopover.call).toHaveBeenCalled();
- });
-
- it('calls hide', () => {
- expect(context.hide).toHaveBeenCalled();
- });
- });
-
- describe('mouseleave', () => {
- it('calls hide popover if .popover:hover is false', () => {
- const fakeJquery = {
- length: 0,
- };
-
- spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn));
- spyOn(hidePopover, 'call');
- mouseleave();
- expect(hidePopover.call).toHaveBeenCalled();
- });
-
- it('does not call hide popover if .popover:hover is true', () => {
- const fakeJquery = {
- length: 1,
- };
-
- spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn));
- spyOn(hidePopover, 'call');
- mouseleave();
- expect(hidePopover.call).not.toHaveBeenCalled();
- });
- });
-
- describe('mouseenter', () => {
- const context = {};
-
- it('shows popover', () => {
- spyOn(showPopover, 'call').and.returnValue(false);
- mouseenter.call(context);
- expect(showPopover.call).toHaveBeenCalled();
- });
-
- it('registers mouseleave event if popover is showed', (done) => {
- spyOn(showPopover, 'call').and.returnValue(true);
- spyOn($.fn, 'on').and.callFake((eventName) => {
- expect(eventName).toEqual('mouseleave');
- done();
- });
- mouseenter.call(context);
- });
-
- it('does not register mouseleave event if popover is not showed', () => {
- spyOn(showPopover, 'call').and.returnValue(false);
- const spy = spyOn($.fn, 'on').and.callFake(() => {});
- mouseenter.call(context);
- expect(spy).not.toHaveBeenCalled();
- });
- });
-
- describe('setupDismissButton', () => {
- it('registers click event callback', (done) => {
- const context = {
- getAttribute: () => 'popoverId',
- dataset: {
- highlight: 'cookieId',
- },
- };
-
- spyOn($.fn, 'on').and.callFake((event) => {
- expect(event).toEqual('click');
- done();
- });
- setupDismissButton.call(context);
- });
- });
-});
diff --git a/spec/javascripts/feature_highlight/feature_highlight_options_spec.js b/spec/javascripts/feature_highlight/feature_highlight_options_spec.js
deleted file mode 100644
index 7feb361edec..00000000000
--- a/spec/javascripts/feature_highlight/feature_highlight_options_spec.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import domContentLoaded from '~/feature_highlight/feature_highlight_options';
-import bp from '~/breakpoints';
-
-describe('feature highlight options', () => {
- describe('domContentLoaded', () => {
- const highlightOrder = [];
-
- beforeEach(() => {
- // Check for when highlightFeatures is called
- spyOn(highlightOrder, 'find').and.callFake(() => {});
- });
-
- it('should not call highlightFeatures when breakpoint is xs', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('xs');
-
- domContentLoaded(highlightOrder);
- expect(bp.getBreakpointSize).toHaveBeenCalled();
- expect(highlightOrder.find).not.toHaveBeenCalled();
- });
-
- it('should not call highlightFeatures when breakpoint is sm', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('sm');
-
- domContentLoaded(highlightOrder);
- expect(bp.getBreakpointSize).toHaveBeenCalled();
- expect(highlightOrder.find).not.toHaveBeenCalled();
- });
-
- it('should not call highlightFeatures when breakpoint is md', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('md');
-
- domContentLoaded(highlightOrder);
- expect(bp.getBreakpointSize).toHaveBeenCalled();
- expect(highlightOrder.find).not.toHaveBeenCalled();
- });
-
- it('should call highlightFeatures when breakpoint is lg', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('lg');
-
- domContentLoaded(highlightOrder);
- expect(bp.getBreakpointSize).toHaveBeenCalled();
- expect(highlightOrder.find).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/javascripts/feature_highlight/feature_highlight_spec.js b/spec/javascripts/feature_highlight/feature_highlight_spec.js
deleted file mode 100644
index 6abe8425ee7..00000000000
--- a/spec/javascripts/feature_highlight/feature_highlight_spec.js
+++ /dev/null
@@ -1,122 +0,0 @@
-import Cookies from 'js-cookie';
-import * as featureHighlightHelper from '~/feature_highlight/feature_highlight_helper';
-import * as featureHighlight from '~/feature_highlight/feature_highlight';
-
-describe('feature highlight', () => {
- describe('setupFeatureHighlightPopover', () => {
- const selector = '.js-feature-highlight[data-highlight=test]';
- beforeEach(() => {
- setFixtures(`
- <div>
- <div class="js-feature-highlight" data-highlight="test" disabled>
- Trigger
- </div>
- </div>
- <div class="feature-highlight-popover-content">
- Content
- <div class="dismiss-feature-highlight">
- Dismiss
- </div>
- </div>
- `);
- spyOn(window, 'addEventListener');
- spyOn(window, 'removeEventListener');
- featureHighlight.setupFeatureHighlightPopover('test', 0);
- });
-
- it('setups popover content', () => {
- const $popoverContent = $('.feature-highlight-popover-content');
- const outerHTML = $popoverContent.prop('outerHTML');
-
- expect($(selector).data('content')).toEqual(outerHTML);
- });
-
- it('setups mouseenter', () => {
- const showSpy = spyOn(featureHighlightHelper.showPopover, 'call');
- $(selector).trigger('mouseenter');
-
- expect(showSpy).toHaveBeenCalled();
- });
-
- it('setups debounced mouseleave', (done) => {
- const hideSpy = spyOn(featureHighlightHelper.hidePopover, 'call');
- $(selector).trigger('mouseleave');
-
- // Even though we've set the debounce to 0ms, setTimeout is needed for the debounce
- setTimeout(() => {
- expect(hideSpy).toHaveBeenCalled();
- done();
- }, 0);
- });
-
- it('setups inserted.bs.popover', () => {
- $(selector).trigger('mouseenter');
- const popoverId = $(selector).attr('aria-describedby');
- const spyEvent = spyOnEvent(`#${popoverId} .dismiss-feature-highlight`, 'click');
-
- $(`#${popoverId} .dismiss-feature-highlight`).click();
- expect(spyEvent).toHaveBeenTriggered();
- });
-
- it('setups show.bs.popover', () => {
- $(selector).trigger('show.bs.popover');
- expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function));
- });
-
- it('setups hide.bs.popover', () => {
- $(selector).trigger('hide.bs.popover');
- expect(window.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function));
- });
-
- it('removes disabled attribute', () => {
- expect($('.js-feature-highlight').is(':disabled')).toEqual(false);
- });
-
- it('displays popover', () => {
- expect($(selector).attr('aria-describedby')).toBeFalsy();
- $(selector).trigger('mouseenter');
- expect($(selector).attr('aria-describedby')).toBeTruthy();
- });
- });
-
- describe('shouldHighlightFeature', () => {
- it('should return false if element is not found', () => {
- spyOn(document, 'querySelector').and.returnValue(null);
- spyOn(Cookies, 'get').and.returnValue(null);
-
- expect(featureHighlight.shouldHighlightFeature()).toBeFalsy();
- });
-
- it('should return false if previouslyDismissed', () => {
- spyOn(document, 'querySelector').and.returnValue(document.createElement('div'));
- spyOn(Cookies, 'get').and.returnValue('true');
-
- expect(featureHighlight.shouldHighlightFeature()).toBeFalsy();
- });
-
- it('should return true if element is found and not previouslyDismissed', () => {
- spyOn(document, 'querySelector').and.returnValue(document.createElement('div'));
- spyOn(Cookies, 'get').and.returnValue(null);
-
- expect(featureHighlight.shouldHighlightFeature()).toBeTruthy();
- });
- });
-
- describe('highlightFeatures', () => {
- it('calls setupFeatureHighlightPopover if shouldHighlightFeature returns true', () => {
- // Mimic shouldHighlightFeature set to true
- const highlightOrder = ['issue-boards'];
- spyOn(highlightOrder, 'find').and.returnValue(highlightOrder[0]);
-
- expect(featureHighlight.highlightFeatures(highlightOrder)).toEqual(true);
- });
-
- it('does not call setupFeatureHighlightPopover if shouldHighlightFeature returns false', () => {
- // Mimic shouldHighlightFeature set to false
- const highlightOrder = ['issue-boards'];
- spyOn(highlightOrder, 'find').and.returnValue(null);
-
- expect(featureHighlight.highlightFeatures(highlightOrder)).toEqual(false);
- });
- });
-});
diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js
index b3c9bca64cc..02415485d19 100644
--- a/spec/javascripts/filtered_search/dropdown_user_spec.js
+++ b/spec/javascripts/filtered_search/dropdown_user_spec.js
@@ -10,6 +10,7 @@ describe('Dropdown User', () => {
beforeEach(() => {
spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
+ spyOn(gl.DropdownUser.prototype, 'getGroupId').and.callFake(() => {});
spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {});
dropdownUser = new gl.DropdownUser({
@@ -38,6 +39,7 @@ describe('Dropdown User', () => {
beforeEach(() => {
spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {});
spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {});
+ spyOn(gl.DropdownUser.prototype, 'getGroupId').and.callFake(() => {});
});
it('should return endpoint', () => {
diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js
index f8b37c0edde..f4b4d7980a4 100644
--- a/spec/javascripts/fly_out_nav_spec.js
+++ b/spec/javascripts/fly_out_nav_spec.js
@@ -271,12 +271,19 @@ describe('Fly out sidebar navigation', () => {
});
it('sets transform of sub-items', () => {
+ const sidebar = document.createElement('div');
const subItems = el.querySelector('.sidebar-sub-level-items');
+
+ sidebar.style.width = '200px';
+
+ document.body.appendChild(sidebar);
+
+ setSidebar(sidebar);
showSubLevelItems(el);
expect(
subItems.style.transform,
- ).toBe(`translate3d(0px, ${Math.floor(el.getBoundingClientRect().top) - getHeaderHeight()}px, 0px)`);
+ ).toBe(`translate3d(200px, ${Math.floor(el.getBoundingClientRect().top) - getHeaderHeight()}px, 0px)`);
});
it('sets is-above when element is above', () => {
diff --git a/spec/javascripts/pretty_time_spec.js b/spec/javascripts/pretty_time_spec.js
index 0a6c479a95b..084ffe08917 100644
--- a/spec/javascripts/pretty_time_spec.js
+++ b/spec/javascripts/pretty_time_spec.js
@@ -1,215 +1,133 @@
-import '~/lib/utils/pretty_time';
+import { parseSeconds, abbreviateTime, stringifyTime } from '~/lib/utils/pretty_time';
-(() => {
- const prettyTime = gl.utils.prettyTime;
+function assertTimeUnits(obj, minutes, hours, days, weeks) {
+ expect(obj.minutes).toBe(minutes);
+ expect(obj.hours).toBe(hours);
+ expect(obj.days).toBe(days);
+ expect(obj.weeks).toBe(weeks);
+}
- describe('prettyTime methods', function () {
- describe('parseSeconds', function () {
- it('should correctly parse a negative value', function () {
- const parser = prettyTime.parseSeconds;
+describe('prettyTime methods', () => {
+ describe('parseSeconds', () => {
+ it('should correctly parse a negative value', () => {
+ const zeroSeconds = parseSeconds(-1000);
- const zeroSeconds = parser(-1000);
-
- expect(zeroSeconds.minutes).toBe(16);
- expect(zeroSeconds.hours).toBe(0);
- expect(zeroSeconds.days).toBe(0);
- expect(zeroSeconds.weeks).toBe(0);
- });
-
- it('should correctly parse a zero value', function () {
- const parser = prettyTime.parseSeconds;
-
- const zeroSeconds = parser(0);
-
- expect(zeroSeconds.minutes).toBe(0);
- expect(zeroSeconds.hours).toBe(0);
- expect(zeroSeconds.days).toBe(0);
- expect(zeroSeconds.weeks).toBe(0);
- });
-
- it('should correctly parse a small non-zero second values', function () {
- const parser = prettyTime.parseSeconds;
-
- const subOneMinute = parser(10);
-
- expect(subOneMinute.minutes).toBe(0);
- expect(subOneMinute.hours).toBe(0);
- expect(subOneMinute.days).toBe(0);
- expect(subOneMinute.weeks).toBe(0);
-
- const aboveOneMinute = parser(100);
-
- expect(aboveOneMinute.minutes).toBe(1);
- expect(aboveOneMinute.hours).toBe(0);
- expect(aboveOneMinute.days).toBe(0);
- expect(aboveOneMinute.weeks).toBe(0);
-
- const manyMinutes = parser(1000);
-
- expect(manyMinutes.minutes).toBe(16);
- expect(manyMinutes.hours).toBe(0);
- expect(manyMinutes.days).toBe(0);
- expect(manyMinutes.weeks).toBe(0);
- });
-
- it('should correctly parse large second values', function () {
- const parser = prettyTime.parseSeconds;
-
- const aboveOneHour = parser(4800);
-
- expect(aboveOneHour.minutes).toBe(20);
- expect(aboveOneHour.hours).toBe(1);
- expect(aboveOneHour.days).toBe(0);
- expect(aboveOneHour.weeks).toBe(0);
-
- const aboveOneDay = parser(110000);
-
- expect(aboveOneDay.minutes).toBe(33);
- expect(aboveOneDay.hours).toBe(6);
- expect(aboveOneDay.days).toBe(3);
- expect(aboveOneDay.weeks).toBe(0);
-
- const aboveOneWeek = parser(25000000);
-
- expect(aboveOneWeek.minutes).toBe(26);
- expect(aboveOneWeek.hours).toBe(0);
- expect(aboveOneWeek.days).toBe(3);
- expect(aboveOneWeek.weeks).toBe(173);
- });
+ assertTimeUnits(zeroSeconds, 16, 0, 0, 0);
+ });
- it('should correctly accept a custom param for hoursPerDay', function () {
- const parser = prettyTime.parseSeconds;
- const config = { hoursPerDay: 24 };
+ it('should correctly parse a zero value', () => {
+ const zeroSeconds = parseSeconds(0);
- const aboveOneHour = parser(4800, config);
+ assertTimeUnits(zeroSeconds, 0, 0, 0, 0);
+ });
- expect(aboveOneHour.minutes).toBe(20);
- expect(aboveOneHour.hours).toBe(1);
- expect(aboveOneHour.days).toBe(0);
- expect(aboveOneHour.weeks).toBe(0);
+ it('should correctly parse a small non-zero second values', () => {
+ const subOneMinute = parseSeconds(10);
+ const aboveOneMinute = parseSeconds(100);
+ const manyMinutes = parseSeconds(1000);
- const aboveOneDay = parser(110000, config);
+ assertTimeUnits(subOneMinute, 0, 0, 0, 0);
+ assertTimeUnits(aboveOneMinute, 1, 0, 0, 0);
+ assertTimeUnits(manyMinutes, 16, 0, 0, 0);
+ });
- expect(aboveOneDay.minutes).toBe(33);
- expect(aboveOneDay.hours).toBe(6);
- expect(aboveOneDay.days).toBe(1);
- expect(aboveOneDay.weeks).toBe(0);
+ it('should correctly parse large second values', () => {
+ const aboveOneHour = parseSeconds(4800);
+ const aboveOneDay = parseSeconds(110000);
+ const aboveOneWeek = parseSeconds(25000000);
- const aboveOneWeek = parser(25000000, config);
+ assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
+ assertTimeUnits(aboveOneDay, 33, 6, 3, 0);
+ assertTimeUnits(aboveOneWeek, 26, 0, 3, 173);
+ });
- expect(aboveOneWeek.minutes).toBe(26);
- expect(aboveOneWeek.hours).toBe(8);
- expect(aboveOneWeek.days).toBe(4);
+ it('should correctly accept a custom param for hoursPerDay', () => {
+ const config = { hoursPerDay: 24 };
- expect(aboveOneWeek.weeks).toBe(57);
- });
+ const aboveOneHour = parseSeconds(4800, config);
+ const aboveOneDay = parseSeconds(110000, config);
+ const aboveOneWeek = parseSeconds(25000000, config);
- it('should correctly accept a custom param for daysPerWeek', function () {
- const parser = prettyTime.parseSeconds;
- const config = { daysPerWeek: 7 };
+ assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
+ assertTimeUnits(aboveOneDay, 33, 6, 1, 0);
+ assertTimeUnits(aboveOneWeek, 26, 8, 4, 57);
+ });
- const aboveOneHour = parser(4800, config);
+ it('should correctly accept a custom param for daysPerWeek', () => {
+ const config = { daysPerWeek: 7 };
- expect(aboveOneHour.minutes).toBe(20);
- expect(aboveOneHour.hours).toBe(1);
- expect(aboveOneHour.days).toBe(0);
- expect(aboveOneHour.weeks).toBe(0);
+ const aboveOneHour = parseSeconds(4800, config);
+ const aboveOneDay = parseSeconds(110000, config);
+ const aboveOneWeek = parseSeconds(25000000, config);
- const aboveOneDay = parser(110000, config);
+ assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
+ assertTimeUnits(aboveOneDay, 33, 6, 3, 0);
+ assertTimeUnits(aboveOneWeek, 26, 0, 0, 124);
+ });
- expect(aboveOneDay.minutes).toBe(33);
- expect(aboveOneDay.hours).toBe(6);
- expect(aboveOneDay.days).toBe(3);
- expect(aboveOneDay.weeks).toBe(0);
+ it('should correctly accept custom params for daysPerWeek and hoursPerDay', () => {
+ const config = { daysPerWeek: 55, hoursPerDay: 14 };
- const aboveOneWeek = parser(25000000, config);
+ const aboveOneHour = parseSeconds(4800, config);
+ const aboveOneDay = parseSeconds(110000, config);
+ const aboveOneWeek = parseSeconds(25000000, config);
- expect(aboveOneWeek.minutes).toBe(26);
- expect(aboveOneWeek.hours).toBe(0);
- expect(aboveOneWeek.days).toBe(0);
+ assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
+ assertTimeUnits(aboveOneDay, 33, 2, 2, 0);
+ assertTimeUnits(aboveOneWeek, 26, 0, 1, 9);
+ });
+ });
- expect(aboveOneWeek.weeks).toBe(124);
- });
+ describe('stringifyTime', () => {
+ it('should stringify values with all non-zero units', () => {
+ const timeObject = {
+ weeks: 1,
+ days: 4,
+ hours: 7,
+ minutes: 20,
+ };
- it('should correctly accept custom params for daysPerWeek and hoursPerDay', function () {
- const parser = prettyTime.parseSeconds;
- const config = { daysPerWeek: 55, hoursPerDay: 14 };
+ const timeString = stringifyTime(timeObject);
- const aboveOneHour = parser(4800, config);
+ expect(timeString).toBe('1w 4d 7h 20m');
+ });
- expect(aboveOneHour.minutes).toBe(20);
- expect(aboveOneHour.hours).toBe(1);
- expect(aboveOneHour.days).toBe(0);
- expect(aboveOneHour.weeks).toBe(0);
+ it('should stringify values with some non-zero units', () => {
+ const timeObject = {
+ weeks: 0,
+ days: 4,
+ hours: 0,
+ minutes: 20,
+ };
- const aboveOneDay = parser(110000, config);
+ const timeString = stringifyTime(timeObject);
- expect(aboveOneDay.minutes).toBe(33);
- expect(aboveOneDay.hours).toBe(2);
- expect(aboveOneDay.days).toBe(2);
- expect(aboveOneDay.weeks).toBe(0);
+ expect(timeString).toBe('4d 20m');
+ });
- const aboveOneWeek = parser(25000000, config);
+ it('should stringify values with no non-zero units', () => {
+ const timeObject = {
+ weeks: 0,
+ days: 0,
+ hours: 0,
+ minutes: 0,
+ };
- expect(aboveOneWeek.minutes).toBe(26);
- expect(aboveOneWeek.hours).toBe(0);
- expect(aboveOneWeek.days).toBe(1);
+ const timeString = stringifyTime(timeObject);
- expect(aboveOneWeek.weeks).toBe(9);
- });
+ expect(timeString).toBe('0m');
});
+ });
- describe('stringifyTime', function () {
- it('should stringify values with all non-zero units', function () {
- const timeObject = {
- weeks: 1,
- days: 4,
- hours: 7,
- minutes: 20,
- };
-
- const timeString = prettyTime.stringifyTime(timeObject);
-
- expect(timeString).toBe('1w 4d 7h 20m');
- });
-
- it('should stringify values with some non-zero units', function () {
- const timeObject = {
- weeks: 0,
- days: 4,
- hours: 0,
- minutes: 20,
- };
-
- const timeString = prettyTime.stringifyTime(timeObject);
-
- expect(timeString).toBe('4d 20m');
- });
-
- it('should stringify values with no non-zero units', function () {
- const timeObject = {
- weeks: 0,
- days: 0,
- hours: 0,
- minutes: 0,
- };
-
- const timeString = prettyTime.stringifyTime(timeObject);
-
- expect(timeString).toBe('0m');
- });
+ describe('abbreviateTime', () => {
+ it('should abbreviate stringified times for weeks', () => {
+ const fullTimeString = '1w 3d 4h 5m';
+ expect(abbreviateTime(fullTimeString)).toBe('1w');
});
- describe('abbreviateTime', function () {
- it('should abbreviate stringified times for weeks', function () {
- const fullTimeString = '1w 3d 4h 5m';
- expect(prettyTime.abbreviateTime(fullTimeString)).toBe('1w');
- });
-
- it('should abbreviate stringified times for non-weeks', function () {
- const fullTimeString = '0w 3d 4h 5m';
- expect(prettyTime.abbreviateTime(fullTimeString)).toBe('3d');
- });
+ it('should abbreviate stringified times for non-weeks', () => {
+ const fullTimeString = '0w 3d 4h 5m';
+ expect(abbreviateTime(fullTimeString)).toBe('3d');
});
});
-})(window.gl || (window.gl = {}));
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
index c763487d12f..690665ae12c 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -37,6 +37,26 @@ describe('MRWidgetPipeline', () => {
});
});
+ describe('hasPipeline', () => {
+ it('should return true when there is a pipeline', () => {
+ expect(Object.keys(mockData.pipeline).length).toBeGreaterThan(0);
+
+ const vm = createComponent({
+ pipeline: mockData.pipeline,
+ });
+
+ expect(vm.hasPipeline).toBeTruthy();
+ });
+
+ it('should return false when there is no pipeline', () => {
+ const vm = createComponent({
+ pipeline: null,
+ });
+
+ expect(vm.hasPipeline).toBeFalsy();
+ });
+ });
+
describe('hasCIError', () => {
it('should return false when there is no CI error', () => {
const vm = createComponent({
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index c607c9746a4..03a52f1f91c 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -11,6 +11,7 @@ const createComponent = (customConfig = {}) => {
isPipelineActive: false,
pipeline: null,
isPipelineFailed: false,
+ isPipelinePassing: false,
onlyAllowMergeIfPipelineSucceeds: false,
hasCI: false,
ciStatus: null,
@@ -68,6 +69,18 @@ describe('MRWidgetReadyToMerge', () => {
});
describe('computed', () => {
+ describe('shouldShowMergeWhenPipelineSucceedsText', () => {
+ it('should return true with active pipeline', () => {
+ vm.mr.isPipelineActive = true;
+ expect(vm.shouldShowMergeWhenPipelineSucceedsText).toBeTruthy();
+ });
+
+ it('should return false with inactive pipeline', () => {
+ vm.mr.isPipelineActive = false;
+ expect(vm.shouldShowMergeWhenPipelineSucceedsText).toBeFalsy();
+ });
+ });
+
describe('commitMessageLinkTitle', () => {
const withDesc = 'Include description in commit message';
const withoutDesc = "Don't include description in commit message";
@@ -83,7 +96,7 @@ describe('MRWidgetReadyToMerge', () => {
});
describe('mergeButtonClass', () => {
- const defaultClass = 'btn btn-small btn-success accept-merge-request';
+ const defaultClass = 'btn btn-sm btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`;
@@ -203,20 +216,55 @@ describe('MRWidgetReadyToMerge', () => {
describe('methods', () => {
describe('isMergeAllowed', () => {
- it('should return false with initial data', () => {
+ it('should return true when no pipeline and not required to succeed', () => {
+ vm.mr.onlyAllowMergeIfPipelineSucceeds = false;
+ vm.mr.isPipelinePassing = false;
expect(vm.isMergeAllowed()).toBeTruthy();
});
- it('should return false when MR is set only merge when pipeline succeeds', () => {
- vm.mr.onlyAllowMergeIfPipelineSucceeds = true;
+ it('should return true when pipeline failed and not required to succeed', () => {
+ vm.mr.onlyAllowMergeIfPipelineSucceeds = false;
+ vm.mr.isPipelinePassing = false;
expect(vm.isMergeAllowed()).toBeTruthy();
});
- it('should return true true', () => {
+ it('should return false when pipeline failed and required to succeed', () => {
vm.mr.onlyAllowMergeIfPipelineSucceeds = true;
- vm.mr.isPipelineFailed = true;
+ vm.mr.isPipelinePassing = false;
expect(vm.isMergeAllowed()).toBeFalsy();
});
+
+ it('should return true when pipeline succeeded and required to succeed', () => {
+ vm.mr.onlyAllowMergeIfPipelineSucceeds = true;
+ vm.mr.isPipelinePassing = true;
+ expect(vm.isMergeAllowed()).toBeTruthy();
+ });
+ });
+
+ describe('shouldShowMergeControls', () => {
+ it('should return false when an external pipeline is running and required to succeed', () => {
+ spyOn(vm, 'isMergeAllowed').and.returnValue(false);
+ vm.mr.isPipelineActive = false;
+ expect(vm.shouldShowMergeControls()).toBeFalsy();
+ });
+
+ it('should return true when the build succeeded or build not required to succeed', () => {
+ spyOn(vm, 'isMergeAllowed').and.returnValue(true);
+ vm.mr.isPipelineActive = false;
+ expect(vm.shouldShowMergeControls()).toBeTruthy();
+ });
+
+ it('should return true when showing the MWPS button and a pipeline is running that needs to be successful', () => {
+ spyOn(vm, 'isMergeAllowed').and.returnValue(false);
+ vm.mr.isPipelineActive = true;
+ expect(vm.shouldShowMergeControls()).toBeTruthy();
+ });
+
+ it('should return true when showing the MWPS button but not required for the pipeline to succeed', () => {
+ spyOn(vm, 'isMergeAllowed').and.returnValue(true);
+ vm.mr.isPipelineActive = true;
+ expect(vm.shouldShowMergeControls()).toBeTruthy();
+ });
});
describe('updateCommitMessage', () => {
diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
index 669ee248bf1..da66c7504cb 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -59,23 +59,15 @@ describe('mrWidgetOptions', () => {
});
describe('shouldRenderPipelines', () => {
- it('should return true for the initial data', () => {
- expect(vm.shouldRenderPipelines).toBeTruthy();
- });
+ it('should return true when hasCI is true', () => {
+ vm.mr.hasCI = true;
- it('should return true when pipeline is empty but MR.hasCI is set to true', () => {
- vm.mr.pipeline = {};
expect(vm.shouldRenderPipelines).toBeTruthy();
});
- it('should return true when pipeline available', () => {
+ it('should return false when hasCI is false', () => {
vm.mr.hasCI = false;
- expect(vm.shouldRenderPipelines).toBeTruthy();
- });
- it('should return false when there is no pipeline', () => {
- vm.mr.pipeline = {};
- vm.mr.hasCI = false;
expect(vm.shouldRenderPipelines).toBeFalsy();
});
});
diff --git a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
index 56dd0198ae2..8e5614b20f0 100644
--- a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
+++ b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
@@ -18,5 +18,39 @@ describe('MergeRequestStore', () => {
store.setData({ ...mockData, work_in_progress: !mockData.work_in_progress });
expect(store.hasSHAChanged).toBe(false);
});
+
+ describe('isPipelinePassing', () => {
+ it('is true when the CI status is `success`', () => {
+ store.setData({ ...mockData, ci_status: 'success' });
+ expect(store.isPipelinePassing).toBe(true);
+ });
+
+ it('is true when the CI status is `success_with_warnings`', () => {
+ store.setData({ ...mockData, ci_status: 'success_with_warnings' });
+ expect(store.isPipelinePassing).toBe(true);
+ });
+
+ it('is false when the CI status is `failed`', () => {
+ store.setData({ ...mockData, ci_status: 'failed' });
+ expect(store.isPipelinePassing).toBe(false);
+ });
+
+ it('is false when the CI status is anything except `success`', () => {
+ store.setData({ ...mockData, ci_status: 'foobarbaz' });
+ expect(store.isPipelinePassing).toBe(false);
+ });
+ });
+
+ describe('isPipelineSkipped', () => {
+ it('should set isPipelineSkipped=true when the CI status is `skipped`', () => {
+ store.setData({ ...mockData, ci_status: 'skipped' });
+ expect(store.isPipelineSkipped).toBe(true);
+ });
+
+ it('should set isPipelineSkipped=false when the CI status is anything except `skipped`', () => {
+ store.setData({ ...mockData, ci_status: 'foobarbaz' });
+ expect(store.isPipelineSkipped).toBe(false);
+ });
+ });
});
});
diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
index ebd6c79077e..fe7a8c84c9e 100644
--- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
@@ -296,7 +296,7 @@ describe Banzai::Filter::MilestoneReferenceFilter do
context 'project milestones' do
let(:milestone) { create(:milestone, project: project) }
- let(:reference) { milestone.to_reference }
+ let(:reference) { milestone.to_reference(format: :iid) }
include_examples 'reference parsing'
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 4f4a27e4c41..af1db2c3455 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -16,20 +16,20 @@ describe Gitlab::Auth do
expect(subject::DEFAULT_SCOPES).to eq [:api]
end
- it 'OPTIONAL_SCOPES contains all non-default scopes' do
+ it 'optional_scopes contains all non-default scopes' do
stub_container_registry_config(enabled: true)
- expect(subject::OPTIONAL_SCOPES).to eq %i[read_user read_registry openid]
+ expect(subject.optional_scopes).to eq %i[read_user read_registry openid]
end
- context 'REGISTRY_SCOPES' do
+ context 'registry_scopes' do
context 'when registry is disabled' do
before do
stub_container_registry_config(enabled: false)
end
it 'is empty' do
- expect(subject::REGISTRY_SCOPES).to eq []
+ expect(subject.registry_scopes).to eq []
end
end
@@ -39,7 +39,7 @@ describe Gitlab::Auth do
end
it 'contains all registry related scopes' do
- expect(subject::REGISTRY_SCOPES).to eq %i[read_registry]
+ expect(subject.registry_scopes).to eq %i[read_registry]
end
end
end
diff --git a/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb b/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb
new file mode 100644
index 00000000000..5c471cbdeda
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::DeleteConflictingRedirectRoutesRange, :migration, schema: 20170907170235 do
+ let!(:redirect_routes) { table(:redirect_routes) }
+ let!(:routes) { table(:routes) }
+
+ before do
+ routes.create!(id: 1, source_id: 1, source_type: 'Namespace', path: 'foo1')
+ routes.create!(id: 2, source_id: 2, source_type: 'Namespace', path: 'foo2')
+ routes.create!(id: 3, source_id: 3, source_type: 'Namespace', path: 'foo3')
+ routes.create!(id: 4, source_id: 4, source_type: 'Namespace', path: 'foo4')
+ routes.create!(id: 5, source_id: 5, source_type: 'Namespace', path: 'foo5')
+
+ # Valid redirects
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar2')
+ redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'bar3')
+
+ # Conflicting redirects
+ redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'foo1')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo2')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo3')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo4')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo5')
+ end
+
+ it 'deletes the conflicting redirect_routes in the range' do
+ expect(redirect_routes.count).to eq(8)
+
+ expect do
+ described_class.new.perform(1, 3)
+ end.to change { redirect_routes.where("path like 'foo%'").count }.from(5).to(2)
+
+ expect do
+ described_class.new.perform(4, 5)
+ end.to change { redirect_routes.where("path like 'foo%'").count }.from(2).to(0)
+
+ expect(redirect_routes.count).to eq(3)
+ end
+end
diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/gitlab/backup/manager_spec.rb
index 8772d3d5ada..ac7f1569e34 100644
--- a/spec/lib/gitlab/backup/manager_spec.rb
+++ b/spec/lib/gitlab/backup/manager_spec.rb
@@ -26,6 +26,8 @@ describe Backup::Manager do
[
'1451606400_2016_01_01_1.2.3_gitlab_backup.tar',
'1451520000_2015_12_31_4.5.6_gitlab_backup.tar',
+ '1451520000_2015_12_31_4.5.6-pre_gitlab_backup.tar',
+ '1451520000_2015_12_31_4.5.6-rc1_gitlab_backup.tar',
'1451510000_2015_12_30_gitlab_backup.tar',
'1450742400_2015_12_22_gitlab_backup.tar',
'1449878400_gitlab_backup.tar',
@@ -57,6 +59,30 @@ describe Backup::Manager do
end
end
+ context 'when no valid file is found' do
+ let(:files) do
+ [
+ '14516064000_2016_01_01_1.2.3_gitlab_backup.tar',
+ 'foo_1451520000_2015_12_31_4.5.6_gitlab_backup.tar',
+ '1451520000_2015_12_31_4.5.6-foo_gitlab_backup.tar'
+ ]
+ end
+
+ before do
+ allow(Gitlab.config.backup).to receive(:keep_time).and_return(1)
+
+ subject.remove_old
+ end
+
+ it 'removes no files' do
+ expect(FileUtils).not_to have_received(:rm)
+ end
+
+ it 'prints a done message' do
+ expect(progress).to have_received(:puts).with('done. (0 removed)')
+ end
+ end
+
context 'when there are no files older than keep_time' do
before do
# Set to 30 days
@@ -84,28 +110,30 @@ describe Backup::Manager do
it 'removes matching files with a human-readable versioned timestamp' do
expect(FileUtils).to have_received(:rm).with(files[1])
- end
-
- it 'removes matching files with a human-readable non-versioned timestamp' do
expect(FileUtils).to have_received(:rm).with(files[2])
expect(FileUtils).to have_received(:rm).with(files[3])
end
- it 'removes matching files without a human-readable timestamp' do
+ it 'removes matching files with a human-readable non-versioned timestamp' do
expect(FileUtils).to have_received(:rm).with(files[4])
expect(FileUtils).to have_received(:rm).with(files[5])
end
+ it 'removes matching files without a human-readable timestamp' do
+ expect(FileUtils).to have_received(:rm).with(files[6])
+ expect(FileUtils).to have_received(:rm).with(files[7])
+ end
+
it 'does not remove files that are not old enough' do
expect(FileUtils).not_to have_received(:rm).with(files[0])
end
it 'does not remove non-matching files' do
- expect(FileUtils).not_to have_received(:rm).with(files[6])
+ expect(FileUtils).not_to have_received(:rm).with(files[8])
end
it 'prints a done message' do
- expect(progress).to have_received(:puts).with('done. (5 removed)')
+ expect(progress).to have_received(:puts).with('done. (7 removed)')
end
end
@@ -121,14 +149,14 @@ describe Backup::Manager do
end
it 'removes the remaining expected files' do
- expect(FileUtils).to have_received(:rm).with(files[2])
- expect(FileUtils).to have_received(:rm).with(files[3])
expect(FileUtils).to have_received(:rm).with(files[4])
expect(FileUtils).to have_received(:rm).with(files[5])
+ expect(FileUtils).to have_received(:rm).with(files[6])
+ expect(FileUtils).to have_received(:rm).with(files[7])
end
it 'sets the correct removed count' do
- expect(progress).to have_received(:puts).with('done. (4 removed)')
+ expect(progress).to have_received(:puts).with('done. (6 removed)')
end
it 'prints the error from file that could not be removed' do
diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb
index f8c8b83a3ac..2c7ef622c51 100644
--- a/spec/lib/gitlab/checks/force_push_spec.rb
+++ b/spec/lib/gitlab/checks/force_push_spec.rb
@@ -5,13 +5,13 @@ describe Gitlab::Checks::ForcePush do
context "exit code checking", skip_gitaly_mock: true do
it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do
- allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0])
+ allow_any_instance_of(Gitlab::Git::RevList).to receive(:popen).and_return(['normal output', 0])
expect { described_class.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error
end
it "raises a runtime error if the `popen` call to git returns a non-zero exit code" do
- allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1])
+ allow_any_instance_of(Gitlab::Git::RevList).to receive(:popen).and_return(['error', 1])
expect { described_class.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError)
end
diff --git a/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb b/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb
new file mode 100644
index 00000000000..15eb01eb472
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/policy/kubernetes_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Policy::Kubernetes do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ context 'when kubernetes service is active' do
+ set(:project) { create(:kubernetes_project) }
+
+ it 'is satisfied by a kubernetes pipeline' do
+ expect(described_class.new('active'))
+ .to be_satisfied_by(pipeline)
+ end
+ end
+
+ context 'when kubernetes service is inactive' do
+ set(:project) { create(:project) }
+
+ it 'is not satisfied by a pipeline without kubernetes available' do
+ expect(described_class.new('active'))
+ .not_to be_satisfied_by(pipeline)
+ end
+ end
+
+ context 'when kubernetes policy is invalid' do
+ it 'raises an error' do
+ expect { described_class.new('unknown') }
+ .to raise_error(described_class::UnknownPolicyError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/policy/refs_spec.rb b/spec/lib/gitlab/ci/build/policy/refs_spec.rb
new file mode 100644
index 00000000000..7211187e511
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/policy/refs_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Policy::Refs do
+ describe '#satisfied_by?' do
+ context 'when matching ref' do
+ let(:pipeline) { build_stubbed(:ci_pipeline, ref: 'master') }
+
+ it 'is satisfied when pipeline branch matches' do
+ expect(described_class.new(%w[master deploy]))
+ .to be_satisfied_by(pipeline)
+ end
+
+ it 'is not satisfied when pipeline branch does not match' do
+ expect(described_class.new(%w[feature fix]))
+ .not_to be_satisfied_by(pipeline)
+ end
+ end
+
+ context 'when maching tags' do
+ context 'when pipeline runs for a tag' do
+ let(:pipeline) do
+ build_stubbed(:ci_pipeline, ref: 'feature', tag: true)
+ end
+
+ it 'is satisfied when tags matcher is specified' do
+ expect(described_class.new(%w[master tags]))
+ .to be_satisfied_by(pipeline)
+ end
+ end
+
+ context 'when pipeline is not created for a tag' do
+ let(:pipeline) do
+ build_stubbed(:ci_pipeline, ref: 'feature', tag: false)
+ end
+
+ it 'is not satisfied when tag match is specified' do
+ expect(described_class.new(%w[master tags]))
+ .not_to be_satisfied_by(pipeline)
+ end
+ end
+ end
+
+ context 'when also matching a path' do
+ let(:pipeline) do
+ build_stubbed(:ci_pipeline, ref: 'master')
+ end
+
+ it 'is satisfied when provided patch matches specified one' do
+ expect(described_class.new(%W[master@#{pipeline.project_full_path}]))
+ .to be_satisfied_by(pipeline)
+ end
+
+ it 'is not satisfied when path differs' do
+ expect(described_class.new(%w[master@some/fork/repository]))
+ .not_to be_satisfied_by(pipeline)
+ end
+ end
+
+ context 'when maching a source' do
+ let(:pipeline) { build_stubbed(:ci_pipeline, source: :push) }
+
+ it 'is satisifed when provided source keyword matches' do
+ expect(described_class.new(%w[pushes]))
+ .to be_satisfied_by(pipeline)
+ end
+
+ it 'is not satisfied when provided source keyword does not match' do
+ expect(described_class.new(%w[triggers]))
+ .not_to be_satisfied_by(pipeline)
+ end
+ end
+
+ context 'when matching a ref by a regular expression' do
+ let(:pipeline) { build_stubbed(:ci_pipeline, ref: 'docs-something') }
+
+ it 'is satisfied when regexp matches pipeline ref' do
+ expect(described_class.new(['/docs-.*/']))
+ .to be_satisfied_by(pipeline)
+ end
+
+ it 'is not satisfied when regexp does not match pipeline ref' do
+ expect(described_class.new(['/fix-.*/']))
+ .not_to be_satisfied_by(pipeline)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/policy_spec.rb b/spec/lib/gitlab/ci/build/policy_spec.rb
new file mode 100644
index 00000000000..20ee3dd3e89
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/policy_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Policy do
+ let(:policy) { spy('policy specification') }
+
+ before do
+ stub_const("#{described_class}::Something", policy)
+ end
+
+ describe '.fabricate' do
+ context 'when policy exists' do
+ it 'fabricates and initializes relevant policy' do
+ specs = described_class.fabricate(something: 'some value')
+
+ expect(specs).to be_an Array
+ expect(specs).to be_one
+ expect(policy).to have_received(:new).with('some value')
+ end
+ end
+
+ context 'when some policies are not defined' do
+ it 'gracefully skips unknown policies' do
+ expect { described_class.fabricate(unknown: 'first') }
+ .to raise_error(NameError)
+ end
+ end
+
+ context 'when passing a nil value as specs' do
+ it 'returns an empty array' do
+ specs = described_class.fabricate(nil)
+
+ expect(specs).to be_an Array
+ expect(specs).to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 2278230f338..d72f8553f55 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -3,8 +3,7 @@ require 'spec_helper'
module Gitlab
module Ci
describe YamlProcessor, :lib do
- subject { described_class.new(config, path) }
- let(:path) { 'path' }
+ subject { described_class.new(config) }
describe 'our current .gitlab-ci.yml' do
let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") }
@@ -17,7 +16,7 @@ module Gitlab
end
describe '#build_attributes' do
- subject { described_class.new(config, path).build_attributes(:rspec) }
+ subject { described_class.new(config).build_attributes(:rspec) }
describe 'coverage entry' do
describe 'code coverage regexp' do
@@ -167,8 +166,6 @@ module Gitlab
end
context 'when kubernetes policy is specified' do
- let(:pipeline) { create(:ci_empty_pipeline) }
-
let(:config) do
YAML.dump(
spinach: { stage: 'test', script: 'spinach' },
@@ -204,7 +201,7 @@ module Gitlab
end
end
- describe "#builds_for_stage_and_ref" do
+ describe "#pipeline_stage_builds" do
let(:type) { 'test' }
it "returns builds if no branch specified" do
@@ -213,10 +210,10 @@ module Gitlab
rspec: { script: "rspec" }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).first).to eq({
stage: "test",
stage_idx: 1,
name: "rspec",
@@ -241,9 +238,9 @@ module Gitlab
rspec: { script: "rspec", only: ["deploy"] }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0)
end
it "does not return builds if only has regexp with another branch" do
@@ -252,9 +249,9 @@ module Gitlab
rspec: { script: "rspec", only: ["/^deploy$/"] }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0)
end
it "returns builds if only has specified this branch" do
@@ -263,9 +260,9 @@ module Gitlab
rspec: { script: "rspec", only: ["master"] }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1)
end
it "returns builds if only has a list of branches including specified" do
@@ -274,9 +271,9 @@ module Gitlab
rspec: { script: "rspec", type: type, only: %w(master deploy) }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1)
end
it "returns builds if only has a branches keyword specified" do
@@ -285,9 +282,9 @@ module Gitlab
rspec: { script: "rspec", type: type, only: ["branches"] }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1)
end
it "does not return builds if only has a tags keyword" do
@@ -296,9 +293,9 @@ module Gitlab
rspec: { script: "rspec", type: type, only: ["tags"] }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0)
end
it "returns builds if only has special keywords specified and source matches" do
@@ -315,9 +312,9 @@ module Gitlab
rspec: { script: "rspec", type: type, only: [possibility[:keyword]] }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(1)
end
end
@@ -335,21 +332,27 @@ module Gitlab
rspec: { script: "rspec", type: type, only: [possibility[:keyword]] }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0)
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(0)
end
end
it "returns builds if only has current repository path" do
+ seed_pipeline = pipeline(ref: 'deploy')
+
config = YAML.dump({
before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: ["branches@path"] }
+ rspec: {
+ script: "rspec",
+ type: type,
+ only: ["branches@#{seed_pipeline.project_full_path}"]
+ }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ expect(config_processor.pipeline_stage_builds(type, seed_pipeline).size).to eq(1)
end
it "does not return builds if only has different repository path" do
@@ -358,9 +361,9 @@ module Gitlab
rspec: { script: "rspec", type: type, only: ["branches@fork"] }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0)
end
it "returns build only for specified type" do
@@ -371,11 +374,11 @@ module Gitlab
production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, 'fork')
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
- expect(config_processor.builds_for_stage_and_ref("test", "deploy").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(1)
+ expect(config_processor.pipeline_stage_builds("deploy", pipeline(ref: "deploy")).size).to eq(2)
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "deploy")).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds("deploy", pipeline(ref: "master")).size).to eq(1)
end
context 'for invalid value' do
@@ -418,9 +421,9 @@ module Gitlab
rspec: { script: "rspec", except: ["deploy"] }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1)
end
it "returns builds if except has regexp with another branch" do
@@ -429,9 +432,9 @@ module Gitlab
rspec: { script: "rspec", except: ["/^deploy$/"] }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1)
end
it "does not return builds if except has specified this branch" do
@@ -440,9 +443,9 @@ module Gitlab
rspec: { script: "rspec", except: ["master"] }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0)
end
it "does not return builds if except has a list of branches including specified" do
@@ -451,9 +454,9 @@ module Gitlab
rspec: { script: "rspec", type: type, except: %w(master deploy) }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0)
end
it "does not return builds if except has a branches keyword specified" do
@@ -462,9 +465,9 @@ module Gitlab
rspec: { script: "rspec", type: type, except: ["branches"] }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0)
end
it "returns builds if except has a tags keyword" do
@@ -473,9 +476,9 @@ module Gitlab
rspec: { script: "rspec", type: type, except: ["tags"] }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1)
end
it "does not return builds if except has special keywords specified and source matches" do
@@ -492,9 +495,9 @@ module Gitlab
rspec: { script: "rspec", type: type, except: [possibility[:keyword]] }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0)
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(0)
end
end
@@ -512,21 +515,27 @@ module Gitlab
rspec: { script: "rspec", type: type, except: [possibility[:keyword]] }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(1)
end
end
it "does not return builds if except has current repository path" do
+ seed_pipeline = pipeline(ref: 'deploy')
+
config = YAML.dump({
before_script: ["pwd"],
- rspec: { script: "rspec", type: type, except: ["branches@path"] }
+ rspec: {
+ script: "rspec",
+ type: type,
+ except: ["branches@#{seed_pipeline.project_full_path}"]
+ }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ expect(config_processor.pipeline_stage_builds(type, seed_pipeline).size).to eq(0)
end
it "returns builds if except has different repository path" do
@@ -535,24 +544,28 @@ module Gitlab
rspec: { script: "rspec", type: type, except: ["branches@fork"] }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1)
end
it "returns build except specified type" do
+ master_pipeline = pipeline(ref: 'master')
+ test_pipeline = pipeline(ref: 'test')
+ deploy_pipeline = pipeline(ref: 'deploy')
+
config = YAML.dump({
before_script: ["pwd"],
- rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@fork"] },
+ rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@#{test_pipeline.project_full_path}"] },
staging: { script: "deploy", type: "deploy", except: ["master"] },
- production: { script: "deploy", type: "deploy", except: ["master@fork"] }
+ production: { script: "deploy", type: "deploy", except: ["master@#{master_pipeline.project_full_path}"] }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, 'fork')
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
- expect(config_processor.builds_for_stage_and_ref("test", "test").size).to eq(0)
- expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(0)
+ expect(config_processor.pipeline_stage_builds("deploy", deploy_pipeline).size).to eq(2)
+ expect(config_processor.pipeline_stage_builds("test", test_pipeline).size).to eq(0)
+ expect(config_processor.pipeline_stage_builds("deploy", master_pipeline).size).to eq(0)
end
context 'for invalid value' do
@@ -591,9 +604,9 @@ module Gitlab
describe "Scripts handling" do
let(:config_data) { YAML.dump(config) }
- let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config_data, path) }
+ let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config_data) }
- subject { config_processor.builds_for_stage_and_ref("test", "master").first }
+ subject { config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first }
describe "before_script" do
context "in global context" do
@@ -674,10 +687,10 @@ module Gitlab
before_script: ["pwd"],
rspec: { script: "rspec" } })
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({
stage: "test",
stage_idx: 1,
name: "rspec",
@@ -709,10 +722,10 @@ module Gitlab
command: ["/usr/local/bin/init", "run"] }, "docker:dind"],
script: "rspec" } })
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({
stage: "test",
stage_idx: 1,
name: "rspec",
@@ -742,10 +755,10 @@ module Gitlab
before_script: ["pwd"],
rspec: { script: "rspec" } })
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({
stage: "test",
stage_idx: 1,
name: "rspec",
@@ -771,10 +784,10 @@ module Gitlab
before_script: ["pwd"],
rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } })
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({
stage: "test",
stage_idx: 1,
name: "rspec",
@@ -797,7 +810,7 @@ module Gitlab
end
describe 'Variables' do
- let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config), path) }
+ let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
subject { config_processor.builds.first[:yaml_variables] }
@@ -918,9 +931,9 @@ module Gitlab
rspec: { script: "rspec", when: when_state }
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- builds = config_processor.builds_for_stage_and_ref("test", "master")
+ builds = config_processor.pipeline_stage_builds("test", pipeline(ref: "master"))
expect(builds.size).to eq(1)
expect(builds.first[:when]).to eq(when_state)
end
@@ -951,8 +964,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
key: 'key',
@@ -970,8 +983,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
key: 'key',
@@ -990,8 +1003,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq(
paths: ["test/"],
untracked: false,
key: 'local',
@@ -1019,8 +1032,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config)
- expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1)
+ expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({
stage: "test",
stage_idx: 1,
name: "rspec",
@@ -1055,9 +1068,9 @@ module Gitlab
}
})
- config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
+ config_processor = Gitlab::Ci::YamlProcessor.new(config)
- builds = config_processor.builds_for_stage_and_ref("test", "master")
+ builds = config_processor.pipeline_stage_builds("test", pipeline(ref: "master"))
expect(builds.size).to eq(1)
expect(builds.first[:options][:artifacts][:when]).to eq(when_state)
end
@@ -1072,7 +1085,7 @@ module Gitlab
end
let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
- let(:builds) { processor.builds_for_stage_and_ref('deploy', 'master') }
+ let(:builds) { processor.pipeline_stage_builds('deploy', pipeline(ref: 'master')) }
context 'when a production environment is specified' do
let(:environment) { 'production' }
@@ -1229,7 +1242,7 @@ module Gitlab
describe "Hidden jobs" do
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) }
- subject { config_processor.builds_for_stage_and_ref("test", "master") }
+ subject { config_processor.pipeline_stage_builds("test", pipeline(ref: "master")) }
shared_examples 'hidden_job_handling' do
it "doesn't create jobs that start with dot" do
@@ -1277,7 +1290,7 @@ module Gitlab
describe "YAML Alias/Anchor" do
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) }
- subject { config_processor.builds_for_stage_and_ref("build", "master") }
+ subject { config_processor.pipeline_stage_builds("build", pipeline(ref: "master")) }
shared_examples 'job_templates_handling' do
it "is correctly supported for jobs" do
@@ -1377,182 +1390,182 @@ EOT
it "returns errors if tags parameter is invalid" do
config = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec tags should be an array of strings")
end
it "returns errors if before_script parameter is invalid" do
config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "before_script config should be an array of strings")
end
it "returns errors if job before_script parameter is not an array of strings" do
config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings")
end
it "returns errors if after_script parameter is invalid" do
config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "after_script config should be an array of strings")
end
it "returns errors if job after_script parameter is not an array of strings" do
config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings")
end
it "returns errors if image parameter is invalid" do
config = YAML.dump({ image: ["test"], rspec: { script: "test" } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "image config should be a hash or a string")
end
it "returns errors if job name is blank" do
config = YAML.dump({ '' => { script: "test" } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:job name can't be blank")
end
it "returns errors if job name is non-string" do
config = YAML.dump({ 10 => { script: "test" } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:10 name should be a symbol")
end
it "returns errors if job image parameter is invalid" do
config = YAML.dump({ rspec: { script: "test", image: ["test"] } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:image config should be a hash or a string")
end
it "returns errors if services parameter is not an array" do
config = YAML.dump({ services: "test", rspec: { script: "test" } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "services config should be a array")
end
it "returns errors if services parameter is not an array of strings" do
config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string")
end
it "returns errors if job services parameter is not an array" do
config = YAML.dump({ rspec: { script: "test", services: "test" } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:services config should be a array")
end
it "returns errors if job services parameter is not an array of strings" do
config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string")
end
it "returns error if job configuration is invalid" do
config = YAML.dump({ extra: "bundle update" })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:extra config should be a hash")
end
it "returns errors if services configuration is not correct" do
config = YAML.dump({ extra: { script: 'rspec', services: "test" } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:extra:services config should be a array")
end
it "returns errors if there are no jobs defined" do
config = YAML.dump({ before_script: ["bundle update"] })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs config should contain at least one visible job")
end
it "returns errors if there are no visible jobs defined" do
config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs config should contain at least one visible job")
end
it "returns errors if job allow_failure parameter is not an boolean" do
config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value")
end
it "returns errors if job stage is not a string" do
config = YAML.dump({ rspec: { script: "test", type: 1 } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:type config should be a string")
end
it "returns errors if job stage is not a pre-defined stage" do
config = YAML.dump({ rspec: { script: "test", type: "acceptance" } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy")
end
it "returns errors if job stage is not a defined stage" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test")
end
it "returns errors if stages is not an array" do
config = YAML.dump({ stages: "test", rspec: { script: "test" } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "stages config should be an array of strings")
end
it "returns errors if stages is not an array of strings" do
config = YAML.dump({ stages: [true, "test"], rspec: { script: "test" } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "stages config should be an array of strings")
end
it "returns errors if variables is not a map" do
config = YAML.dump({ variables: "test", rspec: { script: "test" } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
end
it "returns errors if variables is not a map of key-value strings" do
config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
end
it "returns errors if job when is not on_success, on_failure or always" do
config = YAML.dump({ rspec: { script: "test", when: 1 } })
expect do
- Gitlab::Ci::YamlProcessor.new(config, path)
+ Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always or manual")
end
@@ -1694,6 +1707,10 @@ EOT
end
end
end
+
+ def pipeline(**attributes)
+ build_stubbed(:ci_empty_pipeline, **attributes)
+ end
end
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 1bcdc369c44..3c8350b3aad 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -914,4 +914,126 @@ describe Gitlab::Database::MigrationHelpers do
.to raise_error(RuntimeError, /Your database user is not allowed/)
end
end
+
+ describe '#bulk_queue_background_migration_jobs_by_range', :sidekiq do
+ context 'when the model has an ID column' do
+ let!(:id1) { create(:user).id }
+ let!(:id2) { create(:user).id }
+ let!(:id3) { create(:user).id }
+
+ before do
+ User.class_eval do
+ include EachBatch
+ end
+ end
+
+ context 'with enough rows to bulk queue jobs more than once' do
+ before do
+ stub_const('Gitlab::Database::MigrationHelpers::BACKGROUND_MIGRATION_JOB_BUFFER_SIZE', 1)
+ end
+
+ it 'queues jobs correctly' do
+ Sidekiq::Testing.fake! do
+ model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2)
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]])
+ expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]])
+ end
+ end
+
+ it 'queues jobs in groups of buffer size 1' do
+ expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id1, id2]]])
+ expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id3, id3]]])
+
+ model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2)
+ end
+ end
+
+ context 'with not enough rows to bulk queue jobs more than once' do
+ it 'queues jobs correctly' do
+ Sidekiq::Testing.fake! do
+ model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2)
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]])
+ expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]])
+ end
+ end
+
+ it 'queues jobs in bulk all at once (big buffer size)' do
+ expect(BackgroundMigrationWorker).to receive(:perform_bulk).with([['FooJob', [id1, id2]],
+ ['FooJob', [id3, id3]]])
+
+ model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob', batch_size: 2)
+ end
+ end
+
+ context 'without specifying batch_size' do
+ it 'queues jobs correctly' do
+ Sidekiq::Testing.fake! do
+ model.bulk_queue_background_migration_jobs_by_range(User, 'FooJob')
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3]])
+ end
+ end
+ end
+ end
+
+ context "when the model doesn't have an ID column" do
+ it 'raises error (for now)' do
+ expect do
+ model.bulk_queue_background_migration_jobs_by_range(ProjectAuthorization, 'FooJob')
+ end.to raise_error(StandardError, /does not have an ID/)
+ end
+ end
+ end
+
+ describe '#queue_background_migration_jobs_by_range_at_intervals', :sidekiq do
+ context 'when the model has an ID column' do
+ let!(:id1) { create(:user).id }
+ let!(:id2) { create(:user).id }
+ let!(:id3) { create(:user).id }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ before do
+ User.class_eval do
+ include EachBatch
+ end
+ end
+
+ context 'with batch_size option' do
+ it 'queues jobs correctly' do
+ Sidekiq::Testing.fake! do
+ model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.seconds, batch_size: 2)
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id2]])
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[1]['args']).to eq(['FooJob', [id3, id3]])
+ expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.seconds.from_now.to_f)
+ end
+ end
+ end
+
+ context 'without batch_size option' do
+ it 'queues jobs correctly' do
+ Sidekiq::Testing.fake! do
+ model.queue_background_migration_jobs_by_range_at_intervals(User, 'FooJob', 10.seconds)
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq(['FooJob', [id1, id3]])
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f)
+ end
+ end
+ end
+ end
+
+ context "when the model doesn't have an ID column" do
+ it 'raises error (for now)' do
+ expect do
+ model.queue_background_migration_jobs_by_range_at_intervals(ProjectAuthorization, 'FooJob', 10.seconds)
+ end.to raise_error(StandardError, /does not have an ID/)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb
index 8beebc10040..4fa30d8df8b 100644
--- a/spec/lib/gitlab/diff/position_tracer_spec.rb
+++ b/spec/lib/gitlab/diff/position_tracer_spec.rb
@@ -1761,17 +1761,9 @@ describe Gitlab::Diff::PositionTracer do
let(:merge_commit) do
update_file_again_commit
- committer = repository.user_to_committer(current_user)
-
- options = {
- message: "Merge branches",
- author: committer,
- committer: committer
- }
-
merge_request = create(:merge_request, source_branch: second_create_file_commit.sha, target_branch: branch_name, source_project: project)
- repository.merge(current_user, merge_request.diff_head_sha, merge_request, options)
+ repository.merge(current_user, merge_request.diff_head_sha, merge_request, "Merge branches")
project.commit(branch_name)
end
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
index a3d323fe28a..7dc06c90078 100644
--- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -1,11 +1,14 @@
require 'spec_helper'
describe Gitlab::Gfm::ReferenceRewriter do
- let(:text) { 'some text' }
- let(:old_project) { create(:project, name: 'old-project') }
- let(:new_project) { create(:project, name: 'new-project') }
+ let(:group) { create(:group) }
+ let(:old_project) { create(:project, name: 'old-project', group: group) }
+ let(:new_project) { create(:project, name: 'new-project', group: group) }
let(:user) { create(:user) }
+ let(:old_project_ref) { old_project.to_reference(new_project) }
+ let(:text) { 'some text' }
+
before do
old_project.team << [user, :reporter]
end
@@ -39,7 +42,7 @@ describe Gitlab::Gfm::ReferenceRewriter do
it { is_expected.not_to include merge_request.to_reference(new_project) }
end
- context 'description ambigous elements' do
+ context 'rewrite ambigous references' do
context 'url' do
let(:url) { 'http://gitlab.com/#1' }
let(:text) { "This references #1, but not #{url}" }
@@ -66,23 +69,21 @@ describe Gitlab::Gfm::ReferenceRewriter do
context 'description with project labels' do
let!(:label) { create(:label, id: 123, name: 'test', project: old_project) }
- let(:project_ref) { old_project.to_reference(new_project) }
context 'label referenced by id' do
let(:text) { '#1 and ~123' }
- it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} }
+ it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~123} }
end
context 'label referenced by text' do
let(:text) { '#1 and ~"test"' }
- it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~123} }
+ it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~123} }
end
end
context 'description with group labels' do
let(:old_group) { create(:group) }
let!(:group_label) { create(:group_label, id: 321, name: 'group label', group: old_group) }
- let(:project_ref) { old_project.to_reference(new_project) }
before do
old_project.update(namespace: old_group)
@@ -90,21 +91,53 @@ describe Gitlab::Gfm::ReferenceRewriter do
context 'label referenced by id' do
let(:text) { '#1 and ~321' }
- it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} }
+ it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~321} }
end
context 'label referenced by text' do
let(:text) { '#1 and ~"group label"' }
- it { is_expected.to eq %Q{#{project_ref}#1 and #{project_ref}~321} }
+ it { is_expected.to eq %Q{#{old_project_ref}#1 and #{old_project_ref}~321} }
end
end
end
+ end
+
+ context 'reference contains project milestone' do
+ let!(:milestone) do
+ create(:milestone, title: '9.0', project: old_project)
+ end
+
+ let(:text) { 'milestone: %"9.0"' }
+
+ it { is_expected.to eq %Q[milestone: #{old_project_ref}%"9.0"] }
+ end
+
+ context 'when referring to group milestone' do
+ let!(:milestone) do
+ create(:milestone, title: '10.0', group: group)
+ end
+
+ let(:text) { 'milestone %"10.0"' }
+
+ it { is_expected.to eq text }
+ end
+
+ context 'when referable has a nil reference' do
+ before do
+ create(:milestone, title: '9.0', project: old_project)
+
+ allow_any_instance_of(Milestone)
+ .to receive(:to_reference)
+ .and_return(nil)
+ end
- context 'reference contains milestone' do
- let(:milestone) { create(:milestone) }
- let(:text) { "milestone ref: #{milestone.to_reference}" }
+ let(:text) { 'milestone: %"9.0"' }
- it { is_expected.to eq text }
+ it 'raises an error that should be fixed' do
+ expect { subject }.to raise_error(
+ described_class::RewriteError,
+ 'Unspecified reference detected for Milestone'
+ )
end
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 556a148c3bc..4fc26c625a5 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -481,7 +481,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
it 'raises an error if it failed' do
- expect(Gitlab::Popen).to receive(:popen).and_return(['Error', 1])
+ expect(@repo).to receive(:popen).and_return(['Error', 1])
expect do
@repo.delete_refs('refs/heads/fix')
diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb
index b051a088171..c0eac98d718 100644
--- a/spec/lib/gitlab/git/rev_list_spec.rb
+++ b/spec/lib/gitlab/git/rev_list_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Git::RevList do
let(:rev_list) { described_class.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) }
it 'calls out to `popen`' do
- expect(Gitlab::Popen).to receive(:popen).with([
+ expect(rev_list).to receive(:popen).with([
Gitlab.config.git.bin_path,
"--git-dir=#{project.repository.path_to_repo}",
'rev-list',
@@ -36,7 +36,7 @@ describe Gitlab::Git::RevList do
let(:rev_list) { described_class.new(oldrev: 'oldrev', newrev: 'newrev', path_to_repo: project.repository.path_to_repo) }
it 'calls out to `popen`' do
- expect(Gitlab::Popen).to receive(:popen).with([
+ expect(rev_list).to receive(:popen).with([
Gitlab.config.git.bin_path,
"--git-dir=#{project.repository.path_to_repo}",
'rev-list',
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index ec3abcb0953..1ef3e2e3a5d 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -51,6 +51,10 @@ describe Gitlab::GitalyClient::CommitService do
expect(ret).to be_kind_of(Gitlab::GitalyClient::DiffStitcher)
end
+
+ it 'encodes paths correctly' do
+ expect { client.diff_from_parent(commit, paths: ['encoding/test.txt', 'encoding/テスト.txt']) }.not_to raise_error
+ end
end
describe '#commit_deltas' do
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index a9b861fcff2..9a84d6e6a67 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -38,6 +38,130 @@ describe Gitlab::GitalyClient, skip_gitaly_mock: true do
end
end
+ describe 'allow_n_plus_1_calls' do
+ context 'when RequestStore is enabled', :request_store do
+ it 'returns the result of the allow_n_plus_1_calls block' do
+ expect(described_class.allow_n_plus_1_calls { "result" }).to eq("result")
+ end
+ end
+
+ context 'when RequestStore is not active' do
+ it 'returns the result of the allow_n_plus_1_calls block' do
+ expect(described_class.allow_n_plus_1_calls { "something" }).to eq("something")
+ end
+ end
+ end
+
+ describe 'enforce_gitaly_request_limits?' do
+ def call_gitaly(count = 1)
+ (1..count).each do
+ described_class.enforce_gitaly_request_limits(:test)
+ end
+ end
+
+ context 'when RequestStore is enabled', :request_store do
+ it 'allows up the maximum number of allowed calls' do
+ expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS) }.not_to raise_error
+ end
+
+ context 'when the maximum number of calls has been reached' do
+ before do
+ call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS)
+ end
+
+ it 'fails on the next call' do
+ expect { call_gitaly(1) }.to raise_error(Gitlab::GitalyClient::TooManyInvocationsError)
+ end
+ end
+
+ it 'allows the maximum number of calls to be exceeded within an allow_n_plus_1_calls block' do
+ expect do
+ described_class.allow_n_plus_1_calls do
+ call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1)
+ end
+ end.not_to raise_error
+ end
+
+ context 'when the maximum number of calls has been reached within an allow_n_plus_1_calls block' do
+ before do
+ described_class.allow_n_plus_1_calls do
+ call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS)
+ end
+ end
+
+ it 'allows up to the maximum number of calls outside of an allow_n_plus_1_calls block' do
+ expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS) }.not_to raise_error
+ end
+
+ it 'does not allow the maximum number of calls to be exceeded outside of an allow_n_plus_1_calls block' do
+ expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1) }.to raise_error(Gitlab::GitalyClient::TooManyInvocationsError)
+ end
+ end
+ end
+
+ context 'when RequestStore is not active' do
+ it 'does not raise errors when the maximum number of allowed calls is exceeded' do
+ expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 2) }.not_to raise_error
+ end
+
+ it 'does not fail when the maximum number of calls is exceeded within an allow_n_plus_1_calls block' do
+ expect do
+ described_class.allow_n_plus_1_calls do
+ call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1)
+ end
+ end.not_to raise_error
+ end
+ end
+ end
+
+ describe 'get_request_count' do
+ context 'when RequestStore is enabled', :request_store do
+ context 'when enforce_gitaly_request_limits is called outside of allow_n_plus_1_calls blocks' do
+ before do
+ described_class.enforce_gitaly_request_limits(:call)
+ end
+
+ it 'counts gitaly calls' do
+ expect(described_class.get_request_count).to eq(1)
+ end
+ end
+
+ context 'when enforce_gitaly_request_limits is called inside and outside of allow_n_plus_1_calls blocks' do
+ before do
+ described_class.enforce_gitaly_request_limits(:call)
+ described_class.allow_n_plus_1_calls do
+ described_class.enforce_gitaly_request_limits(:call)
+ end
+ end
+
+ it 'counts gitaly calls' do
+ expect(described_class.get_request_count).to eq(2)
+ end
+ end
+
+ context 'when reset_counts is called' do
+ before do
+ described_class.enforce_gitaly_request_limits(:call)
+ described_class.reset_counts
+ end
+
+ it 'resets counts' do
+ expect(described_class.get_request_count).to eq(0)
+ end
+ end
+ end
+
+ context 'when RequestStore is not active' do
+ before do
+ described_class.enforce_gitaly_request_limits(:call)
+ end
+
+ it 'returns zero' do
+ expect(described_class.get_request_count).to eq(0)
+ end
+ end
+ end
+
describe 'feature_enabled?' do
let(:feature_name) { 'my_feature' }
let(:real_feature_name) { "gitaly_#{feature_name}" }
diff --git a/spec/lib/gitlab/group_hierarchy_spec.rb b/spec/lib/gitlab/group_hierarchy_spec.rb
index 08010c2d0e2..8dc83a6db7f 100644
--- a/spec/lib/gitlab/group_hierarchy_spec.rb
+++ b/spec/lib/gitlab/group_hierarchy_spec.rb
@@ -23,6 +23,11 @@ describe Gitlab::GroupHierarchy, :postgresql do
expect(relation).to include(parent, child1, child2)
end
+
+ it 'does not allow the use of #update_all' do
+ expect { relation.update_all(share_with_group_lock: false) }
+ .to raise_error(ActiveRecord::ReadOnlyRecord)
+ end
end
describe '#base_and_descendants' do
@@ -43,6 +48,11 @@ describe Gitlab::GroupHierarchy, :postgresql do
expect(relation).to include(parent, child1, child2)
end
+
+ it 'does not allow the use of #update_all' do
+ expect { relation.update_all(share_with_group_lock: false) }
+ .to raise_error(ActiveRecord::ReadOnlyRecord)
+ end
end
describe '#all_groups' do
@@ -73,5 +83,10 @@ describe Gitlab::GroupHierarchy, :postgresql do
expect(relation).to include(child2)
end
+
+ it 'does not allow the use of #update_all' do
+ expect { relation.update_all(share_with_group_lock: false) }
+ .to raise_error(ActiveRecord::ReadOnlyRecord)
+ end
end
end
diff --git a/spec/migrations/clean_stages_statuses_migration_spec.rb b/spec/migrations/clean_stages_statuses_migration_spec.rb
new file mode 100644
index 00000000000..38705f8eaae
--- /dev/null
+++ b/spec/migrations/clean_stages_statuses_migration_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20170912113435_clean_stages_statuses_migration.rb')
+
+describe CleanStagesStatusesMigration, :migration, :sidekiq, :redis do
+ let(:migration) { spy('migration') }
+
+ before do
+ allow(Gitlab::BackgroundMigration::MigrateStageStatus)
+ .to receive(:new).and_return(migration)
+ end
+
+ context 'when there are pending background migrations' do
+ it 'processes pending jobs synchronously' do
+ Sidekiq::Testing.disable! do
+ BackgroundMigrationWorker
+ .perform_in(2.minutes, 'MigrateStageStatus', [1, 1])
+ BackgroundMigrationWorker
+ .perform_async('MigrateStageStatus', [1, 1])
+
+ migrate!
+
+ expect(migration).to have_received(:perform).with(1, 1).twice
+ end
+ end
+ end
+
+ context 'when there are no background migrations pending' do
+ it 'does nothing' do
+ Sidekiq::Testing.disable! do
+ migrate!
+
+ expect(migration).not_to have_received(:perform)
+ end
+ end
+ end
+
+ context 'when there are still unmigrated stages afterwards' do
+ let(:stages) { table('ci_stages') }
+
+ before do
+ stages.create!(status: nil, name: 'build')
+ stages.create!(status: nil, name: 'test')
+ end
+
+ it 'migrates statuses sequentially in batches' do
+ migrate!
+
+ expect(migration).to have_received(:perform).once
+ end
+ end
+end
diff --git a/spec/migrations/delete_conflicting_redirect_routes_spec.rb b/spec/migrations/delete_conflicting_redirect_routes_spec.rb
new file mode 100644
index 00000000000..1df2477da51
--- /dev/null
+++ b/spec/migrations/delete_conflicting_redirect_routes_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170907170235_delete_conflicting_redirect_routes')
+
+describe DeleteConflictingRedirectRoutes, :migration, :sidekiq do
+ let!(:redirect_routes) { table(:redirect_routes) }
+ let!(:routes) { table(:routes) }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ before do
+ stub_const("DeleteConflictingRedirectRoutes::BATCH_SIZE", 2)
+ stub_const("Gitlab::Database::MigrationHelpers::BACKGROUND_MIGRATION_JOB_BUFFER_SIZE", 2)
+
+ routes.create!(id: 1, source_id: 1, source_type: 'Namespace', path: 'foo1')
+ routes.create!(id: 2, source_id: 2, source_type: 'Namespace', path: 'foo2')
+ routes.create!(id: 3, source_id: 3, source_type: 'Namespace', path: 'foo3')
+ routes.create!(id: 4, source_id: 4, source_type: 'Namespace', path: 'foo4')
+ routes.create!(id: 5, source_id: 5, source_type: 'Namespace', path: 'foo5')
+
+ # Valid redirects
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'bar2')
+ redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'bar3')
+
+ # Conflicting redirects
+ redirect_routes.create!(source_id: 2, source_type: 'Namespace', path: 'foo1')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo2')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo3')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo4')
+ redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo5')
+ end
+
+ it 'correctly schedules background migrations' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([described_class::MIGRATION, [1, 2]])
+ expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(12.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([described_class::MIGRATION, [3, 4]])
+ expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(24.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs[2]['args']).to eq([described_class::MIGRATION, [5, 5]])
+ expect(BackgroundMigrationWorker.jobs[2]['at']).to eq(36.seconds.from_now.to_f)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 3
+ end
+ end
+ end
+
+ it 'schedules background migrations' do
+ Sidekiq::Testing.inline! do
+ expect do
+ migrate!
+ end.to change { redirect_routes.count }.from(8).to(3)
+ end
+ end
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 95da97b7bc5..9c1e460ab20 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -26,6 +26,7 @@ describe Ci::Pipeline, :mailer do
it { is_expected.to respond_to :git_author_name }
it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :short_sha }
+ it { is_expected.to delegate_method(:full_path).to(:project).with_prefix }
describe '#source' do
context 'when creating new pipeline' do
@@ -1439,4 +1440,24 @@ describe Ci::Pipeline, :mailer do
it_behaves_like 'not sending any notification'
end
end
+
+ describe '#latest_builds_with_artifacts' do
+ let!(:pipeline) { create(:ci_pipeline, :success) }
+
+ let!(:build) do
+ create(:ci_build, :success, :artifacts, pipeline: pipeline)
+ end
+
+ it 'returns the latest builds' do
+ expect(pipeline.latest_builds_with_artifacts).to eq([build])
+ end
+
+ it 'memoizes the returned relation' do
+ query_count = ActiveRecord::QueryRecorder
+ .new { 2.times { pipeline.latest_builds_with_artifacts.to_a } }
+ .count
+
+ expect(query_count).to eq(1)
+ end
+ end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 11e64a0f877..e3cfa149e3a 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -207,11 +207,6 @@ eos
context 'of a merge commit' do
let(:repository) { project.repository }
- let(:commit_options) do
- author = repository.user_to_committer(user)
- { message: 'Test message', committer: author, author: author }
- end
-
let(:merge_request) do
create(:merge_request,
source_branch: 'video',
@@ -224,7 +219,7 @@ eos
merge_commit_id = repository.merge(user,
merge_request.diff_head_sha,
merge_request,
- commit_options)
+ 'Test message')
repository.commit(merge_commit_id)
end
diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb
index 9c99c3e5c08..fadc8bfeb61 100644
--- a/spec/models/gpg_key_spec.rb
+++ b/spec/models/gpg_key_spec.rb
@@ -140,18 +140,6 @@ describe GpgKey do
end
end
- describe 'notification', :mailer do
- let(:user) { create(:user) }
-
- it 'sends a notification' do
- perform_enqueued_jobs do
- create(:gpg_key, user: user)
- end
-
- should_email(user)
- end
- end
-
describe '#revoke' do
it 'invalidates all associated gpg signatures and destroys the key' do
gpg_key = create :gpg_key
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 96baeaff0a4..dbc4aba8547 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -169,16 +169,4 @@ describe Key, :mailer do
expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key)
end
end
-
- describe 'notification' do
- let(:user) { create(:user) }
-
- it 'sends a notification' do
- perform_enqueued_jobs do
- create(:key, user: user)
- end
-
- should_email(user)
- end
- end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index d3da0107d5c..13e37fffa4e 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -238,7 +238,7 @@ describe Milestone do
let(:milestone) { build_stubbed(:milestone, iid: 1, project: project, name: 'milestone') }
it 'returns a String reference to the object' do
- expect(milestone.to_reference).to eq '%1'
+ expect(milestone.to_reference).to eq '%"milestone"'
end
it 'returns a reference by name when the format is set to :name' do
@@ -246,24 +246,29 @@ describe Milestone do
end
it 'supports a cross-project reference' do
- expect(milestone.to_reference(another_project)).to eq 'sample-project%1'
+ expect(milestone.to_reference(another_project)).to eq 'sample-project%"milestone"'
end
end
context 'for a group milestone' do
let(:milestone) { build_stubbed(:milestone, iid: 1, group: group, name: 'milestone') }
- it 'returns nil with the default format' do
- expect(milestone.to_reference).to be_nil
+ it 'returns a group milestone reference with a default format' do
+ expect(milestone.to_reference).to eq '%"milestone"'
end
it 'returns a reference by name when the format is set to :name' do
expect(milestone.to_reference(format: :name)).to eq '%"milestone"'
end
- it 'does not supports cross-project references' do
+ it 'does supports cross-project references within a group' do
expect(milestone.to_reference(another_project, format: :name)).to eq '%"milestone"'
end
+
+ it 'raises an error when using iid format' do
+ expect { milestone.to_reference(format: :iid) }
+ .to raise_error(ArgumentError, 'Cannot refer to a group milestone by an internal id!')
+ end
end
end
diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb
index ca13af4d73e..12069575866 100644
--- a/spec/models/project_auto_devops_spec.rb
+++ b/spec/models/project_auto_devops_spec.rb
@@ -8,7 +8,21 @@ describe ProjectAutoDevops do
it { is_expected.to respond_to(:created_at) }
it { is_expected.to respond_to(:updated_at) }
- describe 'variables' do
+ describe '#has_domain?' do
+ context 'when domain is defined' do
+ let(:auto_devops) { build_stubbed(:project_auto_devops, project: project, domain: 'domain.com') }
+
+ it { expect(auto_devops).to have_domain }
+ end
+
+ context 'when domain is empty' do
+ let(:auto_devops) { build_stubbed(:project_auto_devops, project: project, domain: '') }
+
+ it { expect(auto_devops).not_to have_domain }
+ end
+ end
+
+ describe '#variables' do
let(:auto_devops) { build_stubbed(:project_auto_devops, project: project, domain: domain) }
context 'when domain is defined' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 48fc77423ff..78226c6c3fa 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -2682,4 +2682,60 @@ describe Project do
end
end
end
+
+ describe '#latest_successful_builds_for' do
+ let(:project) { build(:project) }
+
+ before do
+ allow(project).to receive(:default_branch).and_return('master')
+ end
+
+ context 'without a ref' do
+ it 'returns a pipeline for the default branch' do
+ expect(project)
+ .to receive(:latest_successful_pipeline_for_default_branch)
+
+ project.latest_successful_pipeline_for
+ end
+ end
+
+ context 'with the ref set to the default branch' do
+ it 'returns a pipeline for the default branch' do
+ expect(project)
+ .to receive(:latest_successful_pipeline_for_default_branch)
+
+ project.latest_successful_pipeline_for(project.default_branch)
+ end
+ end
+
+ context 'with a ref that is not the default branch' do
+ it 'returns the latest successful pipeline for the given ref' do
+ expect(project.pipelines).to receive(:latest_successful_for).with('foo')
+
+ project.latest_successful_pipeline_for('foo')
+ end
+ end
+ end
+
+ describe '#latest_successful_pipeline_for_default_branch' do
+ let(:project) { build(:project) }
+
+ before do
+ allow(project).to receive(:default_branch).and_return('master')
+ end
+
+ it 'memoizes and returns the latest successful pipeline for the default branch' do
+ pipeline = double(:pipeline)
+
+ expect(project.pipelines).to receive(:latest_successful_for)
+ .with(project.default_branch)
+ .and_return(pipeline)
+ .once
+
+ 2.times do
+ expect(project.latest_successful_pipeline_for_default_branch)
+ .to eq(pipeline)
+ end
+ end
+ end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 53280f2c1cf..76bb658b10d 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -10,10 +10,7 @@ describe Repository, models: true do
let(:user) { create(:user) }
let(:git_user) { Gitlab::Git::User.from_gitlab(user) }
- let(:commit_options) do
- author = repository.user_to_committer(user)
- { message: 'Test message', committer: author, author: author }
- end
+ let(:message) { 'Test message' }
let(:merge_commit) do
merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
@@ -21,7 +18,7 @@ describe Repository, models: true do
merge_commit_id = repository.merge(user,
merge_request.diff_head_sha,
merge_request,
- commit_options)
+ message)
repository.commit(merge_commit_id)
end
@@ -1287,10 +1284,7 @@ describe Repository, models: true do
describe '#merge' do
let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) }
- let(:commit_options) do
- author = repository.user_to_committer(user)
- { message: 'Test \r\n\r\n message', committer: author, author: author }
- end
+ let(:message) { 'Test \r\n\r\n message' }
it 'merges the code and returns the commit id' do
expect(merge_commit).to be_present
@@ -1298,43 +1292,44 @@ describe Repository, models: true do
end
it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
- merge_commit_id = merge(repository, user, merge_request, commit_options)
+ merge_commit_id = merge(repository, user, merge_request, message)
expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
end
it 'removes carriage returns from commit message' do
- merge_commit_id = merge(repository, user, merge_request, commit_options)
+ merge_commit_id = merge(repository, user, merge_request, message)
- expect(repository.commit(merge_commit_id).message).to eq(commit_options[:message].delete("\r"))
+ expect(repository.commit(merge_commit_id).message).to eq(message.delete("\r"))
end
- def merge(repository, user, merge_request, options = {})
- repository.merge(user, merge_request.diff_head_sha, merge_request, options)
+ def merge(repository, user, merge_request, message)
+ repository.merge(user, merge_request.diff_head_sha, merge_request, message)
end
end
describe '#revert' do
let(:new_image_commit) { repository.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') }
let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
+ let(:message) { 'revert message' }
context 'when there is a conflict' do
it 'raises an error' do
- expect { repository.revert(user, new_image_commit, 'master') }.to raise_error(/Failed to/)
+ expect { repository.revert(user, new_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
end
end
context 'when commit was already reverted' do
it 'raises an error' do
- repository.revert(user, update_image_commit, 'master')
+ repository.revert(user, update_image_commit, 'master', message)
- expect { repository.revert(user, update_image_commit, 'master') }.to raise_error(/Failed to/)
+ expect { repository.revert(user, update_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
end
end
context 'when commit can be reverted' do
it 'reverts the changes' do
- expect(repository.revert(user, update_image_commit, 'master')).to be_truthy
+ expect(repository.revert(user, update_image_commit, 'master', message)).to be_truthy
end
end
@@ -1343,7 +1338,7 @@ describe Repository, models: true do
merge_commit
expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).to be_present
- repository.revert(user, merge_commit, 'master')
+ repository.revert(user, merge_commit, 'master', message)
expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_to be_present
end
end
@@ -1353,24 +1348,25 @@ describe Repository, models: true do
let(:conflict_commit) { repository.commit('c642fe9b8b9f28f9225d7ea953fe14e74748d53b') }
let(:pickable_commit) { repository.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') }
+ let(:message) { 'cherry-pick message' }
context 'when there is a conflict' do
it 'raises an error' do
- expect { repository.cherry_pick(user, conflict_commit, 'master') }.to raise_error(/Failed to/)
+ expect { repository.cherry_pick(user, conflict_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
end
end
context 'when commit was already cherry-picked' do
it 'raises an error' do
- repository.cherry_pick(user, pickable_commit, 'master')
+ repository.cherry_pick(user, pickable_commit, 'master', message)
- expect { repository.cherry_pick(user, pickable_commit, 'master') }.to raise_error(/Failed to/)
+ expect { repository.cherry_pick(user, pickable_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
end
end
context 'when commit can be cherry-picked' do
it 'cherry-picks the changes' do
- expect(repository.cherry_pick(user, pickable_commit, 'master')).to be_truthy
+ expect(repository.cherry_pick(user, pickable_commit, 'master', message)).to be_truthy
end
end
@@ -1378,11 +1374,11 @@ describe Repository, models: true do
it 'cherry-picks the changes' do
expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).to be_nil
- cherry_pick_commit_sha = repository.cherry_pick(user, pickable_merge, 'improve/awesome')
+ cherry_pick_commit_sha = repository.cherry_pick(user, pickable_merge, 'improve/awesome', message)
cherry_pick_commit_message = project.commit(cherry_pick_commit_sha).message
expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).not_to be_nil
- expect(cherry_pick_commit_message).to include('cherry picked from')
+ expect(cherry_pick_commit_message).to eq(message)
end
end
end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index b186a78e44a..17dc3bb4f48 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -11,10 +11,11 @@ describe GroupPolicy do
let(:reporter_permissions) { [:admin_label] }
+ let(:developer_permissions) { [:admin_milestones] }
+
let(:master_permissions) do
[
- :create_projects,
- :admin_milestones
+ :create_projects
]
end
@@ -52,6 +53,7 @@ describe GroupPolicy do
it do
expect_allowed(:read_group)
expect_disallowed(*reporter_permissions)
+ expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
@@ -63,6 +65,7 @@ describe GroupPolicy do
it do
expect_allowed(:read_group)
expect_disallowed(*reporter_permissions)
+ expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
@@ -74,6 +77,7 @@ describe GroupPolicy do
it do
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
+ expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
@@ -85,6 +89,7 @@ describe GroupPolicy do
it do
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
+ expect_allowed(*developer_permissions)
expect_disallowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
@@ -96,6 +101,7 @@ describe GroupPolicy do
it do
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
+ expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
@@ -109,6 +115,7 @@ describe GroupPolicy do
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
+ expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
expect_allowed(*owner_permissions)
end
@@ -122,6 +129,7 @@ describe GroupPolicy do
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
+ expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
expect_allowed(*owner_permissions)
end
@@ -180,6 +188,7 @@ describe GroupPolicy do
it do
expect_disallowed(:read_group)
expect_disallowed(*reporter_permissions)
+ expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
@@ -191,6 +200,7 @@ describe GroupPolicy do
it do
expect_allowed(:read_group)
expect_disallowed(*reporter_permissions)
+ expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
@@ -202,6 +212,7 @@ describe GroupPolicy do
it do
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
+ expect_disallowed(*developer_permissions)
expect_disallowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
@@ -213,6 +224,7 @@ describe GroupPolicy do
it do
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
+ expect_allowed(*developer_permissions)
expect_disallowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
@@ -224,6 +236,7 @@ describe GroupPolicy do
it do
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
+ expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
expect_disallowed(*owner_permissions)
end
@@ -237,6 +250,7 @@ describe GroupPolicy do
expect_allowed(:read_group)
expect_allowed(*reporter_permissions)
+ expect_allowed(*developer_permissions)
expect_allowed(*master_permissions)
expect_allowed(*owner_permissions)
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 4dbaf7fb025..c0cbdeed03d 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -33,7 +33,7 @@ describe ProjectPolicy do
let(:developer_permissions) do
%i[
- admin_merge_request update_merge_request create_commit_status
+ admin_milestone admin_merge_request update_merge_request create_commit_status
update_commit_status create_build update_build create_pipeline
update_pipeline create_merge_request create_wiki push_code
resolve_note create_container_image update_container_image
@@ -44,7 +44,7 @@ describe ProjectPolicy do
let(:master_permissions) do
%i[
delete_protected_branch update_project_snippet update_environment
- update_deployment admin_milestone admin_project_snippet
+ update_deployment admin_project_snippet
admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index 6bd17697c33..50d0f72f6bc 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -1,12 +1,12 @@
require 'spec_helper'
describe API::AccessRequests do
- let(:master) { create(:user) }
- let(:developer) { create(:user) }
- let(:access_requester) { create(:user) }
- let(:stranger) { create(:user) }
+ set(:master) { create(:user) }
+ set(:developer) { create(:user) }
+ set(:access_requester) { create(:user) }
+ set(:stranger) { create(:user) }
- let(:project) do
+ set(:project) do
create(:project, :public, :access_requestable, creator_id: master.id, namespace: master.namespace) do |project|
project.team << [developer, :developer]
project.team << [master, :master]
@@ -14,7 +14,7 @@ describe API::AccessRequests do
end
end
- let(:group) do
+ set(:group) do
create(:group, :public, :access_requestable) do |group|
group.add_developer(developer)
group.add_owner(master)
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 593068b8cd7..7a0765c1fae 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
describe API::AwardEmoji do
- let(:user) { create(:user) }
- let!(:project) { create(:project) }
- let(:issue) { create(:issue, project: project) }
- let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) }
- let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
- let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) }
- let!(:note) { create(:note, project: project, noteable: issue) }
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+ set(:issue) { create(:issue, project: project) }
+ set(:award_emoji) { create(:award_emoji, awardable: issue, user: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) }
+ set(:note) { create(:note, project: project, noteable: issue) }
before do
project.team << [user, :master]
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index f698d5dddb3..fcfa4ddfbfe 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -1,34 +1,34 @@
require 'spec_helper'
describe API::Boards do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:non_member) { create(:user) }
- let(:guest) { create(:user) }
- let(:admin) { create(:user, :admin) }
- let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
-
- let!(:dev_label) do
+ set(:user) { create(:user) }
+ set(:user2) { create(:user) }
+ set(:non_member) { create(:user) }
+ set(:guest) { create(:user) }
+ set(:admin) { create(:user, :admin) }
+ set(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
+
+ set(:dev_label) do
create(:label, title: 'Development', color: '#FFAABB', project: project)
end
- let!(:test_label) do
+ set(:test_label) do
create(:label, title: 'Testing', color: '#FFAACC', project: project)
end
- let!(:ux_label) do
+ set(:ux_label) do
create(:label, title: 'UX', color: '#FF0000', project: project)
end
- let!(:dev_list) do
+ set(:dev_list) do
create(:list, label: dev_label, position: 1)
end
- let!(:test_list) do
+ set(:test_list) do
create(:list, label: test_label, position: 2)
end
- let!(:board) do
+ set(:board) do
create(:board, project: project, lists: [dev_list, test_list])
end
@@ -187,8 +187,11 @@ describe API::Boards do
end
context "when the user is project owner" do
- let(:owner) { create(:user) }
- let(:project) { create(:project, namespace: owner.namespace) }
+ set(:owner) { create(:user) }
+
+ before do
+ project.update(namespace: owner.namespace)
+ end
it "deletes the list if an admin requests it" do
delete api("#{base_url}/#{dev_list.id}", owner)
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index cc794fad3a7..16b12446ed4 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
describe API::Branches do
- let(:user) { create(:user) }
- let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ set(:user) { create(:user) }
let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+ let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
let(:branch_name) { 'feature' }
let(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
let(:branch_with_dot) { project.repository.find_branch('ends-with.json') }
@@ -40,7 +40,9 @@ describe API::Branches do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ before do
+ project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
it_behaves_like 'repository branches'
end
@@ -118,7 +120,9 @@ describe API::Branches do
end
context 'when unauthenticated', 'and project is public' do
- let(:project) { create(:project, :public, :repository) }
+ before do
+ project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
it_behaves_like 'repository branch'
end
diff --git a/spec/requests/api/broadcast_messages_spec.rb b/spec/requests/api/broadcast_messages_spec.rb
index b043a333d33..eacc575d97f 100644
--- a/spec/requests/api/broadcast_messages_spec.rb
+++ b/spec/requests/api/broadcast_messages_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
describe API::BroadcastMessages do
- let(:user) { create(:user) }
- let(:admin) { create(:admin) }
+ set(:user) { create(:user) }
+ set(:admin) { create(:admin) }
+ set(:message) { create(:broadcast_message) }
describe 'GET /broadcast_messages' do
it 'returns a 401 for anonymous users' do
@@ -31,8 +32,6 @@ describe API::BroadcastMessages do
end
describe 'GET /broadcast_messages/:id' do
- let!(:message) { create(:broadcast_message) }
-
it 'returns a 401 for anonymous users' do
get api("/broadcast_messages/#{message.id}")
@@ -103,8 +102,6 @@ describe API::BroadcastMessages do
end
describe 'PUT /broadcast_messages/:id' do
- let!(:message) { create(:broadcast_message) }
-
it 'returns a 401 for anonymous users' do
put api("/broadcast_messages/#{message.id}"),
attributes_for(:broadcast_message)
@@ -155,8 +152,6 @@ describe API::BroadcastMessages do
end
describe 'DELETE /broadcast_messages/:id' do
- let!(:message) { create(:broadcast_message) }
-
it 'returns a 401 for anonymous users' do
delete api("/broadcast_messages/#{message.id}"),
attributes_for(:broadcast_message)
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 42f0079e173..1671a046fdf 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -159,11 +159,14 @@ describe API::Groups do
context 'when using owned in the request' do
it 'returns an array of groups the user owns' do
+ group1.add_master(user2)
+
get api('/groups', user2), owned: true
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
expect(json_response.first['name']).to eq(group2.name)
end
end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 1583d1c2435..972e57bc373 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -22,7 +22,8 @@ describe API::Issues, :mailer do
state: :closed,
milestone: milestone,
created_at: generate(:past_time),
- updated_at: 3.hours.ago
+ updated_at: 3.hours.ago,
+ closed_at: 1.hour.ago
end
let!(:confidential_issue) do
create :issue,
@@ -738,6 +739,7 @@ describe API::Issues, :mailer do
expect(json_response['title']).to eq(issue.title)
expect(json_response['description']).to eq(issue.description)
expect(json_response['state']).to eq(issue.state)
+ expect(json_response['closed_at']).to be_falsy
expect(json_response['created_at']).to be_present
expect(json_response['updated_at']).to be_present
expect(json_response['labels']).to eq(issue.label_names)
@@ -748,6 +750,13 @@ describe API::Issues, :mailer do
expect(json_response['confidential']).to be_falsy
end
+ it "exposes the 'closed_at' attribute" do
+ get api("/projects/#{project.id}/issues/#{closed_issue.iid}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['closed_at']).to be_present
+ end
+
context 'links exposure' do
it 'exposes related resources full URIs' do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 21d2c9644fb..c4f6e97b915 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -28,10 +28,29 @@ describe API::MergeRequests do
describe 'GET /merge_requests' do
context 'when unauthenticated' do
- it 'returns authentication error' do
- get api('/merge_requests')
+ it 'returns an array of all merge requests' do
+ get api('/merge_requests', user), scope: 'all'
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ end
+
+ it "returns authentication error without any scope" do
+ get api("/merge_requests")
+
+ expect(response).to have_http_status(401)
+ end
+
+ it "returns authentication error when scope is assigned-to-me" do
+ get api("/merge_requests"), scope: 'assigned-to-me'
- expect(response).to have_gitlab_http_status(401)
+ expect(response).to have_http_status(401)
+ end
+
+ it "returns authentication error when scope is created-by-me" do
+ get api("/merge_requests"), scope: 'created-by-me'
+
+ expect(response).to have_http_status(401)
end
end
@@ -134,10 +153,18 @@ describe API::MergeRequests do
describe "GET /projects/:id/merge_requests" do
context "when unauthenticated" do
- it "returns authentication error" do
+ it 'returns merge requests for public projects' do
+ get api("/projects/#{project.id}/merge_requests")
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ end
+
+ it "returns 404 for non public projects" do
+ project = create(:project, :private)
get api("/projects/#{project.id}/merge_requests")
- expect(response).to have_gitlab_http_status(401)
+ expect(response).to have_http_status(404)
end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 92e7d797cbd..508df990952 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -1181,6 +1181,59 @@ describe API::Projects do
end
end
end
+
+ describe 'GET /projects/:id/forks' do
+ let(:private_fork) { create(:project, :private, :empty_repo) }
+ let(:member) { create(:user) }
+ let(:non_member) { create(:user) }
+
+ before do
+ private_fork.add_developer(member)
+ end
+
+ context 'for a forked project' do
+ before do
+ post api("/projects/#{private_fork.id}/fork/#{project_fork_source.id}", admin)
+ private_fork.reload
+ expect(private_fork.forked_from_project).not_to be_nil
+ expect(private_fork.forked?).to be_truthy
+ project_fork_source.reload
+ expect(project_fork_source.forks.length).to eq(1)
+ expect(project_fork_source.forks).to include(private_fork)
+ end
+
+ context 'for a user that can access the forks' do
+ it 'returns the forks' do
+ get api("/projects/#{project_fork_source.id}/forks", member)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response.length).to eq(1)
+ expect(json_response[0]['name']).to eq(private_fork.name)
+ end
+ end
+
+ context 'for a user that cannot access the forks' do
+ it 'returns an empty array' do
+ get api("/projects/#{project_fork_source.id}/forks", non_member)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response.length).to eq(0)
+ end
+ end
+ end
+
+ context 'for a non-forked project' do
+ it 'returns an empty array' do
+ get api("/projects/#{project_fork_source.id}/forks")
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response.length).to eq(0)
+ end
+ end
+ end
end
describe "POST /projects/:id/share" do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 37cb95a16e3..5b306ec6cbf 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -23,8 +23,7 @@ describe API::Users do
it "returns the user when a valid `username` parameter is passed" do
get api("/users"), username: user.username
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
+ expect(response).to match_response_schema('public_api/v4/user/basics')
expect(json_response.size).to eq(1)
expect(json_response[0]['id']).to eq(user.id)
expect(json_response[0]['username']).to eq(user.username)
@@ -68,7 +67,7 @@ describe API::Users do
it "renders 200" do
get api("/users", user)
- expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/basics')
end
end
@@ -76,7 +75,7 @@ describe API::Users do
it "renders 200" do
get api("/users", admin)
- expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/basics')
end
end
end
@@ -84,9 +83,8 @@ describe API::Users do
it "returns an array of users" do
get api("/users", user)
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/basics')
expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
username = user.username
expect(json_response.detect do |user|
user['username'] == username
@@ -99,18 +97,16 @@ describe API::Users do
get api("/users?blocked=true", user)
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/basics')
expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
expect(json_response).to all(include('state' => /(blocked|ldap_blocked)/))
end
it "returns one user" do
get api("/users?username=#{omniauth_user.username}", user)
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/basics')
expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
expect(json_response.first['username']).to eq(omniauth_user.username)
end
@@ -123,6 +119,7 @@ describe API::Users do
it 'does not reveal the `is_admin` flag of the user' do
get api('/users', user)
+ expect(response).to match_response_schema('public_api/v4/user/basics')
expect(json_response.first.keys).not_to include 'is_admin'
end
end
@@ -131,17 +128,8 @@ describe API::Users do
it "returns an array of users" do
get api("/users", admin)
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/admins')
expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.first.keys).to include 'email'
- expect(json_response.first.keys).to include 'organization'
- expect(json_response.first.keys).to include 'identities'
- expect(json_response.first.keys).to include 'can_create_project'
- expect(json_response.first.keys).to include 'two_factor_enabled'
- expect(json_response.first.keys).to include 'last_sign_in_at'
- expect(json_response.first.keys).to include 'confirmed_at'
- expect(json_response.first.keys).to include 'is_admin'
end
it "returns an array of external users" do
@@ -149,17 +137,15 @@ describe API::Users do
get api("/users?external=true", admin)
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/admins')
expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
expect(json_response).to all(include('external' => true))
end
it "returns one user by external UID" do
get api("/users?extern_uid=#{omniauth_user.identities.first.extern_uid}&provider=#{omniauth_user.identities.first.provider}", admin)
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
+ expect(response).to match_response_schema('public_api/v4/user/admins')
expect(json_response.size).to eq(1)
expect(json_response.first['username']).to eq(omniauth_user.username)
end
@@ -181,7 +167,7 @@ describe API::Users do
get api("/users?created_before=2000-01-02T00:00:00.060Z", admin)
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/admins')
expect(json_response.size).to eq(1)
expect(json_response.first['username']).to eq(user.username)
end
@@ -191,7 +177,7 @@ describe API::Users do
get api("/users?created_before=2000-01-02T00:00:00.060Z", admin)
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/admins')
expect(json_response.size).to eq(0)
end
@@ -200,7 +186,7 @@ describe API::Users do
get api("/users?created_before=2001-01-02T00:00:00.060Z&created_after=1999-01-02T00:00:00.060", admin)
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/admins')
expect(json_response.size).to eq(1)
expect(json_response.first['username']).to eq(user.username)
end
@@ -211,22 +197,22 @@ describe API::Users do
it "returns a user by id" do
get api("/users/#{user.id}", user)
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/basic')
expect(json_response['username']).to eq(user.username)
end
it "does not return the user's `is_admin` flag" do
get api("/users/#{user.id}", user)
- expect(response).to have_http_status(200)
- expect(json_response['is_admin']).to be_nil
+ expect(response).to match_response_schema('public_api/v4/user/basic')
+ expect(json_response.keys).not_to include 'is_admin'
end
context 'when authenticated as admin' do
it 'includes the `is_admin` field' do
get api("/users/#{user.id}", admin)
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/admin')
expect(json_response['is_admin']).to be(false)
end
end
@@ -235,7 +221,7 @@ describe API::Users do
it "returns a user by id" do
get api("/users/#{user.id}")
- expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/user/basic')
expect(json_response['username']).to eq(user.username)
end
@@ -251,6 +237,7 @@ describe API::Users do
it "returns a 404 error if user id not found" do
get api("/users/9999", user)
+
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
diff --git a/spec/requests/api/v3/award_emoji_spec.rb b/spec/requests/api/v3/award_emoji_spec.rb
index 681e8e04295..36d793f505d 100644
--- a/spec/requests/api/v3/award_emoji_spec.rb
+++ b/spec/requests/api/v3/award_emoji_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
describe API::V3::AwardEmoji do
- let(:user) { create(:user) }
- let!(:project) { create(:project) }
- let(:issue) { create(:issue, project: project) }
- let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) }
- let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
- let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) }
- let!(:note) { create(:note, project: project, noteable: issue) }
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+ set(:issue) { create(:issue, project: project) }
+ set(:award_emoji) { create(:award_emoji, awardable: issue, user: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) }
+ set(:note) { create(:note, project: project, noteable: issue) }
before { project.team << [user, :master] }
diff --git a/spec/requests/api/v3/boards_spec.rb b/spec/requests/api/v3/boards_spec.rb
index b86aab2ec70..ea2627142bf 100644
--- a/spec/requests/api/v3/boards_spec.rb
+++ b/spec/requests/api/v3/boards_spec.rb
@@ -1,28 +1,28 @@
require 'spec_helper'
describe API::V3::Boards do
- let(:user) { create(:user) }
- let(:guest) { create(:user) }
- let(:non_member) { create(:user) }
- let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
+ set(:user) { create(:user) }
+ set(:guest) { create(:user) }
+ set(:non_member) { create(:user) }
+ set(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
- let!(:dev_label) do
+ set(:dev_label) do
create(:label, title: 'Development', color: '#FFAABB', project: project)
end
- let!(:test_label) do
+ set(:test_label) do
create(:label, title: 'Testing', color: '#FFAACC', project: project)
end
- let!(:dev_list) do
+ set(:dev_list) do
create(:list, label: dev_label, position: 1)
end
- let!(:test_list) do
+ set(:test_list) do
create(:list, label: test_label, position: 2)
end
- let!(:board) do
+ set(:board) do
create(:board, project: project, lists: [dev_list, test_list])
end
@@ -98,8 +98,11 @@ describe API::V3::Boards do
end
context "when the user is project owner" do
- let(:owner) { create(:user) }
- let(:project) { create(:project, namespace: owner.namespace) }
+ set(:owner) { create(:user) }
+
+ before do
+ project.update(namespace: owner.namespace)
+ end
it "deletes the list if an admin requests it" do
delete v3_api("#{base_url}/#{dev_list.id}", owner)
diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb
index c88f7788697..9cd11a67712 100644
--- a/spec/requests/api/v3/branches_spec.rb
+++ b/spec/requests/api/v3/branches_spec.rb
@@ -2,11 +2,11 @@ require 'spec_helper'
require 'mime/types'
describe API::V3::Branches do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let!(:project) { create(:project, :repository, creator: user) }
- let!(:master) { create(:project_member, :master, user: user, project: project) }
- let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
+ set(:user) { create(:user) }
+ set(:user2) { create(:user) }
+ set(:project) { create(:project, :repository, creator: user) }
+ set(:master) { create(:project_member, :master, user: user, project: project) }
+ set(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:branch_name) { 'feature' }
let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") }
diff --git a/spec/requests/api/v3/broadcast_messages_spec.rb b/spec/requests/api/v3/broadcast_messages_spec.rb
index 948cd78c177..d04b1c72004 100644
--- a/spec/requests/api/v3/broadcast_messages_spec.rb
+++ b/spec/requests/api/v3/broadcast_messages_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'
describe API::V3::BroadcastMessages do
- let(:user) { create(:user) }
- let(:admin) { create(:admin) }
+ set(:user) { create(:user) }
+ set(:admin) { create(:admin) }
describe 'DELETE /broadcast_messages/:id' do
- let!(:message) { create(:broadcast_message) }
+ set(:message) { create(:broadcast_message) }
it 'returns a 401 for anonymous users' do
delete v3_api("/broadcast_messages/#{message.id}"),
diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb
index dc95599546c..0a2ff1058e3 100644
--- a/spec/requests/api/v3/builds_spec.rb
+++ b/spec/requests/api/v3/builds_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
describe API::V3::Builds do
- let(:user) { create(:user) }
+ set(:user) { create(:user) }
let(:api_user) { user }
- let!(:project) { create(:project, :repository, creator: user, public_builds: false) }
- let!(:developer) { create(:project_member, :developer, user: user, project: project) }
- let(:reporter) { create(:project_member, :reporter, project: project) }
- let(:guest) { create(:project_member, :guest, project: project) }
- let!(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
+ set(:project) { create(:project, :repository, creator: user, public_builds: false) }
+ set(:developer) { create(:project_member, :developer, user: user, project: project) }
+ set(:reporter) { create(:project_member, :reporter, project: project) }
+ set(:guest) { create(:project_member, :guest, project: project) }
+ set(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
let!(:build) { create(:ci_build, pipeline: pipeline) }
describe 'GET /projects/:id/builds ' do
diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb
index 9a0e6647ebf..86768d7397a 100644
--- a/spec/requests/api/v3/issues_spec.rb
+++ b/spec/requests/api/v3/issues_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
describe API::V3::Issues, :mailer do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:non_member) { create(:user) }
- let(:guest) { create(:user) }
- let(:author) { create(:author) }
- let(:assignee) { create(:assignee) }
- let(:admin) { create(:user, :admin) }
+ set(:user) { create(:user) }
+ set(:user2) { create(:user) }
+ set(:non_member) { create(:user) }
+ set(:guest) { create(:user) }
+ set(:author) { create(:author) }
+ set(:assignee) { create(:assignee) }
+ set(:admin) { create(:user, :admin) }
let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
let!(:closed_issue) do
create :closed_issue,
@@ -822,7 +822,8 @@ describe API::V3::Issues, :mailer do
end
context 'resolving issues in a merge request' do
- let(:discussion) { create(:diff_note_on_merge_request).to_discussion }
+ set(:diff_note_on_merge_request) { create(:diff_note_on_merge_request) }
+ let(:discussion) { diff_note_on_merge_request.to_discussion }
let(:merge_request) { discussion.noteable }
let(:project) { merge_request.source_project }
before do
@@ -1169,7 +1170,7 @@ describe API::V3::Issues, :mailer do
end
context "when the user is project owner" do
- let(:owner) { create(:user) }
+ set(:owner) { create(:user) }
let(:project) { create(:project, namespace: owner.namespace) }
it "deletes the issue if an admin requests it" do
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index 2de8daba6b5..3baf9b1edab 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -103,9 +103,15 @@ describe PipelineSerializer do
let(:project) { create(:project) }
before do
- Ci::Pipeline::AVAILABLE_STATUSES.each do |status|
- create_pipeline(status)
+ # Since RequestStore.active? is true we have to allow the
+ # gitaly calls in this block
+ # Issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/37772
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ Ci::Pipeline::AVAILABLE_STATUSES.each do |status|
+ create_pipeline(status)
+ end
end
+ Gitlab::GitalyClient.reset_counts
end
shared_examples 'no N+1 queries' do
diff --git a/spec/services/deploy_keys/create_service_spec.rb b/spec/services/deploy_keys/create_service_spec.rb
new file mode 100644
index 00000000000..7a604c0cadd
--- /dev/null
+++ b/spec/services/deploy_keys/create_service_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe DeployKeys::CreateService do
+ let(:user) { create(:user) }
+ let(:params) { attributes_for(:deploy_key) }
+
+ subject { described_class.new(user, params) }
+
+ it "creates a deploy key" do
+ expect { subject.execute }.to change { DeployKey.where(params.merge(user: user)).count }.by(1)
+ end
+end
diff --git a/spec/services/gpg_keys/create_service_spec.rb b/spec/services/gpg_keys/create_service_spec.rb
new file mode 100644
index 00000000000..20382a3a618
--- /dev/null
+++ b/spec/services/gpg_keys/create_service_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe GpgKeys::CreateService do
+ let(:user) { create(:user) }
+ let(:params) { attributes_for(:gpg_key) }
+
+ subject { described_class.new(user, params) }
+
+ context 'notification', :mailer do
+ it 'sends a notification' do
+ perform_enqueued_jobs do
+ subject.execute
+ end
+ should_email(user)
+ end
+ end
+
+ it 'creates a gpg key' do
+ expect { subject.execute }.to change { user.gpg_keys.where(params).count }.by(1)
+ end
+end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 171f70c32a8..5c27e8fd561 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -42,7 +42,7 @@ describe Issues::CloseService do
service.execute(issue)
end
- it 'refreshes the number of open issues' do
+ it 'refreshes the number of open issues', :use_clean_rails_memory_store_caching do
expect { service.execute(issue) }
.to change { project.open_issues_count }.from(1).to(0)
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index cc3d648c340..d86da244520 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -35,7 +35,7 @@ describe Issues::CreateService do
expect(issue.due_date).to eq Date.tomorrow
end
- it 'refreshes the number of open issues' do
+ it 'refreshes the number of open issues', :use_clean_rails_memory_store_caching do
expect { issue }.to change { project.open_issues_count }.from(0).to(1)
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 15a50b85f19..a8a8aeed1bd 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -64,6 +64,13 @@ describe Issues::UpdateService, :mailer do
expect(issue.due_date).to eq Date.tomorrow
end
+ it 'refreshes the number of open issues when the issue is made confidential', :use_clean_rails_memory_store_caching do
+ issue # make sure the issue is created first so our counts are correct.
+
+ expect { update_issue(confidential: true) }
+ .to change { project.open_issues_count }.from(1).to(0)
+ end
+
it 'updates open issue counter for assignees when issue is reassigned' do
update_issue(assignee_ids: [user2.id])
diff --git a/spec/services/keys/create_service_spec.rb b/spec/services/keys/create_service_spec.rb
new file mode 100644
index 00000000000..bcb436c1e46
--- /dev/null
+++ b/spec/services/keys/create_service_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Keys::CreateService do
+ let(:user) { create(:user) }
+ let(:params) { attributes_for(:key) }
+
+ subject { described_class.new(user, params) }
+
+ context 'notification', :mailer do
+ it 'sends a notification' do
+ perform_enqueued_jobs do
+ subject.execute
+ end
+ should_email(user)
+ end
+ end
+
+ it 'creates a key' do
+ expect { subject.execute }.to change { user.keys.where(params).count }.by(1)
+ end
+end
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index 7e65369762c..b3886987316 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -52,7 +52,7 @@ describe MergeRequests::CloseService do
end
end
- it 'refreshes the number of open merge requests for a valid MR' do
+ it 'refreshes the number of open merge requests for a valid MR', :use_clean_rails_memory_store_caching do
service = described_class.new(project, user, {})
expect { service.execute(merge_request) }
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index d6409c0d625..a047f891ab2 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -37,7 +37,7 @@ describe MergeRequests::CreateService do
expect(service).to have_received(:execute_hooks).with(merge_request)
end
- it 'refreshes the number of open merge requests' do
+ it 'refreshes the number of open merge requests', :use_clean_rails_memory_store_caching do
expect { service.execute }
.to change { project.open_merge_requests_count }.from(0).to(1)
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 2af2485eeed..64e676f22a0 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -150,9 +150,7 @@ describe MergeRequests::RefreshService do
context 'manual merge of source branch' do
before do
# Merge master -> feature branch
- author = { email: 'test@gitlab.com', time: Time.now, name: "Me" }
- commit_options = { message: 'Test message', committer: author, author: author }
- @project.repository.merge(@user, @merge_request.diff_head_sha, @merge_request, commit_options)
+ @project.repository.merge(@user, @merge_request.diff_head_sha, @merge_request, 'Test message')
commit = @project.repository.commit('feature')
service.new(@project, @user).execute(@oldrev, commit.id, 'refs/heads/feature')
reload_mrs
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 3e493148b32..f4b36eb7eeb 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -84,7 +84,6 @@ describe NotificationService, :mailer do
let!(:key) { create(:personal_key, key_options) }
it { expect(notification.new_key(key)).to be_truthy }
- it { should_email(key.user) }
describe 'never emails the ghost user' do
let(:key_options) { { user: User.ghost } }
diff --git a/spec/services/projects/count_service_spec.rb b/spec/services/projects/count_service_spec.rb
index 79b01e7620e..cc496501bad 100644
--- a/spec/services/projects/count_service_spec.rb
+++ b/spec/services/projects/count_service_spec.rb
@@ -66,8 +66,8 @@ describe Projects::CountService do
describe '#cache_key' do
it 'returns the cache key as an Array' do
- allow(service).to receive(:cache_key_name).and_return('count_service')
- expect(service.cache_key).to eq(['projects', 1, 'count_service'])
+ allow(service).to receive(:cache_key_name).and_return('foo')
+ expect(service.cache_key).to eq(['projects', 'count_service', described_class::VERSION, 1, 'foo'])
end
end
end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 92cc9a37795..c551083ac90 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -57,6 +57,21 @@ describe Projects::UpdateService, '#execute' do
end
end
end
+
+ context 'When project visibility is higher than parent group' do
+ let(:group) { create(:group, visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
+
+ before do
+ project.update(namespace: group, visibility_level: group.visibility_level)
+ end
+
+ it 'does not update project visibility level' do
+ result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+
+ expect(result).to eq({ status: :error, message: 'Visibility level public is not allowed in a internal group.' })
+ expect(project.reload).to be_internal
+ end
+ end
end
describe 'when updating project that has forks' do
@@ -148,7 +163,7 @@ describe Projects::UpdateService, '#execute' do
result = update_project(project, admin, path: 'existing')
expect(result).to include(status: :error)
- expect(result[:message]).to match('Project could not be updated!')
+ expect(result[:message]).to match('There is already a repository with that name on disk')
expect(project).not_to be_valid
expect(project.errors.messages).to have_key(:base)
expect(project.errors.messages[:base]).to include('There is already a repository with that name on disk')
@@ -159,8 +174,10 @@ describe Projects::UpdateService, '#execute' do
it 'returns an error result when record cannot be updated' do
result = update_project(project, admin, { name: 'foo&bar' })
- expect(result).to eq({ status: :error,
- message: 'Project could not be updated!' })
+ expect(result).to eq({
+ status: :error,
+ message: "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'."
+ })
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index c2d6d7781b9..b1241cd8d0b 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -232,7 +232,9 @@ describe SystemNoteService do
context 'when milestone added' do
it 'sets the note text' do
- expect(subject.note).to eq "changed milestone to #{milestone.to_reference}"
+ reference = milestone.to_reference(format: :iid)
+
+ expect(subject.note).to eq "changed milestone to #{reference}"
end
end
diff --git a/spec/support/matchers/navigation_matcher.rb b/spec/support/matchers/navigation_matcher.rb
new file mode 100644
index 00000000000..5b6d9c1a4df
--- /dev/null
+++ b/spec/support/matchers/navigation_matcher.rb
@@ -0,0 +1,6 @@
+RSpec::Matchers.define :have_active_navigation do |expected|
+ match do |page|
+ expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1)
+ expect(page.find('.sidebar-top-level-items > li.active')).to have_content(expected)
+ end
+end
diff --git a/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb b/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb
new file mode 100644
index 00000000000..c92c7f603d6
--- /dev/null
+++ b/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb
@@ -0,0 +1,21 @@
+shared_examples 'issuable user dropdown behaviors' do
+ include FilteredSearchHelpers
+
+ before do
+ issuable # ensure we have at least one issuable
+ sign_in(user_in_dropdown)
+ end
+
+ %w[author assignee].each do |dropdown|
+ describe "#{dropdown} dropdown", :js do
+ it 'only includes members of the project/group' do
+ visit issuables_path
+
+ filtered_search.set("#{dropdown}:")
+
+ expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).to have_content(user_in_dropdown.name)
+ expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).not_to have_content(user_not_in_dropdown.name)
+ end
+ end
+ end
+end
diff --git a/spec/support/project_features_apply_to_issuables_shared_examples.rb b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb
index 639b0924197..639b0924197 100644
--- a/spec/support/project_features_apply_to_issuables_shared_examples.rb
+++ b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb
diff --git a/spec/support/shared_examples/features/search_shared_examples.rb b/spec/support/shared_examples/features/search_shared_examples.rb
new file mode 100644
index 00000000000..25ebbf011d5
--- /dev/null
+++ b/spec/support/shared_examples/features/search_shared_examples.rb
@@ -0,0 +1,5 @@
+shared_examples 'top right search form' do
+ it 'does not show top right search form' do
+ expect(page).not_to have_selector('.search')
+ end
+end
diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb
index 9695f35bd25..78a2ff73746 100644
--- a/spec/support/stub_gitlab_calls.rb
+++ b/spec/support/stub_gitlab_calls.rb
@@ -26,11 +26,9 @@ module StubGitlabCalls
end
def stub_container_registry_config(registry_settings)
+ allow(Gitlab.config.registry).to receive_messages(registry_settings)
allow(Auth::ContainerRegistryAuthenticationService)
.to receive(:full_access_token).and_return('token')
-
- allow(Gitlab.config.registry).to receive_messages(registry_settings)
- load 'lib/gitlab/auth.rb'
end
def stub_container_registry_tags(repository: :any, tags:)
diff --git a/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb b/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb
new file mode 100644
index 00000000000..c757ccf02d3
--- /dev/null
+++ b/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe 'projects/pipelines_settings/_show' do
+ let(:project) { create(:project, :repository) }
+
+ before do
+ assign :project, project
+ end
+
+ context 'when kubernetes is not active' do
+ context 'when auto devops domain is not defined' do
+ it 'shows warning message' do
+ render
+
+ expect(rendered).to have_css('.settings-message')
+ expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name and the')
+ expect(rendered).to have_link('Kubernetes service')
+ end
+ end
+
+ context 'when auto devops domain is defined' do
+ before do
+ project.build_auto_devops(domain: 'example.com')
+ end
+
+ it 'shows warning message' do
+ render
+
+ expect(rendered).to have_css('.settings-message')
+ expect(rendered).to have_text('Auto Review Apps and Auto Deploy need the')
+ expect(rendered).to have_link('Kubernetes service')
+ end
+ end
+ end
+
+ context 'when kubernetes is active' do
+ before do
+ project.build_kubernetes_service(active: true)
+ end
+
+ context 'when auto devops domain is not defined' do
+ it 'shows warning message' do
+ render
+
+ expect(rendered).to have_css('.settings-message')
+ expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name to work correctly.')
+ end
+ end
+
+ context 'when auto devops domain is defined' do
+ before do
+ project.build_auto_devops(domain: 'example.com')
+ end
+
+ it 'does not show warning message' do
+ render
+
+ expect(rendered).not_to have_css('.settings-message')
+ end
+ end
+ end
+end
diff --git a/spec/views/shared/milestones/_issuable.html.haml.rb b/spec/views/shared/milestones/_issuable.html.haml.rb
new file mode 100644
index 00000000000..0a3f877cae0
--- /dev/null
+++ b/spec/views/shared/milestones/_issuable.html.haml.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe 'shared/milestones/_issuable.html.haml' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:milestone) { create(:milestone, project: project) }
+ let(:issuable) { create(:issue, project: project, assignees: [user]) }
+
+ before do
+ assign(:project, project)
+ assign(:milestone, milestone)
+ end
+
+ it 'avatar links to issues page' do
+ render 'shared/milestones/issuable', issuable: issuable, show_project_name: true
+
+ expect(rendered).to have_css("a[href='#{project_issues_path(project, milestone_title: milestone.title, assignee_id: user.id, state: 'all')}']")
+ end
+end
diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
index a07edd264c3..c93e6567baf 100644
--- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
@@ -271,10 +271,6 @@ production:
--version="$CI_PIPELINE_ID-$CI_JOB_ID" \
"$name" \
chart/
-
- if [[ "$track" == "stable" ]]; then
- kubectl rollout status -n "$KUBE_NAMESPACE" -w "deployment/${CI_ENVIRONMENT_SLUG}-auto-deploy"
- fi
}
function install_dependencies() {
diff --git a/yarn.lock b/yarn.lock
index de4a9ac4487..02917125f05 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -49,14 +49,7 @@ ajv-keywords@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0"
-ajv@^4.7.0:
- version "4.11.2"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.2.tgz#f166c3c11cbc6cb9dcc102a5bcfe5b72c95287e6"
- dependencies:
- co "^4.6.0"
- json-stable-stringify "^1.0.1"
-
-ajv@^4.9.1:
+ajv@^4.7.0, ajv@^4.9.1:
version "4.11.8"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
dependencies:
@@ -64,11 +57,11 @@ ajv@^4.9.1:
json-stable-stringify "^1.0.1"
ajv@^5.1.5:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.0.tgz#c1735024c5da2ef75cc190713073d44f098bf486"
+ version "5.2.2"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39"
dependencies:
co "^4.6.0"
- fast-deep-equal "^0.1.0"
+ fast-deep-equal "^1.0.0"
json-schema-traverse "^0.3.0"
json-stable-stringify "^1.0.1"
@@ -877,7 +870,7 @@ backo2@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
-balanced-match@^0.4.1, balanced-match@^0.4.2:
+balanced-match@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838"
@@ -983,14 +976,7 @@ bootstrap-sass@^3.3.6:
version "3.3.6"
resolved "https://registry.yarnpkg.com/bootstrap-sass/-/bootstrap-sass-3.3.6.tgz#363b0d300e868d3e70134c1a742bb17288444fd1"
-brace-expansion@^1.0.0:
- version "1.1.6"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9"
- dependencies:
- balanced-match "^0.4.1"
- concat-map "0.0.1"
-
-brace-expansion@^1.1.8:
+brace-expansion@^1.0.0, brace-expansion@^1.1.7, brace-expansion@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
dependencies:
@@ -1712,7 +1698,7 @@ decompress-response@^3.2.0:
dependencies:
mimic-response "^1.0.0"
-deep-equal@^1.0.1:
+deep-equal@^1.0.1, deep-equal@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
@@ -1730,7 +1716,14 @@ default-require-extensions@^1.0.0:
dependencies:
strip-bom "^2.0.0"
-defined@^1.0.0:
+define-properties@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94"
+ dependencies:
+ foreach "^2.0.5"
+ object-keys "^1.0.8"
+
+defined@^1.0.0, defined@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693"
@@ -2037,6 +2030,24 @@ error-ex@^1.2.0:
dependencies:
is-arrayish "^0.2.1"
+es-abstract@^1.5.0:
+ version "1.8.2"
+ resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.8.2.tgz#25103263dc4decbda60e0c737ca32313518027ee"
+ dependencies:
+ es-to-primitive "^1.1.1"
+ function-bind "^1.1.1"
+ has "^1.0.1"
+ is-callable "^1.1.3"
+ is-regex "^1.0.4"
+
+es-to-primitive@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d"
+ dependencies:
+ is-callable "^1.1.1"
+ is-date-object "^1.0.1"
+ is-symbol "^1.0.1"
+
es5-ext@^0.10.14, es5-ext@^0.10.8, es5-ext@^0.10.9, es5-ext@~0.10.14, es5-ext@~0.10.2:
version "0.10.24"
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.24.tgz#a55877c9924bc0c8d9bd3c2cbe17495ac1709b14"
@@ -2429,9 +2440,9 @@ extsprintf@1.3.0, extsprintf@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
-fast-deep-equal@^0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-0.1.0.tgz#5c6f4599aba6b333ee3342e2ed978672f1001f8d"
+fast-deep-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff"
fast-levenshtein@~2.0.4:
version "2.0.6"
@@ -2564,6 +2575,12 @@ follow-redirects@^1.2.3:
dependencies:
debug "^2.4.5"
+for-each@~0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.2.tgz#2c40450b9348e97f281322593ba96704b9abd4d4"
+ dependencies:
+ is-function "~1.0.0"
+
for-in@^0.1.5:
version "0.1.6"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.6.tgz#c9f96e89bfad18a545af5ec3ed352a1d9e5b4dc8"
@@ -2574,6 +2591,10 @@ for-own@^0.1.4:
dependencies:
for-in "^0.1.5"
+foreach@^2.0.5:
+ version "2.0.5"
+ resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"
+
forever-agent@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
@@ -2646,6 +2667,10 @@ function-bind@^1.0.2:
version "1.1.0"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771"
+function-bind@^1.1.1, function-bind@~1.1.0:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
+
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
@@ -2720,7 +2745,7 @@ glob@^6.0.4:
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^7.0.0, glob@^7.1.1:
+glob@^7.0.0, glob@^7.1.1, glob@~7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
@@ -2742,11 +2767,7 @@ glob@^7.0.3, glob@^7.0.5:
once "^1.3.0"
path-is-absolute "^1.0.0"
-globals@^9.0.0:
- version "9.14.0"
- resolved "https://registry.yarnpkg.com/globals/-/globals-9.14.0.tgz#8859936af0038741263053b39d0e76ca241e4034"
-
-globals@^9.14.0:
+globals@^9.0.0, globals@^9.14.0:
version "9.18.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a"
@@ -2888,7 +2909,7 @@ has-unicode@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
-has@^1.0.1:
+has@^1.0.1, has@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28"
dependencies:
@@ -3163,6 +3184,14 @@ is-builtin-module@^1.0.0:
dependencies:
builtin-modules "^1.0.0"
+is-callable@^1.1.1, is-callable@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2"
+
+is-date-object@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
+
is-dotfile@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.2.tgz#2c132383f39199f8edc268ca01b9b007d205cc4d"
@@ -3201,6 +3230,10 @@ is-fullwidth-code-point@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
+is-function@~1.0.0:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5"
+
is-glob@^2.0.0, is-glob@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
@@ -3276,6 +3309,12 @@ is-redirect@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
+is-regex@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
+ dependencies:
+ has "^1.0.1"
+
is-relative@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-0.2.1.tgz#d27f4c7d516d175fb610db84bbeef23c3bc97aa5"
@@ -3302,6 +3341,10 @@ is-svg@^2.0.0:
dependencies:
html-comment-regex "^1.1.0"
+is-symbol@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572"
+
is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
@@ -4087,7 +4130,7 @@ minimist@0.0.8, minimist@~0.0.1:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
-minimist@^1.1.3, minimist@^1.2.0:
+minimist@^1.1.3, minimist@^1.2.0, minimist@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
@@ -4225,8 +4268,8 @@ node-libs-browser@^2.0.0:
vm-browserify "0.0.4"
node-pre-gyp@^0.6.36:
- version "0.6.36"
- resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786"
+ version "0.6.37"
+ resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.37.tgz#3c872b236b2e266e4140578fe1ee88f693323a05"
dependencies:
mkdirp "^0.5.1"
nopt "^4.0.1"
@@ -4235,6 +4278,7 @@ node-pre-gyp@^0.6.36:
request "^2.81.0"
rimraf "^2.6.1"
semver "^5.3.0"
+ tape "^4.6.3"
tar "^2.2.1"
tar-pack "^3.4.0"
@@ -4356,6 +4400,14 @@ object-component@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291"
+object-inspect@~1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.3.0.tgz#5b1eb8e6742e2ee83342a637034d844928ba2f6d"
+
+object-keys@^1.0.8:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d"
+
object.omit@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa"
@@ -5383,6 +5435,12 @@ resolve@^1.1.6, resolve@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.2.0.tgz#9589c3f2f6149d1417a40becc1663db6ec6bc26c"
+resolve@~1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.4.0.tgz#a75be01c53da25d934a98ebd0e4c4a7312f92a86"
+ dependencies:
+ path-parse "^1.0.5"
+
restore-cursor@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
@@ -5390,6 +5448,12 @@ restore-cursor@^1.0.1:
exit-hook "^1.0.0"
onetime "^1.0.0"
+resumer@~0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/resumer/-/resumer-0.0.0.tgz#f1e8f461e4064ba39e82af3cdc2a8c893d076759"
+ dependencies:
+ through "~2.3.4"
+
right-align@^0.1.1:
version "0.1.3"
resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
@@ -5805,6 +5869,14 @@ string-width@^2.0.0:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^3.0.0"
+string.prototype.trim@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz#d04de2c89e137f4d7d206f086b5ed2fae6be8cea"
+ dependencies:
+ define-properties "^1.1.2"
+ es-abstract "^1.5.0"
+ function-bind "^1.0.2"
+
string_decoder@^0.10.25, string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
@@ -5896,6 +5968,24 @@ tapable@^0.2.7:
version "0.2.8"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22"
+tape@^4.6.3:
+ version "4.8.0"
+ resolved "https://registry.yarnpkg.com/tape/-/tape-4.8.0.tgz#f6a9fec41cc50a1de50fa33603ab580991f6068e"
+ dependencies:
+ deep-equal "~1.0.1"
+ defined "~1.0.0"
+ for-each "~0.3.2"
+ function-bind "~1.1.0"
+ glob "~7.1.2"
+ has "~1.0.1"
+ inherits "~2.0.3"
+ minimist "~1.2.0"
+ object-inspect "~1.3.0"
+ resolve "~1.4.0"
+ resumer "~0.0.0"
+ string.prototype.trim "~1.1.2"
+ through "~2.3.8"
+
tar-pack@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/tar-pack/-/tar-pack-3.4.0.tgz#23be2d7f671a8339376cbdb0b8fe3fdebf317984"
@@ -5943,7 +6033,7 @@ three@^0.84.0:
version "0.84.0"
resolved "https://registry.yarnpkg.com/three/-/three-0.84.0.tgz#95be85a55a0fa002aa625ed559130957dcffd918"
-through@2, through@^2.3.6, through@~2.3, through@~2.3.1:
+through@2, through@^2.3.6, through@~2.3, through@~2.3.1, through@~2.3.4, through@~2.3.8:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"