summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRegis <boudinot.regis@yahoo.com>2017-03-21 09:08:28 -0600
committerRegis <boudinot.regis@yahoo.com>2017-03-21 09:08:28 -0600
commit0b75b821c6cfd173291fcfd88c41da9922d082dd (patch)
tree41b578d299bd77423aa3591955a4cb5ca07ab025
parent6342da7bb6cbba1b1e026fc62a1da42b811b25f4 (diff)
parenta08c707c928092426e2334423e71c6b841309ddf (diff)
downloadgitlab-ce-issue-title-vue.tar.gz
update to current master and fix conflictsissue-title-vue
-rw-r--r--.babelrc21
-rw-r--r--.gitattributes1
-rw-r--r--.gitlab-ci.yml29
-rw-r--r--.gitlab/issue_templates/Feature Proposal.md10
-rw-r--r--CHANGELOG.md15
-rw-r--r--CONTRIBUTING.md7
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock6
-rw-r--r--app/assets/javascripts/awards_handler.js18
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji.js19
-rw-r--r--app/assets/javascripts/behaviors/gl_emoji/is_emoji_name_valid.js11
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js7
-rw-r--r--app/assets/javascripts/boards/components/board.js5
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.js119
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.js4
-rw-r--r--app/assets/javascripts/boards/components/modal/filters.js57
-rw-r--r--app/assets/javascripts/boards/components/modal/filters/label.js54
-rw-r--r--app/assets/javascripts/boards/components/modal/filters/milestone.js55
-rw-r--r--app/assets/javascripts/boards/components/modal/filters/user.js96
-rw-r--r--app/assets/javascripts/boards/components/modal/header.js16
-rw-r--r--app/assets/javascripts/boards/components/modal/index.js18
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.js2
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js11
-rw-r--r--app/assets/javascripts/boards/models/list.js21
-rw-r--r--app/assets/javascripts/boards/stores/modal_store.js14
-rw-r--r--app/assets/javascripts/boards/utils/query_data.js21
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js9
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_service.js44
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js146
-rw-r--r--app/assets/javascripts/compare_autocomplete.js5
-rw-r--r--app/assets/javascripts/copy_as_gfm.js65
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js1
-rw-r--r--app/assets/javascripts/dispatcher.js13
-rw-r--r--app/assets/javascripts/due_date_select.js2
-rw-r--r--app/assets/javascripts/environments/components/environment.js14
-rw-r--r--app/assets/javascripts/environments/components/environment_external_url.js1
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js8
-rw-r--r--app/assets/javascripts/environments/components/environments_table.js4
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.js15
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js3
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js1
-rw-r--r--app/assets/javascripts/filtered_search/container.js14
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_hint.js16
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js25
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js3
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js20
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js37
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_token_keys.js4
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js28
-rw-r--r--app/assets/javascripts/group_name.js40
-rw-r--r--app/assets/javascripts/issue.js8
-rw-r--r--app/assets/javascripts/labels_select.js2
-rw-r--r--app/assets/javascripts/merge_request_widget.js6
-rw-r--r--app/assets/javascripts/milestone_select.js14
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js6
-rw-r--r--app/assets/javascripts/monitoring/prometheus_graph.js34
-rw-r--r--app/assets/javascripts/profile/profile.js1
-rw-r--r--app/assets/javascripts/todos.js67
-rw-r--r--app/assets/javascripts/users_select.js4
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/async_button.js92
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js56
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js71
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js32
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/stage.js116
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/status.js60
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/time_ago.js71
-rw-r--r--app/assets/javascripts/vue_pipelines_index/event_hub.js3
-rw-r--r--app/assets/javascripts/vue_pipelines_index/index.js23
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_actions.js119
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_url.js63
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js190
-rw-r--r--app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js44
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stage.js119
-rw-r--r--app/assets/javascripts/vue_pipelines_index/status.js64
-rw-r--r--app/assets/javascripts/vue_pipelines_index/store.js31
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js (renamed from app/assets/javascripts/commit/pipelines/pipelines_store.js)35
-rw-r--r--app/assets/javascripts/vue_pipelines_index/time_ago.js78
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.js307
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table.js80
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table_row.js393
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.js254
-rw-r--r--app/assets/javascripts/vue_shared/vue_resource_interceptor.js10
-rw-r--r--app/assets/stylesheets/framework/awards.scss16
-rw-r--r--app/assets/stylesheets/framework/common.scss6
-rw-r--r--app/assets/stylesheets/framework/emojis.scss1
-rw-r--r--app/assets/stylesheets/framework/filters.scss43
-rw-r--r--app/assets/stylesheets/framework/header.scss22
-rw-r--r--app/assets/stylesheets/framework/nav.scss1
-rw-r--r--app/assets/stylesheets/framework/typography.scss5
-rw-r--r--app/assets/stylesheets/pages/boards.scss9
-rw-r--r--app/assets/stylesheets/pages/commits.scss32
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss10
-rw-r--r--app/assets/stylesheets/pages/issues.scss5
-rw-r--r--app/assets/stylesheets/pages/note_form.scss12
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss24
-rw-r--r--app/controllers/concerns/service_params.rb1
-rw-r--r--app/controllers/dashboard/projects_controller.rb2
-rw-r--r--app/controllers/dashboard/todos_controller.rb12
-rw-r--r--app/controllers/explore/projects_controller.rb14
-rw-r--r--app/controllers/profiles/notifications_controller.rb2
-rw-r--r--app/controllers/projects/application_controller.rb5
-rw-r--r--app/controllers/projects/blame_controller.rb5
-rw-r--r--app/controllers/projects/blob_controller.rb5
-rw-r--r--app/controllers/projects/branches_controller.rb10
-rw-r--r--app/controllers/projects/issues_controller.rb4
-rw-r--r--app/controllers/projects/merge_requests_controller.rb2
-rw-r--r--app/controllers/projects/settings/members_controller.rb1
-rw-r--r--app/controllers/projects/tags_controller.rb4
-rw-r--r--app/controllers/projects/tree_controller.rb1
-rw-r--r--app/controllers/projects/wikis_controller.rb3
-rw-r--r--app/controllers/projects_controller.rb8
-rw-r--r--app/finders/issuable_finder.rb6
-rw-r--r--app/finders/todos_finder.rb12
-rw-r--r--app/helpers/blob_helper.rb14
-rw-r--r--app/helpers/ci_status_helper.rb18
-rw-r--r--app/helpers/commits_helper.rb2
-rw-r--r--app/helpers/gitlab_markdown_helper.rb4
-rw-r--r--app/helpers/groups_helper.rb11
-rw-r--r--app/helpers/import_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb11
-rw-r--r--app/helpers/milestones_helper.rb9
-rw-r--r--app/helpers/nav_helper.rb1
-rw-r--r--app/helpers/projects_helper.rb7
-rw-r--r--app/helpers/sorting_helper.rb11
-rw-r--r--app/helpers/todos_helper.rb13
-rw-r--r--app/mailers/emails/builds.rb30
-rw-r--r--app/mailers/notify.rb1
-rw-r--r--app/models/application_setting.rb9
-rw-r--r--app/models/ci/build.rb21
-rw-r--r--app/models/ci/pipeline.rb16
-rw-r--r--app/models/ci/pipeline_status.rb86
-rw-r--r--app/models/ci/runner.rb4
-rw-r--r--app/models/ci/runner_project.rb6
-rw-r--r--app/models/ci/trigger.rb2
-rw-r--r--app/models/ci/variable.rb4
-rw-r--r--app/models/commit.rb13
-rw-r--r--app/models/commit_status.rb8
-rw-r--r--app/models/concerns/has_status.rb1
-rw-r--r--app/models/concerns/issuable.rb36
-rw-r--r--app/models/concerns/routable.rb10
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/issue.rb16
-rw-r--r--app/models/label.rb4
-rw-r--r--app/models/merge_request.rb10
-rw-r--r--app/models/milestone.rb1
-rw-r--r--app/models/project.rb26
-rw-r--r--app/models/project_services/builds_email_service.rb102
-rw-r--r--app/models/project_services/chat_message/build_message.rb102
-rw-r--r--app/models/project_services/chat_notification_service.rb18
-rw-r--r--app/models/project_services/hipchat_service.rb45
-rw-r--r--app/models/project_services/mattermost_service.rb1
-rw-r--r--app/models/project_services/prometheus_service.rb9
-rw-r--r--app/models/project_services/slack_service.rb1
-rw-r--r--app/models/project_wiki.rb7
-rw-r--r--app/models/route.rb2
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/todo.rb8
-rw-r--r--app/models/user.rb2
-rw-r--r--app/models/wiki_page.rb14
-rw-r--r--app/services/ci/register_job_service.rb6
-rw-r--r--app/services/create_branch_service.rb6
-rw-r--r--app/services/merge_requests/build_service.rb2
-rw-r--r--app/services/merge_requests/get_urls_service.rb7
-rw-r--r--app/services/notification_recipient_service.rb293
-rw-r--r--app/services/notification_service.rb328
-rw-r--r--app/services/projects/import_service.rb3
-rw-r--r--app/services/system_hooks_service.rb9
-rw-r--r--app/services/todo_service.rb8
-rw-r--r--app/services/users/refresh_authorized_projects_service.rb4
-rw-r--r--app/validators/importable_url_validator.rb11
-rw-r--r--app/views/admin/appearances/_form.html.haml2
-rw-r--r--app/views/admin/application_settings/_form.html.haml2
-rw-r--r--app/views/dashboard/todos/_todo.html.haml6
-rw-r--r--app/views/dashboard/todos/index.html.haml25
-rw-r--r--app/views/events/_event.atom.builder2
-rw-r--r--app/views/events/event/_note.html.haml2
-rw-r--r--app/views/explore/groups/index.html.haml2
-rw-r--r--app/views/groups/_settings_head.html.haml14
-rw-r--r--app/views/groups/edit.html.haml1
-rw-r--r--app/views/groups/projects.html.haml2
-rw-r--r--app/views/help/index.html.haml2
-rw-r--r--app/views/import/bitbucket/status.html.haml6
-rw-r--r--app/views/import/gitlab/status.html.haml2
-rw-r--r--app/views/import/google_code/new.html.haml2
-rw-r--r--app/views/import/google_code/status.html.haml6
-rw-r--r--app/views/issues/_issue.atom.builder4
-rw-r--r--app/views/koding/index.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/nav/_group.html.haml6
-rw-r--r--app/views/layouts/nav/_group_settings.html.haml18
-rw-r--r--app/views/notify/build_fail_email.html.haml24
-rw-r--r--app/views/notify/build_fail_email.text.erb11
-rw-r--r--app/views/notify/build_success_email.html.haml24
-rw-r--r--app/views/notify/build_success_email.text.erb11
-rw-r--r--app/views/profiles/notifications/show.html.haml5
-rw-r--r--app/views/profiles/show.html.haml2
-rw-r--r--app/views/projects/_merge_request_merge_settings.html.haml4
-rw-r--r--app/views/projects/blame/show.html.haml9
-rw-r--r--app/views/projects/blob/_actions.html.haml26
-rw-r--r--app/views/projects/blob/_blob.html.haml10
-rw-r--r--app/views/projects/blob/_header.html.haml39
-rw-r--r--app/views/projects/blob/_image.html.haml2
-rw-r--r--app/views/projects/blob/_text.html.haml14
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/boards/_show.html.haml1
-rw-r--r--app/views/projects/boards/components/_blank_state.html.haml15
-rw-r--r--app/views/projects/boards/components/_board.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml2
-rw-r--r--app/views/projects/buttons/_download.html.haml3
-rw-r--r--app/views/projects/buttons/_koding.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml14
-rw-r--r--app/views/projects/cycle_analytics/_overview.html.haml2
-rw-r--r--app/views/projects/diffs/_file_header.html.haml7
-rw-r--r--app/views/projects/empty.html.haml1
-rw-r--r--app/views/projects/environments/_external_url.html.haml2
-rw-r--r--app/views/projects/environments/metrics.html.haml3
-rw-r--r--app/views/projects/issues/index.html.haml17
-rw-r--r--app/views/projects/issues/show.html.haml57
-rw-r--r--app/views/projects/merge_requests/_show.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_how_to_merge.html.haml2
-rw-r--r--app/views/projects/milestones/edit.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml2
-rw-r--r--app/views/projects/new.html.haml3
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml4
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_help.html.haml2
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml4
-rw-r--r--app/views/projects/snippets/show.html.haml8
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/wikis/_main_links.html.haml2
-rw-r--r--app/views/shared/_sort_dropdown.html.haml3
-rw-r--r--app/views/shared/empty_states/_issues.html.haml5
-rw-r--r--app/views/shared/empty_states/_labels.html.haml2
-rw-r--r--app/views/shared/groups/_group.html.haml7
-rw-r--r--app/views/shared/issuable/_filter.html.haml2
-rw-r--r--app/views/shared/issuable/_form.html.haml2
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml41
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml10
-rw-r--r--app/views/shared/issuable/form/_metadata.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml7
-rw-r--r--app/views/shared/snippets/_blob.html.haml27
-rw-r--r--app/views/shared/snippets/_form.html.haml2
-rw-r--r--app/views/snippets/show.html.haml12
-rw-r--r--app/views/users/show.html.haml2
-rw-r--r--app/workers/build_email_worker.rb20
-rw-r--r--changelogs/unreleased/21451-allow-disable-mr-link.yml4
-rw-r--r--changelogs/unreleased/23993-drop-ci_projects.yml6
-rw-r--r--changelogs/unreleased/24683-sidebar-spinners.yml4
-rw-r--r--changelogs/unreleased/26236-monospace-gfm.yml4
-rw-r--r--changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml4
-rw-r--r--changelogs/unreleased/27114-add-undo-mark-all-as-done-to-todos.yml4
-rw-r--r--changelogs/unreleased/27142-api-replace-destroy-with-stop-environment.yml2
-rw-r--r--changelogs/unreleased/27174-filter-filters.yml4
-rw-r--r--changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml4
-rw-r--r--changelogs/unreleased/27376-cache-default-branch-pipeline-on-project.yml4
-rw-r--r--changelogs/unreleased/27988-fix-transient-failure-in-commits-api.yml5
-rw-r--r--changelogs/unreleased/28058-hide-emails-in-atom-feeds.yml4
-rw-r--r--changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml4
-rw-r--r--changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml4
-rw-r--r--changelogs/unreleased/28494-mini-pipeline-graph-commit-view.yml4
-rw-r--r--changelogs/unreleased/28499-fix-large-text-tooltip-in-diff-file-name.yml4
-rw-r--r--changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml4
-rw-r--r--changelogs/unreleased/28890-allow-creating-mr-without-target-branch-in-url.yml5
-rw-r--r--changelogs/unreleased/28991-viewing-old-wiki-page-version-edit-button-exists.yml4
-rw-r--r--changelogs/unreleased/29405-fix-project-wiki-update.yml4
-rw-r--r--changelogs/unreleased/29428-new-directory-from-existing-branch.yml4
-rw-r--r--changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml4
-rw-r--r--changelogs/unreleased/29469-message-for-project-x-will-be-deleted-should-include-namespace.yml4
-rw-r--r--changelogs/unreleased/29565-name-of-the-uncompressed-folder-of-a-tag-archive-changed.yml4
-rw-r--r--changelogs/unreleased/29604-v3-fix-branch-creation.yml4
-rw-r--r--changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml4
-rw-r--r--changelogs/unreleased/add-blob-copy-button.yml4
-rw-r--r--changelogs/unreleased/add-labels-to-issue-hook.yml4
-rw-r--r--changelogs/unreleased/add_quick_submit_for_snippets_form.yml4
-rw-r--r--changelogs/unreleased/allow-resolving-conflicts-in-utf-8.yml4
-rw-r--r--changelogs/unreleased/api-project-issues-404.yml4
-rw-r--r--changelogs/unreleased/better-priority-sorting-2.yml4
-rw-r--r--changelogs/unreleased/better-priority-sorting.yml4
-rw-r--r--changelogs/unreleased/bugfix-systemhook.yml4
-rw-r--r--changelogs/unreleased/chore-23493-remaining-time-tooltip.yml5
-rw-r--r--changelogs/unreleased/dm-copy-code-as-gfm.yml4
-rw-r--r--changelogs/unreleased/dz-hide-share-group-ancestors.yml4
-rw-r--r--changelogs/unreleased/feature-tokens-rake-task.yml4
-rw-r--r--changelogs/unreleased/feature-use-gitaly-for-commit-show.yml4
-rw-r--r--changelogs/unreleased/fix-milestone-name-on-show.yml4
-rw-r--r--changelogs/unreleased/fix-prometheus-including-d3-main-bundle.yml4
-rw-r--r--changelogs/unreleased/fix-slow-queries-for-branches-index.yml4
-rw-r--r--changelogs/unreleased/fix_visibility_level.yml4
-rw-r--r--changelogs/unreleased/fl-remove-ujs-pipelines.yml4
-rw-r--r--changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml4
-rw-r--r--changelogs/unreleased/issue_27212.yml4
-rw-r--r--changelogs/unreleased/issue_29449.yml4
-rw-r--r--changelogs/unreleased/make-karma-fast-again.yml4
-rw-r--r--changelogs/unreleased/migrate-pipeline-events-and-email-service.yml4
-rw-r--r--changelogs/unreleased/option-to-be-notified-of-own-activity.yml4
-rw-r--r--changelogs/unreleased/pipeline-tooltips-overflow.yml4
-rw-r--r--changelogs/unreleased/remove-unused-ci-tables.yml4
-rw-r--r--changelogs/unreleased/rename-ci_commits-to-ci_pipeline.yml4
-rw-r--r--changelogs/unreleased/rename_all_issues.yml4
-rw-r--r--changelogs/unreleased/routes-lower-case.yml4
-rw-r--r--changelogs/unreleased/simplify-docs-trigger.yml4
-rw-r--r--changelogs/unreleased/ssrf-protections.yml4
-rw-r--r--config/application.rb2
-rw-r--r--config/gitlab.yml.example4
-rw-r--r--config/initializers/1_settings.rb4
-rw-r--r--config/initializers/rspec_profiling.rb2
-rw-r--r--config/initializers/sidekiq.rb2
-rw-r--r--config/karma.config.js34
-rw-r--r--config/routes/dashboard.rb1
-rw-r--r--config/webpack.config.js15
-rw-r--r--db/fixtures/development/17_cycle_analytics.rb2
-rw-r--r--db/migrate/20160919145149_add_group_id_to_labels.rb2
-rw-r--r--db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb2
-rw-r--r--db/migrate/20161031171301_add_project_id_to_subscriptions.rb1
-rw-r--r--db/migrate/20161201160452_migrate_project_statistics.rb5
-rw-r--r--db/migrate/20161207231621_create_environment_name_unique_index.rb4
-rw-r--r--db/migrate/20161209153400_add_unique_index_for_environment_slug.rb2
-rw-r--r--db/migrate/20161212142807_add_lower_path_index_to_routes.rb2
-rw-r--r--db/migrate/20170123061730_add_notified_of_own_activity_to_users.rb14
-rw-r--r--db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb3
-rw-r--r--db/migrate/20170130204620_add_index_to_project_authorizations.rb5
-rw-r--r--db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb2
-rw-r--r--db/migrate/20170216141440_drop_index_for_builds_project_status.rb2
-rw-r--r--db/migrate/20170222143317_drop_ci_projects.rb34
-rw-r--r--db/migrate/20170222143500_remove_old_project_id_columns.rb28
-rw-r--r--db/migrate/20170222143603_rename_gl_project_id_to_project_id.rb14
-rw-r--r--db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb18
-rw-r--r--db/migrate/20170301195939_rename_ci_commits_to_ci_pipelines.rb10
-rw-r--r--db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb83
-rw-r--r--db/migrate/20170305203726_add_owner_id_foreign_key.rb6
-rw-r--r--db/migrate/20170313213916_add_index_to_user_ghost.rb24
-rw-r--r--db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb24
-rw-r--r--db/migrate/20170315194013_add_closed_at_to_issues.rb7
-rw-r--r--db/migrate/20170317203554_index_routes_path_for_like.rb29
-rw-r--r--db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb3
-rw-r--r--db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb87
-rw-r--r--db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb4
-rw-r--r--db/post_migrate/20170313133418_rename_more_reserved_project_names.rb31
-rw-r--r--db/schema.rb123
-rw-r--r--doc/administration/monitoring/prometheus/index.md53
-rw-r--r--doc/administration/pages/index.md27
-rw-r--r--doc/administration/pages/source.md82
-rw-r--r--doc/api/branches.md5
-rw-r--r--doc/api/issues.md12
-rw-r--r--doc/api/services.md40
-rw-r--r--doc/api/v3_to_v4.md60
-rw-r--r--doc/ci/docker/using_docker_build.md14
-rw-r--r--doc/ci/environments.md38
-rw-r--r--doc/ci/quick_start/README.md9
-rw-r--r--doc/ci/review_apps/index.md2
-rw-r--r--doc/ci/ssh_keys/README.md14
-rw-r--r--doc/ci/variables/README.md52
-rw-r--r--doc/ci/yaml/README.md26
-rw-r--r--doc/development/changelog.md20
-rw-r--r--doc/development/frontend.md22
-rw-r--r--doc/development/polling.md6
-rw-r--r--doc/development/testing.md4
-rw-r--r--doc/install/requirements.md14
-rw-r--r--doc/project_services/builds_emails.md1
-rw-r--r--doc/ssh/README.md6
-rw-r--r--doc/update/8.12-to-8.13.md2
-rw-r--r--doc/update/8.17-to-9.0.md8
-rw-r--r--doc/user/award_emojis.md51
-rw-r--r--doc/user/group/subgroups/img/create_new_group.pngbin0 -> 18503 bytes
-rw-r--r--doc/user/group/subgroups/img/create_subgroup_button.pngbin0 -> 8402 bytes
-rw-r--r--doc/user/group/subgroups/img/group_members.pngbin0 -> 48240 bytes
-rw-r--r--doc/user/group/subgroups/img/mention_subgroups.pngbin0 -> 39666 bytes
-rw-r--r--doc/user/group/subgroups/index.md164
-rw-r--r--doc/user/img/award_emoji_comment_awarded.png (renamed from doc/workflow/img/award_emoji_comment_awarded.png)bin19159 -> 19159 bytes
-rw-r--r--doc/user/img/award_emoji_comment_picker.png (renamed from doc/workflow/img/award_emoji_comment_picker.png)bin72883 -> 72883 bytes
-rw-r--r--doc/user/img/award_emoji_select.pngbin0 -> 17827 bytes
-rw-r--r--doc/user/img/award_emoji_votes_sort_options.pngbin0 -> 99941 bytes
-rw-r--r--doc/user/permissions.md1
-rw-r--r--doc/user/profile/account/two_factor_authentication.md11
-rw-r--r--doc/user/project/integrations/builds_emails.md15
-rw-r--r--doc/user/project/integrations/img/prometheus_environment_detail_with_metrics.pngbin0 -> 113092 bytes
-rw-r--r--doc/user/project/integrations/img/prometheus_gcp_firewall_rule.pngbin0 -> 15247 bytes
-rw-r--r--doc/user/project/integrations/img/prometheus_gcp_node_name.pngbin0 -> 52622 bytes
-rw-r--r--doc/user/project/integrations/img/prometheus_service_configuration.pngbin0 -> 18100 bytes
-rw-r--r--doc/user/project/integrations/img/prometheus_yaml_deploy.pngbin0 -> 23567 bytes
-rw-r--r--doc/user/project/integrations/mattermost.md2
-rw-r--r--doc/user/project/integrations/project_services.md3
-rw-r--r--doc/user/project/integrations/prometheus.md196
-rw-r--r--doc/user/project/integrations/samples/prometheus.yml69
-rw-r--r--doc/user/project/integrations/slack.md2
-rw-r--r--doc/user/project/integrations/webhooks.md14
-rw-r--r--doc/user/project/issue_board.md11
-rw-r--r--doc/user/project/labels.md13
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md4
-rw-r--r--doc/user/project/repository/web_editor.md7
-rw-r--r--doc/user/project/settings/import_export.md2
-rw-r--r--doc/workflow/README.md1
-rw-r--r--doc/workflow/award_emoji.md66
-rw-r--r--doc/workflow/img/award_emoji_select.pngbin23779 -> 0 bytes
-rw-r--r--doc/workflow/img/award_emoji_votes_least_popular.pngbin50191 -> 0 bytes
-rw-r--r--doc/workflow/img/award_emoji_votes_most_popular.pngbin48342 -> 0 bytes
-rw-r--r--doc/workflow/img/award_emoji_votes_sort_options.pngbin57145 -> 0 bytes
-rw-r--r--doc/workflow/milestones.md19
-rw-r--r--features/steps/dashboard/todos.rb6
-rw-r--r--features/steps/project/commits/commits.rb2
-rw-r--r--features/steps/project/pages.rb6
-rw-r--r--features/steps/project/source/browse_files.rb4
-rw-r--r--lib/api/access_requests.rb2
-rw-r--r--lib/api/award_emoji.rb6
-rw-r--r--lib/api/boards.rb2
-rw-r--r--lib/api/branches.rb4
-rw-r--r--lib/api/commit_statuses.rb7
-rw-r--r--lib/api/commits.rb2
-rw-r--r--lib/api/deploy_keys.rb2
-rw-r--r--lib/api/deployments.rb2
-rw-r--r--lib/api/entities.rb4
-rw-r--r--lib/api/environments.rb2
-rw-r--r--lib/api/files.rb2
-rw-r--r--lib/api/groups.rb4
-rw-r--r--lib/api/helpers/runner.rb8
-rw-r--r--lib/api/issues.rb6
-rw-r--r--lib/api/jobs.rb2
-rw-r--r--lib/api/labels.rb2
-rw-r--r--lib/api/members.rb2
-rw-r--r--lib/api/merge_request_diffs.rb7
-rw-r--r--lib/api/merge_requests.rb2
-rw-r--r--lib/api/milestones.rb2
-rw-r--r--lib/api/notes.rb2
-rw-r--r--lib/api/notification_settings.rb9
-rw-r--r--lib/api/pipelines.rb2
-rw-r--r--lib/api/project_hooks.rb2
-rw-r--r--lib/api/project_snippets.rb2
-rw-r--r--lib/api/projects.rb2
-rw-r--r--lib/api/repositories.rb2
-rw-r--r--lib/api/runner.rb20
-rw-r--r--lib/api/runners.rb2
-rw-r--r--lib/api/services.rb32
-rw-r--r--lib/api/subscriptions.rb2
-rw-r--r--lib/api/tags.rb2
-rw-r--r--lib/api/todos.rb2
-rw-r--r--lib/api/triggers.rb4
-rw-r--r--lib/api/v3/award_emoji.rb2
-rw-r--r--lib/api/v3/boards.rb2
-rw-r--r--lib/api/v3/branches.rb23
-rw-r--r--lib/api/v3/commits.rb2
-rw-r--r--lib/api/v3/deploy_keys.rb2
-rw-r--r--lib/api/v3/deployments.rb2
-rw-r--r--lib/api/v3/environments.rb2
-rw-r--r--lib/api/v3/files.rb2
-rw-r--r--lib/api/v3/groups.rb4
-rw-r--r--lib/api/v3/issues.rb6
-rw-r--r--lib/api/v3/labels.rb2
-rw-r--r--lib/api/v3/members.rb2
-rw-r--r--lib/api/v3/merge_request_diffs.rb7
-rw-r--r--lib/api/v3/merge_requests.rb2
-rw-r--r--lib/api/v3/milestones.rb2
-rw-r--r--lib/api/v3/notes.rb2
-rw-r--r--lib/api/v3/pipelines.rb2
-rw-r--r--lib/api/v3/project_hooks.rb2
-rw-r--r--lib/api/v3/project_snippets.rb2
-rw-r--r--lib/api/v3/projects.rb2
-rw-r--r--lib/api/v3/repositories.rb2
-rw-r--r--lib/api/v3/runners.rb2
-rw-r--r--lib/api/v3/services.rb7
-rw-r--r--lib/api/v3/subscriptions.rb2
-rw-r--r--lib/api/v3/tags.rb2
-rw-r--r--lib/api/v3/todos.rb4
-rw-r--r--lib/api/v3/triggers.rb4
-rw-r--r--lib/api/v3/variables.rb2
-rw-r--r--lib/api/variables.rb2
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb7
-rw-r--r--lib/banzai/filter/image_link_filter.rb4
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb13
-rw-r--r--lib/banzai/filter/user_reference_filter.rb5
-rw-r--r--lib/banzai/filter/video_link_filter.rb1
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb4
-rw-r--r--lib/banzai/reference_parser/base_parser.rb4
-rw-r--r--lib/gitlab/conflict/parser.rb8
-rw-r--r--lib/gitlab/database.rb6
-rw-r--r--lib/gitlab/emoji.rb18
-rw-r--r--lib/gitlab/git/diff.rb21
-rw-r--r--lib/gitlab/git/diff_collection.rb4
-rw-r--r--lib/gitlab/git/repository.rb2
-rw-r--r--lib/gitlab/git_ref_validator.rb3
-rw-r--r--lib/gitlab/gitaly_client.rb14
-rw-r--r--lib/gitlab/gitaly_client/commit.rb25
-rw-r--r--lib/gitlab/highlight.rb4
-rw-r--r--lib/gitlab/import_export/import_export.yml4
-rw-r--r--lib/gitlab/import_export/relation_factory.rb8
-rw-r--r--lib/gitlab/redis.rb8
-rw-r--r--lib/gitlab/url_blocker.rb59
-rw-r--r--lib/gitlab/url_sanitizer.rb6
-rw-r--r--lib/gitlab/visibility_level.rb4
-rw-r--r--lib/rouge/formatters/html_gitlab.rb10
-rw-r--r--lib/support/init.d/gitlab.default.example4
-rw-r--r--lib/support/nginx/gitlab7
-rw-r--r--lib/support/nginx/gitlab-ssl7
-rw-r--r--lib/tasks/gitlab/dev.rake2
-rw-r--r--lib/tasks/migrate/setup_postgresql.rake2
-rw-r--r--lib/tasks/tokens.rake38
-rw-r--r--package.json18
-rw-r--r--qa/.gitignore1
-rw-r--r--qa/Dockerfile5
-rw-r--r--qa/Gemfile.lock61
-rw-r--r--qa/qa/page/main/entry.rb10
-rw-r--r--spec/config/mail_room_spec.rb61
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb28
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb14
-rw-r--r--spec/controllers/profiles/notifications_controller_spec.rb45
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb29
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb18
-rw-r--r--spec/controllers/projects/variables_controller_spec.rb2
-rw-r--r--spec/controllers/projects_controller_spec.rb2
-rw-r--r--spec/factories/ci/runner_projects.rb2
-rw-r--r--spec/factories/merge_requests.rb1
-rw-r--r--spec/features/admin/admin_settings_spec.rb10
-rw-r--r--spec/features/atom/dashboard_issues_spec.rb15
-rw-r--r--spec/features/atom/issues_spec.rb11
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb6
-rw-r--r--spec/features/boards/modal_filter_spec.rb183
-rw-r--r--spec/features/copy_as_gfm_spec.rb785
-rw-r--r--spec/features/dashboard/projects_spec.rb26
-rw-r--r--spec/features/groups/group_name_toggle_spec.rb44
-rw-r--r--spec/features/issues/award_emoji_spec.rb15
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb5
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb8
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb11
-rw-r--r--spec/features/issues_spec.rb20
-rw-r--r--spec/features/merge_requests/created_from_fork_spec.rb3
-rw-r--r--spec/features/merge_requests/reset_filters_spec.rb20
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb1
-rw-r--r--spec/features/profiles/user_changes_notified_of_own_activity_spec.rb32
-rw-r--r--spec/features/projects/blobs/user_create_spec.rb18
-rw-r--r--spec/features/projects/branches_spec.rb8
-rw-r--r--spec/features/projects/commit/mini_pipeline_graph_spec.rb55
-rw-r--r--spec/features/projects/compare_spec.rb8
-rw-r--r--spec/features/projects/group_links_spec.rb24
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin681799 -> 679892 bytes
-rw-r--r--spec/features/projects/labels/issues_sorted_by_priority_spec.rb4
-rw-r--r--spec/features/projects/new_project_spec.rb9
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb32
-rw-r--r--spec/features/projects/services/slack_service_spec.rb4
-rw-r--r--spec/features/projects/settings/merge_requests_settings_spec.rb23
-rw-r--r--spec/features/projects/user_create_dir_spec.rb72
-rw-r--r--spec/features/projects/wiki/user_git_access_wiki_page_spec.rb26
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb33
-rw-r--r--spec/features/projects/wiki/user_views_project_wiki_page_spec.rb44
-rw-r--r--spec/features/projects_spec.rb4
-rw-r--r--spec/features/tags/master_views_tags_spec.rb10
-rw-r--r--spec/features/todos/todos_spec.rb56
-rw-r--r--spec/finders/issues_finder_spec.rb35
-rw-r--r--spec/helpers/blob_helper_spec.rb14
-rw-r--r--spec/helpers/ci_status_helper_spec.rb7
-rw-r--r--spec/helpers/events_helper_spec.rb8
-rw-r--r--spec/helpers/milestones_helper_spec.rb54
-rw-r--r--spec/helpers/projects_helper_spec.rb40
-rw-r--r--spec/helpers/todos_helper_spec.rb57
-rw-r--r--spec/javascripts/awards_handler_spec.js14
-rw-r--r--spec/javascripts/boards/board_blank_state_spec.js93
-rw-r--r--spec/javascripts/build_spec.js2
-rw-r--r--spec/javascripts/commit/pipelines/mock_data.js5
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js17
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_store_spec.js30
-rw-r--r--spec/javascripts/filtered_search/dropdown_utils_spec.js31
-rw-r--r--spec/javascripts/filtered_search/filtered_search_manager_spec.js12
-rw-r--r--spec/javascripts/gl_emoji_spec.js36
-rw-r--r--spec/javascripts/issue_spec.js15
-rw-r--r--spec/javascripts/test_bundle.js39
-rw-r--r--spec/javascripts/vue_pipelines_index/async_button_spec.js93
-rw-r--r--spec/javascripts/vue_pipelines_index/pipeline_url_spec.js100
-rw-r--r--spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js62
-rw-r--r--spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js40
-rw-r--r--spec/javascripts/vue_pipelines_index/pipelines_store_spec.js72
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js27
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_row_spec.js14
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_spec.js31
-rw-r--r--spec/javascripts/vue_shared/components/table_pagination_spec.js52
-rw-r--r--spec/lib/banzai/filter/syntax_highlight_filter_spec.rb6
-rw-r--r--spec/lib/expand_variables_spec.rb4
-rw-r--r--spec/lib/git_ref_validator_spec.rb5
-rw-r--r--spec/lib/gitlab/ci/config/entry/environment_spec.rb4
-rw-r--r--spec/lib/gitlab/conflict/parser_spec.rb89
-rw-r--r--spec/lib/gitlab/database_spec.rb14
-rw-r--r--spec/lib/gitlab/diff/highlight_spec.rb6
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb37
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb8
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_spec.rb58
-rw-r--r--spec/lib/gitlab/highlight_spec.rb8
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/import_export/project.json51
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb19
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml5
-rw-r--r--spec/lib/gitlab/redis_spec.rb59
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb31
-rw-r--r--spec/lib/gitlab/url_sanitizer_spec.rb34
-rw-r--r--spec/lib/gitlab/visibility_level_spec.rb21
-rw-r--r--spec/mailers/emails/builds_spec.rb64
-rw-r--r--spec/mailers/notify_spec.rb63
-rw-r--r--spec/migrations/migrate_build_events_to_pipeline_events_spec.rb74
-rw-r--r--spec/models/ci/build_spec.rb4
-rw-r--r--spec/models/ci/pipeline_spec.rb26
-rw-r--r--spec/models/ci/pipeline_status_spec.rb173
-rw-r--r--spec/models/ci/variable_spec.rb2
-rw-r--r--spec/models/commit_spec.rb47
-rw-r--r--spec/models/concerns/has_status_spec.rb18
-rw-r--r--spec/models/concerns/issuable_spec.rb50
-rw-r--r--spec/models/concerns/milestoneish_spec.rb26
-rw-r--r--spec/models/environment_spec.rb2
-rw-r--r--spec/models/issue_spec.rb35
-rw-r--r--spec/models/merge_request_spec.rb6
-rw-r--r--spec/models/project_services/builds_email_service_spec.rb111
-rw-r--r--spec/models/project_services/chat_message/build_message_spec.rb77
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb42
-rw-r--r--spec/models/project_spec.rb36
-rw-r--r--spec/models/project_wiki_spec.rb21
-rw-r--r--spec/models/route_spec.rb12
-rw-r--r--spec/models/user_spec.rb4
-rw-r--r--spec/requests/api/branches_spec.rb181
-rw-r--r--spec/requests/api/commits_spec.rb16
-rw-r--r--spec/requests/api/internal_spec.rb11
-rw-r--r--spec/requests/api/issues_spec.rb16
-rw-r--r--spec/requests/api/runner_spec.rb88
-rw-r--r--spec/requests/api/triggers_spec.rb30
-rw-r--r--spec/requests/api/v3/branches_spec.rb52
-rw-r--r--spec/requests/api/v3/commits_spec.rb15
-rw-r--r--spec/requests/api/v3/issues_spec.rb6
-rw-r--r--spec/requests/api/v3/triggers_spec.rb28
-rw-r--r--spec/requests/ci/api/builds_spec.rb10
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb2
-rw-r--r--spec/services/ci/retry_build_service_spec.rb2
-rw-r--r--spec/services/create_branch_service_spec.rb24
-rw-r--r--spec/services/create_deployment_service_spec.rb8
-rw-r--r--spec/services/merge_requests/build_service_spec.rb5
-rw-r--r--spec/services/merge_requests/get_urls_service_spec.rb10
-rw-r--r--spec/services/notification_service_spec.rb130
-rw-r--r--spec/services/projects/import_service_spec.rb20
-rw-r--r--spec/services/system_hooks_service_spec.rb3
-rw-r--r--spec/services/todo_service_spec.rb24
-rw-r--r--spec/simplecov_env.rb6
-rw-r--r--spec/support/matchers/email_matchers.rb5
-rw-r--r--spec/support/seed_helper.rb6
-rw-r--r--spec/support/target_branch_helpers.rb16
-rw-r--r--spec/tasks/tokens_spec.rb21
-rw-r--r--spec/views/projects/commit/_commit_box.html.haml_spec.rb2
-rw-r--r--spec/workers/build_email_worker_spec.rb36
-rw-r--r--yarn.lock70
642 files changed, 8618 insertions, 4999 deletions
diff --git a/.babelrc b/.babelrc
new file mode 100644
index 00000000000..ee4c391da30
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,21 @@
+{
+ "presets": [
+ ["latest", { "es2015": { "modules": false } }],
+ "stage-2"
+ ],
+ "env": {
+ "coverage": {
+ "plugins": [
+ ["istanbul", {
+ "exclude": [
+ "app/assets/javascripts/droplab/**/*",
+ "spec/javascripts/**/*"
+ ]
+ }],
+ ["transform-define", {
+ "process.env.BABEL_ENV": "coverage"
+ }]
+ ]
+ }
+ }
+}
diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index 70cce05d2b5..00000000000
--- a/.gitattributes
+++ /dev/null
@@ -1 +0,0 @@
-*.js.es6 gitlab-language=javascript
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 492f5ef715d..34c10b3b77f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -58,7 +58,7 @@ stages:
<<: *dedicated-runner
<<: *use-db
script:
- - JOB_NAME=( $CI_BUILD_NAME )
+ - JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
@@ -78,7 +78,7 @@ stages:
<<: *dedicated-runner
<<: *use-db
script:
- - JOB_NAME=( $CI_BUILD_NAME )
+ - JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[1]}
- export CI_NODE_TOTAL=${JOB_NAME[2]}
- export KNAPSACK_REPORT_PATH=knapsack/spinach_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
@@ -180,7 +180,7 @@ spinach 9 10: *spinach-knapsack
<<: *dedicated-runner
stage: test
script:
- - bundle exec $CI_BUILD_NAME
+ - bundle exec $CI_JOB_NAME
rubocop:
<<: *ruby-static-analysis
@@ -211,7 +211,7 @@ rake ee_compat_check:
- ee_compat_check/repo/
- vendor/ruby
artifacts:
- name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}_${CI_BUILD_REF}"
+ name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}"
when: on_failure
expire_in: 10d
paths:
@@ -224,6 +224,14 @@ rake db:migrate:reset:
script:
- bundle exec rake db:migrate:reset
+rake db:rollback:
+ stage: test
+ <<: *use-db
+ <<: *dedicated-runner
+ script:
+ - bundle exec rake db:rollback STEP=120
+ - bundle exec rake db:migrate
+
rake db:seed_fu:
stage: test
<<: *use-db
@@ -269,6 +277,8 @@ rake karma:
stage: test
<<: *use-db
<<: *dedicated-runner
+ variables:
+ BABEL_ENV: "coverage"
script:
- bundle exec rake karma
artifacts:
@@ -322,7 +332,7 @@ migration paths:
- sed -i 's/localhost/redis/g' config/resque.yml
- bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3
- bundle exec rake db:drop db:create db:schema:load db:seed_fu
- - git checkout $CI_BUILD_REF
+ - git checkout $CI_COMMIT_SHA
- source scripts/prepare_build.sh
- bundle exec rake db:migrate
@@ -360,7 +370,7 @@ lint:javascript:report:
stage: post-test
before_script: []
script:
- - find app/ spec/ -name '*.js' -or -name '*.js.es6' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files
+ - find app/ spec/ -name '*.js' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files
- yarn run eslint-report || true # ignore exit code
artifacts:
name: eslint-report
@@ -381,12 +391,13 @@ trigger_docs:
cache: {}
artifacts: {}
script:
- - "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=ce https://gitlab.com/api/v3/projects/1794617/trigger/builds"
+ - "HTTP_STATUS=$(curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=${CI_PROJECT_NAME} --silent --output curl.log --write-out '%{http_code}' https://gitlab.com/api/v3/projects/1794617/trigger/builds)"
+ - if [ "${HTTP_STATUS}" -ne "201" ]; then echo "Error ${HTTP_STATUS}"; cat curl.log; echo; exit 1; fi
only:
- master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
# Notify slack in the end
-
notify:slack:
stage: post-test
<<: *dedicated-runner
@@ -394,7 +405,7 @@ notify:slack:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
script:
- - ./scripts/notify_slack.sh "#development" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/pipelines>"
+ - ./scripts/notify_slack.sh "#development" "Build on \`$CI_COMMIT_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_COMMIT_SHA"/pipelines>"
when: on_failure
only:
- master@gitlab-org/gitlab-ce
diff --git a/.gitlab/issue_templates/Feature Proposal.md b/.gitlab/issue_templates/Feature Proposal.md
index ea895ee6275..2636010e2fb 100644
--- a/.gitlab/issue_templates/Feature Proposal.md
+++ b/.gitlab/issue_templates/Feature Proposal.md
@@ -5,3 +5,13 @@
### Proposal
### Links / references
+
+### Documentation blurb
+
+(Write the start of the documentation of this feature here, include:
+
+1. Why should someone use it; what's the underlying problem.
+2. What is the solution.
+3. How does someone use this
+
+During implementation, this can then be copied and used as a starter for the documentation.)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 42e094bdfc6..da1898e3770 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 8.17.4 (2017-03-19)
+
+- Only show public emails in atom feeds.
+- To protect against Server-side Request Forgery project import URLs are now prohibited against localhost or the server IP except for the assigned instance URL and port. Imports are also prohibited from ports below 1024 with the exception of ports 22, 80, and 443.
+
## 8.17.3 (2017-03-07)
- Fix the redirect to custom home page URL. !9518
@@ -210,6 +215,11 @@ entry.
- Remove deprecated GitlabCiService.
- Requeue pending deletion projects.
+## 8.16.8 (2017-03-19)
+
+- Only show public emails in atom feeds.
+- To protect against Server-side Request Forgery project import URLs are now prohibited against localhost or the server IP except for the assigned instance URL and port. Imports are also prohibited from ports below 1024 with the exception of ports 22, 80, and 443.
+
## 8.16.7 (2017-02-27)
- No changes.
@@ -411,6 +421,11 @@ entry.
- Add margin to markdown math blocks.
- Add hover state to MR comment reply button.
+## 8.15.8 (2017-03-19)
+
+- Only show public emails in atom feeds.
+- To protect against Server-side Request Forgery project import URLs are now prohibited against localhost or the server IP except for the assigned instance URL and port. Imports are also prohibited from ports below 1024 with the exception of ports 22, 80, and 443.
+
## 8.15.7 (2017-02-15)
- No changes.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ae143c58290..a285e8ab74f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -78,6 +78,13 @@ towards getting your issue resolved.
Issues and merge requests should be in English and contain appropriate language
for audiences of all ages.
+If a contributor is no longer actively working on a submitted merge request
+we can decide that the merge request will be finished by one of our
+[Merge request coaches][team] or close the merge request. We make this decision
+based on how important the change is for our product vision. If a Merge request
+coach is going to finish the merge request we assign the
+~"coach will finish" label.
+
## Helping others
Please help other GitLab users when you can. The channels people will reach out
diff --git a/Gemfile b/Gemfile
index 2f813324a35..6af27ce0f3e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -352,4 +352,4 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client
-gem 'gitaly', '~> 0.2.1'
+gem 'gitaly', '~> 0.3.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index e38f45b8e98..043ca4f8800 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -250,7 +250,7 @@ GEM
json
get_process_mem (0.2.0)
gherkin-ruby (0.3.2)
- gitaly (0.2.1)
+ gitaly (0.3.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@@ -304,7 +304,7 @@ GEM
multi_json (~> 1.10)
retriable (~> 1.4)
signet (~> 0.6)
- google-protobuf (3.2.0.1)
+ google-protobuf (3.2.0.2)
googleauth (0.5.1)
faraday (~> 0.9)
jwt (~> 1.4)
@@ -896,7 +896,7 @@ DEPENDENCIES
fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.0)
- gitaly (~> 0.2.1)
+ gitaly (~> 0.3.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1)
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 8a077f0081a..9349918f7a0 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -3,6 +3,7 @@
import emojiMap from 'emojis/digests.json';
import emojiAliases from 'emojis/aliases.json';
import { glEmojiTag } from './behaviors/gl_emoji';
+import isEmojiNameValid from './behaviors/gl_emoji/is_emoji_name_valid';
const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd';
const requestAnimationFrame = window.requestAnimationFrame ||
@@ -454,14 +455,21 @@ AwardsHandler.prototype.normalizeEmojiName = function normalizeEmojiName(emoji)
AwardsHandler
.prototype
.addEmojiToFrequentlyUsedList = function addEmojiToFrequentlyUsedList(emoji) {
- const frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
- frequentlyUsedEmojis.push(emoji);
- Cookies.set('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 });
+ if (isEmojiNameValid(emoji)) {
+ this.frequentlyUsedEmojis = _.uniq(this.getFrequentlyUsedEmojis().concat(emoji));
+ Cookies.set('frequently_used_emojis', this.frequentlyUsedEmojis.join(','), { expires: 365 });
+ }
};
AwardsHandler.prototype.getFrequentlyUsedEmojis = function getFrequentlyUsedEmojis() {
- const frequentlyUsedEmojis = (Cookies.get('frequently_used_emojis') || '').split(',');
- return _.compact(_.uniq(frequentlyUsedEmojis));
+ return this.frequentlyUsedEmojis || (() => {
+ const frequentlyUsedEmojis = _.uniq((Cookies.get('frequently_used_emojis') || '').split(','));
+ this.frequentlyUsedEmojis = frequentlyUsedEmojis.filter(
+ inputName => isEmojiNameValid(inputName),
+ );
+
+ return this.frequentlyUsedEmojis;
+ })();
};
AwardsHandler.prototype.setupSearch = function setupSearch() {
diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js
index 59741cc9b1a..19a607309e4 100644
--- a/app/assets/javascripts/behaviors/gl_emoji.js
+++ b/app/assets/javascripts/behaviors/gl_emoji.js
@@ -13,9 +13,14 @@ function emojiImageTag(name, src) {
}
function assembleFallbackImageSrc(inputName) {
- const name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
+ let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
emojiAliases[inputName] : inputName;
- const emojiInfo = emojiMap[name];
+ let emojiInfo = emojiMap[name];
+ // Fallback to question mark for unknown emojis
+ if (!emojiInfo) {
+ name = 'grey_question';
+ emojiInfo = emojiMap[name];
+ }
const fallbackImageSrc = `${gon.asset_host || ''}${gon.relative_url_root || ''}/assets/emoji/${name}-${emojiInfo.digest}.png`;
return fallbackImageSrc;
@@ -26,9 +31,15 @@ const glEmojiTagDefaults = {
};
function glEmojiTag(inputName, options) {
const opts = Object.assign({}, glEmojiTagDefaults, options);
- const name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
+ let name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
emojiAliases[inputName] : inputName;
- const emojiInfo = emojiMap[name];
+ let emojiInfo = emojiMap[name];
+ // Fallback to question mark for unknown emojis
+ if (!emojiInfo) {
+ name = 'grey_question';
+ emojiInfo = emojiMap[name];
+ }
+
const fallbackImageSrc = assembleFallbackImageSrc(name);
const fallbackSpriteClass = `emoji-${name}`;
diff --git a/app/assets/javascripts/behaviors/gl_emoji/is_emoji_name_valid.js b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_name_valid.js
new file mode 100644
index 00000000000..be4aeb32c46
--- /dev/null
+++ b/app/assets/javascripts/behaviors/gl_emoji/is_emoji_name_valid.js
@@ -0,0 +1,11 @@
+import emojiMap from 'emojis/digests.json';
+import emojiAliases from 'emojis/aliases.json';
+
+function isEmojiNameValid(inputName) {
+ const name = Object.prototype.hasOwnProperty.call(emojiAliases, inputName) ?
+ emojiAliases[inputName] : inputName;
+
+ return name && emojiMap[name];
+}
+
+export default isEmojiNameValid;
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 0726c6c9636..92f3bb3ff52 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -21,8 +21,13 @@
// %a.js-toggle-button
// %div.js-toggle-content
//
- $('body').on('click', '.js-toggle-button', function() {
+ $('body').on('click', '.js-toggle-button', function(e) {
toggleContainer($(this).closest('.js-toggle-container'));
+
+ const targetTag = e.target.tagName.toLowerCase();
+ if (targetTag === 'a' || targetTag === 'button') {
+ e.preventDefault();
+ }
});
// If we're accessing a permalink, ensure it is not inside a
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index 30d3be453be..67c0c419713 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -2,7 +2,8 @@
/* global Vue */
/* global Sortable */
-require('./board_blank_state');
+import boardBlankState from './board_blank_state';
+
require('./board_delete');
require('./board_list');
@@ -17,7 +18,7 @@ require('./board_list');
components: {
'board-list': gl.issueBoards.BoardList,
'board-delete': gl.issueBoards.BoardDelete,
- 'board-blank-state': gl.issueBoards.BoardBlankState
+ boardBlankState,
},
props: {
list: Object,
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js b/app/assets/javascripts/boards/components/board_blank_state.js
index d76314c1892..52893d4642b 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.js
+++ b/app/assets/javascripts/boards/components/board_blank_state.js
@@ -1,53 +1,84 @@
-/* eslint-disable space-before-function-paren, comma-dangle */
-/* global Vue */
/* global ListLabel */
+/* global Cookies */
+const Store = gl.issueBoards.BoardsStore;
-(() => {
- const Store = gl.issueBoards.BoardsStore;
+export default {
+ template: `
+ <div class="board-blank-state">
+ <p>
+ Add the following default lists to your Issue Board with one click:
+ </p>
+ <ul class="board-blank-state-list">
+ <li v-for="label in predefinedLabels">
+ <span
+ class="label-color"
+ :style="{ backgroundColor: label.color }">
+ </span>
+ {{ label.title }}
+ </li>
+ </ul>
+ <p>
+ Starting out with the default set of lists will get you right on the way to making the most of your board.
+ </p>
+ <button
+ class="btn btn-create btn-inverted btn-block"
+ type="button"
+ @click.stop="addDefaultLists">
+ Add default lists
+ </button>
+ <button
+ class="btn btn-default btn-block"
+ type="button"
+ @click.stop="clearBlankState">
+ Nevermind, I'll use my own
+ </button>
+ </div>
+ `,
+ data() {
+ return {
+ predefinedLabels: [
+ new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
+ new ListLabel({ title: 'Doing', color: '#5CB85C' }),
+ ],
+ };
+ },
+ methods: {
+ addDefaultLists() {
+ this.clearBlankState();
- window.gl = window.gl || {};
- window.gl.issueBoards = window.gl.issueBoards || {};
-
- gl.issueBoards.BoardBlankState = Vue.extend({
- data () {
- return {
- predefinedLabels: [
- new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
- new ListLabel({ title: 'Doing', color: '#5CB85C' })
- ]
- };
- },
- methods: {
- addDefaultLists () {
- this.clearBlankState();
-
- this.predefinedLabels.forEach((label, i) => {
- Store.addList({
+ this.predefinedLabels.forEach((label, i) => {
+ Store.addList({
+ title: label.title,
+ position: i,
+ list_type: 'label',
+ label: {
title: label.title,
- position: i,
- list_type: 'label',
- label: {
- title: label.title,
- color: label.color
- }
- });
+ color: label.color,
+ },
});
+ });
- Store.state.lists = _.sortBy(Store.state.lists, 'position');
+ Store.state.lists = _.sortBy(Store.state.lists, 'position');
- // Save the labels
- gl.boardService.generateDefaultLists()
- .then((resp) => {
- resp.json().forEach((listObj) => {
- const list = Store.findList('title', listObj.title);
+ // Save the labels
+ gl.boardService.generateDefaultLists()
+ .then((resp) => {
+ resp.json().forEach((listObj) => {
+ const list = Store.findList('title', listObj.title);
- list.id = listObj.id;
- list.label.id = listObj.label.id;
- list.getIssues();
- });
+ list.id = listObj.id;
+ list.label.id = listObj.label.id;
+ list.getIssues();
});
- },
- clearBlankState: Store.removeBlankState.bind(Store)
- }
- });
-})();
+ })
+ .catch(() => {
+ Store.removeList(undefined, 'label');
+ Cookies.remove('issue_board_welcome_hidden', {
+ path: '',
+ });
+ Store.addBlankState();
+ });
+ },
+ clearBlankState: Store.removeBlankState.bind(Store),
+ },
+};
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js
index 9538f5b69e9..e6973c3fd59 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.js
+++ b/app/assets/javascripts/boards/components/modal/empty_state.js
@@ -30,7 +30,7 @@
if (this.activeTab === 'selected') {
obj.title = 'You haven\'t selected any issues yet';
obj.content = `
- Go back to <strong>All issues</strong> and select some issues
+ Go back to <strong>Open issues</strong> and select some issues
to add to your board.
`;
}
@@ -59,7 +59,7 @@
class="btn btn-default"
@click="changeTab('all')"
v-if="activeTab === 'selected'">
- All issues
+ Open issues
</button>
</div>
</div>
diff --git a/app/assets/javascripts/boards/components/modal/filters.js b/app/assets/javascripts/boards/components/modal/filters.js
index 6de06811d94..bd394a2318c 100644
--- a/app/assets/javascripts/boards/components/modal/filters.js
+++ b/app/assets/javascripts/boards/components/modal/filters.js
@@ -1,49 +1,24 @@
-/* global Vue */
-const userFilter = require('./filters/user');
-const milestoneFilter = require('./filters/milestone');
-const labelFilter = require('./filters/label');
+import FilteredSearchBoards from '../../filtered_search_boards';
+import FilteredSearchContainer from '../../../filtered_search/container';
-module.exports = Vue.extend({
+export default {
name: 'modal-filters',
props: {
- projectId: {
- type: Number,
- required: true,
- },
- milestonePath: {
- type: String,
- required: true,
- },
- labelPath: {
- type: String,
+ store: {
+ type: Object,
required: true,
},
},
- destroyed() {
- gl.issueBoards.ModalStore.setDefaultFilter();
+ mounted() {
+ FilteredSearchContainer.container = this.$el;
+
+ this.filteredSearch = new FilteredSearchBoards(this.store);
+ this.filteredSearch.removeTokens();
},
- components: {
- userFilter,
- milestoneFilter,
- labelFilter,
+ beforeDestroy() {
+ this.filteredSearch.cleanup();
+ FilteredSearchContainer.container = document;
+ this.store.path = '';
},
- template: `
- <div class="modal-filters">
- <user-filter
- dropdown-class-name="dropdown-menu-author"
- toggle-class-name="js-user-search js-author-search"
- toggle-label="Author"
- field-name="author_id"
- :project-id="projectId"></user-filter>
- <user-filter
- dropdown-class-name="dropdown-menu-author"
- toggle-class-name="js-assignee-search"
- toggle-label="Assignee"
- field-name="assignee_id"
- :null-user="true"
- :project-id="projectId"></user-filter>
- <milestone-filter :milestone-path="milestonePath"></milestone-filter>
- <label-filter :label-path="labelPath"></label-filter>
- </div>
- `,
-});
+ template: '#js-board-modal-filter',
+};
diff --git a/app/assets/javascripts/boards/components/modal/filters/label.js b/app/assets/javascripts/boards/components/modal/filters/label.js
deleted file mode 100644
index 4fc8f72a145..00000000000
--- a/app/assets/javascripts/boards/components/modal/filters/label.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/* eslint-disable no-new */
-/* global Vue */
-/* global LabelsSelect */
-module.exports = Vue.extend({
- name: 'filter-label',
- props: {
- labelPath: {
- type: String,
- required: true,
- },
- },
- mounted() {
- new LabelsSelect(this.$refs.dropdown);
- },
- template: `
- <div class="dropdown">
- <button
- class="dropdown-menu-toggle js-label-select js-multiselect js-extra-options"
- type="button"
- data-toggle="dropdown"
- data-show-any="true"
- data-show-no="true"
- :data-labels="labelPath"
- ref="dropdown">
- <span class="dropdown-toggle-text">
- Label
- </span>
- <i class="fa fa-chevron-down"></i>
- </button>
- <div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable">
- <div class="dropdown-title">
- Filter by label
- <button
- class="dropdown-title-button dropdown-menu-close"
- aria-label="Close"
- type="button">
- <i class="fa fa-times dropdown-menu-close-icon"></i>
- </button>
- </div>
- <div class="dropdown-input">
- <input
- type="search"
- class="dropdown-input-field"
- placeholder="Search"
- autocomplete="off" />
- <i class="fa fa-search dropdown-input-search"></i>
- <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i>
- </div>
- <div class="dropdown-content"></div>
- <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
- </div>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/filters/milestone.js b/app/assets/javascripts/boards/components/modal/filters/milestone.js
deleted file mode 100644
index d555599d300..00000000000
--- a/app/assets/javascripts/boards/components/modal/filters/milestone.js
+++ /dev/null
@@ -1,55 +0,0 @@
-/* eslint-disable no-new */
-/* global Vue */
-/* global MilestoneSelect */
-module.exports = Vue.extend({
- name: 'filter-milestone',
- props: {
- milestonePath: {
- type: String,
- required: true,
- },
- },
- mounted() {
- new MilestoneSelect(null, this.$refs.dropdown);
- },
- template: `
- <div class="dropdown">
- <button
- class="dropdown-menu-toggle js-milestone-select"
- type="button"
- data-toggle="dropdown"
- data-show-any="true"
- data-show-upcoming="true"
- data-field-name="milestone_title"
- :data-milestones="milestonePath"
- ref="dropdown">
- <span class="dropdown-toggle-text">
- Milestone
- </span>
- <i class="fa fa-chevron-down"></i>
- </button>
- <div class="dropdown-menu dropdown-select dropdown-menu-selectable dropdown-menu-milestone">
- <div class="dropdown-title">
- <span>Filter by milestone</span>
- <button
- class="dropdown-title-button dropdown-menu-close"
- aria-label="Close"
- type="button">
- <i class="fa fa-times dropdown-menu-close-icon"></i>
- </button>
- </div>
- <div class="dropdown-input">
- <input
- type="search"
- class="dropdown-input-field"
- placeholder="Search milestones"
- autocomplete="off" />
- <i class="fa fa-search dropdown-input-search"></i>
- <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i>
- </div>
- <div class="dropdown-content"></div>
- <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
- </div>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/filters/user.js b/app/assets/javascripts/boards/components/modal/filters/user.js
deleted file mode 100644
index 8523028c29c..00000000000
--- a/app/assets/javascripts/boards/components/modal/filters/user.js
+++ /dev/null
@@ -1,96 +0,0 @@
-/* eslint-disable no-new */
-/* global Vue */
-/* global UsersSelect */
-module.exports = Vue.extend({
- name: 'filter-user',
- props: {
- toggleClassName: {
- type: String,
- required: true,
- },
- dropdownClassName: {
- type: String,
- required: false,
- default: '',
- },
- toggleLabel: {
- type: String,
- required: true,
- },
- fieldName: {
- type: String,
- required: true,
- },
- nullUser: {
- type: Boolean,
- required: false,
- default: false,
- },
- projectId: {
- type: Number,
- required: true,
- },
- },
- mounted() {
- new UsersSelect(null, this.$refs.dropdown);
- },
- computed: {
- currentUsername() {
- return gon.current_username;
- },
- dropdownTitle() {
- return `Filter by ${this.toggleLabel.toLowerCase()}`;
- },
- inputPlaceholder() {
- return `Search ${this.toggleLabel.toLowerCase()}`;
- },
- },
- template: `
- <div class="dropdown">
- <button
- class="dropdown-menu-toggle js-user-search"
- :class="toggleClassName"
- type="button"
- data-toggle="dropdown"
- data-current-user="true"
- :data-any-user="'Any ' + toggleLabel"
- :data-null-user="nullUser"
- :data-field-name="fieldName"
- :data-project-id="projectId"
- :data-first-user="currentUsername"
- ref="dropdown">
- <span class="dropdown-toggle-text">
- {{ toggleLabel }}
- </span>
- <i class="fa fa-chevron-down"></i>
- </button>
- <div
- class="dropdown-menu dropdown-select dropdown-menu-user dropdown-menu-selectable"
- :class="dropdownClassName">
- <div class="dropdown-title">
- {{ dropdownTitle }}
- <button
- class="dropdown-title-button dropdown-menu-close"
- aria-label="Close"
- type="button">
- <i class="fa fa-times dropdown-menu-close-icon"></i>
- </button>
- </div>
- <div class="dropdown-input">
- <input
- type="search"
- class="dropdown-input-field"
- autocomplete="off"
- :placeholder="inputPlaceholder" />
- <i class="fa fa-search dropdown-input-search"></i>
- <i
- role="button"
- class="fa fa-times dropdown-input-clear js-dropdown-input-clear">
- </i>
- </div>
- <div class="dropdown-content"></div>
- <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div>
- </div>
- </div>
- `,
-});
diff --git a/app/assets/javascripts/boards/components/modal/header.js b/app/assets/javascripts/boards/components/modal/header.js
index 70c088f9054..116e29cd177 100644
--- a/app/assets/javascripts/boards/components/modal/header.js
+++ b/app/assets/javascripts/boards/components/modal/header.js
@@ -1,6 +1,7 @@
-/* global Vue */
+import Vue from 'vue';
+import modalFilters from './filters';
+
require('./tabs');
-const modalFilters = require('./filters');
(() => {
const ModalStore = gl.issueBoards.ModalStore;
@@ -66,16 +67,7 @@ const modalFilters = require('./filters');
<div
class="add-issues-search append-bottom-10"
v-if="showSearch">
- <modal-filters
- :project-id="projectId"
- :milestone-path="milestonePath"
- :label-path="labelPath">
- </modal-filters>
- <input
- placeholder="Search issues..."
- class="form-control"
- type="search"
- v-model="searchTerm" />
+ <modal-filters :store="filter" />
<button
type="button"
class="btn btn-success btn-inverted prepend-left-10"
diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js
index f290cd13763..4240c97617d 100644
--- a/app/assets/javascripts/boards/components/modal/index.js
+++ b/app/assets/javascripts/boards/components/modal/index.js
@@ -1,5 +1,6 @@
/* global Vue */
/* global ListIssue */
+import queryData from '../../utils/query_data';
require('./header');
require('./list');
@@ -47,9 +48,6 @@ require('./empty_state');
page() {
this.loadIssues();
},
- searchTerm() {
- this.searchOperation();
- },
showAddIssuesModal() {
if (this.showAddIssuesModal && !this.issues.length) {
this.loading = true;
@@ -66,25 +64,20 @@ require('./empty_state');
},
filter: {
handler() {
+ this.page = 1;
this.loadIssues(true);
},
deep: true,
},
},
methods: {
- searchOperation: _.debounce(function searchOperationDebounce() {
- this.loadIssues(true);
- }, 500),
loadIssues(clearIssues = false) {
if (!this.showAddIssuesModal) return false;
- const queryData = Object.assign({}, this.filter, {
- search: this.searchTerm,
+ return gl.boardService.getBacklog(queryData(this.filter.path, {
page: this.page,
per: this.perPage,
- });
-
- return gl.boardService.getBacklog(queryData).then((res) => {
+ })).then((res) => {
const data = res.json();
if (clearIssues) {
@@ -123,6 +116,9 @@ require('./empty_state');
return this.activeTab === 'selected' && this.selectedIssues.length === 0;
},
},
+ created() {
+ this.page = 1;
+ },
components: {
'modal-header': gl.issueBoards.ModalHeader,
'modal-list': gl.issueBoards.ModalList,
diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js
index e8cb43f3503..1cd6ca0ee88 100644
--- a/app/assets/javascripts/boards/components/modal/tabs.js
+++ b/app/assets/javascripts/boards/components/modal/tabs.js
@@ -23,7 +23,7 @@
href="#"
role="button"
@click.prevent="changeTab('all')">
- All issues
+ Open issues
<span class="badge">
{{ issuesCount }}
</span>
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index 47448b02bdd..101732309ea 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -1,3 +1,6 @@
+/* eslint-disable class-methods-use-this */
+import FilteredSearchContainer from '../filtered_search/container';
+
export default class FilteredSearchBoards extends gl.FilteredSearchManager {
constructor(store, updateUrl = false) {
super('boards');
@@ -18,13 +21,17 @@ export default class FilteredSearchBoards extends gl.FilteredSearchManager {
}
}
- updateTokens() {
- const tokens = document.querySelectorAll('.js-visual-token');
+ removeTokens() {
+ const tokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token');
// Remove all the tokens as they will be replaced by the search manager
[].forEach.call(tokens, (el) => {
el.parentNode.removeChild(el);
});
+ }
+
+ updateTokens() {
+ this.removeTokens();
this.loadSearchParamsFromURL();
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 3251ca76b26..f18ad2a0fac 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -1,6 +1,7 @@
/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */
/* global ListIssue */
/* global ListLabel */
+import queryData from '../utils/query_data';
class List {
constructor (obj) {
@@ -64,25 +65,7 @@ class List {
}
getIssues (emptyIssues = true) {
- const data = gl.issueBoards.BoardsStore.filter.path.split('&').reduce((data, filterParam) => {
- if (filterParam === '') return data;
- const paramSplit = filterParam.split('=');
- const paramKeyNormalized = paramSplit[0].replace('[]', '');
- const isArray = paramSplit[0].indexOf('[]');
- const value = decodeURIComponent(paramSplit[1]).replace(/\+/g, ' ');
-
- if (isArray !== -1) {
- if (!data[paramKeyNormalized]) {
- data[paramKeyNormalized] = [];
- }
-
- data[paramKeyNormalized].push(value);
- } else {
- data[paramKeyNormalized] = value;
- }
-
- return data;
- }, { page: this.page });
+ const data = queryData(gl.issueBoards.BoardsStore.filter.path, { page: this.page });
if (this.label && data.label_name) {
data.label_name = data.label_name.filter(label => label !== this.label.title);
diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js
index 15fc6c79e8d..7ee266a831f 100644
--- a/app/assets/javascripts/boards/stores/modal_store.js
+++ b/app/assets/javascripts/boards/stores/modal_store.js
@@ -17,17 +17,9 @@
loadingNewPage: false,
page: 1,
perPage: 50,
- };
-
- this.setDefaultFilter();
- }
-
- setDefaultFilter() {
- this.store.filter = {
- author_id: '',
- assignee_id: '',
- milestone_title: '',
- label_name: [],
+ filter: {
+ path: '',
+ },
};
}
diff --git a/app/assets/javascripts/boards/utils/query_data.js b/app/assets/javascripts/boards/utils/query_data.js
new file mode 100644
index 00000000000..2cd3c146f11
--- /dev/null
+++ b/app/assets/javascripts/boards/utils/query_data.js
@@ -0,0 +1,21 @@
+export default (path, extraData) => path.split('&').reduce((dataParam, filterParam) => {
+ if (filterParam === '') return dataParam;
+
+ const data = dataParam;
+ const paramSplit = filterParam.split('=');
+ const paramKeyNormalized = paramSplit[0].replace('[]', '');
+ const isArray = paramSplit[0].indexOf('[]');
+ const value = decodeURIComponent(paramSplit[1]).replace(/\+/g, ' ');
+
+ if (isArray !== -1) {
+ if (!data[paramKeyNormalized]) {
+ data[paramKeyNormalized] = [];
+ }
+
+ data[paramKeyNormalized].push(value);
+ } else {
+ data[paramKeyNormalized] = value;
+ }
+
+ return data;
+}, extraData);
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index b5a988df897..a9f2d462c31 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -1,8 +1,9 @@
-/* eslint-disable no-new, no-param-reassign */
-/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
+/* eslint-disable no-param-reassign */
+import CommitPipelinesTable from './pipelines_table';
window.Vue = require('vue');
-require('./pipelines_table');
+window.Vue.use(require('vue-resource'));
+
/**
* Commits View > Pipelines Tab > Pipelines Table.
* Merge Request View > Pipelines Tab > Pipelines Table.
@@ -21,7 +22,7 @@ $(() => {
}
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
- gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView();
+ gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable();
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js b/app/assets/javascripts/commit/pipelines/pipelines_service.js
deleted file mode 100644
index 8ae98f9bf97..00000000000
--- a/app/assets/javascripts/commit/pipelines/pipelines_service.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/* globals Vue */
-/* eslint-disable no-unused-vars, no-param-reassign */
-
-/**
- * Pipelines service.
- *
- * Used to fetch the data used to render the pipelines table.
- * Uses Vue.Resource
- */
-class PipelinesService {
-
- /**
- * FIXME: The url provided to request the pipelines in the new merge request
- * page already has `.json`.
- * This should be fixed when the endpoint is improved.
- *
- * @param {String} root
- */
- constructor(root) {
- let endpoint;
-
- if (root.indexOf('.json') === -1) {
- endpoint = `${root}.json`;
- } else {
- endpoint = root;
- }
- this.pipelines = Vue.resource(endpoint);
- }
-
- /**
- * Given the root param provided when the class is initialized, will
- * make a GET request.
- *
- * @return {Promise}
- */
- all() {
- return this.pipelines.get();
- }
-}
-
-window.gl = window.gl || {};
-gl.commits = gl.commits || {};
-gl.commits.pipelines = gl.commits.pipelines || {};
-gl.commits.pipelines.PipelinesService = PipelinesService;
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js
index 631ed34851c..832c4b1bd2a 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js
@@ -1,13 +1,12 @@
-/* eslint-disable no-new, no-param-reassign */
-/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
-
-window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../../lib/utils/common_utils');
-require('../../vue_shared/vue_resource_interceptor');
-require('../../vue_shared/components/pipelines_table');
-require('./pipelines_service');
-const PipelineStore = require('./pipelines_store');
+/* eslint-disable no-new*/
+/* global Flash */
+import Vue from 'vue';
+import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
+import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
+import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
+import eventHub from '../../vue_pipelines_index/event_hub';
+import '../../lib/utils/common_utils';
+import '../../vue_shared/vue_resource_interceptor';
/**
*
@@ -20,48 +19,59 @@ const PipelineStore = require('./pipelines_store');
* as soon as we have Webpack and can load them directly into JS files.
*/
-(() => {
- window.gl = window.gl || {};
- gl.commits = gl.commits || {};
- gl.commits.pipelines = gl.commits.pipelines || {};
+export default Vue.component('pipelines-table', {
+ components: {
+ 'pipelines-table-component': PipelinesTableComponent,
+ },
- gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', {
+ /**
+ * Accesses the DOM to provide the needed data.
+ * Returns the necessary props to render `pipelines-table-component` component.
+ *
+ * @return {Object}
+ */
+ data() {
+ const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
+ const store = new PipelineStore();
- components: {
- 'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
- },
+ return {
+ endpoint: pipelinesTableData.endpoint,
+ store,
+ state: store.state,
+ isLoading: false,
+ };
+ },
- /**
- * Accesses the DOM to provide the needed data.
- * Returns the necessary props to render `pipelines-table-component` component.
- *
- * @return {Object}
- */
- data() {
- const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
- const store = new PipelineStore();
+ /**
+ * When the component is about to be mounted, tell the service to fetch the data
+ *
+ * A request to fetch the pipelines will be made.
+ * In case of a successfull response we will store the data in the provided
+ * store, in case of a failed response we need to warn the user.
+ *
+ */
+ beforeMount() {
+ this.service = new PipelinesService(this.endpoint);
- return {
- endpoint: pipelinesTableData.endpoint,
- store,
- state: store.state,
- isLoading: false,
- };
- },
+ this.fetchPipelines();
+
+ eventHub.$on('refreshPipelines', this.fetchPipelines);
+ },
+
+ beforeUpdate() {
+ if (this.state.pipelines.length && this.$children) {
+ this.store.startTimeAgoLoops.call(this, Vue);
+ }
+ },
- /**
- * When the component is about to be mounted, tell the service to fetch the data
- *
- * A request to fetch the pipelines will be made.
- * In case of a successfull response we will store the data in the provided
- * store, in case of a failed response we need to warn the user.
- *
- */
- beforeMount() {
- const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint);
+ beforeDestroyed() {
+ eventHub.$off('refreshPipelines');
+ },
+ methods: {
+ fetchPipelines() {
this.isLoading = true;
- return pipelinesService.all()
+ return this.service.getPipelines()
.then(response => response.json())
.then((json) => {
// depending of the endpoint the response can either bring a `pipelines` key or not.
@@ -71,34 +81,30 @@ const PipelineStore = require('./pipelines_store');
})
.catch(() => {
this.isLoading = false;
- new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert');
+ new Flash('An error occurred while fetching the pipelines, please reload the page again.');
});
},
+ },
- beforeUpdate() {
- if (this.state.pipelines.length && this.$children) {
- PipelineStore.startTimeAgoLoops.call(this, Vue);
- }
- },
-
- template: `
- <div class="pipelines">
- <div class="realtime-loading" v-if="isLoading">
- <i class="fa fa-spinner fa-spin"></i>
- </div>
+ template: `
+ <div class="pipelines">
+ <div class="realtime-loading" v-if="isLoading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
- <div class="blank-state blank-state-no-icon"
- v-if="!isLoading && state.pipelines.length === 0">
- <h2 class="blank-state-title js-blank-state-title">
- No pipelines to show
- </h2>
- </div>
+ <div class="blank-state blank-state-no-icon"
+ v-if="!isLoading && state.pipelines.length === 0">
+ <h2 class="blank-state-title js-blank-state-title">
+ No pipelines to show
+ </h2>
+ </div>
- <div class="table-holder pipelines"
- v-if="!isLoading && state.pipelines.length > 0">
- <pipelines-table-component :pipelines="state.pipelines"/>
- </div>
+ <div class="table-holder pipelines"
+ v-if="!isLoading && state.pipelines.length > 0">
+ <pipelines-table-component
+ :pipelines="state.pipelines"
+ :service="service" />
</div>
- `,
- });
-})();
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index d91bfb1ccbd..72c0d98d47c 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -18,7 +18,8 @@ window.CompareAutocomplete = (function() {
return $.ajax({
url: $dropdown.data('refs-url'),
data: {
- ref: $dropdown.data('ref')
+ ref: $dropdown.data('ref'),
+ search: term,
}
}).done(function(refs) {
return callback(refs);
@@ -26,7 +27,7 @@ window.CompareAutocomplete = (function() {
},
selectable: true,
filterable: true,
- filterByText: true,
+ filterRemote: true,
fieldName: $dropdown.data('field-name'),
filterInput: 'input[type="search"]',
renderRow: function(ref) {
diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js
index 0fb7bde1fd6..570799c030e 100644
--- a/app/assets/javascripts/copy_as_gfm.js
+++ b/app/assets/javascripts/copy_as_gfm.js
@@ -118,10 +118,10 @@ const gfmRules = {
},
SyntaxHighlightFilter: {
'pre.code.highlight'(el, t) {
- const text = t.trim();
+ const text = t.trimRight();
let lang = el.getAttribute('lang');
- if (lang === 'plaintext') {
+ if (!lang || lang === 'plaintext') {
lang = '';
}
@@ -157,7 +157,7 @@ const gfmRules = {
const backticks = Array(backtickCount + 1).join('`');
const spaceOrNoSpace = backtickCount > 1 ? ' ' : '';
- return backticks + spaceOrNoSpace + text + spaceOrNoSpace + backticks;
+ return backticks + spaceOrNoSpace + text.trim() + spaceOrNoSpace + backticks;
},
'blockquote'(el, text) {
return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
@@ -273,28 +273,29 @@ const gfmRules = {
class CopyAsGFM {
constructor() {
- $(document).on('copy', '.md, .wiki', this.handleCopy);
- $(document).on('paste', '.js-gfm-input', this.handlePaste);
+ $(document).on('copy', '.md, .wiki', (e) => { this.copyAsGFM(e, CopyAsGFM.transformGFMSelection); });
+ $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { this.copyAsGFM(e, CopyAsGFM.transformCodeSelection); });
+ $(document).on('paste', '.js-gfm-input', this.pasteGFM.bind(this));
}
- handleCopy(e) {
+ copyAsGFM(e, transformer) {
const clipboardData = e.originalEvent.clipboardData;
if (!clipboardData) return;
const documentFragment = window.gl.utils.getSelectedFragment();
if (!documentFragment) return;
- // If the documentFragment contains more than just Markdown, don't copy as GFM.
- if (documentFragment.querySelector('.md, .wiki')) return;
+ const el = transformer(documentFragment.cloneNode(true));
+ if (!el) return;
e.preventDefault();
- clipboardData.setData('text/plain', documentFragment.textContent);
+ e.stopPropagation();
- const gfm = CopyAsGFM.nodeToGFM(documentFragment);
- clipboardData.setData('text/x-gfm', gfm);
+ clipboardData.setData('text/plain', el.textContent);
+ clipboardData.setData('text/x-gfm', CopyAsGFM.nodeToGFM(el));
}
- handlePaste(e) {
+ pasteGFM(e) {
const clipboardData = e.originalEvent.clipboardData;
if (!clipboardData) return;
@@ -306,7 +307,47 @@ class CopyAsGFM {
window.gl.utils.insertText(e.target, gfm);
}
+ static transformGFMSelection(documentFragment) {
+ // If the documentFragment contains more than just Markdown, don't copy as GFM.
+ if (documentFragment.querySelector('.md, .wiki')) return null;
+
+ return documentFragment;
+ }
+
+ static transformCodeSelection(documentFragment) {
+ const lineEls = documentFragment.querySelectorAll('.line');
+
+ let codeEl;
+ if (lineEls.length > 1) {
+ codeEl = document.createElement('pre');
+ codeEl.className = 'code highlight';
+
+ const lang = lineEls[0].getAttribute('lang');
+ if (lang) {
+ codeEl.setAttribute('lang', lang);
+ }
+ } else {
+ codeEl = document.createElement('code');
+ }
+
+ if (lineEls.length > 0) {
+ for (let i = 0; i < lineEls.length; i += 1) {
+ const lineEl = lineEls[i];
+ codeEl.appendChild(lineEl);
+ codeEl.appendChild(document.createTextNode('\n'));
+ }
+ } else {
+ codeEl.appendChild(documentFragment);
+ }
+
+ return codeEl;
+ }
+
static nodeToGFM(node) {
+ if (node.nodeType === Node.COMMENT_NODE) {
+ return '';
+ }
+
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent;
}
diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
index 788daa96b3d..dd7081aefb7 100644
--- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -25,6 +25,7 @@ import collapseIcon from '../icons/collapse_icon.svg';
role="button"
data-container="body"
data-placement="top"
+ data-html="true"
:data-line-type="lineType"
:title="note.authorName + ': ' + note.noteTruncated"
:src="note.authorAvatar"
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 6d8174e199e..3557f6f617e 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -1,4 +1,3 @@
-import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make this a bundle
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
/* global UsernameValidator */
/* global ActiveTabMemoizer */
@@ -37,6 +36,7 @@ import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make
import Issue from './issue';
import BindInOut from './behaviors/bind_in_out';
+import GroupName from './group_name';
import GroupsList from './groups_list';
import ProjectsList from './projects_list';
import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
@@ -200,10 +200,13 @@ const UserCallout = require('./user_callout');
new gl.Diff();
new ZenMode();
shortcut_handler = new ShortcutsNavigation();
+ new MiniPipelineGraph({
+ container: '.js-commit-pipeline-graph',
+ }).bindEvents();
break;
case 'projects:commit:pipelines':
new MiniPipelineGraph({
- container: '.js-pipeline-table',
+ container: '.js-commit-pipeline-graph',
}).bindEvents();
break;
case 'projects:commits:show':
@@ -325,8 +328,6 @@ const UserCallout = require('./user_callout');
case 'ci:lints:show':
new gl.CILintEditor();
break;
- case 'projects:environments:metrics':
- new PrometheusGraph();
case 'users:show':
new UserCallout();
break;
@@ -368,6 +369,9 @@ const UserCallout = require('./user_callout');
shortcut_handler = new ShortcutsDashboardNavigation();
new UserCallout();
break;
+ case 'groups':
+ new GroupName();
+ break;
case 'profiles':
new NotificationsForm();
new NotificationsDropdown();
@@ -375,6 +379,7 @@ const UserCallout = require('./user_callout');
case 'projects':
new Project();
new ProjectAvatar();
+ new GroupName();
switch (path[1]) {
case 'compare':
new CompareAutocomplete();
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index fdbb4644971..db10b383913 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -132,7 +132,7 @@ class DueDateSelect {
const selectedDateValue = this.datePayload[this.abilityName].due_date;
const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
- this.$loading.fadeIn();
+ this.$loading.removeClass('hidden').fadeIn();
if (isDropdown) {
this.$dropdown.trigger('loading.gl.dropdown');
diff --git a/app/assets/javascripts/environments/components/environment.js b/app/assets/javascripts/environments/components/environment.js
index 0923ce6b550..51aab8460f6 100644
--- a/app/assets/javascripts/environments/components/environment.js
+++ b/app/assets/javascripts/environments/components/environment.js
@@ -1,21 +1,18 @@
-/* eslint-disable no-param-reassign, no-new */
+/* eslint-disable no-new */
/* global Flash */
+import Vue from 'vue';
import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from './environments_table';
import EnvironmentsStore from '../stores/environments_store';
+import TablePaginationComponent from '../../vue_shared/components/table_pagination';
+import '../../lib/utils/common_utils';
import eventHub from '../event_hub';
-const Vue = window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../../vue_shared/components/table_pagination');
-require('../../lib/utils/common_utils');
-require('../../vue_shared/vue_resource_interceptor');
-
export default Vue.component('environment-component', {
components: {
'environment-table': EnvironmentTable,
- 'table-pagination': gl.VueGlPagination,
+ 'table-pagination': TablePaginationComponent,
},
data() {
@@ -59,7 +56,6 @@ export default Vue.component('environment-component', {
canCreateEnvironmentParsed() {
return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment);
},
-
},
/**
diff --git a/app/assets/javascripts/environments/components/environment_external_url.js b/app/assets/javascripts/environments/components/environment_external_url.js
index a554998f52c..b4f9eb357fd 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.js
+++ b/app/assets/javascripts/environments/components/environment_external_url.js
@@ -14,6 +14,7 @@ export default {
class="btn external_url"
:href="externalUrl"
target="_blank"
+ rel="noopener noreferrer"
title="Environment external URL">
<i class="fa fa-external-link" aria-hidden="true"></i>
</a>
diff --git a/app/assets/javascripts/environments/components/environment_item.js b/app/assets/javascripts/environments/components/environment_item.js
index 93919d41c60..66ed10e19d1 100644
--- a/app/assets/javascripts/environments/components/environment_item.js
+++ b/app/assets/javascripts/environments/components/environment_item.js
@@ -1,24 +1,22 @@
import Timeago from 'timeago.js';
+import '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions';
import ExternalUrlComponent from './environment_external_url';
import StopComponent from './environment_stop';
import RollbackComponent from './environment_rollback';
import TerminalButtonComponent from './environment_terminal_button';
-import '../../lib/utils/text_utility';
-import '../../vue_shared/components/commit';
+import CommitComponent from '../../vue_shared/components/commit';
/**
* Envrionment Item Component
*
* Renders a table row for each environment.
*/
-
const timeagoInstance = new Timeago();
export default {
-
components: {
- 'commit-component': gl.CommitComponent,
+ 'commit-component': CommitComponent,
'actions-component': ActionsComponent,
'external-url-component': ExternalUrlComponent,
'stop-component': StopComponent,
diff --git a/app/assets/javascripts/environments/components/environments_table.js b/app/assets/javascripts/environments/components/environments_table.js
index 5f07b612b91..338dff40bc9 100644
--- a/app/assets/javascripts/environments/components/environments_table.js
+++ b/app/assets/javascripts/environments/components/environments_table.js
@@ -1,11 +1,11 @@
/**
* Render environments table.
*/
-import EnvironmentItem from './environment_item';
+import EnvironmentTableRowComponent from './environment_item';
export default {
components: {
- 'environment-item': EnvironmentItem,
+ 'environment-item': EnvironmentTableRowComponent,
},
props: {
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js b/app/assets/javascripts/environments/folder/environments_folder_view.js
index 7abcf6dbbea..8abbcf0c227 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.js
@@ -1,20 +1,17 @@
-/* eslint-disable no-param-reassign, no-new */
+/* eslint-disable no-new */
/* global Flash */
+import Vue from 'vue';
import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from '../components/environments_table';
import EnvironmentsStore from '../stores/environments_store';
-
-const Vue = window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../../vue_shared/components/table_pagination');
-require('../../lib/utils/common_utils');
-require('../../vue_shared/vue_resource_interceptor');
+import TablePaginationComponent from '../../vue_shared/components/table_pagination';
+import '../../lib/utils/common_utils';
+import '../../vue_shared/vue_resource_interceptor';
export default Vue.component('environment-folder-view', {
-
components: {
'environment-table': EnvironmentTable,
- 'table-pagination': gl.VueGlPagination,
+ 'table-pagination': TablePaginationComponent,
},
data() {
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
index 76296c83d11..07040bf0d73 100644
--- a/app/assets/javascripts/environments/services/environments_service.js
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -1,5 +1,8 @@
/* eslint-disable class-methods-use-this */
import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
export default class EnvironmentsService {
constructor(endpoint) {
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index d3fe3872c56..3c3084f3b78 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -1,5 +1,4 @@
import '~/lib/utils/common_utils';
-
/**
* Environments Store.
*
diff --git a/app/assets/javascripts/filtered_search/container.js b/app/assets/javascripts/filtered_search/container.js
new file mode 100644
index 00000000000..2243c4dd2c5
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/container.js
@@ -0,0 +1,14 @@
+/* eslint-disable class-methods-use-this */
+let container = document;
+
+class FilteredSearchContainerClass {
+ set container(containerParam) {
+ container = containerParam;
+ }
+
+ get container() {
+ return container;
+ }
+}
+
+export default new FilteredSearchContainerClass();
diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js
index 38ff3fb7158..98dcb697af9 100644
--- a/app/assets/javascripts/filtered_search/dropdown_hint.js
+++ b/app/assets/javascripts/filtered_search/dropdown_hint.js
@@ -45,7 +45,7 @@ require('./filtered_search_dropdown');
gl.FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' '));
}
- gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''));
+ gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container);
}
this.dismissDropdown();
this.dispatchInputEvent();
@@ -57,13 +57,15 @@ require('./filtered_search_dropdown');
const dropdownData = [];
[].forEach.call(this.input.closest('.filtered-search-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => {
- const { icon, hint, tag } = dropdownMenu.dataset;
+ const { icon, hint, tag, type } = dropdownMenu.dataset;
if (icon && hint && tag) {
- dropdownData.push({
- icon: `fa-${icon}`,
- hint,
- tag: `&lt;${tag}&gt;`,
- });
+ dropdownData.push(
+ Object.assign({
+ icon: `fa-${icon}`,
+ hint,
+ tag: `&lt;${tag}&gt;`,
+ }, type && { type }),
+ );
}
});
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index a5a6b56a0d3..432b0c0dfd2 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -1,3 +1,5 @@
+import FilteredSearchContainer from './container';
+
(() => {
class DropdownUtils {
static getEscapedText(text) {
@@ -51,14 +53,18 @@
static filterHint(input, item) {
const updatedItem = item;
- const searchInput = gl.DropdownUtils.getSearchInput(input);
- let { lastToken } = gl.FilteredSearchTokenizer.processTokens(searchInput);
- lastToken = lastToken.key || lastToken || '';
-
- if (!lastToken || searchInput.split('').last() === ' ') {
+ const searchInput = gl.DropdownUtils.getSearchQuery(input);
+ const { lastToken, tokens } = gl.FilteredSearchTokenizer.processTokens(searchInput);
+ const lastKey = lastToken.key || lastToken || '';
+ const allowMultiple = item.type === 'array';
+ const itemInExistingTokens = tokens.some(t => t.key === item.hint);
+
+ if (!allowMultiple && itemInExistingTokens) {
+ updatedItem.droplab_hidden = true;
+ } else if (!lastKey || searchInput.split('').last() === ' ') {
updatedItem.droplab_hidden = false;
- } else if (lastToken) {
- const split = lastToken.split(':');
+ } else if (lastKey) {
+ const split = lastKey.split(':');
const tokenName = split[0].split(' ').last();
const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1;
@@ -81,7 +87,8 @@
// Determines the full search query (visual tokens + input)
static getSearchQuery(untilInput = false) {
- const tokens = [].slice.call(document.querySelectorAll('.tokens-container li'));
+ const container = FilteredSearchContainer.container;
+ const tokens = [].slice.call(container.querySelectorAll('.tokens-container li'));
const values = [];
if (untilInput) {
@@ -110,7 +117,7 @@
const { isLastVisualTokenValid } =
gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
- const input = document.querySelector('.filtered-search');
+ const input = FilteredSearchContainer.container.querySelector('.filtered-search');
const inputValue = input && input.value;
if (isLastVisualTokenValid) {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
index 134bdc6ad80..e7bf530d343 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
@@ -38,6 +38,7 @@
gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
}
+ this.resetFilters();
this.dismissDropdown();
this.dispatchInputEvent();
}
@@ -107,7 +108,7 @@
const hook = this.getCurrentHook();
if (hook) {
- const data = hook.list.data;
+ const data = hook.list.data || [];
const results = data.map((o) => {
const updated = o;
updated.droplab_hidden = false;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index d37c812c1f7..5fbe0450bb8 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -1,12 +1,14 @@
/* global DropLab */
+import FilteredSearchContainer from './container';
(() => {
class FilteredSearchDropdownManager {
constructor(baseEndpoint = '', page) {
+ this.container = FilteredSearchContainer.container;
this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
this.tokenizer = gl.FilteredSearchTokenizer;
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
- this.filteredSearchInput = document.querySelector('.filtered-search');
+ this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.page = page;
this.setupMapping();
@@ -31,35 +33,35 @@
author: {
reference: null,
gl: 'DropdownUser',
- element: document.querySelector('#js-dropdown-author'),
+ element: this.container.querySelector('#js-dropdown-author'),
},
assignee: {
reference: null,
gl: 'DropdownUser',
- element: document.querySelector('#js-dropdown-assignee'),
+ element: this.container.querySelector('#js-dropdown-assignee'),
},
milestone: {
reference: null,
gl: 'DropdownNonUser',
extraArguments: [`${this.baseEndpoint}/milestones.json`, '%'],
- element: document.querySelector('#js-dropdown-milestone'),
+ element: this.container.querySelector('#js-dropdown-milestone'),
},
label: {
reference: null,
gl: 'DropdownNonUser',
extraArguments: [`${this.baseEndpoint}/labels.json`, '~'],
- element: document.querySelector('#js-dropdown-label'),
+ element: this.container.querySelector('#js-dropdown-label'),
},
hint: {
reference: null,
gl: 'DropdownHint',
- element: document.querySelector('#js-dropdown-hint'),
+ element: this.container.querySelector('#js-dropdown-hint'),
},
};
}
static addWordToInput(tokenName, tokenValue = '', clicked = false) {
- const input = document.querySelector('.filtered-search');
+ const input = FilteredSearchContainer.container.querySelector('.filtered-search');
gl.FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue);
input.value = '';
@@ -75,13 +77,13 @@
updateDropdownOffset(key) {
// Always align dropdown with the input field
- let offset = this.filteredSearchInput.getBoundingClientRect().left - document.querySelector('.scroll-container').getBoundingClientRect().left;
+ let offset = this.filteredSearchInput.getBoundingClientRect().left - this.container.querySelector('.scroll-container').getBoundingClientRect().left;
const maxInputWidth = 240;
const currentDropdownWidth = this.mapping[key].element.clientWidth || maxInputWidth;
// Make sure offset never exceeds the input container
- const offsetMaxWidth = document.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
+ const offsetMaxWidth = this.container.querySelector('.scroll-container').clientWidth - currentDropdownWidth;
if (offsetMaxWidth < offset) {
offset = offsetMaxWidth;
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index f885932bd91..c6bb7fda8f2 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -1,9 +1,12 @@
+import FilteredSearchContainer from './container';
+
(() => {
class FilteredSearchManager {
constructor(page) {
- this.filteredSearchInput = document.querySelector('.filtered-search');
- this.clearSearchButton = document.querySelector('.clear-search');
- this.tokensContainer = document.querySelector('.tokens-container');
+ this.container = FilteredSearchContainer.container;
+ this.filteredSearchInput = this.container.querySelector('.filtered-search');
+ this.clearSearchButton = this.container.querySelector('.clear-search');
+ this.tokensContainer = this.container.querySelector('.tokens-container');
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
if (this.filteredSearchInput) {
@@ -37,6 +40,8 @@
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
this.editTokenWrapper = this.editToken.bind(this);
this.tokenChange = this.tokenChange.bind(this);
+ this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
+ this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
this.filteredSearchInputForm = this.filteredSearchInput.form;
this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
@@ -48,11 +53,13 @@
this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.addEventListener('click', this.tokenChange);
this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
+ this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.addEventListener('click', this.unselectEditTokensWrapper);
+ document.addEventListener('click', this.removeInputContainerFocusWrapper);
document.addEventListener('keydown', this.removeSelectedTokenWrapper);
}
@@ -66,11 +73,13 @@
this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.removeEventListener('click', this.tokenChange);
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
+ this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.removeEventListener('click', this.unselectEditTokensWrapper);
+ document.removeEventListener('click', this.removeInputContainerFocusWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
}
@@ -121,6 +130,26 @@
}
}
+ addInputContainerFocus() {
+ const inputContainer = this.filteredSearchInput.closest('.filtered-search-input-container');
+
+ if (inputContainer) {
+ inputContainer.classList.add('focus');
+ }
+ }
+
+ removeInputContainerFocus(e) {
+ const inputContainer = this.filteredSearchInput.closest('.filtered-search-input-container');
+ const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
+ const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
+ const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
+
+ if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown &&
+ !isElementInStaticFilterDropdown && inputContainer) {
+ inputContainer.classList.remove('focus');
+ }
+ }
+
static selectToken(e) {
const button = e.target.closest('.selectable');
@@ -132,7 +161,7 @@
}
unselectEditTokens(e) {
- const inputContainer = document.querySelector('.filtered-search-input-container');
+ const inputContainer = this.container.querySelector('.filtered-search-input-container');
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
const isElementInFilterDropdown = e.target.closest('.filter-dropdown') !== null;
const isElementTokensContainer = e.target.classList.contains('tokens-container');
diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
index e6b53cd4b55..6d5df86f2a5 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js
@@ -43,6 +43,10 @@
tokenKey: 'milestone',
value: 'upcoming',
}, {
+ url: 'milestone_title=%23started',
+ tokenKey: 'milestone',
+ value: 'started',
+ }, {
url: 'label_name[]=No+Label',
tokenKey: 'label',
value: 'none',
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
index 320afa26130..a5657fc8720 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -1,6 +1,8 @@
+import FilteredSearchContainer from './container';
+
class FilteredSearchVisualTokens {
static getLastVisualTokenBeforeInput() {
- const inputLi = document.querySelector('.input-token');
+ const inputLi = FilteredSearchContainer.container.querySelector('.input-token');
const lastVisualToken = inputLi && inputLi.previousElementSibling;
return {
@@ -10,7 +12,7 @@ class FilteredSearchVisualTokens {
}
static unselectTokens() {
- const otherTokens = document.querySelectorAll('.js-visual-token .selectable.selected');
+ const otherTokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token .selectable.selected');
[].forEach.call(otherTokens, t => t.classList.remove('selected'));
}
@@ -24,7 +26,7 @@ class FilteredSearchVisualTokens {
}
static removeSelectedToken() {
- const selected = document.querySelector('.js-visual-token .selected');
+ const selected = FilteredSearchContainer.container.querySelector('.js-visual-token .selected');
if (selected) {
const li = selected.closest('.js-visual-token');
@@ -54,8 +56,8 @@ class FilteredSearchVisualTokens {
}
li.querySelector('.name').innerText = name;
- const tokensContainer = document.querySelector('.tokens-container');
- const input = document.querySelector('.filtered-search');
+ const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
+ const input = FilteredSearchContainer.container.querySelector('.filtered-search');
tokensContainer.insertBefore(li, input.parentElement);
}
@@ -77,14 +79,14 @@ class FilteredSearchVisualTokens {
const addVisualTokenElement = FilteredSearchVisualTokens.addVisualTokenElement;
if (isLastVisualTokenValid) {
- addVisualTokenElement(tokenName, tokenValue);
+ addVisualTokenElement(tokenName, tokenValue, false);
} else {
const previousTokenName = lastVisualToken.querySelector('.name').innerText;
- const tokensContainer = document.querySelector('.tokens-container');
+ const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
tokensContainer.removeChild(lastVisualToken);
const value = tokenValue || tokenName;
- addVisualTokenElement(previousTokenName, value);
+ addVisualTokenElement(previousTokenName, value, false);
}
}
@@ -129,7 +131,7 @@ class FilteredSearchVisualTokens {
}
static tokenizeInput() {
- const input = document.querySelector('.filtered-search');
+ const input = FilteredSearchContainer.container.querySelector('.filtered-search');
const { isLastVisualTokenValid } =
gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
@@ -145,7 +147,7 @@ class FilteredSearchVisualTokens {
}
static editToken(token) {
- const input = document.querySelector('.filtered-search');
+ const input = FilteredSearchContainer.container.querySelector('.filtered-search');
FilteredSearchVisualTokens.tokenizeInput();
@@ -157,7 +159,7 @@ class FilteredSearchVisualTokens {
const name = token.querySelector('.name');
const value = token.querySelector('.value');
- if (token.classList.contains('filtered-search-token')) {
+ if (token.classList.contains('filtered-search-token') && value) {
FilteredSearchVisualTokens.addFilterVisualToken(name.innerText);
input.value = value.innerText;
} else {
@@ -174,9 +176,9 @@ class FilteredSearchVisualTokens {
}
static moveInputToTheRight() {
- const input = document.querySelector('.filtered-search');
+ const input = FilteredSearchContainer.container.querySelector('.filtered-search');
const inputLi = input.parentElement;
- const tokenContainer = document.querySelector('.tokens-container');
+ const tokenContainer = FilteredSearchContainer.container.querySelector('.tokens-container');
FilteredSearchVisualTokens.tokenizeInput();
diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js
new file mode 100644
index 00000000000..6a028f299b1
--- /dev/null
+++ b/app/assets/javascripts/group_name.js
@@ -0,0 +1,40 @@
+const GROUP_LIMIT = 2;
+
+export default class GroupName {
+ constructor() {
+ this.titleContainer = document.querySelector('.title');
+ this.groups = document.querySelectorAll('.group-path');
+ this.groupTitle = document.querySelector('.group-title');
+ this.toggle = null;
+ this.isHidden = false;
+ this.init();
+ }
+
+ init() {
+ if (this.groups.length > GROUP_LIMIT) {
+ this.groups[this.groups.length - 1].classList.remove('hidable');
+ this.addToggle();
+ }
+ this.render();
+ }
+
+ addToggle() {
+ const header = document.querySelector('.header-content');
+ this.toggle = document.createElement('button');
+ this.toggle.className = 'text-expander group-name-toggle';
+ this.toggle.setAttribute('aria-label', 'Toggle full path');
+ this.toggle.innerHTML = '...';
+ this.toggle.addEventListener('click', this.toggleGroups.bind(this));
+ header.insertBefore(this.toggle, this.titleContainer);
+ this.toggleGroups();
+ }
+
+ toggleGroups() {
+ this.isHidden = !this.isHidden;
+ this.groupTitle.classList.toggle('is-hidden');
+ }
+
+ render() {
+ this.titleContainer.classList.remove('initializing');
+ }
+}
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index ef4029a8623..47e675f537e 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -2,6 +2,7 @@
/* global Flash */
require('./flash');
+require('~/lib/utils/text_utility');
require('vendor/jquery.waitforimages');
require('./task_list');
@@ -50,20 +51,21 @@ class Issue {
success: function(data, textStatus, jqXHR) {
if ('id' in data) {
$(document).trigger('issuable:change');
- const currentTotal = Number($('.issue_counter').text());
+ let total = Number($('.issue_counter').text().replace(/[^\d]/, ''));
if (isClose) {
$('a.btn-close').addClass('hidden');
$('a.btn-reopen').removeClass('hidden');
$('div.status-box-closed').removeClass('hidden');
$('div.status-box-open').addClass('hidden');
- $('.issue_counter').text(currentTotal - 1);
+ total -= 1;
} else {
$('a.btn-reopen').addClass('hidden');
$('a.btn-close').removeClass('hidden');
$('div.status-box-closed').addClass('hidden');
$('div.status-box-open').removeClass('hidden');
- $('.issue_counter').text(currentTotal + 1);
+ total += 1;
}
+ $('.issue_counter').text(gl.text.addDelimiter(total));
} else {
new Flash(issueFailMessage, 'alert');
}
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index c648a0f076c..443fb3e0ca9 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -76,7 +76,7 @@
if (!selected.length) {
data[abilityName].label_ids = [''];
}
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js
index 66cc270ab4d..0e2af3df071 100644
--- a/app/assets/javascripts/merge_request_widget.js
+++ b/app/assets/javascripts/merge_request_widget.js
@@ -14,13 +14,13 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
<%= ci_success_icon %>
<span>
Deployed to
- <a href="<%- url %>" target="_blank" class="environment">
+ <a href="<%- url %>" target="_blank" rel="noopener noreferrer" class="environment">
<%- name %>
</a>
<span class="js-environment-timeago" data-toggle="tooltip" data-placement="top" data-title="<%- deployed_at_formatted %>">
<%- deployed_at %>
</span>
- <a class="js-environment-link" href="<%- external_url %>" target="_blank">
+ <a class="js-environment-link" href="<%- external_url %>" target="_blank" rel="noopener noreferrer">
<i class="fa fa-external-link"></i>
View on <%- external_url_formatted %>
</a>
@@ -176,7 +176,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
_this.opts.ci_sha = data.sha;
_this.updateCommitUrls(data.sha);
}
- if (showNotification) {
+ if (showNotification && data.status) {
status = _this.ciLabelForStatus(data.status);
if (status === "preparing") {
title = _this.opts.ci_title.preparing;
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 4c4f94cb9f3..40e977df693 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -19,7 +19,7 @@
}
$els.each(function(i, dropdown) {
- var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove;
+ var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, showStarted, useId, showMenuAbove;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
milestonesUrl = $dropdown.data('milestones');
@@ -29,6 +29,7 @@
showAny = $dropdown.data('show-any');
showMenuAbove = $dropdown.data('showMenuAbove');
showUpcoming = $dropdown.data('show-upcoming');
+ showStarted = $dropdown.data('show-started');
useId = $dropdown.data('use-id');
defaultLabel = $dropdown.data('default-label');
issuableId = $dropdown.data('issuable-id');
@@ -71,6 +72,13 @@
title: 'Upcoming'
});
}
+ if (showStarted) {
+ extraOptions.push({
+ id: -3,
+ name: '#started',
+ title: 'Started'
+ });
+ }
if (extraOptions.length) {
extraOptions.push('divider');
}
@@ -151,7 +159,7 @@
}
$dropdown.trigger('loading.gl.dropdown');
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () {
@@ -163,7 +171,7 @@
data = {};
data[abilityName] = {};
data[abilityName].milestone_id = selected != null ? selected : null;
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
new file mode 100644
index 00000000000..b3ce9310417
--- /dev/null
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -0,0 +1,6 @@
+import PrometheusGraph from './prometheus_graph';
+
+document.addEventListener('DOMContentLoaded', function onLoad() {
+ document.removeEventListener('DOMContentLoaded', onLoad, false);
+ return new PrometheusGraph();
+}, false);
diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js
index 71eb746edac..fcffc11a2df 100644
--- a/app/assets/javascripts/monitoring/prometheus_graph.js
+++ b/app/assets/javascripts/monitoring/prometheus_graph.js
@@ -2,10 +2,9 @@
/* global Flash */
import d3 from 'd3';
-import _ from 'underscore';
import statusCodes from '~/lib/utils/http_status';
-import '~/lib/utils/common_utils';
-import '~/flash';
+import '../lib/utils/common_utils';
+import '../flash';
const prometheusGraphsContainer = '.prometheus-graph';
const metricsEndpoint = 'metrics.json';
@@ -31,22 +30,21 @@ class PrometheusGraph {
}
createGraph() {
- const self = this;
- _.each(this.data, (value, key) => {
- if (value.length > 0 && (key === 'cpu_values' || key === 'memory_values')) {
- self.plotValues(value, key);
+ Object.keys(this.data).forEach((key) => {
+ const value = this.data[key];
+ if (value.length > 0) {
+ this.plotValues(value, key);
}
});
}
init() {
- const self = this;
this.getData().then((metricsResponse) => {
- if (metricsResponse === {}) {
+ if (Object.keys(metricsResponse).length === 0) {
new Flash('Empty metrics', 'alert');
} else {
- self.transformData(metricsResponse);
- self.createGraph();
+ this.transformData(metricsResponse);
+ this.createGraph();
}
});
}
@@ -321,12 +319,14 @@ class PrometheusGraph {
transformData(metricsResponse) {
const metricTypes = {};
- _.each(metricsResponse.metrics, (value, key) => {
- const metricValues = value[0].values;
- metricTypes[key] = _.map(metricValues, metric => ({
- time: new Date(metric[0] * 1000),
- value: metric[1],
- }));
+ Object.keys(metricsResponse.metrics).forEach((key) => {
+ if (key === 'cpu_values' || key === 'memory_values') {
+ const metricValues = (metricsResponse.metrics[key])[0];
+ metricTypes[key] = metricValues.values.map(metric => ({
+ time: new Date(metric[0] * 1000),
+ value: metric[1],
+ }));
+ }
});
this.data = metricTypes;
}
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 4ccea0624ee..c38bc762675 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -25,7 +25,6 @@
bindEvents() {
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
$('#user_notification_email').on('change', this.submitForm);
- $('#user_notified_of_own_activity').on('change', this.submitForm);
$('.update-username').on('ajax:before', this.beforeUpdateUsername);
$('.update-username').on('ajax:complete', this.afterUpdateUsername);
$('.update-notifications').on('ajax:success', this.onUpdateNotifs);
diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js
index caaf6484a34..8be58023c84 100644
--- a/app/assets/javascripts/todos.js
+++ b/app/assets/javascripts/todos.js
@@ -5,6 +5,7 @@ class Todos {
constructor() {
this.initFilters();
this.bindEvents();
+ this.todo_ids = [];
this.cleanupWrapper = this.cleanup.bind(this);
document.addEventListener('beforeunload', this.cleanupWrapper);
@@ -17,16 +18,16 @@ class Todos {
unbindEvents() {
$('.js-done-todo, .js-undo-todo, .js-add-todo').off('click', this.updateRowStateClickedWrapper);
- $('.js-todos-mark-all').off('click', this.allDoneClickedWrapper);
+ $('.js-todos-mark-all', '.js-todos-undo-all').off('click', this.updateallStateClickedWrapper);
$('.todo').off('click', this.goToTodoUrl);
}
bindEvents() {
this.updateRowStateClickedWrapper = this.updateRowStateClicked.bind(this);
- this.allDoneClickedWrapper = this.allDoneClicked.bind(this);
+ this.updateAllStateClickedWrapper = this.updateAllStateClicked.bind(this);
$('.js-done-todo, .js-undo-todo, .js-add-todo').on('click', this.updateRowStateClickedWrapper);
- $('.js-todos-mark-all').on('click', this.allDoneClickedWrapper);
+ $('.js-todos-mark-all, .js-todos-undo-all').on('click', this.updateAllStateClickedWrapper);
$('.todo').on('click', this.goToTodoUrl);
}
@@ -57,14 +58,14 @@ class Todos {
e.preventDefault();
const target = e.target;
- target.setAttribute('disabled', '');
+ target.setAttribute('disabled', true);
target.classList.add('disabled');
$.ajax({
type: 'POST',
- url: target.getAttribute('href'),
+ url: target.dataset.href,
dataType: 'json',
data: {
- '_method': target.getAttribute('data-method'),
+ '_method': target.dataset.method,
},
success: (data) => {
this.updateRowState(target);
@@ -73,25 +74,6 @@ class Todos {
});
}
- allDoneClicked(e) {
- e.preventDefault();
- const $target = $(e.currentTarget);
- $target.disable();
- $.ajax({
- type: 'POST',
- url: $target.attr('href'),
- dataType: 'json',
- data: {
- '_method': 'delete',
- },
- success: (data) => {
- $target.remove();
- $('.js-todos-all').html('<div class="nothing-here-block">You\'re all done!</div>');
- this.updateBadges(data);
- },
- });
- }
-
updateRowState(target) {
const row = target.closest('li');
const restoreBtn = row.querySelector('.js-undo-todo');
@@ -112,6 +94,41 @@ class Todos {
}
}
+ updateAllStateClicked(e) {
+ e.preventDefault();
+
+ const target = e.currentTarget;
+ const requestData = { '_method': target.dataset.method, ids: this.todo_ids };
+ target.setAttribute('disabled', true);
+ target.classList.add('disabled');
+ $.ajax({
+ type: 'POST',
+ url: target.dataset.href,
+ dataType: 'json',
+ data: requestData,
+ success: (data) => {
+ this.updateAllState(target, data);
+ return this.updateBadges(data);
+ },
+ });
+ }
+
+ updateAllState(target, data) {
+ const markAllDoneBtn = document.querySelector('.js-todos-mark-all');
+ const undoAllBtn = document.querySelector('.js-todos-undo-all');
+ const todoListContainer = document.querySelector('.js-todos-list-container');
+ const nothingHereContainer = document.querySelector('.js-nothing-here-container');
+
+ target.removeAttribute('disabled');
+ target.classList.remove('disabled');
+
+ this.todo_ids = (target === markAllDoneBtn) ? data.updated_ids : [];
+ undoAllBtn.classList.toggle('hidden');
+ markAllDoneBtn.classList.toggle('hidden');
+ todoListContainer.classList.toggle('hidden');
+ nothingHereContainer.classList.toggle('hidden');
+ }
+
updateBadges(data) {
$(document).trigger('todo:toggle', data.count);
document.querySelector('.todos-pending .badge').innerHTML = data.count;
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index c7a57b47834..eb897e9dfe9 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -53,7 +53,7 @@
$loading = $block.find('.block-loading').fadeOut();
var updateIssueBoardsIssue = function () {
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () {
$loading.fadeOut();
@@ -90,7 +90,7 @@
data = {};
data[abilityName] = {};
data[abilityName].assignee_id = selected != null ? selected : null;
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
diff --git a/app/assets/javascripts/vue_pipelines_index/components/async_button.js b/app/assets/javascripts/vue_pipelines_index/components/async_button.js
new file mode 100644
index 00000000000..aaebf29d8ae
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/async_button.js
@@ -0,0 +1,92 @@
+/* eslint-disable no-new, no-alert */
+/* global Flash */
+import '~/flash';
+import eventHub from '../event_hub';
+
+export default {
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+
+ title: {
+ type: String,
+ required: true,
+ },
+
+ icon: {
+ type: String,
+ required: true,
+ },
+
+ cssClass: {
+ type: String,
+ required: true,
+ },
+
+ confirmActionMessage: {
+ type: String,
+ required: false,
+ },
+ },
+
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+
+ computed: {
+ iconClass() {
+ return `fa fa-${this.icon}`;
+ },
+
+ buttonClass() {
+ return `btn has-tooltip ${this.cssClass}`;
+ },
+ },
+
+ methods: {
+ onClick() {
+ if (this.confirmActionMessage && confirm(this.confirmActionMessage)) {
+ this.makeRequest();
+ } else if (!this.confirmActionMessage) {
+ this.makeRequest();
+ }
+ },
+
+ makeRequest() {
+ this.isLoading = true;
+
+ this.service.postAction(this.endpoint)
+ .then(() => {
+ this.isLoading = false;
+ eventHub.$emit('refreshPipelines');
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occured while making the request.');
+ });
+ },
+ },
+
+ template: `
+ <button
+ type="button"
+ @click="onClick"
+ :class="buttonClass"
+ :title="title"
+ :aria-label="title"
+ data-placement="top"
+ :disabled="isLoading">
+ <i :class="iconClass" aria-hidden="true"/>
+ <i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading" />
+ </button>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js b/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js
new file mode 100644
index 00000000000..4e183d5c8ec
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js
@@ -0,0 +1,56 @@
+export default {
+ props: [
+ 'pipeline',
+ ],
+ computed: {
+ user() {
+ return !!this.pipeline.user;
+ },
+ },
+ template: `
+ <td>
+ <a
+ :href="pipeline.path"
+ class="js-pipeline-url-link">
+ <span class="pipeline-id">#{{pipeline.id}}</span>
+ </a>
+ <span>by</span>
+ <a
+ class="js-pipeline-url-user"
+ v-if="user"
+ :href="pipeline.user.web_url">
+ <img
+ v-if="user"
+ class="avatar has-tooltip s20 "
+ :title="pipeline.user.name"
+ data-container="body"
+ :src="pipeline.user.avatar_url"
+ >
+ </a>
+ <span
+ v-if="!user"
+ class="js-pipeline-url-api api monospace">
+ API
+ </span>
+ <span
+ v-if="pipeline.flags.latest"
+ class="js-pipeline-url-lastest label label-success has-tooltip"
+ title="Latest pipeline for this branch"
+ data-original-title="Latest pipeline for this branch">
+ latest
+ </span>
+ <span
+ v-if="pipeline.flags.yaml_errors"
+ class="js-pipeline-url-yaml label label-danger has-tooltip"
+ :title="pipeline.yaml_errors"
+ :data-original-title="pipeline.yaml_errors">
+ yaml invalid
+ </span>
+ <span
+ v-if="pipeline.flags.stuck"
+ class="js-pipeline-url-stuck label label-warning">
+ stuck
+ </span>
+ </td>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js
new file mode 100644
index 00000000000..4bb2b048884
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js
@@ -0,0 +1,71 @@
+/* eslint-disable no-new */
+/* global Flash */
+import '~/flash';
+import playIconSvg from 'icons/_icon_play.svg';
+import eventHub from '../event_hub';
+
+export default {
+ props: {
+ actions: {
+ type: Array,
+ required: true,
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ playIconSvg,
+ isLoading: false,
+ };
+ },
+
+ methods: {
+ onClickAction(endpoint) {
+ this.isLoading = true;
+
+ this.service.postAction(endpoint)
+ .then(() => {
+ this.isLoading = false;
+ eventHub.$emit('refreshPipelines');
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occured while making the request.');
+ });
+ },
+ },
+
+ template: `
+ <div class="btn-group" v-if="actions">
+ <button
+ type="button"
+ class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
+ title="Manual job"
+ data-toggle="dropdown"
+ data-placement="top"
+ aria-label="Manual job"
+ :disabled="isLoading">
+ ${playIconSvg}
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+ </button>
+
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for="action in actions">
+ <button
+ type="button"
+ class="js-pipeline-action-link no-btn"
+ @click="onClickAction(action.path)">
+ ${playIconSvg}
+ <span>{{action.name}}</span>
+ </button>
+ </li>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js
new file mode 100644
index 00000000000..3555040d60f
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js
@@ -0,0 +1,32 @@
+export default {
+ props: {
+ artifacts: {
+ type: Array,
+ required: true,
+ },
+ },
+
+ template: `
+ <div class="btn-group" role="group">
+ <button
+ class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
+ title="Artifacts"
+ data-placement="top"
+ data-toggle="dropdown"
+ aria-label="Artifacts">
+ <i class="fa fa-download" aria-hidden="true"></i>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for="artifact in artifacts">
+ <a
+ rel="nofollow"
+ :href="artifact.path">
+ <i class="fa fa-download" aria-hidden="true"></i>
+ <span>Download {{artifact.name}} artifacts</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/stage.js b/app/assets/javascripts/vue_pipelines_index/components/stage.js
new file mode 100644
index 00000000000..a2c29002707
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/stage.js
@@ -0,0 +1,116 @@
+/* global Flash */
+import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
+import createdSvg from 'icons/_icon_status_created_borderless.svg';
+import failedSvg from 'icons/_icon_status_failed_borderless.svg';
+import manualSvg from 'icons/_icon_status_manual_borderless.svg';
+import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
+import runningSvg from 'icons/_icon_status_running_borderless.svg';
+import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
+import successSvg from 'icons/_icon_status_success_borderless.svg';
+import warningSvg from 'icons/_icon_status_warning_borderless.svg';
+
+export default {
+ data() {
+ const svgsDictionary = {
+ icon_status_canceled: canceledSvg,
+ icon_status_created: createdSvg,
+ icon_status_failed: failedSvg,
+ icon_status_manual: manualSvg,
+ icon_status_pending: pendingSvg,
+ icon_status_running: runningSvg,
+ icon_status_skipped: skippedSvg,
+ icon_status_success: successSvg,
+ icon_status_warning: warningSvg,
+ };
+
+ return {
+ builds: '',
+ spinner: '<span class="fa fa-spinner fa-spin"></span>',
+ svg: svgsDictionary[this.stage.status.icon],
+ };
+ },
+
+ props: {
+ stage: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ updated() {
+ if (this.builds) {
+ this.stopDropdownClickPropagation();
+ }
+ },
+
+ methods: {
+ fetchBuilds(e) {
+ const ariaExpanded = e.currentTarget.attributes['aria-expanded'];
+
+ if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null;
+
+ return this.$http.get(this.stage.dropdown_path)
+ .then((response) => {
+ this.builds = JSON.parse(response.body).html;
+ }, () => {
+ const flash = new Flash('Something went wrong on our end.');
+ return flash;
+ });
+ },
+
+ /**
+ * When the user right clicks or cmd/ctrl + click in the job name
+ * the dropdown should not be closed and the link should open in another tab,
+ * so we stop propagation of the click event inside the dropdown.
+ *
+ * Since this component is rendered multiple times per page we need to guarantee we only
+ * target the click event of this component.
+ */
+ stopDropdownClickPropagation() {
+ $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => {
+ e.stopPropagation();
+ });
+ },
+ },
+ computed: {
+ buildsOrSpinner() {
+ return this.builds ? this.builds : this.spinner;
+ },
+ dropdownClass() {
+ if (this.builds) return 'js-builds-dropdown-container';
+ return 'js-builds-dropdown-loading builds-dropdown-loading';
+ },
+ buildStatus() {
+ return `Build: ${this.stage.status.label}`;
+ },
+ tooltip() {
+ return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
+ },
+ triggerButtonClass() {
+ return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
+ },
+ },
+ template: `
+ <div>
+ <button
+ @click="fetchBuilds($event)"
+ :class="triggerButtonClass"
+ :title="stage.title"
+ data-placement="top"
+ data-toggle="dropdown"
+ type="button"
+ :aria-label="stage.title">
+ <span v-html="svg" aria-hidden="true"></span>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
+ <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
+ <div class="arrow-up" aria-hidden="true"></div>
+ <div
+ :class="dropdownClass"
+ class="js-builds-dropdown-list scrollable-menu"
+ v-html="buildsOrSpinner">
+ </div>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/status.js b/app/assets/javascripts/vue_pipelines_index/components/status.js
new file mode 100644
index 00000000000..21a281af438
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/status.js
@@ -0,0 +1,60 @@
+import canceledSvg from 'icons/_icon_status_canceled.svg';
+import createdSvg from 'icons/_icon_status_created.svg';
+import failedSvg from 'icons/_icon_status_failed.svg';
+import manualSvg from 'icons/_icon_status_manual.svg';
+import pendingSvg from 'icons/_icon_status_pending.svg';
+import runningSvg from 'icons/_icon_status_running.svg';
+import skippedSvg from 'icons/_icon_status_skipped.svg';
+import successSvg from 'icons/_icon_status_success.svg';
+import warningSvg from 'icons/_icon_status_warning.svg';
+
+export default {
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ const svgsDictionary = {
+ icon_status_canceled: canceledSvg,
+ icon_status_created: createdSvg,
+ icon_status_failed: failedSvg,
+ icon_status_manual: manualSvg,
+ icon_status_pending: pendingSvg,
+ icon_status_running: runningSvg,
+ icon_status_skipped: skippedSvg,
+ icon_status_success: successSvg,
+ icon_status_warning: warningSvg,
+ };
+
+ return {
+ svg: svgsDictionary[this.pipeline.details.status.icon],
+ };
+ },
+
+ computed: {
+ cssClasses() {
+ return `ci-status ci-${this.pipeline.details.status.group}`;
+ },
+
+ detailsPath() {
+ const { status } = this.pipeline.details;
+ return status.has_details ? status.details_path : false;
+ },
+
+ content() {
+ return `${this.svg} ${this.pipeline.details.status.text}`;
+ },
+ },
+ template: `
+ <td class="commit-link">
+ <a
+ :class="cssClasses"
+ :href="detailsPath"
+ v-html="content">
+ </a>
+ </td>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/time_ago.js b/app/assets/javascripts/vue_pipelines_index/components/time_ago.js
new file mode 100644
index 00000000000..498d0715f54
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/time_ago.js
@@ -0,0 +1,71 @@
+import iconTimerSvg from 'icons/_icon_timer.svg';
+import '../../lib/utils/datetime_utility';
+
+export default {
+ data() {
+ return {
+ currentTime: new Date(),
+ iconTimerSvg,
+ };
+ },
+ props: ['pipeline'],
+ computed: {
+ timeAgo() {
+ return gl.utils.getTimeago();
+ },
+ localTimeFinished() {
+ return gl.utils.formatDate(this.pipeline.details.finished_at);
+ },
+ timeStopped() {
+ const changeTime = this.currentTime;
+ const options = {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ };
+ options.timeZoneName = 'short';
+ const finished = this.pipeline.details.finished_at;
+ if (!finished && changeTime) return false;
+ return ({ words: this.timeAgo.format(finished) });
+ },
+ duration() {
+ const { duration } = this.pipeline.details;
+ const date = new Date(duration * 1000);
+
+ let hh = date.getUTCHours();
+ let mm = date.getUTCMinutes();
+ let ss = date.getSeconds();
+
+ if (hh < 10) hh = `0${hh}`;
+ if (mm < 10) mm = `0${mm}`;
+ if (ss < 10) ss = `0${ss}`;
+
+ if (duration !== null) return `${hh}:${mm}:${ss}`;
+ return false;
+ },
+ },
+ methods: {
+ changeTime() {
+ this.currentTime = new Date();
+ },
+ },
+ template: `
+ <td class="pipelines-time-ago">
+ <p class="duration" v-if='duration'>
+ <span v-html="iconTimerSvg"></span>
+ {{duration}}
+ </p>
+ <p class="finished-at" v-if='timeStopped'>
+ <i class="fa fa-calendar"></i>
+ <time
+ data-toggle="tooltip"
+ data-placement="top"
+ data-container="body"
+ :data-original-title='localTimeFinished'>
+ {{timeStopped.words}}
+ </time>
+ </p>
+ </td>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/event_hub.js b/app/assets/javascripts/vue_pipelines_index/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/vue_pipelines_index/index.js b/app/assets/javascripts/vue_pipelines_index/index.js
index a90bd1518e9..b4e2d3a1143 100644
--- a/app/assets/javascripts/vue_pipelines_index/index.js
+++ b/app/assets/javascripts/vue_pipelines_index/index.js
@@ -1,29 +1,28 @@
-/* eslint-disable no-param-reassign */
-/* global Vue, VueResource, gl */
-window.Vue = require('vue');
+import PipelinesStore from './stores/pipelines_store';
+import PipelinesComponent from './pipelines';
+import '../vue_shared/vue_resource_interceptor';
+
+const Vue = window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
-require('../lib/utils/common_utils');
-require('../vue_shared/vue_resource_interceptor');
-require('./pipelines');
$(() => new Vue({
el: document.querySelector('.vue-pipelines-index'),
data() {
const project = document.querySelector('.pipelines');
+ const store = new PipelinesStore();
return {
- scope: project.dataset.url,
- store: new gl.PipelineStore(),
+ store,
+ endpoint: project.dataset.url,
};
},
components: {
- 'vue-pipelines': gl.VuePipelines,
+ 'vue-pipelines': PipelinesComponent,
},
template: `
<vue-pipelines
- :scope="scope"
- :store="store">
- </vue-pipelines>
+ :endpoint="endpoint"
+ :store="store" />
`,
}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js
deleted file mode 100644
index 891f1f17fb3..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js
+++ /dev/null
@@ -1,119 +0,0 @@
-/* global Vue, Flash, gl */
-/* eslint-disable no-param-reassign, no-alert */
-const playIconSvg = require('icons/_icon_play.svg');
-
-((gl) => {
- gl.VuePipelineActions = Vue.extend({
- props: ['pipeline'],
- computed: {
- actions() {
- return this.pipeline.details.manual_actions.length > 0;
- },
- artifacts() {
- return this.pipeline.details.artifacts.length > 0;
- },
- },
- methods: {
- download(name) {
- return `Download ${name} artifacts`;
- },
-
- /**
- * Shows a dialog when the user clicks in the cancel button.
- * We need to prevent the default behavior and stop propagation because the
- * link relies on UJS.
- *
- * @param {Event} event
- */
- confirmAction(event) {
- if (!confirm('Are you sure you want to cancel this pipeline?')) {
- event.preventDefault();
- event.stopPropagation();
- }
- },
- },
-
- data() {
- return { playIconSvg };
- },
-
- template: `
- <td class="pipeline-actions">
- <div class="pull-right">
- <div class="btn-group">
- <div class="btn-group" v-if="actions">
- <button
- class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
- data-toggle="dropdown"
- title="Manual job"
- data-placement="top"
- aria-label="Manual job">
- <span v-html="playIconSvg" aria-hidden="true"></span>
- <i class="fa fa-caret-down" aria-hidden="true"></i>
- </button>
- <ul class="dropdown-menu dropdown-menu-align-right">
- <li v-for='action in pipeline.details.manual_actions'>
- <a
- rel="nofollow"
- data-method="post"
- :href="action.path" >
- <span v-html="playIconSvg" aria-hidden="true"></span>
- <span>{{action.name}}</span>
- </a>
- </li>
- </ul>
- </div>
-
- <div class="btn-group" v-if="artifacts">
- <button
- class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
- title="Artifacts"
- data-placement="top"
- data-toggle="dropdown"
- aria-label="Artifacts">
- <i class="fa fa-download" aria-hidden="true"></i>
- <i class="fa fa-caret-down" aria-hidden="true"></i>
- </button>
- <ul class="dropdown-menu dropdown-menu-align-right">
- <li v-for='artifact in pipeline.details.artifacts'>
- <a
- rel="nofollow"
- :href="artifact.path">
- <i class="fa fa-download" aria-hidden="true"></i>
- <span>{{download(artifact.name)}}</span>
- </a>
- </li>
- </ul>
- </div>
- <div class="btn-group" v-if="pipeline.flags.retryable">
- <a
- class="btn btn-default btn-retry has-tooltip"
- title="Retry"
- rel="nofollow"
- data-method="post"
- data-placement="top"
- data-toggle="dropdown"
- :href='pipeline.retry_path'
- aria-label="Retry">
- <i class="fa fa-repeat" aria-hidden="true"></i>
- </a>
- </div>
- <div class="btn-group" v-if="pipeline.flags.cancelable">
- <a
- class="btn btn-remove has-tooltip"
- title="Cancel"
- rel="nofollow"
- data-method="post"
- data-placement="top"
- data-toggle="dropdown"
- :href='pipeline.cancel_path'
- aria-label="Cancel">
- <i class="fa fa-remove" aria-hidden="true"></i>
- </a>
- </div>
- </div>
- </div>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js
deleted file mode 100644
index ae5649f0519..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
- gl.VuePipelineUrl = Vue.extend({
- props: [
- 'pipeline',
- ],
- computed: {
- user() {
- return !!this.pipeline.user;
- },
- },
- template: `
- <td>
- <a :href='pipeline.path'>
- <span class="pipeline-id">#{{pipeline.id}}</span>
- </a>
- <span>by</span>
- <a
- v-if='user'
- :href='pipeline.user.web_url'
- >
- <img
- v-if='user'
- class="avatar has-tooltip s20 "
- :title='pipeline.user.name'
- data-container="body"
- :src='pipeline.user.avatar_url'
- >
- </a>
- <span
- v-if='!user'
- class="api monospace"
- >
- API
- </span>
- <span
- v-if='pipeline.flags.latest'
- class="label label-success has-tooltip"
- title="Latest pipeline for this branch"
- data-original-title="Latest pipeline for this branch"
- >
- latest
- </span>
- <span
- v-if='pipeline.flags.yaml_errors'
- class="label label-danger has-tooltip"
- :title='pipeline.yaml_errors'
- :data-original-title='pipeline.yaml_errors'
- >
- yaml invalid
- </span>
- <span
- v-if='pipeline.flags.stuck'
- class="label label-warning"
- >
- stuck
- </span>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js b/app/assets/javascripts/vue_pipelines_index/pipelines.js
index 601ef41e917..f389e5e4950 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipelines.js
+++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js
@@ -1,87 +1,121 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
+/* global Flash */
+/* eslint-disable no-new */
+import '~/flash';
+import Vue from 'vue';
+import PipelinesService from './services/pipelines_service';
+import eventHub from './event_hub';
+import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
+import TablePaginationComponent from '../vue_shared/components/table_pagination';
-window.Vue = require('vue');
-require('../vue_shared/components/table_pagination');
-require('./store');
-require('../vue_shared/components/pipelines_table');
-const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_store');
-
-((gl) => {
- gl.VuePipelines = Vue.extend({
-
- components: {
- 'gl-pagination': gl.VueGlPagination,
- 'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
+export default {
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
},
- data() {
- return {
- pipelines: [],
- timeLoopInterval: '',
- intervalId: '',
- apiScope: 'all',
- pageInfo: {},
- pagenum: 1,
- count: {},
- pageRequest: false,
- };
- },
- props: ['scope', 'store'],
- created() {
- const pagenum = gl.utils.getParameterByName('page');
- const scope = gl.utils.getParameterByName('scope');
- if (pagenum) this.pagenum = pagenum;
- if (scope) this.apiScope = scope;
-
- this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope);
+ store: {
+ type: Object,
+ required: true,
},
+ },
+
+ components: {
+ 'gl-pagination': TablePaginationComponent,
+ 'pipelines-table-component': PipelinesTableComponent,
+ },
+
+ data() {
+ return {
+ state: this.store.state,
+ apiScope: 'all',
+ pagenum: 1,
+ pageRequest: false,
+ };
+ },
+
+ created() {
+ this.service = new PipelinesService(this.endpoint);
+
+ this.fetchPipelines();
+
+ eventHub.$on('refreshPipelines', this.fetchPipelines);
+ },
+
+ beforeUpdate() {
+ if (this.state.pipelines.length && this.$children) {
+ this.store.startTimeAgoLoops.call(this, Vue);
+ }
+ },
- beforeUpdate() {
- if (this.pipelines.length && this.$children) {
- CommitPipelinesStoreWithTimeAgo.startTimeAgoLoops.call(this, Vue);
- }
+ beforeDestroyed() {
+ eventHub.$off('refreshPipelines');
+ },
+
+ methods: {
+ /**
+ * Will change the page number and update the URL.
+ *
+ * @param {Number} pageNumber desired page to go to.
+ */
+ change(pageNumber) {
+ const param = gl.utils.setParamInURL('page', pageNumber);
+
+ gl.utils.visitUrl(param);
+ return param;
},
- methods: {
- /**
- * Will change the page number and update the URL.
- *
- * @param {Number} pageNumber desired page to go to.
- */
- change(pageNumber) {
- const param = gl.utils.setParamInURL('page', pageNumber);
-
- gl.utils.visitUrl(param);
- return param;
- },
+ fetchPipelines() {
+ const pageNumber = gl.utils.getParameterByName('page') || this.pagenum;
+ const scope = gl.utils.getParameterByName('scope') || this.apiScope;
+
+ this.pageRequest = true;
+ return this.service.getPipelines(scope, pageNumber)
+ .then(resp => ({
+ headers: resp.headers,
+ body: resp.json(),
+ }))
+ .then((response) => {
+ this.store.storeCount(response.body.count);
+ this.store.storePipelines(response.body.pipelines);
+ this.store.storePagination(response.headers);
+ })
+ .then(() => {
+ this.pageRequest = false;
+ })
+ .catch(() => {
+ this.pageRequest = false;
+ new Flash('An error occurred while fetching the pipelines, please reload the page again.');
+ });
},
- template: `
- <div>
- <div class="pipelines realtime-loading" v-if='pageRequest'>
- <i class="fa fa-spinner fa-spin"></i>
- </div>
-
- <div class="blank-state blank-state-no-icon"
- v-if="!pageRequest && pipelines.length === 0">
- <h2 class="blank-state-title js-blank-state-title">
- No pipelines to show
- </h2>
- </div>
-
- <div class="table-holder" v-if='!pageRequest && pipelines.length'>
- <pipelines-table-component :pipelines='pipelines'/>
- </div>
-
- <gl-pagination
- v-if='!pageRequest && pipelines.length && pageInfo.total > pageInfo.perPage'
- :pagenum='pagenum'
- :change='change'
- :count='count.all'
- :pageInfo='pageInfo'
- >
- </gl-pagination>
+ },
+ template: `
+ <div>
+ <div class="pipelines realtime-loading" v-if="pageRequest">
+ <i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+ </div>
+
+ <div class="blank-state blank-state-no-icon"
+ v-if="!pageRequest && state.pipelines.length === 0">
+ <h2 class="blank-state-title js-blank-state-title">
+ No pipelines to show
+ </h2>
+ </div>
+
+ <div class="table-holder" v-if="!pageRequest && state.pipelines.length">
+ <pipelines-table-component
+ :pipelines="state.pipelines"
+ :service="service"/>
</div>
- `,
- });
-})(window.gl || (window.gl = {}));
+
+ <gl-pagination
+ v-if="!pageRequest && state.pipelines.length && state.pageInfo.total > state.pageInfo.perPage"
+ :pagenum="pagenum"
+ :change="change"
+ :count="state.count.all"
+ :pageInfo="state.pageInfo"
+ >
+ </gl-pagination>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js b/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js
new file mode 100644
index 00000000000..708f5068dd3
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js
@@ -0,0 +1,44 @@
+/* eslint-disable class-methods-use-this */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+export default class PipelinesService {
+
+ /**
+ * Commits and merge request endpoints need to be requested with `.json`.
+ *
+ * The url provided to request the pipelines in the new merge request
+ * page already has `.json`.
+ *
+ * @param {String} root
+ */
+ constructor(root) {
+ let endpoint;
+
+ if (root.indexOf('.json') === -1) {
+ endpoint = `${root}.json`;
+ } else {
+ endpoint = root;
+ }
+
+ this.pipelines = Vue.resource(endpoint);
+ }
+
+ getPipelines(scope, page) {
+ return this.pipelines.get({ scope, page });
+ }
+
+ /**
+ * Post request for all pipelines actions.
+ * Endpoint content type needs to be:
+ * `Content-Type:application/x-www-form-urlencoded`
+ *
+ * @param {String} endpoint
+ * @return {Promise}
+ */
+ postAction(endpoint) {
+ return Vue.http.post(endpoint, {}, { emulateJSON: true });
+ }
+}
diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js b/app/assets/javascripts/vue_pipelines_index/stage.js
deleted file mode 100644
index ae4f0b4a53b..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/stage.js
+++ /dev/null
@@ -1,119 +0,0 @@
-/* global Vue, Flash, gl */
-/* eslint-disable no-param-reassign */
-import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
-import createdSvg from 'icons/_icon_status_created_borderless.svg';
-import failedSvg from 'icons/_icon_status_failed_borderless.svg';
-import manualSvg from 'icons/_icon_status_manual_borderless.svg';
-import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
-import runningSvg from 'icons/_icon_status_running_borderless.svg';
-import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
-import successSvg from 'icons/_icon_status_success_borderless.svg';
-import warningSvg from 'icons/_icon_status_warning_borderless.svg';
-
-((gl) => {
- gl.VueStage = Vue.extend({
- data() {
- const svgsDictionary = {
- icon_status_canceled: canceledSvg,
- icon_status_created: createdSvg,
- icon_status_failed: failedSvg,
- icon_status_manual: manualSvg,
- icon_status_pending: pendingSvg,
- icon_status_running: runningSvg,
- icon_status_skipped: skippedSvg,
- icon_status_success: successSvg,
- icon_status_warning: warningSvg,
- };
-
- return {
- builds: '',
- spinner: '<span class="fa fa-spinner fa-spin"></span>',
- svg: svgsDictionary[this.stage.status.icon],
- };
- },
-
- props: {
- stage: {
- type: Object,
- required: true,
- },
- },
-
- updated() {
- if (this.builds) {
- this.stopDropdownClickPropagation();
- }
- },
-
- methods: {
- fetchBuilds(e) {
- const areaExpanded = e.currentTarget.attributes['aria-expanded'];
-
- if (areaExpanded && (areaExpanded.textContent === 'true')) return null;
-
- return this.$http.get(this.stage.dropdown_path)
- .then((response) => {
- this.builds = JSON.parse(response.body).html;
- }, () => {
- const flash = new Flash('Something went wrong on our end.');
- return flash;
- });
- },
-
- /**
- * When the user right clicks or cmd/ctrl + click in the job name
- * the dropdown should not be closed and the link should open in another tab,
- * so we stop propagation of the click event inside the dropdown.
- *
- * Since this component is rendered multiple times per page we need to guarantee we only
- * target the click event of this component.
- */
- stopDropdownClickPropagation() {
- $(this.$el).on('click', '.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item', (e) => {
- e.stopPropagation();
- });
- },
- },
- computed: {
- buildsOrSpinner() {
- return this.builds ? this.builds : this.spinner;
- },
- dropdownClass() {
- if (this.builds) return 'js-builds-dropdown-container';
- return 'js-builds-dropdown-loading builds-dropdown-loading';
- },
- buildStatus() {
- return `Build: ${this.stage.status.label}`;
- },
- tooltip() {
- return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
- },
- triggerButtonClass() {
- return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
- },
- },
- template: `
- <div>
- <button
- @click="fetchBuilds($event)"
- :class="triggerButtonClass"
- :title="stage.title"
- data-placement="top"
- data-toggle="dropdown"
- type="button"
- :aria-label="stage.title">
- <span v-html="svg" aria-hidden="true"></span>
- <i class="fa fa-caret-down" aria-hidden="true"></i>
- </button>
- <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
- <div class="arrow-up" aria-hidden="true"></div>
- <div
- :class="dropdownClass"
- class="js-builds-dropdown-list scrollable-menu"
- v-html="buildsOrSpinner">
- </div>
- </ul>
- </div>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/status.js b/app/assets/javascripts/vue_pipelines_index/status.js
deleted file mode 100644
index 8d9f83ac113..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/status.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-import canceledSvg from 'icons/_icon_status_canceled.svg';
-import createdSvg from 'icons/_icon_status_created.svg';
-import failedSvg from 'icons/_icon_status_failed.svg';
-import manualSvg from 'icons/_icon_status_manual.svg';
-import pendingSvg from 'icons/_icon_status_pending.svg';
-import runningSvg from 'icons/_icon_status_running.svg';
-import skippedSvg from 'icons/_icon_status_skipped.svg';
-import successSvg from 'icons/_icon_status_success.svg';
-import warningSvg from 'icons/_icon_status_warning.svg';
-
-((gl) => {
- gl.VueStatusScope = Vue.extend({
- props: [
- 'pipeline',
- ],
-
- data() {
- const svgsDictionary = {
- icon_status_canceled: canceledSvg,
- icon_status_created: createdSvg,
- icon_status_failed: failedSvg,
- icon_status_manual: manualSvg,
- icon_status_pending: pendingSvg,
- icon_status_running: runningSvg,
- icon_status_skipped: skippedSvg,
- icon_status_success: successSvg,
- icon_status_warning: warningSvg,
- };
-
- return {
- svg: svgsDictionary[this.pipeline.details.status.icon],
- };
- },
-
- computed: {
- cssClasses() {
- const cssObject = { 'ci-status': true };
- cssObject[`ci-${this.pipeline.details.status.group}`] = true;
- return cssObject;
- },
-
- detailsPath() {
- const { status } = this.pipeline.details;
- return status.has_details ? status.details_path : false;
- },
-
- content() {
- return `${this.svg} ${this.pipeline.details.status.text}`;
- },
- },
- template: `
- <td class="commit-link">
- <a
- :class="cssClasses"
- :href="detailsPath"
- v-html="content">
- </a>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/store.js b/app/assets/javascripts/vue_pipelines_index/store.js
deleted file mode 100644
index 909007267b9..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/store.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/* global gl, Flash */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
- const pageValues = (headers) => {
- const normalized = gl.utils.normalizeHeaders(headers);
- const paginationInfo = gl.utils.parseIntPagination(normalized);
- return paginationInfo;
- };
-
- gl.PipelineStore = class {
- fetchDataLoop(Vue, pageNum, url, apiScope) {
- this.pageRequest = true;
-
- return this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
- .then((response) => {
- const pageInfo = pageValues(response.headers);
- this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
-
- const res = JSON.parse(response.body);
- this.count = Object.assign({}, this.count, res.count);
- this.pipelines = Object.assign([], this.pipelines, res.pipelines);
-
- this.pageRequest = false;
- }, () => {
- this.pageRequest = false;
- return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
- });
- }
- };
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
index 9d6abbc057e..6f19e46c7dc 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_store.js
+++ b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
@@ -1,33 +1,48 @@
/* eslint-disable no-underscore-dangle*/
-/**
- * Pipelines' Store for commits view.
- *
- * Used to store the Pipelines rendered in the commit view in the pipelines table.
- */
-const VueRealtimeListener = require('../../vue_realtime_listener');
+import VueRealtimeListener from '../../vue_realtime_listener';
gl.VueRealtimeListener = VueRealtimeListener;
-class PipelinesStore {
+export default class PipelinesStore {
constructor() {
this.state = {};
+
this.state.pipelines = [];
+ this.state.count = {};
+ this.state.pageInfo = {};
}
storePipelines(pipelines = []) {
this.state.pipelines = pipelines;
+ }
+
+ storeCount(count = {}) {
+ this.state.count = count;
+ }
- return pipelines;
+ storePagination(pagination = {}) {
+ let paginationInfo;
+
+ if (Object.keys(pagination).length) {
+ const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
+ paginationInfo = gl.utils.parseIntPagination(normalizedHeaders);
+ } else {
+ paginationInfo = pagination;
+ }
+
+ this.state.pageInfo = paginationInfo;
}
/**
+ * FIXME: Move this inside the component.
+ *
* Once the data is received we will start the time ago loops.
*
* Everytime a request is made like retry or cancel a pipeline, every 10 seconds we
* update the time to show how long as passed.
*
*/
- static startTimeAgoLoops() {
+ startTimeAgoLoops() {
const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => {
this.$children[0].$children.reduce((acc, component) => {
@@ -46,5 +61,3 @@ class PipelinesStore {
gl.VueRealtimeListener(removeIntervals, startIntervals);
}
}
-
-module.exports = PipelinesStore;
diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js b/app/assets/javascripts/vue_pipelines_index/time_ago.js
deleted file mode 100644
index a383570857d..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/time_ago.js
+++ /dev/null
@@ -1,78 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-window.Vue = require('vue');
-require('../lib/utils/datetime_utility');
-
-const iconTimerSvg = require('../../../views/shared/icons/_icon_timer.svg');
-
-((gl) => {
- gl.VueTimeAgo = Vue.extend({
- data() {
- return {
- currentTime: new Date(),
- iconTimerSvg,
- };
- },
- props: ['pipeline'],
- computed: {
- timeAgo() {
- return gl.utils.getTimeago();
- },
- localTimeFinished() {
- return gl.utils.formatDate(this.pipeline.details.finished_at);
- },
- timeStopped() {
- const changeTime = this.currentTime;
- const options = {
- weekday: 'long',
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- };
- options.timeZoneName = 'short';
- const finished = this.pipeline.details.finished_at;
- if (!finished && changeTime) return false;
- return ({ words: this.timeAgo.format(finished) });
- },
- duration() {
- const { duration } = this.pipeline.details;
- const date = new Date(duration * 1000);
-
- let hh = date.getUTCHours();
- let mm = date.getUTCMinutes();
- let ss = date.getSeconds();
-
- if (hh < 10) hh = `0${hh}`;
- if (mm < 10) mm = `0${mm}`;
- if (ss < 10) ss = `0${ss}`;
-
- if (duration !== null) return `${hh}:${mm}:${ss}`;
- return false;
- },
- },
- methods: {
- changeTime() {
- this.currentTime = new Date();
- },
- },
- template: `
- <td class="pipelines-time-ago">
- <p class="duration" v-if='duration'>
- <span v-html="iconTimerSvg"></span>
- {{duration}}
- </p>
- <p class="finished-at" v-if='timeStopped'>
- <i class="fa fa-calendar"></i>
- <time
- data-toggle="tooltip"
- data-placement="top"
- data-container="body"
- :data-original-title='localTimeFinished'>
- {{timeStopped.words}}
- </time>
- </p>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_shared/components/commit.js b/app/assets/javascripts/vue_shared/components/commit.js
index 4381487b79e..fb68abd95a2 100644
--- a/app/assets/javascripts/vue_shared/components/commit.js
+++ b/app/assets/javascripts/vue_shared/components/commit.js
@@ -1,164 +1,157 @@
-/* global Vue */
-window.Vue = require('vue');
-const commitIconSvg = require('icons/_icon_commit.svg');
-
-(() => {
- window.gl = window.gl || {};
-
- window.gl.CommitComponent = Vue.component('commit-component', {
-
- props: {
- /**
- * Indicates the existance of a tag.
- * Used to render the correct icon, if true will render `fa-tag` icon,
- * if false will render `fa-code-fork` icon.
- */
- tag: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- /**
- * If provided is used to render the branch name and url.
- * Should contain the following properties:
- * name
- * ref_url
- */
- commitRef: {
- type: Object,
- required: false,
- default: () => ({}),
- },
-
- /**
- * Used to link to the commit sha.
- */
- commitUrl: {
- type: String,
- required: false,
- default: '',
- },
-
- /**
- * Used to show the commit short sha that links to the commit url.
- */
- shortSha: {
- type: String,
- required: false,
- default: '',
- },
-
- /**
- * If provided shows the commit tile.
- */
- title: {
- type: String,
- required: false,
- default: '',
- },
-
- /**
- * If provided renders information about the author of the commit.
- * When provided should include:
- * `avatar_url` to render the avatar icon
- * `web_url` to link to user profile
- * `username` to render alt and title tags
- */
- author: {
- type: Object,
- required: false,
- default: () => ({}),
- },
+import commitIconSvg from 'icons/_icon_commit.svg';
+
+export default {
+ props: {
+ /**
+ * Indicates the existance of a tag.
+ * Used to render the correct icon, if true will render `fa-tag` icon,
+ * if false will render `fa-code-fork` icon.
+ */
+ tag: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- computed: {
- /**
- * Used to verify if all the properties needed to render the commit
- * ref section were provided.
- *
- * TODO: Improve this! Use lodash _.has when we have it.
- *
- * @returns {Boolean}
- */
- hasCommitRef() {
- return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
- },
-
- /**
- * Used to verify if all the properties needed to render the commit
- * author section were provided.
- *
- * TODO: Improve this! Use lodash _.has when we have it.
- *
- * @returns {Boolean}
- */
- hasAuthor() {
- return this.author &&
- this.author.avatar_url &&
- this.author.web_url &&
- this.author.username;
- },
-
- /**
- * If information about the author is provided will return a string
- * to be rendered as the alt attribute of the img tag.
- *
- * @returns {String}
- */
- userImageAltDescription() {
- return this.author &&
- this.author.username ? `${this.author.username}'s avatar` : null;
- },
+ /**
+ * If provided is used to render the branch name and url.
+ * Should contain the following properties:
+ * name
+ * ref_url
+ */
+ commitRef: {
+ type: Object,
+ required: false,
+ default: () => ({}),
},
- data() {
- return { commitIconSvg };
+ /**
+ * Used to link to the commit sha.
+ */
+ commitUrl: {
+ type: String,
+ required: false,
+ default: '',
},
- template: `
- <div class="branch-commit">
-
- <div v-if="hasCommitRef" class="icon-container">
- <i v-if="tag" class="fa fa-tag"></i>
- <i v-if="!tag" class="fa fa-code-fork"></i>
- </div>
-
- <a v-if="hasCommitRef"
- class="monospace branch-name"
- :href="commitRef.ref_url">
- {{commitRef.name}}
- </a>
-
- <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
-
- <a class="commit-id monospace"
- :href="commitUrl">
- {{shortSha}}
- </a>
-
- <p class="commit-title">
- <span v-if="title">
- <a v-if="hasAuthor"
- class="avatar-image-container"
- :href="author.web_url">
- <img
- class="avatar has-tooltip s20"
- :src="author.avatar_url"
- :alt="userImageAltDescription"
- :title="author.username" />
- </a>
-
- <a class="commit-row-message"
- :href="commitUrl">
- {{title}}
- </a>
- </span>
- <span v-else>
- Cant find HEAD commit for this branch
- </span>
- </p>
+ /**
+ * Used to show the commit short sha that links to the commit url.
+ */
+ shortSha: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * If provided shows the commit tile.
+ */
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * If provided renders information about the author of the commit.
+ * When provided should include:
+ * `avatar_url` to render the avatar icon
+ * `web_url` to link to user profile
+ * `username` to render alt and title tags
+ */
+ author: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+
+ computed: {
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * ref section were provided.
+ *
+ * TODO: Improve this! Use lodash _.has when we have it.
+ *
+ * @returns {Boolean}
+ */
+ hasCommitRef() {
+ return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
+ },
+
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * author section were provided.
+ *
+ * TODO: Improve this! Use lodash _.has when we have it.
+ *
+ * @returns {Boolean}
+ */
+ hasAuthor() {
+ return this.author &&
+ this.author.avatar_url &&
+ this.author.web_url &&
+ this.author.username;
+ },
+
+ /**
+ * If information about the author is provided will return a string
+ * to be rendered as the alt attribute of the img tag.
+ *
+ * @returns {String}
+ */
+ userImageAltDescription() {
+ return this.author &&
+ this.author.username ? `${this.author.username}'s avatar` : null;
+ },
+ },
+
+ data() {
+ return { commitIconSvg };
+ },
+
+ template: `
+ <div class="branch-commit">
+
+ <div v-if="hasCommitRef" class="icon-container">
+ <i v-if="tag" class="fa fa-tag"></i>
+ <i v-if="!tag" class="fa fa-code-fork"></i>
</div>
- `,
- });
-})();
+
+ <a v-if="hasCommitRef"
+ class="monospace branch-name"
+ :href="commitRef.ref_url">
+ {{commitRef.name}}
+ </a>
+
+ <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
+
+ <a class="commit-id monospace"
+ :href="commitUrl">
+ {{shortSha}}
+ </a>
+
+ <p class="commit-title">
+ <span v-if="title">
+ <a v-if="hasAuthor"
+ class="avatar-image-container"
+ :href="author.web_url">
+ <img
+ class="avatar has-tooltip s20"
+ :src="author.avatar_url"
+ :alt="userImageAltDescription"
+ :title="author.username" />
+ </a>
+
+ <a class="commit-row-message"
+ :href="commitUrl">
+ {{title}}
+ </a>
+ </span>
+ <span v-else>
+ Cant find HEAD commit for this branch
+ </span>
+ </p>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js b/app/assets/javascripts/vue_shared/components/pipelines_table.js
index 0d8f85db965..afd8d7acf6b 100644
--- a/app/assets/javascripts/vue_shared/components/pipelines_table.js
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js
@@ -1,52 +1,48 @@
-/* eslint-disable no-param-reassign */
-/* global Vue */
+import PipelinesTableRowComponent from './pipelines_table_row';
-require('./pipelines_table_row');
/**
* Pipelines Table Component.
*
* Given an array of objects, renders a table.
*/
-
-(() => {
- window.gl = window.gl || {};
- gl.pipelines = gl.pipelines || {};
-
- gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', {
-
- props: {
- pipelines: {
- type: Array,
- required: true,
- default: () => ([]),
- },
-
+export default {
+ props: {
+ pipelines: {
+ type: Array,
+ required: true,
+ default: () => ([]),
},
- components: {
- 'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent,
+ service: {
+ type: Object,
+ required: true,
},
+ },
+
+ components: {
+ 'pipelines-table-row-component': PipelinesTableRowComponent,
+ },
- template: `
- <table class="table ci-table">
- <thead>
- <tr>
- <th class="js-pipeline-status pipeline-status">Status</th>
- <th class="js-pipeline-info pipeline-info">Pipeline</th>
- <th class="js-pipeline-commit pipeline-commit">Commit</th>
- <th class="js-pipeline-stages pipeline-stages">Stages</th>
- <th class="js-pipeline-date pipeline-date"></th>
- <th class="js-pipeline-actions pipeline-actions"></th>
- </tr>
- </thead>
- <tbody>
- <template v-for="model in pipelines"
- v-bind:model="model">
- <tr is="pipelines-table-row-component"
- :pipeline="model"></tr>
- </template>
- </tbody>
- </table>
- `,
- });
-})();
+ template: `
+ <table class="table ci-table">
+ <thead>
+ <tr>
+ <th class="js-pipeline-status pipeline-status">Status</th>
+ <th class="js-pipeline-info pipeline-info">Pipeline</th>
+ <th class="js-pipeline-commit pipeline-commit">Commit</th>
+ <th class="js-pipeline-stages pipeline-stages">Stages</th>
+ <th class="js-pipeline-date pipeline-date"></th>
+ <th class="js-pipeline-actions pipeline-actions"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <template v-for="model in pipelines"
+ v-bind:model="model">
+ <tr is="pipelines-table-row-component"
+ :pipeline="model"
+ :service="service"></tr>
+ </template>
+ </tbody>
+ </table>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
index e5e88186a85..f5b3cb9214e 100644
--- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
@@ -1,199 +1,228 @@
/* eslint-disable no-param-reassign */
-/* global Vue */
-
-require('../../vue_pipelines_index/status');
-require('../../vue_pipelines_index/pipeline_url');
-require('../../vue_pipelines_index/stage');
-require('../../vue_pipelines_index/pipeline_actions');
-require('../../vue_pipelines_index/time_ago');
-require('./commit');
+
+import AsyncButtonComponent from '../../vue_pipelines_index/components/async_button';
+import PipelinesActionsComponent from '../../vue_pipelines_index/components/pipelines_actions';
+import PipelinesArtifactsComponent from '../../vue_pipelines_index/components/pipelines_artifacts';
+import PipelinesStatusComponent from '../../vue_pipelines_index/components/status';
+import PipelinesStageComponent from '../../vue_pipelines_index/components/stage';
+import PipelinesUrlComponent from '../../vue_pipelines_index/components/pipeline_url';
+import PipelinesTimeagoComponent from '../../vue_pipelines_index/components/time_ago';
+import CommitComponent from './commit';
+
/**
* Pipeline table row.
*
* Given the received object renders a table row in the pipelines' table.
*/
-(() => {
- window.gl = window.gl || {};
- gl.pipelines = gl.pipelines || {};
-
- gl.pipelines.PipelinesTableRowComponent = Vue.component('pipelines-table-row-component', {
-
- props: {
- pipeline: {
- type: Object,
- required: true,
- default: () => ({}),
- },
+export default {
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ components: {
+ 'async-button-component': AsyncButtonComponent,
+ 'pipelines-actions-component': PipelinesActionsComponent,
+ 'pipelines-artifacts-component': PipelinesArtifactsComponent,
+ 'commit-component': CommitComponent,
+ 'dropdown-stage': PipelinesStageComponent,
+ 'pipeline-url': PipelinesUrlComponent,
+ 'status-scope': PipelinesStatusComponent,
+ 'time-ago': PipelinesTimeagoComponent,
+ },
+
+ computed: {
+ /**
+ * If provided, returns the commit tag.
+ * Needed to render the commit component column.
+ *
+ * This field needs a lot of verification, because of different possible cases:
+ *
+ * 1. person who is an author of a commit might be a GitLab user
+ * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
+ * 3. If GitLab user does not have avatar he/she might have a Gravatar
+ * 4. If committer is not a GitLab User he/she can have a Gravatar
+ * 5. We do not have consistent API object in this case
+ * 6. We should improve API and the code
+ *
+ * @returns {Object|Undefined}
+ */
+ commitAuthor() {
+ let commitAuthorInformation;
+
+ // 1. person who is an author of a commit might be a GitLab user
+ if (this.pipeline &&
+ this.pipeline.commit &&
+ this.pipeline.commit.author) {
+ // 2. if person who is an author of a commit is a GitLab user
+ // he/she can have a GitLab avatar
+ if (this.pipeline.commit.author.avatar_url) {
+ commitAuthorInformation = this.pipeline.commit.author;
+
+ // 3. If GitLab user does not have avatar he/she might have a Gravatar
+ } else if (this.pipeline.commit.author_gravatar_url) {
+ commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
+ avatar_url: this.pipeline.commit.author_gravatar_url,
+ });
+ }
+ }
+
+ // 4. If committer is not a GitLab User he/she can have a Gravatar
+ if (this.pipeline &&
+ this.pipeline.commit) {
+ commitAuthorInformation = {
+ avatar_url: this.pipeline.commit.author_gravatar_url,
+ web_url: `mailto:${this.pipeline.commit.author_email}`,
+ username: this.pipeline.commit.author_name,
+ };
+ }
+
+ return commitAuthorInformation;
},
- components: {
- 'commit-component': gl.CommitComponent,
- 'pipeline-actions': gl.VuePipelineActions,
- 'dropdown-stage': gl.VueStage,
- 'pipeline-url': gl.VuePipelineUrl,
- 'status-scope': gl.VueStatusScope,
- 'time-ago': gl.VueTimeAgo,
+ /**
+ * If provided, returns the commit tag.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTag() {
+ if (this.pipeline.ref &&
+ this.pipeline.ref.tag) {
+ return this.pipeline.ref.tag;
+ }
+ return undefined;
},
- computed: {
- /**
- * If provided, returns the commit tag.
- * Needed to render the commit component column.
- *
- * This field needs a lot of verification, because of different possible cases:
- *
- * 1. person who is an author of a commit might be a GitLab user
- * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
- * 3. If GitLab user does not have avatar he/she might have a Gravatar
- * 4. If committer is not a GitLab User he/she can have a Gravatar
- * 5. We do not have consistent API object in this case
- * 6. We should improve API and the code
- *
- * @returns {Object|Undefined}
- */
- commitAuthor() {
- let commitAuthorInformation;
-
- // 1. person who is an author of a commit might be a GitLab user
- if (this.pipeline &&
- this.pipeline.commit &&
- this.pipeline.commit.author) {
- // 2. if person who is an author of a commit is a GitLab user
- // he/she can have a GitLab avatar
- if (this.pipeline.commit.author.avatar_url) {
- commitAuthorInformation = this.pipeline.commit.author;
-
- // 3. If GitLab user does not have avatar he/she might have a Gravatar
- } else if (this.pipeline.commit.author_gravatar_url) {
- commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
- avatar_url: this.pipeline.commit.author_gravatar_url,
- });
+ /**
+ * If provided, returns the commit ref.
+ * Needed to render the commit component column.
+ *
+ * Matches `path` prop sent in the API to `ref_url` prop needed
+ * in the commit component.
+ *
+ * @returns {Object|Undefined}
+ */
+ commitRef() {
+ if (this.pipeline.ref) {
+ return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
+ if (prop === 'path') {
+ accumulator.ref_url = this.pipeline.ref[prop];
+ } else {
+ accumulator[prop] = this.pipeline.ref[prop];
}
- }
+ return accumulator;
+ }, {});
+ }
- // 4. If committer is not a GitLab User he/she can have a Gravatar
- if (this.pipeline &&
- this.pipeline.commit) {
- commitAuthorInformation = {
- avatar_url: this.pipeline.commit.author_gravatar_url,
- web_url: `mailto:${this.pipeline.commit.author_email}`,
- username: this.pipeline.commit.author_name,
- };
- }
+ return undefined;
+ },
- return commitAuthorInformation;
- },
-
- /**
- * If provided, returns the commit tag.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitTag() {
- if (this.pipeline.ref &&
- this.pipeline.ref.tag) {
- return this.pipeline.ref.tag;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit ref.
- * Needed to render the commit component column.
- *
- * Matches `path` prop sent in the API to `ref_url` prop needed
- * in the commit component.
- *
- * @returns {Object|Undefined}
- */
- commitRef() {
- if (this.pipeline.ref) {
- return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
- if (prop === 'path') {
- accumulator.ref_url = this.pipeline.ref[prop];
- } else {
- accumulator[prop] = this.pipeline.ref[prop];
- }
- return accumulator;
- }, {});
- }
+ /**
+ * If provided, returns the commit url.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitUrl() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.commit_path) {
+ return this.pipeline.commit.commit_path;
+ }
+ return undefined;
+ },
- return undefined;
- },
-
- /**
- * If provided, returns the commit url.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitUrl() {
- if (this.pipeline.commit &&
- this.pipeline.commit.commit_path) {
- return this.pipeline.commit.commit_path;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit short sha.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitShortSha() {
- if (this.pipeline.commit &&
- this.pipeline.commit.short_id) {
- return this.pipeline.commit.short_id;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit title.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitTitle() {
- if (this.pipeline.commit &&
- this.pipeline.commit.title) {
- return this.pipeline.commit.title;
- }
- return undefined;
- },
+ /**
+ * If provided, returns the commit short sha.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitShortSha() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.short_id) {
+ return this.pipeline.commit.short_id;
+ }
+ return undefined;
},
- template: `
- <tr class="commit">
- <status-scope :pipeline="pipeline"/>
-
- <pipeline-url :pipeline="pipeline"></pipeline-url>
-
- <td>
- <commit-component
- :tag="commitTag"
- :commit-ref="commitRef"
- :commit-url="commitUrl"
- :short-sha="commitShortSha"
- :title="commitTitle"
- :author="commitAuthor"/>
- </td>
-
- <td class="stage-cell">
- <div class="stage-container dropdown js-mini-pipeline-graph"
- v-if="pipeline.details.stages.length > 0"
- v-for="stage in pipeline.details.stages">
- <dropdown-stage :stage="stage"/>
- </div>
- </td>
-
- <time-ago :pipeline="pipeline"/>
-
- <pipeline-actions :pipeline="pipeline" />
- </tr>
- `,
- });
-})();
+ /**
+ * If provided, returns the commit title.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTitle() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.title) {
+ return this.pipeline.commit.title;
+ }
+ return undefined;
+ },
+ },
+
+ template: `
+ <tr class="commit">
+ <status-scope :pipeline="pipeline"/>
+
+ <pipeline-url :pipeline="pipeline"></pipeline-url>
+
+ <td>
+ <commit-component
+ :tag="commitTag"
+ :commit-ref="commitRef"
+ :commit-url="commitUrl"
+ :short-sha="commitShortSha"
+ :title="commitTitle"
+ :author="commitAuthor"/>
+ </td>
+
+ <td class="stage-cell">
+ <div class="stage-container dropdown js-mini-pipeline-graph"
+ v-if="pipeline.details.stages.length > 0"
+ v-for="stage in pipeline.details.stages">
+ <dropdown-stage :stage="stage"/>
+ </div>
+ </td>
+
+ <time-ago :pipeline="pipeline"/>
+
+ <td class="pipeline-actions">
+ <div class="pull-right btn-group">
+ <pipelines-actions-component
+ v-if="pipeline.details.manual_actions.length"
+ :actions="pipeline.details.manual_actions"
+ :service="service" />
+
+ <pipelines-artifacts-component
+ v-if="pipeline.details.artifacts.length"
+ :artifacts="pipeline.details.artifacts" />
+
+ <async-button-component
+ v-if="pipeline.flags.retryable"
+ :service="service"
+ :endpoint="pipeline.retry_path"
+ css-class="js-pipelines-retry-button btn-default btn-retry"
+ title="Retry"
+ icon="repeat" />
+
+ <async-button-component
+ v-if="pipeline.flags.cancelable"
+ :service="service"
+ :endpoint="pipeline.cancel_path"
+ css-class="js-pipelines-cancel-button btn-remove"
+ title="Cancel"
+ icon="remove"
+ confirm-action-message="Are you sure you want to cancel this pipeline?" />
+ </div>
+ </td>
+ </tr>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js b/app/assets/javascripts/vue_shared/components/table_pagination.js
index 8943b850a72..b9cd28f6249 100644
--- a/app/assets/javascripts/vue_shared/components/table_pagination.js
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.js
@@ -1,147 +1,135 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign, no-plusplus */
-
-window.Vue = require('vue');
-
-((gl) => {
- const PAGINATION_UI_BUTTON_LIMIT = 4;
- const UI_LIMIT = 6;
- const SPREAD = '...';
- const PREV = 'Prev';
- const NEXT = 'Next';
- const FIRST = '<< First';
- const LAST = 'Last >>';
-
- gl.VueGlPagination = Vue.extend({
- props: {
-
- // TODO: Consider refactoring in light of turbolinks removal.
-
- /**
- This function will take the information given by the pagination component
-
- Here is an example `change` method:
-
- change(pagenum) {
- gl.utils.visitUrl(`?page=${pagenum}`);
- },
- */
-
- change: {
- type: Function,
- required: true,
+const PAGINATION_UI_BUTTON_LIMIT = 4;
+const UI_LIMIT = 6;
+const SPREAD = '...';
+const PREV = 'Prev';
+const NEXT = 'Next';
+const FIRST = '<< First';
+const LAST = 'Last >>';
+
+export default {
+ props: {
+ /**
+ This function will take the information given by the pagination component
+
+ Here is an example `change` method:
+
+ change(pagenum) {
+ gl.utils.visitUrl(`?page=${pagenum}`);
},
+ */
+ change: {
+ type: Function,
+ required: true,
+ },
- /**
- pageInfo will come from the headers of the API call
- in the `.then` clause of the VueResource API call
- there should be a function that contructs the pageInfo for this component
-
- This is an example:
-
- const pageInfo = headers => ({
- perPage: +headers['X-Per-Page'],
- page: +headers['X-Page'],
- total: +headers['X-Total'],
- totalPages: +headers['X-Total-Pages'],
- nextPage: +headers['X-Next-Page'],
- previousPage: +headers['X-Prev-Page'],
- });
- */
-
- pageInfo: {
- type: Object,
- required: true,
- },
+ /**
+ pageInfo will come from the headers of the API call
+ in the `.then` clause of the VueResource API call
+ there should be a function that contructs the pageInfo for this component
+
+ This is an example:
+
+ const pageInfo = headers => ({
+ perPage: +headers['X-Per-Page'],
+ page: +headers['X-Page'],
+ total: +headers['X-Total'],
+ totalPages: +headers['X-Total-Pages'],
+ nextPage: +headers['X-Next-Page'],
+ previousPage: +headers['X-Prev-Page'],
+ });
+ */
+ pageInfo: {
+ type: Object,
+ required: true,
},
- methods: {
- changePage(e) {
- const text = e.target.innerText;
- const { totalPages, nextPage, previousPage } = this.pageInfo;
-
- switch (text) {
- case SPREAD:
- break;
- case LAST:
- this.change(totalPages);
- break;
- case NEXT:
- this.change(nextPage);
- break;
- case PREV:
- this.change(previousPage);
- break;
- case FIRST:
- this.change(1);
- break;
- default:
- this.change(+text);
- break;
- }
- },
+ },
+ methods: {
+ changePage(e) {
+ const text = e.target.innerText;
+ const { totalPages, nextPage, previousPage } = this.pageInfo;
+
+ switch (text) {
+ case SPREAD:
+ break;
+ case LAST:
+ this.change(totalPages);
+ break;
+ case NEXT:
+ this.change(nextPage);
+ break;
+ case PREV:
+ this.change(previousPage);
+ break;
+ case FIRST:
+ this.change(1);
+ break;
+ default:
+ this.change(+text);
+ break;
+ }
},
- computed: {
- prev() {
- return this.pageInfo.previousPage;
- },
- next() {
- return this.pageInfo.nextPage;
- },
- getItems() {
- const total = this.pageInfo.totalPages;
- const page = this.pageInfo.page;
- const items = [];
+ },
+ computed: {
+ prev() {
+ return this.pageInfo.previousPage;
+ },
+ next() {
+ return this.pageInfo.nextPage;
+ },
+ getItems() {
+ const total = this.pageInfo.totalPages;
+ const page = this.pageInfo.page;
+ const items = [];
- if (page > 1) items.push({ title: FIRST });
+ if (page > 1) items.push({ title: FIRST });
- if (page > 1) {
- items.push({ title: PREV, prev: true });
- } else {
- items.push({ title: PREV, disabled: true, prev: true });
- }
+ if (page > 1) {
+ items.push({ title: PREV, prev: true });
+ } else {
+ items.push({ title: PREV, disabled: true, prev: true });
+ }
- if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
+ if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
- const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
- const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
+ const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
+ const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
- for (let i = start; i <= end; i++) {
- const isActive = i === page;
- items.push({ title: i, active: isActive, page: true });
- }
+ for (let i = start; i <= end; i += 1) {
+ const isActive = i === page;
+ items.push({ title: i, active: isActive, page: true });
+ }
- if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
- items.push({ title: SPREAD, separator: true, page: true });
- }
+ if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
+ items.push({ title: SPREAD, separator: true, page: true });
+ }
- if (page === total) {
- items.push({ title: NEXT, disabled: true, next: true });
- } else if (total - page >= 1) {
- items.push({ title: NEXT, next: true });
- }
+ if (page === total) {
+ items.push({ title: NEXT, disabled: true, next: true });
+ } else if (total - page >= 1) {
+ items.push({ title: NEXT, next: true });
+ }
- if (total - page >= 1) items.push({ title: LAST, last: true });
+ if (total - page >= 1) items.push({ title: LAST, last: true });
- return items;
- },
+ return items;
},
- template: `
- <div class="gl-pagination">
- <ul class="pagination clearfix">
- <li v-for='item in getItems'
- :class='{
- page: item.page,
- prev: item.prev,
- next: item.next,
- separator: item.separator,
- active: item.active,
- disabled: item.disabled
- }'
- >
- <a @click="changePage($event)">{{item.title}}</a>
- </li>
- </ul>
- </div>
- `,
- });
-})(window.gl || (window.gl = {}));
+ },
+ template: `
+ <div class="gl-pagination">
+ <ul class="pagination clearfix">
+ <li v-for='item in getItems'
+ :class='{
+ page: item.page,
+ prev: item.prev,
+ next: item.next,
+ separator: item.separator,
+ active: item.active,
+ disabled: item.disabled
+ }'
+ >
+ <a @click="changePage($event)">{{item.title}}</a>
+ </li>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
index 4157fefddc9..f1c1e553b16 100644
--- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
+++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
@@ -1,11 +1,13 @@
-/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars,
-no-param-reassign, no-plusplus */
-/* global Vue */
+/* eslint-disable no-param-reassign, no-plusplus */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
Vue.http.interceptors.push((request, next) => {
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
- next((response) => {
+ next(() => {
Vue.activeResources--;
});
});
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index f363affa46c..546718ddaf8 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -96,11 +96,9 @@
.award-control {
margin: 3px 5px 3px 0;
- padding: 5px 6px;
+ padding: .35em .4em;
outline: 0;
- line-height: 1;
-
&.disabled {
cursor: default;
@@ -140,10 +138,12 @@
}
.icon,
+ gl-emoji,
.award-control-icon {
- float: left;
- margin-right: 5px;
- font-size: 18px;
+ vertical-align: middle;
+ margin-right: 0.15em;
+ font-size: 1.5em;
+ line-height: 1;
}
.award-control-icon-loading {
@@ -154,4 +154,8 @@
color: $border-gray-normal;
margin-top: 1px;
}
+
+ .award-control-text {
+ vertical-align: middle;
+ }
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index a4b38723bbd..2c33b235980 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -429,3 +429,9 @@ table {
@include str-truncated(100%);
}
}
+
+.tooltip {
+ .tooltip-inner {
+ word-wrap: break-word;
+ }
+}
diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index 0a8bc95590e..d86ae57cd9a 100644
--- a/app/assets/stylesheets/framework/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -2,5 +2,6 @@ gl-emoji {
display: inline-block;
display: inline-flex;
vertical-align: middle;
+ font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1.5em;
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index dd2daa4b872..51805c5d734 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -76,12 +76,14 @@
}
.input-token {
- flex: 1;
- -webkit-flex: 1;
+ max-width: 200px;
}
- .filtered-search-token + .input-token:not(:last-child) {
- max-width: 200px;
+ .input-token:only-child,
+ .input-token:last-child {
+ flex: 1;
+ -webkit-flex: 1;
+ max-width: initial;
}
}
@@ -144,7 +146,7 @@
.scroll-container {
display: -webkit-flex;
display: flex;
- overflow-x: scroll;
+ overflow-x: auto;
white-space: nowrap;
width: 100%;
}
@@ -158,8 +160,8 @@
background-color: $white-light;
@media (max-width: $screen-xs-min) {
- -webkit-flex: 1 1 100%;
- flex: 1 1 100%;
+ -webkit-flex: 1 1 auto;
+ flex: 1 1 auto;
margin-bottom: 10px;
.dropdown-menu {
@@ -171,17 +173,26 @@
}
}
+ &:hover {
+ @extend .form-control:hover;
+ }
+
+ &.focus,
+ &.focus:hover {
+ border-color: $dropdown-input-focus-border;
+ box-shadow: 0 0 4px $search-input-focus-shadow-color;
+ }
+
+ &.focus .fa-filter {
+ color: $common-gray-dark;
+ }
+
.form-control {
position: relative;
min-width: 200px;
- padding-left: 0;
- padding-right: 25px;
+ padding: 5px 25px 6px 0;
border-color: transparent;
- &:focus ~ .fa-filter {
- color: $common-gray-dark;
- }
-
&:focus,
&:hover {
outline: none;
@@ -221,6 +232,10 @@
.filter-dropdown-container {
display: -webkit-flex;
display: flex;
+
+ .dropdown-toggle {
+ line-height: 22px;
+ }
}
.dropdown-menu .filter-dropdown-item {
@@ -246,7 +261,9 @@
background-color: $white-light;
border-top: 0;
}
+}
+@media (max-width: $screen-xs) {
.filter-dropdown-container {
.dropdown-toggle,
.dropdown {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 5d1aba4e529..6660a022260 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -164,11 +164,25 @@ header {
}
}
+ .group-name-toggle {
+ margin: 0 5px;
+ vertical-align: sub;
+ }
+
+ .group-title {
+ &.is-hidden {
+ .hidable:not(:last-of-type) {
+ display: none;
+ }
+ }
+ }
+
.title {
position: relative;
padding-right: 20px;
margin: 0;
font-size: 18px;
+ max-width: 385px;
display: inline-block;
line-height: $header-height;
font-weight: normal;
@@ -178,6 +192,14 @@ header {
vertical-align: top;
white-space: nowrap;
+ &.initializing {
+ display: none;
+ }
+
+ @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
+ max-width: 300px;
+ }
+
@media (max-width: $screen-xs-max) {
max-width: 190px;
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index ea45aaa0253..205d23b1329 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -138,7 +138,6 @@
.nav-links {
display: inline-block;
- width: 50%;
margin-bottom: 0;
border-bottom: none;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index db5e2c51fe7..c241816788b 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -306,6 +306,11 @@ a > code {
* Textareas intended for GFM
*
*/
+textarea.js-gfm-input {
+ font-family: $monospace_font;
+ font-size: 13px;
+}
+
.strikethrough {
text-decoration: line-through;
}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 9a36d76136b..f9ee33019cd 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -420,12 +420,9 @@
display: -webkit-flex;
display: flex;
- .form-control {
- margin-left: auto;
-
- @media (min-width: $screen-sm-min) {
- max-width: 200px;
- }
+ .issues-filters {
+ -webkit-flex: 1;
+ flex: 1;
}
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 2029b6893ef..da8410eca66 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -38,6 +38,38 @@
}
}
+.pipeline-info {
+ .status-icon-container {
+ display: inline-block;
+ vertical-align: middle;
+ margin-right: 3px;
+
+ svg {
+ display: block;
+ width: 22px;
+ height: 22px;
+ }
+ }
+
+ .mr-widget-pipeline-graph {
+ display: inline-block;
+ vertical-align: middle;
+ margin: 0 -6px 0 0;
+
+ .dropdown-menu {
+ margin-top: 11px;
+ }
+ }
+}
+
+.branch-info .commit-icon {
+ margin-right: 3px;
+
+ svg {
+ top: 3px;
+ }
+}
+
/*
* Commit message textarea for web editor and
* custom merge request message
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 5b777953fb0..ad3dbc7ac48 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -369,13 +369,11 @@
// Custom CSS for components
.item-conmmit-component {
.commit-icon {
- position: relative;
- top: 3px;
- left: 1px;
- display: inline-block;
-
svg {
- float: left;
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ vertical-align: bottom;
}
}
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index b595480561b..cb7ebd61504 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -56,7 +56,10 @@ ul.related-merge-requests > li {
.merge-request-id {
display: inline-block;
- width: 3em;
+}
+
+.merge-request-info {
+ margin-left: 5px;
}
.merge-request-status {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index c2156a5ac69..927bf9805ce 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -148,6 +148,18 @@
.error-alert > .alert {
margin-top: 5px;
margin-bottom: 5px;
+
+ &.alert-dismissable {
+ .close {
+ color: $white-light;
+ opacity: 0.85;
+ font-weight: normal;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+ }
}
.discussion-body,
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 20eabc83142..33b38ca6923 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -72,11 +72,6 @@
color: $gl-text-color-secondary;
font-size: 14px;
}
-
- svg,
- .fa {
- margin-right: 0;
- }
}
.btn-group {
@@ -921,3 +916,22 @@
}
}
}
+
+/**
+ * Play button with icon in dropdowns
+ */
+.ci-table .no-btn {
+ border: none;
+ background: none;
+ outline: none;
+ width: 100%;
+ text-align: left;
+
+ .icon-play {
+ position: relative;
+ top: 2px;
+ margin-right: 5px;
+ height: 13px;
+ width: 12px;
+ }
+}
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index 2992568ae66..a8c0937569c 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -37,7 +37,6 @@ module ServiceParams
:namespace,
:new_issue_url,
:notify,
- :notify_only_broken_builds,
:notify_only_broken_pipelines,
:password,
:priority,
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 325ae565537..be00d765f73 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -42,7 +42,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
private
def load_projects(base_scope)
- projects = base_scope.sorted_by_activity.includes(:namespace)
+ projects = base_scope.sorted_by_activity.includes(:route, namespace: :route)
filter_projects(projects)
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 5848ca62777..096de8032ae 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -22,12 +22,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end
def destroy_all
- TodoService.new.mark_todos_as_done(@todos, current_user)
+ updated_ids = TodoService.new.mark_todos_as_done(@todos, current_user)
respond_to do |format|
format.html { redirect_to dashboard_todos_path, notice: 'All todos were marked as done.' }
format.js { head :ok }
- format.json { render json: todos_counts }
+ format.json { render json: todos_counts.merge(updated_ids: updated_ids) }
end
end
@@ -37,6 +37,12 @@ class Dashboard::TodosController < Dashboard::ApplicationController
render json: todos_counts
end
+ def bulk_restore
+ TodoService.new.mark_todos_as_pending_by_ids(params[:ids], current_user)
+
+ render json: todos_counts
+ end
+
# Used in TodosHelper also
def self.todos_count_format(count)
count >= 100 ? '99+' : count
@@ -45,7 +51,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
private
def find_todos
- @todos ||= TodosFinder.new(current_user, params).execute
+ @todos ||= TodosFinder.new(current_user, params.merge(include_associations: true)).execute
end
def todos_counts
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 26e17a7553e..6167f9bd335 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -2,7 +2,7 @@ class Explore::ProjectsController < Explore::ApplicationController
include FilterProjects
def index
- @projects = ProjectsFinder.new.execute(current_user)
+ @projects = load_projects
@tags = @projects.tags_on(:tags)
@projects = @projects.tagged_with(params[:tag]) if params[:tag].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@@ -21,7 +21,8 @@ class Explore::ProjectsController < Explore::ApplicationController
end
def trending
- @projects = filter_projects(Project.trending)
+ @projects = load_projects(Project.trending)
+ @projects = filter_projects(@projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
@@ -36,7 +37,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end
def starred
- @projects = ProjectsFinder.new.execute(current_user)
+ @projects = load_projects
@projects = filter_projects(@projects)
@projects = @projects.reorder('star_count DESC')
@projects = @projects.page(params[:page])
@@ -50,4 +51,11 @@ class Explore::ProjectsController < Explore::ApplicationController
end
end
end
+
+ protected
+
+ def load_projects(base_scope = nil)
+ base_scope ||= ProjectsFinder.new.execute(current_user)
+ base_scope.includes(:route, namespace: :route)
+ end
end
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index a271e2dfc4b..b8b71d295f6 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -17,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end
def user_params
- params.require(:user).permit(:notification_email, :notified_of_own_activity)
+ params.require(:user).permit(:notification_email)
end
end
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index e2f81b09adc..f1a93ccb3ad 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -89,4 +89,9 @@ class Projects::ApplicationController < ApplicationController
def builds_enabled
return render_404 unless @project.feature_available?(:builds, current_user)
end
+
+ def update_ref
+ branch_exists = @repository.find_branch(@target_branch)
+ @ref = @target_branch if branch_exists
+ end
end
diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb
index 863a766a255..6461eeac11c 100644
--- a/app/controllers/projects/blame_controller.rb
+++ b/app/controllers/projects/blame_controller.rb
@@ -8,9 +8,12 @@ class Projects::BlameController < Projects::ApplicationController
def show
@blob = @repository.blob_at(@commit.id, @path)
-
+
return render_404 unless @blob
+ environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
+ @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
+
@blame_groups = Gitlab::Blame.new(@blob, @commit).groups
end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 52fc67d162c..80a95c6158b 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -89,11 +89,6 @@ class Projects::BlobController < Projects::ApplicationController
private
- def update_ref
- branch_exists = @repository.find_branch(@target_branch)
- @ref = @target_branch if branch_exists
- end
-
def blob
@blob ||= Blob.decorate(@repository.blob_at(@commit.id, @path))
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 22714d9c5a4..840405f38cb 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -11,16 +11,18 @@ class Projects::BranchesController < Projects::ApplicationController
@sort = params[:sort].presence || sort_value_name
@branches = BranchesFinder.new(@repository, params).execute
- @branches = Kaminari.paginate_array(@branches).page(params[:page]) unless params[:show_all].present?
-
respond_to do |format|
format.html do
+ paginate_branches
+ @refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
+
@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
end
format.json do
+ paginate_branches unless params[:show_all]
render json: @branches.map(&:name)
end
end
@@ -91,6 +93,10 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
+ def paginate_branches
+ @branches = Kaminari.paginate_array(@branches).page(params[:page])
+ end
+
def url_to_autodeploy_setup(project, branch_name)
namespace_project_new_blob_path(
project.namespace,
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index cfd2c46fbd8..7f81280673b 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -6,6 +6,8 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuableCollections
include SpammableActions
+ prepend_before_action :authenticate_user!, only: [:new]
+
before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
@@ -146,7 +148,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
format.json do
- render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
+ render json: @issue.to_json(include: { milestone: {}, assignee: { only: [:name, :username], methods: [:avatar_url] }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 82f9b6e06db..677a8a1a73a 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -308,7 +308,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
format.json do
- render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
+ render json: @merge_request.to_json(include: { milestone: {}, assignee: { only: [:name, :username], methods: [:avatar_url] }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
end
end
rescue ActiveRecord::StaleObjectError
diff --git a/app/controllers/projects/settings/members_controller.rb b/app/controllers/projects/settings/members_controller.rb
index cbfa2afa959..54f9dceddef 100644
--- a/app/controllers/projects/settings/members_controller.rb
+++ b/app/controllers/projects/settings/members_controller.rb
@@ -9,6 +9,7 @@ module Projects
@skip_groups = @group_links.pluck(:group_id)
@skip_groups << @project.namespace_id unless @project.personal?
+ @skip_groups += @project.group.ancestors.pluck(:id) if @project.group
@project_members = MembersFinder.new(@project, current_user).execute
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index ea7e4d9f663..e13f0bde315 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -14,7 +14,9 @@ class Projects::TagsController < Projects::ApplicationController
@tags = TagsFinder.new(@repository, params).execute
@tags = Kaminari.paginate_array(@tags).page(params[:page])
- @releases = project.releases.where(tag: @tags.map(&:name))
+ tag_names = @tags.map(&:name)
+ @tags_pipelines = @project.pipelines.latest_successful_for_refs(tag_names)
+ @releases = project.releases.where(tag: tag_names)
end
def show
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index 4f094146348..637b61504d8 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -34,6 +34,7 @@ class Projects::TreeController < Projects::ApplicationController
def create_dir
return render_404 unless @commit_params.values.all?
+ update_ref
create_commit(Files::CreateDirService, success_notice: "The directory has been successfully created.",
success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@target_branch, @dir_name)),
failure_path: namespace_project_tree_path(@project.namespace, @project, @ref))
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 8b6c83d4fed..f210f7e61d2 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -45,8 +45,9 @@ class Projects::WikisController < Projects::ApplicationController
return render('empty') unless can?(current_user, :create_wiki, @project)
@page = @project_wiki.find_page(params[:id])
+ @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page)
- if @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page)
+ if @page.valid?
redirect_to(
namespace_project_wiki_path(@project.namespace, @project, @page),
notice: 'Wiki was successfully updated.'
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 3e2015b7d5e..47f7e0b1b28 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -117,7 +117,7 @@ class ProjectsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :remove_project, @project)
::Projects::DestroyService.new(@project, current_user, {}).async_execute
- flash[:alert] = "Project '#{@project.name}' will be deleted."
+ flash[:alert] = "Project '#{@project.name_with_namespace}' will be deleted."
redirect_to dashboard_projects_path
rescue Projects::DestroyService::DestroyError => ex
@@ -267,8 +267,9 @@ class ProjectsController < Projects::ApplicationController
@project_wiki = @project.wiki
@wiki_home = @project_wiki.find_page('home', params[:version_id])
elsif @project.feature_available?(:issues, current_user)
- @issues = issues_collection
- @issues = @issues.page(params[:page])
+ @issues = issues_collection.page(params[:page])
+ @collection_type = 'Issue'
+ @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
end
render :show
@@ -315,6 +316,7 @@ class ProjectsController < Projects::ApplicationController
:namespace_id,
:only_allow_merge_if_all_discussions_are_resolved,
:only_allow_merge_if_pipeline_succeeds,
+ :printing_merge_request_link_enabled,
:path,
:public_builds,
:request_access_enabled,
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 2fca012252e..f7ebb1807d7 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -310,6 +310,10 @@ class IssuableFinder
params[:milestone_title] == Milestone::Upcoming.name
end
+ def filter_by_started_milestone?
+ params[:milestone_title] == Milestone::Started.name
+ end
+
def by_milestone(items)
if milestones?
if filter_by_no_milestone?
@@ -317,6 +321,8 @@ class IssuableFinder
elsif filter_by_upcoming_milestone?
upcoming_ids = Milestone.upcoming_ids_by_projects(projects(items))
items = items.left_joins_milestones.where(milestone_id: upcoming_ids)
+ elsif filter_by_started_milestone?
+ items = items.left_joins_milestones.where('milestones.start_date <= NOW()')
else
items = items.with_milestone(params[:milestone_title])
items_projects = projects(items)
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index b7f091f334d..13d33a1c31b 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -24,6 +24,7 @@ class TodosFinder
def execute
items = current_user.todos
+ items = include_associations(items)
items = by_action_id(items)
items = by_action(items)
items = by_author(items)
@@ -38,6 +39,17 @@ class TodosFinder
private
+ def include_associations(items)
+ return items unless params[:include_associations]
+
+ items.includes(
+ [
+ target: { project: [:route, namespace: :route] },
+ author: { namespace: :route },
+ ]
+ )
+ end
+
def action_id?
action_id.present? && Todo::ACTION_NAMES.has_key?(action_id.to_i)
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 7f32c1b5300..8631bc54509 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -203,4 +203,18 @@ module BlobHelper
'blob-language' => @blob && @blob.language.try(:ace_mode)
}
end
+
+ def copy_file_path_button(file_path)
+ clipboard_button(clipboard_text: file_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard')
+ end
+
+ def copy_blob_content_button(blob)
+ return if markup?(blob.name)
+
+ clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm", title: "Copy content to clipboard")
+ end
+
+ def open_raw_file_button(path)
+ link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw', data: { container: 'body' }
+ end
end
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index a7cdca9ba2e..2de9e0de310 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -59,6 +59,24 @@ module CiStatusHelper
custom_icon(icon_name)
end
+ def pipeline_status_cache_key(pipeline_status)
+ "pipeline-status/#{pipeline_status.sha}-#{pipeline_status.status}"
+ end
+
+ def render_project_pipeline_status(pipeline_status, tooltip_placement: 'auto left')
+ project = pipeline_status.project
+ path = pipelines_namespace_project_commit_path(
+ project.namespace,
+ project,
+ pipeline_status.sha)
+
+ render_status_with_link(
+ 'commit',
+ pipeline_status.status,
+ path,
+ tooltip_placement: tooltip_placement)
+ end
+
def render_commit_status(commit, ref: nil, tooltip_placement: 'auto left')
project = commit.project
path = pipelines_namespace_project_commit_path(
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 8aad39e148b..cef624430da 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -211,7 +211,7 @@ module CommitsHelper
external_url = environment.external_url_for(diff_new_path, commit_sha)
return unless external_url
- link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do
+ link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do
icon('external-link')
end
end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index 6226cfe25cf..cd442237086 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -172,7 +172,9 @@ module GitlabMarkdownHelper
# text hasn't already been truncated, then append "..." to the node contents
# and return true. Otherwise return false.
def truncate_if_block(node, truncated)
- if node.element? && node.description&.block? && !truncated
+ return true if truncated
+
+ if node.element? && (node.description&.block? || node.matches?('pre > code > .line'))
node.inner_html = "#{node.inner_html}..." if node.next_sibling
true
else
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 926c9703628..a6014088e92 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -12,17 +12,18 @@ module GroupsHelper
end
def group_title(group, name = nil, url = nil)
+ @has_group_title = true
full_title = ''
group.ancestors.each do |parent|
- full_title += link_to(simple_sanitize(parent.name), group_path(parent))
- full_title += ' / '.html_safe
+ full_title += link_to(simple_sanitize(parent.name), group_path(parent), class: 'group-path hidable')
+ full_title += '<span class="hidable"> / </span>'.html_safe
end
- full_title += link_to(simple_sanitize(group.name), group_path(group))
- full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url) if name
+ full_title += link_to(simple_sanitize(group.name), group_path(group), class: 'group-path')
+ full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path') if name
- content_tag :span do
+ content_tag :span, class: 'group-title' do
full_title.html_safe
end
end
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
index a0642a1894b..a57b5a8fea5 100644
--- a/app/helpers/import_helper.rb
+++ b/app/helpers/import_helper.rb
@@ -7,7 +7,7 @@ module ImportHelper
def provider_project_link(provider, path_with_namespace)
url = __send__("#{provider}_project_url", path_with_namespace)
- link_to path_with_namespace, url, target: '_blank'
+ link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer'
end
private
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index aad83731b87..a777db2826b 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -90,11 +90,14 @@ module IssuablesHelper
end
def milestone_dropdown_label(milestone_title, default_label = "Milestone")
- if milestone_title == Milestone::Upcoming.name
- milestone_title = Milestone::Upcoming.title
- end
+ title =
+ case milestone_title
+ when Milestone::Upcoming.name then Milestone::Upcoming.title
+ when Milestone::Started.name then Milestone::Started.title
+ else milestone_title.presence
+ end
- h(milestone_title.presence || default_label)
+ h(title || default_label)
end
def to_url_reference(issuable)
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 7011e670cee..5053b937c02 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -82,12 +82,13 @@ module MilestonesHelper
def milestone_remaining_days(milestone)
if milestone.expired?
content_tag(:strong, 'Past due')
- elsif milestone.due_date
- days = milestone.remaining_days
- content = content_tag(:strong, days)
- content << " #{'day'.pluralize(days)} remaining"
elsif milestone.upcoming?
content_tag(:strong, 'Upcoming')
+ elsif milestone.due_date
+ time_ago = time_ago_in_words(milestone.due_date)
+ content = time_ago.gsub(/\d+/) { |match| "<strong>#{match}</strong>" }
+ content.slice!("about ")
+ content << " remaining"
elsif milestone.start_date && milestone.start_date.past?
days = milestone.elapsed_days
content = content_tag(:strong, days)
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index c1523b4dabf..a8f167cbff2 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -16,6 +16,7 @@ module NavHelper
"page-gutter build-sidebar right-sidebar-expanded"
elsif current_path?('wikis#show') ||
current_path?('wikis#edit') ||
+ current_path?('wikis#update') ||
current_path?('wikis#history') ||
current_path?('wikis#git_access')
"page-gutter wiki-sidebar right-sidebar-expanded"
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 4befeacc135..bd0c2cd661e 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -159,6 +159,13 @@ module ProjectsHelper
choose a GitLab CI Yaml template and commit your changes. #{link_to_autodeploy_doc}".html_safe
end
+ def project_list_cache_key(project)
+ key = [project.namespace.cache_key, project.cache_key, controller.controller_name, controller.action_name, current_application_settings.cache_key, 'v2.3']
+ key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status?
+
+ key
+ end
+
private
def repo_children_classes(field)
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 18734f1411f..959ee310867 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -16,7 +16,8 @@ module SortingHelper
sort_value_oldest_signin => sort_title_oldest_signin,
sort_value_downvotes => sort_title_downvotes,
sort_value_upvotes => sort_title_upvotes,
- sort_value_priority => sort_title_priority
+ sort_value_priority => sort_title_priority,
+ sort_value_label_priority => sort_title_label_priority
}
end
@@ -50,6 +51,10 @@ module SortingHelper
end
def sort_title_priority
+ 'Priority'
+ end
+
+ def sort_title_label_priority
'Label priority'
end
@@ -161,6 +166,10 @@ module SortingHelper
'priority'
end
+ def sort_value_label_priority
+ 'label_priority'
+ end
+
def sort_value_oldest_updated
'updated_asc'
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 7f8efb0a4ac..847a8fdfca6 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -39,9 +39,13 @@ module TodosHelper
namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project,
todo.target, anchor: anchor)
else
- path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target]
-
- path.unshift(:pipelines) if todo.build_failed?
+ if todo.build_failed?
+ # associated namespace and route would be loaded from the db again if todo.project was used
+ project = todo.target.project
+ path = [:pipelines, project.namespace.becomes(Namespace), project, todo.target]
+ else
+ path = [todo.target]
+ end
polymorphic_path(path, anchor: anchor)
end
@@ -99,8 +103,7 @@ module TodosHelper
end
def todo_projects_options
- projects = current_user.authorized_projects.sorted_by_activity.non_archived
- projects = projects.includes(:namespace)
+ projects = current_user.authorized_projects.sorted_by_activity.non_archived.with_route
projects = projects.map do |project|
{ id: project.id, text: project.name_with_namespace }
diff --git a/app/mailers/emails/builds.rb b/app/mailers/emails/builds.rb
deleted file mode 100644
index 3853af6201a..00000000000
--- a/app/mailers/emails/builds.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module Emails
- module Builds
- def build_fail_email(build_id, to)
- @build = Ci::Build.find(build_id)
- @project = @build.project
-
- add_project_headers
- add_build_headers('failed')
-
- mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha))
- end
-
- def build_success_email(build_id, to)
- @build = Ci::Build.find(build_id)
- @project = @build.project
-
- add_project_headers
- add_build_headers('success')
- mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha))
- end
-
- private
-
- def add_build_headers(status)
- headers['X-GitLab-Build-Id'] = @build.id
- headers['X-GitLab-Build-Ref'] = @build.ref
- headers['X-GitLab-Build-Status'] = status.to_s
- end
- end
-end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 5b9226a6b81..14df6f8f0a3 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -6,7 +6,6 @@ class Notify < BaseMailer
include Emails::Notes
include Emails::Projects
include Emails::Profile
- include Emails::Builds
include Emails::Pipelines
include Emails::Members
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index be632930895..671a0fe98cc 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -163,6 +163,8 @@ class ApplicationSetting < ActiveRecord::Base
end
def self.current
+ ensure_cache_setup
+
Rails.cache.fetch(CACHE_KEY) do
ApplicationSetting.last
end
@@ -176,9 +178,16 @@ class ApplicationSetting < ActiveRecord::Base
end
def self.cached
+ ensure_cache_setup
Rails.cache.fetch(CACHE_KEY)
end
+ def self.ensure_cache_setup
+ # This is a workaround for a Rails bug that causes attribute methods not
+ # to be loaded when read from cache: https://github.com/rails/rails/issues/27348
+ ApplicationSetting.define_attribute_methods
+ end
+
def self.defaults_ce
{
after_sign_up_text: nil,
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 3722047251d..ad0be70c32a 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -15,7 +15,7 @@ module Ci
def persisted_environment
@persisted_environment ||= Environment.find_by(
name: expanded_environment_name,
- project_id: gl_project_id
+ project: project
)
end
@@ -223,7 +223,8 @@ module Ci
def merge_request
merge_requests = MergeRequest.includes(:merge_request_diff)
- .where(source_branch: ref, source_project_id: pipeline.gl_project_id)
+ .where(source_branch: ref,
+ source_project: pipeline.project)
.reorder(iid: :asc)
merge_requests.find do |merge_request|
@@ -231,10 +232,6 @@ module Ci
end
end
- def project_id
- gl_project_id
- end
-
def repo_url
auth = "gitlab-ci-token:#{ensure_token!}@"
project.http_url_to_repo.sub(/^https?:\/\//) do |prefix|
@@ -542,6 +539,16 @@ module Ci
Gitlab::Ci::Build::Credentials::Factory.new(self).create!
end
+ def dependencies
+ depended_jobs = depends_on_builds
+
+ return depended_jobs unless options[:dependencies].present?
+
+ depended_jobs.select do |job|
+ options[:dependencies].include?(job.name)
+ end
+ end
+
private
def update_artifacts_size
@@ -561,7 +568,7 @@ module Ci
end
def unscoped_project
- @unscoped_project ||= Project.unscoped.find_by(id: gl_project_id)
+ @unscoped_project ||= Project.unscoped.find_by(id: project_id)
end
CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 8a5a9aa4adb..f12be98c80c 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -5,9 +5,7 @@ module Ci
include Importable
include AfterCommitQueue
- self.table_name = 'ci_commits'
-
- belongs_to :project, foreign_key: :gl_project_id
+ belongs_to :project
belongs_to :user
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
@@ -22,6 +20,7 @@ module Ci
validate :valid_commit_sha, unless: :importing?
after_create :keep_around_commits, unless: :importing?
+ after_create :refresh_build_status_cache
state_machine :status, initial: :created do
event :enqueue do
@@ -114,6 +113,12 @@ module Ci
success.latest(ref).order(id: :desc).first
end
+ def self.latest_successful_for_refs(refs)
+ success.latest(refs).order(id: :desc).each_with_object({}) do |pipeline, hash|
+ hash[pipeline.ref] ||= pipeline
+ end
+ end
+
def self.truncate_sha(sha)
sha[0...8]
end
@@ -328,6 +333,7 @@ module Ci
when 'manual' then block
end
end
+ refresh_build_status_cache
end
def predefined_variables
@@ -369,6 +375,10 @@ module Ci
.fabricate!
end
+ def refresh_build_status_cache
+ Ci::PipelineStatus.new(project, sha: sha, status: status).store_in_cache_if_needed
+ end
+
private
def pipeline_data
diff --git a/app/models/ci/pipeline_status.rb b/app/models/ci/pipeline_status.rb
new file mode 100644
index 00000000000..048047d0e34
--- /dev/null
+++ b/app/models/ci/pipeline_status.rb
@@ -0,0 +1,86 @@
+# This class is not backed by a table in the main database.
+# It loads the latest Pipeline for the HEAD of a repository, and caches that
+# in Redis.
+module Ci
+ class PipelineStatus
+ attr_accessor :sha, :status, :project, :loaded
+
+ delegate :commit, to: :project
+
+ def self.load_for_project(project)
+ new(project).tap do |status|
+ status.load_status
+ end
+ end
+
+ def initialize(project, sha: nil, status: nil)
+ @project = project
+ @sha = sha
+ @status = status
+ end
+
+ def has_status?
+ loaded? && sha.present? && status.present?
+ end
+
+ def load_status
+ return if loaded?
+
+ if has_cache?
+ load_from_cache
+ else
+ load_from_commit
+ store_in_cache
+ end
+
+ self.loaded = true
+ end
+
+ def load_from_commit
+ return unless commit
+
+ self.sha = commit.sha
+ self.status = commit.status
+ end
+
+ # We only cache the status for the HEAD commit of a project
+ # This status is rendered in project lists
+ def store_in_cache_if_needed
+ return unless sha
+ return delete_from_cache unless commit
+ store_in_cache if commit.sha == self.sha
+ end
+
+ def load_from_cache
+ Gitlab::Redis.with do |redis|
+ self.sha, self.status = redis.hmget(cache_key, :sha, :status)
+ end
+ end
+
+ def store_in_cache
+ Gitlab::Redis.with do |redis|
+ redis.mapped_hmset(cache_key, { sha: sha, status: status })
+ end
+ end
+
+ def delete_from_cache
+ Gitlab::Redis.with do |redis|
+ redis.del(cache_key)
+ end
+ end
+
+ def has_cache?
+ Gitlab::Redis.with do |redis|
+ redis.exists(cache_key)
+ end
+ end
+
+ def loaded?
+ self.loaded
+ end
+
+ def cache_key
+ "projects/#{project.id}/build_status"
+ end
+ end
+end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index edd21f984c8..487ba61bc9c 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -9,7 +9,7 @@ module Ci
has_many :builds
has_many :runner_projects, dependent: :destroy
- has_many :projects, through: :runner_projects, foreign_key: :gl_project_id
+ has_many :projects, through: :runner_projects
has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
@@ -24,7 +24,7 @@ module Ci
scope :owned_or_shared, ->(project_id) do
joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
- .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
+ .where("ci_runner_projects.project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
end
scope :assignable_for, ->(project) do
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index 234376a7e4c..5f01a0daae9 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -1,10 +1,10 @@
module Ci
class RunnerProject < ActiveRecord::Base
extend Ci::Model
-
+
belongs_to :runner
- belongs_to :project, foreign_key: :gl_project_id
+ belongs_to :project
- validates :runner_id, uniqueness: { scope: :gl_project_id }
+ validates :runner_id, uniqueness: { scope: :project_id }
end
end
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 90473d41c04..cba1d81a861 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -4,7 +4,7 @@ module Ci
acts_as_paranoid
- belongs_to :project, foreign_key: :gl_project_id
+ belongs_to :project
belongs_to :owner, class_name: "User"
has_many :trigger_requests, dependent: :destroy
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 2c8698d8b5d..6c6586110c5 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -2,11 +2,11 @@ module Ci
class Variable < ActiveRecord::Base
extend Ci::Model
- belongs_to :project, foreign_key: :gl_project_id
+ belongs_to :project
validates :key,
presence: true,
- uniqueness: { scope: :gl_project_id },
+ uniqueness: { scope: :project_id },
length: { maximum: 255 },
format: { with: /\A[a-zA-Z0-9_]+\z/,
message: "can contain only letters, digits and '_'." }
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 0a18986ef26..ce92cc369ad 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -231,6 +231,10 @@ class Commit
project.pipelines.where(sha: sha)
end
+ def latest_pipeline
+ pipelines.last
+ end
+
def status(ref = nil)
@statuses ||= {}
@@ -317,7 +321,14 @@ class Commit
end
def raw_diffs(*args)
- raw.diffs(*args)
+ use_gitaly = Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
+ deltas_only = args.last.is_a?(Hash) && args.last[:deltas_only]
+
+ if use_gitaly && !deltas_only
+ Gitlab::GitalyClient::Commit.diff_from_parent(self, *args)
+ else
+ raw.diffs(*args)
+ end
end
def diffs(diff_options = nil)
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 7e23e14794f..8c71267da65 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -5,7 +5,7 @@ class CommitStatus < ActiveRecord::Base
self.table_name = 'ci_builds'
- belongs_to :project, foreign_key: :gl_project_id
+ belongs_to :project
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
belongs_to :user
@@ -133,6 +133,12 @@ class CommitStatus < ActiveRecord::Base
false
end
+ # Added in 9.0 to keep backward compatibility for projects exported in 8.17
+ # and prior.
+ def gl_project_id
+ 'dummy'
+ end
+
def detailed_status(current_user)
Gitlab::Ci::Status::Factory
.new(self, current_user)
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index 5101cc7e687..0a1a65da05a 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -31,6 +31,7 @@ module HasStatus
WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
WHEN (#{running})+(#{pending})>0 THEN 'running'
WHEN (#{manual})>0 THEN 'manual'
+ WHEN (#{created})>0 THEN 'running'
ELSE 'failed'
END)"
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 3cf4c67d7e7..e7bd20b322a 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -48,11 +48,13 @@ module Issuable
delegate :name,
:email,
+ :public_email,
to: :author,
prefix: true
delegate :name,
:email,
+ :public_email,
to: :assignee,
allow_nil: true,
prefix: true
@@ -144,7 +146,8 @@ module Issuable
when 'milestone_due_desc' then order_milestone_due_desc
when 'downvotes_desc' then order_downvotes_desc
when 'upvotes_desc' then order_upvotes_desc
- when 'priority' then order_labels_priority(excluded_labels: excluded_labels)
+ when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels)
+ when 'priority' then order_due_date_and_labels_priority(excluded_labels: excluded_labels)
when 'position_asc' then order_position_asc
else
order_by(method)
@@ -154,7 +157,28 @@ module Issuable
sorted.order(id: :desc)
end
- def order_labels_priority(excluded_labels: [])
+ def order_due_date_and_labels_priority(excluded_labels: [])
+ # The order_ methods also modify the query in other ways:
+ #
+ # - For milestones, we add a JOIN.
+ # - For label priority, we change the SELECT, and add a GROUP BY.#
+ #
+ # After doing those, we need to reorder to the order we want. The existing
+ # ORDER BYs won't work because:
+ #
+ # 1. We need milestone due date first.
+ # 2. We can't ORDER BY a column that isn't in the GROUP BY and doesn't
+ # have an aggregate function applied, so we do a useless MIN() instead.
+ #
+ milestones_due_date = 'MIN(milestones.due_date)'
+
+ order_milestone_due_asc.
+ order_labels_priority(excluded_labels: excluded_labels, extra_select_columns: [milestones_due_date]).
+ reorder(Gitlab::Database.nulls_last_order(milestones_due_date, 'ASC'),
+ Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
+ end
+
+ def order_labels_priority(excluded_labels: [], extra_select_columns: [])
params = {
target_type: name,
target_column: "#{table_name}.id",
@@ -164,7 +188,12 @@ module Issuable
highest_priority = highest_label_priority(params).to_sql
- select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
+ select_columns = [
+ "#{table_name}.*",
+ "(#{highest_priority}) AS highest_priority"
+ ] + extra_select_columns
+
+ select(select_columns.join(', ')).
group(arel_table[:id]).
reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
end
@@ -234,6 +263,7 @@ module Issuable
user: user.hook_attrs,
project: project.hook_attrs,
object_attributes: hook_attrs,
+ labels: labels.map(&:hook_attrs),
# DEPRECATED
repository: project.hook_attrs.slice(:name, :url, :description, :homepage)
}
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 9f6d215ceb3..529fb5ce988 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -51,11 +51,13 @@ module Routable
paths.each do |path|
path = connection.quote(path)
- where = "(routes.path = #{path})"
- if cast_lower
- where = "(#{where} OR (LOWER(routes.path) = LOWER(#{path})))"
- end
+ where =
+ if cast_lower
+ "(LOWER(routes.path) = LOWER(#{path}))"
+ else
+ "(routes.path = #{path})"
+ end
wheres << where
end
diff --git a/app/models/event.rb b/app/models/event.rb
index d7ca8e3c599..5c34844b5d3 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -16,7 +16,7 @@ class Event < ActiveRecord::Base
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
- delegate :name, :email, to: :author, prefix: true, allow_nil: true
+ delegate :name, :email, :public_email, to: :author, prefix: true, allow_nil: true
delegate :title, to: :issue, prefix: true, allow_nil: true
delegate :title, to: :merge_request, prefix: true, allow_nil: true
delegate :title, to: :note, prefix: true, allow_nil: true
diff --git a/app/models/group.rb b/app/models/group.rb
index bd0ecae3da4..60274386103 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -207,7 +207,7 @@ class Group < Namespace
end
def members_with_parents
- GroupMember.non_request.where(source_id: ancestors.map(&:id).push(id))
+ GroupMember.non_request.where(source_id: ancestors.pluck(:id).push(id))
end
def users_with_parents
diff --git a/app/models/issue.rb b/app/models/issue.rb
index f188dcb673c..c2b261825b2 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -57,10 +57,24 @@ class Issue < ActiveRecord::Base
state :opened
state :reopened
state :closed
+
+ before_transition any => :closed do |issue|
+ issue.closed_at = Time.zone.now
+ end
+
+ before_transition closed: any do |issue|
+ issue.closed_at = nil
+ end
end
def hook_attrs
- attributes
+ attrs = {
+ total_time_spent: total_time_spent,
+ human_total_time_spent: human_total_time_spent,
+ human_time_estimate: human_time_estimate
+ }
+
+ attributes.merge!(attrs)
end
def self.reference_prefix
diff --git a/app/models/label.rb b/app/models/label.rb
index f68a8c9cff2..568fa6d44f5 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -169,6 +169,10 @@ class Label < ActiveRecord::Base
end
end
+ def hook_attrs
+ attributes
+ end
+
private
def issues_count(user, params = {})
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 0f7b8311588..cef8ad76b07 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -7,6 +7,7 @@ class MergeRequest < ActiveRecord::Base
belongs_to :target_project, class_name: "Project"
belongs_to :source_project, class_name: "Project"
+ belongs_to :project, foreign_key: :target_project_id
belongs_to :merge_user, class_name: "User"
has_many :merge_request_diffs, dependent: :destroy
@@ -523,7 +524,10 @@ class MergeRequest < ActiveRecord::Base
source: source_project.try(:hook_attrs),
target: target_project.hook_attrs,
last_commit: nil,
- work_in_progress: work_in_progress?
+ work_in_progress: work_in_progress?,
+ total_time_spent: total_time_spent,
+ human_total_time_spent: human_total_time_spent,
+ human_time_estimate: human_time_estimate
}
if diff_head_commit
@@ -537,10 +541,6 @@ class MergeRequest < ActiveRecord::Base
target_project != source_project
end
- def project
- target_project
- end
-
# If the merge request closes any issues, save this information in the
# `MergeRequestsClosingIssues` model. This is a performance optimization.
# Calculating this information for a number of merge requests requires
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 7331000a9f2..c0deb59ec4c 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -5,6 +5,7 @@ class Milestone < ActiveRecord::Base
None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
Any = MilestoneStruct.new('Any Milestone', '', -1)
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
+ Started = MilestoneStruct.new('Started', '#started', -3)
include CacheMarkdownField
include InternalId
diff --git a/app/models/project.rb b/app/models/project.rb
index 8c2dadf4659..da4704554b3 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -89,7 +89,6 @@ class Project < ActiveRecord::Base
has_one :campfire_service, dependent: :destroy
has_one :drone_ci_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy
- has_one :builds_email_service, dependent: :destroy
has_one :pipelines_email_service, dependent: :destroy
has_one :irker_service, dependent: :destroy
has_one :pivotaltracker_service, dependent: :destroy
@@ -159,13 +158,13 @@ class Project < ActiveRecord::Base
has_one :project_feature, dependent: :destroy
has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete
- has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id
- has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
- has_many :builds, class_name: 'Ci::Build', foreign_key: :gl_project_id # the builds are created from the commit_statuses
- has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject', foreign_key: :gl_project_id
+ has_many :commit_statuses, dependent: :destroy
+ has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline'
+ has_many :builds, class_name: 'Ci::Build' # the builds are created from the commit_statuses
+ has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
- has_many :variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id
- has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id
+ has_many :variables, dependent: :destroy, class_name: 'Ci::Variable'
+ has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger'
has_many :environments, dependent: :destroy
has_many :deployments, dependent: :destroy
@@ -197,6 +196,7 @@ class Project < ActiveRecord::Base
validates :name, uniqueness: { scope: :namespace_id }
validates :path, uniqueness: { scope: :namespace_id }
validates :import_url, addressable_url: true, if: :external_import?
+ validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
validate :avatar_type,
@@ -881,13 +881,9 @@ class Project < ActiveRecord::Base
end
def http_url_to_repo(user = nil)
- url = web_url
+ credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user)
- if user
- url.sub!(%r{\Ahttps?://}) { |protocol| "#{protocol}#{user.username}@" }
- end
-
- "#{url}.git"
+ Gitlab::UrlSanitizer.new("#{web_url}.git", credentials: credentials).full_url
end
# Check if current branch name is marked as protected in the system
@@ -1209,6 +1205,10 @@ class Project < ActiveRecord::Base
end
end
+ def pipeline_status
+ @pipeline_status ||= Ci::PipelineStatus.load_for_project(self)
+ end
+
def mark_import_as_failed(error_message)
original_errors = errors.dup
sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message)
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index ebd21e37189..0c526b53d72 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -1,107 +1,11 @@
+# This class is to be removed with 9.1
+# We should also by then remove BuildsEmailService from database
class BuildsEmailService < Service
- prop_accessor :recipients
- boolean_accessor :add_pusher
- boolean_accessor :notify_only_broken_builds
- validates :recipients, presence: true, if: ->(s) { s.activated? && !s.add_pusher? }
-
- def initialize_properties
- if properties.nil?
- self.properties = {}
- self.notify_only_broken_builds = true
- end
- end
-
- def title
- 'Builds emails'
- end
-
- def description
- 'Email the builds status to a list of recipients.'
- end
-
def self.to_param
'builds_email'
end
def self.supported_events
- %w(build)
- end
-
- def execute(push_data)
- return unless supported_events.include?(push_data[:object_kind])
- return unless should_build_be_notified?(push_data)
-
- recipients = all_recipients(push_data)
-
- if recipients.any?
- BuildEmailWorker.perform_async(
- push_data[:build_id],
- recipients,
- push_data
- )
- end
- end
-
- def can_test?
- project.builds.any?
- end
-
- def disabled_title
- "Please setup a build on your repository."
- end
-
- def test_data(project = nil, user = nil)
- Gitlab::DataBuilder::Build.build(project.builds.last)
- end
-
- def fields
- [
- { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by comma' },
- { type: 'checkbox', name: 'add_pusher', label: 'Add pusher to recipients list' },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
- ]
- end
-
- def test(data)
- begin
- # bypass build status verification when testing
- data[:build_status] = "failed"
- data[:build_allow_failure] = false
-
- result = execute(data)
- rescue StandardError => error
- return { success: false, result: error }
- end
-
- { success: true, result: result }
- end
-
- def should_build_be_notified?(data)
- case data[:build_status]
- when 'success'
- !notify_only_broken_builds?
- when 'failed'
- !allow_failure?(data)
- else
- false
- end
- end
-
- def allow_failure?(data)
- data[:build_allow_failure] == true
- end
-
- def all_recipients(data)
- all_recipients = []
-
- unless recipients.blank?
- all_recipients += recipients.split(',').compact.reject(&:blank?)
- end
-
- if add_pusher? && data[:user][:email]
- all_recipients << data[:user][:email]
- end
-
- all_recipients
+ %w[]
end
end
diff --git a/app/models/project_services/chat_message/build_message.rb b/app/models/project_services/chat_message/build_message.rb
deleted file mode 100644
index c776e0a20c4..00000000000
--- a/app/models/project_services/chat_message/build_message.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-module ChatMessage
- class BuildMessage < BaseMessage
- attr_reader :sha
- attr_reader :ref_type
- attr_reader :ref
- attr_reader :status
- attr_reader :project_name
- attr_reader :project_url
- attr_reader :user_name
- attr_reader :user_url
- attr_reader :duration
- attr_reader :stage
- attr_reader :build_id
- attr_reader :build_name
-
- def initialize(params)
- @sha = params[:sha]
- @ref_type = params[:tag] ? 'tag' : 'branch'
- @ref = params[:ref]
- @project_name = params[:project_name]
- @project_url = params[:project_url]
- @status = params[:commit][:status]
- @user_name = params[:commit][:author_name]
- @user_url = params[:commit][:author_url]
- @duration = params[:commit][:duration]
- @stage = params[:build_stage]
- @build_name = params[:build_name]
- @build_id = params[:build_id]
- end
-
- def pretext
- ''
- end
-
- def fallback
- format(message)
- end
-
- def attachments
- [{ text: format(message), color: attachment_color }]
- end
-
- private
-
- def message
- "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_link} #{humanized_status} on build #{build_link} of stage #{stage} in #{duration} #{'second'.pluralize(duration)}"
- end
-
- def build_url
- "#{project_url}/builds/#{build_id}"
- end
-
- def build_link
- link(build_name, build_url)
- end
-
- def user_link
- link(user_name, user_url)
- end
-
- def format(string)
- Slack::Notifier::LinkFormatter.format(string)
- end
-
- def humanized_status
- case status
- when 'success'
- 'passed'
- else
- status
- end
- end
-
- def attachment_color
- if status == 'success'
- 'good'
- else
- 'danger'
- end
- end
-
- def branch_url
- "#{project_url}/commits/#{ref}"
- end
-
- def branch_link
- link(ref, branch_url)
- end
-
- def project_link
- link(project_name, project_url)
- end
-
- def commit_url
- "#{project_url}/commit/#{sha}/builds"
- end
-
- def commit_link
- link(Commit.truncate_sha(sha), commit_url)
- end
- end
-end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index 8468934425f..200be99f36b 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -6,7 +6,7 @@ class ChatNotificationService < Service
default_value_for :category, 'chat'
prop_accessor :webhook, :username, :channel
- boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
+ boolean_accessor :notify_only_broken_pipelines
validates :webhook, presence: true, url: true, if: :activated?
@@ -16,7 +16,6 @@ class ChatNotificationService < Service
if properties.nil?
self.properties = {}
- self.notify_only_broken_builds = true
self.notify_only_broken_pipelines = true
end
end
@@ -27,7 +26,7 @@ class ChatNotificationService < Service
def self.supported_events
%w[push issue confidential_issue merge_request note tag_push
- build pipeline wiki_page]
+ pipeline wiki_page]
end
def execute(data)
@@ -89,8 +88,6 @@ class ChatNotificationService < Service
ChatMessage::MergeMessage.new(data) unless is_update?(data)
when "note"
ChatMessage::NoteMessage.new(data)
- when "build"
- ChatMessage::BuildMessage.new(data) if should_build_be_notified?(data)
when "pipeline"
ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page"
@@ -125,17 +122,6 @@ class ChatNotificationService < Service
data[:object_attributes][:action] == 'update'
end
- def should_build_be_notified?(data)
- case data[:commit][:status]
- when 'success'
- !notify_only_broken_builds?
- when 'failed'
- true
- else
- false
- end
- end
-
def should_pipeline_be_notified?(data)
case data[:object_attributes][:status]
when 'success'
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index c4142c38b2f..8b181221bb0 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -9,13 +9,13 @@ class HipchatService < Service
].freeze
prop_accessor :token, :room, :server, :color, :api_version
- boolean_accessor :notify_only_broken_builds, :notify
+ boolean_accessor :notify_only_broken_pipelines, :notify
validates :token, presence: true, if: :activated?
def initialize_properties
if properties.nil?
self.properties = {}
- self.notify_only_broken_builds = true
+ self.notify_only_broken_pipelines = true
end
end
@@ -41,12 +41,12 @@ class HipchatService < Service
placeholder: 'Leave blank for default (v2)' },
{ type: 'text', name: 'server',
placeholder: 'Leave blank for default. https://hipchat.example.com' },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
def self.supported_events
- %w(push issue confidential_issue merge_request note tag_push build)
+ %w(push issue confidential_issue merge_request note tag_push pipeline)
end
def execute(data)
@@ -90,8 +90,8 @@ class HipchatService < Service
create_merge_request_message(data) unless is_update?(data)
when "note"
create_note_message(data)
- when "build"
- create_build_message(data) if should_build_be_notified?(data)
+ when "pipeline"
+ create_pipeline_message(data) if should_pipeline_be_notified?(data)
end
end
@@ -240,28 +240,29 @@ class HipchatService < Service
message
end
- def create_build_message(data)
- ref_type = data[:tag] ? 'tag' : 'branch'
- ref = data[:ref]
- sha = data[:sha]
- user_name = data[:commit][:author_name]
- status = data[:commit][:status]
- duration = data[:commit][:duration]
+ def create_pipeline_message(data)
+ pipeline_attributes = data[:object_attributes]
+ pipeline_id = pipeline_attributes[:id]
+ ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+ ref = pipeline_attributes[:ref]
+ user_name = (data[:user] && data[:user][:name]) || 'API'
+ status = pipeline_attributes[:status]
+ duration = pipeline_attributes[:duration]
branch_link = "<a href=\"#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"
- commit_link = "<a href=\"#{project_url}/commit/#{CGI.escape(sha)}/builds\">#{Commit.truncate_sha(sha)}</a>"
+ pipeline_url = "<a href=\"#{project_url}/pipelines/#{pipeline_id}\">##{pipeline_id}</a>"
- "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
+ "#{project_link}: Pipeline #{pipeline_url} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
end
def message_color(data)
- build_status_color(data) || color || 'yellow'
+ pipeline_status_color(data) || color || 'yellow'
end
- def build_status_color(data)
- return unless data && data[:object_kind] == 'build'
+ def pipeline_status_color(data)
+ return unless data && data[:object_kind] == 'pipeline'
- case data[:commit][:status]
+ case data[:object_attributes][:status]
when 'success'
'green'
else
@@ -294,10 +295,10 @@ class HipchatService < Service
end
end
- def should_build_be_notified?(data)
- case data[:commit][:status]
+ def should_pipeline_be_notified?(data)
+ case data[:object_attributes][:status]
when 'success'
- !notify_only_broken_builds?
+ !notify_only_broken_pipelines?
when 'failed'
true
else
diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb
index c13538e9fea..1156d050622 100644
--- a/app/models/project_services/mattermost_service.rb
+++ b/app/models/project_services/mattermost_service.rb
@@ -30,7 +30,6 @@ class MattermostService < ChatNotificationService
[
{ type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/…' },
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index 375966b9efc..4d7c81a721f 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -30,7 +30,14 @@ class PrometheusService < MonitoringService
end
def help
- 'Retrieves `container_cpu_usage_seconds_total` and `container_memory_usage_bytes` from the configured Prometheus server. An `environment` label is required on each metric to identify the Environment.'
+ <<-MD.strip_heredoc
+ Retrieves the Kubernetes node metrics `container_cpu_usage_seconds_total`
+ and `container_memory_usage_bytes` from the configured Prometheus server.
+
+ If you are not using [Auto-Deploy](https://docs.gitlab.com/ee/ci/autodeploy/index.html)
+ or have set up your own Prometheus server, an `environment` label is required on each metric to
+ [identify the Environment](https://docs.gitlab.com/ce/user/project/integrations/prometheus.html#metrics-and-labels).
+ MD
end
def self.to_param
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index da7496573ef..b657db6f9ee 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -29,7 +29,6 @@ class SlackService < ChatNotificationService
[
{ type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/…' },
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 539b31780b3..70eef359cdd 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -42,8 +42,11 @@ class ProjectWiki
url_to_repo
end
- def http_url_to_repo
- [Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('')
+ def http_url_to_repo(user = nil)
+ url = "#{Gitlab.config.gitlab.url}/#{path_with_namespace}.git"
+ credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user)
+
+ Gitlab::UrlSanitizer.new(url, credentials: credentials).full_url
end
def wiki_base_path
diff --git a/app/models/route.rb b/app/models/route.rb
index 73574a6206b..41e6eb7cb73 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -21,7 +21,7 @@ class Route < ActiveRecord::Base
attributes[:path] = route.path.sub(path_was, path)
end
- if name_changed? && route.name.present?
+ if name_changed? && name_was.present? && route.name.present?
attributes[:name] = route.name.sub(name_was, name)
end
diff --git a/app/models/service.rb b/app/models/service.rb
index 2f75a2e4e7f..e73f7e5d1a3 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -215,7 +215,6 @@ class Service < ActiveRecord::Base
assembla
bamboo
buildkite
- builds_email
bugzilla
campfire
custom_issue_tracker
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 47789a21133..da3fa7277c2 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -48,8 +48,14 @@ class Todo < ActiveRecord::Base
after_save :keep_around_commit
class << self
+ # Priority sorting isn't displayed in the dropdown, because we don't show
+ # milestones, but still show something if the user has a URL with that
+ # selected.
def sort(method)
- method == "priority" ? order_by_labels_priority : order_by(method)
+ case method.to_s
+ when 'priority', 'label_priority' then order_by_labels_priority
+ else order_by(method)
+ end
end
# Order by priority depending on which issue/merge request the Todo belongs to
diff --git a/app/models/user.rb b/app/models/user.rb
index 39c1281179b..8c7ad5d5174 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -877,7 +877,7 @@ class User < ActiveRecord::Base
def ci_authorized_runners
@ci_authorized_runners ||= begin
runner_ids = Ci::RunnerProject.
- where("ci_runner_projects.gl_project_id IN (#{ci_projects_union.to_sql})").
+ where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})").
select(:runner_id)
Ci::Runner.specific.where(id: runner_ids)
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 2caebb496db..c771c22f46a 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -149,7 +149,13 @@ class WikiPage
end
# Returns boolean True or False if this instance
- # has been fully saved to disk or not.
+ # is the latest commit version of the page.
+ def latest?
+ !historical?
+ end
+
+ # Returns boolean True or False if this instance
+ # has been fully created on disk or not.
def persisted?
@persisted == true
end
@@ -220,6 +226,8 @@ class WikiPage
end
def save(method, *args)
+ saved = false
+
project_wiki = wiki
if valid? && project_wiki.send(method, *args)
@@ -237,10 +245,10 @@ class WikiPage
set_attributes
@persisted = true
+ saved = true
else
errors.add(:base, project_wiki.error_message) if project_wiki.error_message
- @persisted = false
end
- @persisted
+ saved
end
end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 0ab9042bf24..d6a4280ce4c 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -55,13 +55,13 @@ module Ci
new_builds.
# don't run projects which have not enabled shared runners and builds
joins(:project).where(projects: { shared_runners_enabled: true }).
- joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id').
+ joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id').
where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
# Implement fair scheduling
# this returns builds that are ordered by number of running builds
# we prefer projects that don't use shared runners at all
- joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id").
+ joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id").
order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
end
@@ -71,7 +71,7 @@ module Ci
def running_builds_for_shared_runners
Ci::Build.running.where(runner: Ci::Runner.shared).
- group(:gl_project_id).select(:gl_project_id, 'count(*) AS running_builds')
+ group(:project_id).select(:project_id, 'count(*) AS running_builds')
end
def new_builds
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index b07338d500a..673ed02f952 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -25,12 +25,12 @@ class CreateBranchService < BaseService
private
def create_master_branch
- project.repository.commit_file(
+ project.repository.create_file(
current_user,
'/README.md',
'',
message: 'Add README.md',
- branch_name: 'master',
- update: false)
+ branch_name: 'master'
+ )
end
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 9d4739e37bb..fdce542bd9e 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -6,7 +6,7 @@ module MergeRequests
merge_request.source_project = find_source_project
merge_request.target_project = find_target_project
merge_request.target_branch = find_target_branch
- merge_request.can_be_created = branches_valid? && source_branch_specified? && target_branch_specified?
+ merge_request.can_be_created = branches_valid?
compare_branches if branches_present?
assign_title_and_description if merge_request.can_be_created
diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb
index 1262ecbc29a..f00a33969a8 100644
--- a/app/services/merge_requests/get_urls_service.rb
+++ b/app/services/merge_requests/get_urls_service.rb
@@ -7,6 +7,8 @@ module MergeRequests
end
def execute(changes)
+ return [] unless project.printing_merge_request_link_enabled
+
branches = get_branches(changes)
merge_requests_map = opened_merge_requests_from_source_branches(branches)
branches.map do |branch|
@@ -23,10 +25,7 @@ module MergeRequests
def opened_merge_requests_from_source_branches(branches)
merge_requests = MergeRequest.from_project(project).opened.from_source_branches(branches)
- merge_requests.inject({}) do |hash, mr|
- hash[mr.source_branch] = mr
- hash
- end
+ merge_requests.index_by(&:source_branch)
end
def get_branches(changes)
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
new file mode 100644
index 00000000000..44ae23fad18
--- /dev/null
+++ b/app/services/notification_recipient_service.rb
@@ -0,0 +1,293 @@
+#
+# Used by NotificationService to determine who should receive notification
+#
+class NotificationRecipientService
+ attr_reader :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ def build_recipients(target, current_user, action: nil, previous_assignee: nil, skip_current_user: true)
+ custom_action = build_custom_key(action, target)
+
+ recipients = target.participants(current_user)
+
+ unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
+ recipients = add_project_watchers(recipients)
+ end
+
+ recipients = add_custom_notifications(recipients, custom_action)
+ recipients = reject_mention_users(recipients)
+
+ # Re-assign is considered as a mention of the new assignee so we add the
+ # new assignee to the list of recipients after we rejected users with
+ # the "on mention" notification level
+ if [:reassign_merge_request, :reassign_issue].include?(custom_action)
+ recipients << previous_assignee if previous_assignee
+ recipients << target.assignee
+ end
+
+ recipients = reject_muted_users(recipients)
+ recipients = add_subscribed_users(recipients, target)
+
+ if [:new_issue, :new_merge_request].include?(custom_action)
+ recipients = add_labels_subscribers(recipients, target)
+ end
+
+ recipients = reject_unsubscribed_users(recipients, target)
+ recipients = reject_users_without_access(recipients, target)
+
+ recipients.delete(current_user) if skip_current_user
+
+ recipients.uniq
+ end
+
+ def build_relabeled_recipients(target, current_user, labels:)
+ recipients = add_labels_subscribers([], target, labels: labels)
+ recipients = reject_unsubscribed_users(recipients, target)
+ recipients = reject_users_without_access(recipients, target)
+ recipients.delete(current_user)
+ recipients.uniq
+ end
+
+ def build_new_note_recipients(note)
+ target = note.noteable
+
+ ability, subject = if note.for_personal_snippet?
+ [:read_personal_snippet, note.noteable]
+ else
+ [:read_project, note.project]
+ end
+
+ mentioned_users = note.mentioned_users.select { |user| user.can?(ability, subject) }
+
+ # Add all users participating in the thread (author, assignee, comment authors)
+ recipients =
+ if target.respond_to?(:participants)
+ target.participants(note.author)
+ else
+ mentioned_users
+ end
+
+ unless note.for_personal_snippet?
+ # Merge project watchers
+ recipients = add_project_watchers(recipients)
+
+ # Merge project with custom notification
+ recipients = add_custom_notifications(recipients, :new_note)
+ end
+
+ # Reject users with Mention notification level, except those mentioned in _this_ note.
+ recipients = reject_mention_users(recipients - mentioned_users)
+ recipients = recipients + mentioned_users
+
+ recipients = reject_muted_users(recipients)
+
+ recipients = add_subscribed_users(recipients, note.noteable)
+ recipients = reject_unsubscribed_users(recipients, note.noteable)
+ recipients = reject_users_without_access(recipients, note.noteable)
+
+ recipients.delete(note.author)
+ recipients.uniq
+ end
+
+ # Remove users with disabled notifications from array
+ # Also remove duplications and nil recipients
+ def reject_muted_users(users)
+ reject_users(users, :disabled)
+ end
+
+ protected
+
+ # Get project/group users with CUSTOM notification level
+ def add_custom_notifications(recipients, action)
+ user_ids = []
+
+ # Users with a notification setting on group or project
+ user_ids += user_ids_notifiable_on(project, :custom, action)
+ user_ids += user_ids_notifiable_on(project.group, :custom, action)
+
+ # Users with global level custom
+ user_ids_with_project_level_global = user_ids_notifiable_on(project, :global)
+ user_ids_with_group_level_global = user_ids_notifiable_on(project.group, :global)
+
+ global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global)
+ user_ids += user_ids_with_global_level_custom(global_users_ids, action)
+
+ recipients.concat(User.find(user_ids))
+ end
+
+ def add_project_watchers(recipients)
+ recipients.concat(project_watchers).compact
+ end
+
+ # Get project users with WATCH notification level
+ def project_watchers
+ project_members_ids = user_ids_notifiable_on(project)
+
+ user_ids_with_project_global = user_ids_notifiable_on(project, :global)
+ user_ids_with_group_global = user_ids_notifiable_on(project.group, :global)
+
+ user_ids = user_ids_with_global_level_watch((user_ids_with_project_global + user_ids_with_group_global).uniq)
+
+ user_ids_with_project_setting = select_project_members_ids(project, user_ids_with_project_global, user_ids)
+ user_ids_with_group_setting = select_group_members_ids(project.group, project_members_ids, user_ids_with_group_global, user_ids)
+
+ User.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq).to_a
+ end
+
+ # Remove users with notification level 'Mentioned'
+ def reject_mention_users(users)
+ reject_users(users, :mention)
+ end
+
+ def add_subscribed_users(recipients, target)
+ return recipients unless target.respond_to? :subscribers
+
+ recipients + target.subscribers(project)
+ end
+
+ def user_ids_notifiable_on(resource, notification_level = nil, action = nil)
+ return [] unless resource
+
+ if notification_level
+ settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level])
+ settings = settings.select { |setting| setting.events[action] } if action.present?
+ settings.map(&:user_id)
+ else
+ resource.notification_settings.pluck(:user_id)
+ end
+ end
+
+ # Build a list of user_ids based on project notification settings
+ def select_project_members_ids(project, global_setting, user_ids_global_level_watch)
+ user_ids = user_ids_notifiable_on(project, :watch)
+
+ # If project setting is global, add to watch list if global setting is watch
+ global_setting.each do |user_id|
+ if user_ids_global_level_watch.include?(user_id)
+ user_ids << user_id
+ end
+ end
+
+ user_ids
+ end
+
+ # Build a list of user_ids based on group notification settings
+ def select_group_members_ids(group, project_members, global_setting, user_ids_global_level_watch)
+ uids = user_ids_notifiable_on(group, :watch)
+
+ # Group setting is watch, add to user_ids list if user is not project member
+ user_ids = []
+ uids.each do |user_id|
+ if project_members.exclude?(user_id)
+ user_ids << user_id
+ end
+ end
+
+ # Group setting is global, add to user_ids list if global setting is watch
+ global_setting.each do |user_id|
+ if project_members.exclude?(user_id) && user_ids_global_level_watch.include?(user_id)
+ user_ids << user_id
+ end
+ end
+
+ user_ids
+ end
+
+ def user_ids_with_global_level_watch(ids)
+ settings_with_global_level_of(:watch, ids).pluck(:user_id)
+ end
+
+ def user_ids_with_global_level_custom(ids, action)
+ settings = settings_with_global_level_of(:custom, ids)
+ settings = settings.select { |setting| setting.events[action] }
+ settings.map(&:user_id)
+ end
+
+ def settings_with_global_level_of(level, ids)
+ NotificationSetting.where(
+ user_id: ids,
+ source_type: nil,
+ level: NotificationSetting.levels[level]
+ )
+ end
+
+ # Reject users which has certain notification level
+ #
+ # Example:
+ # reject_users(users, :watch, project)
+ #
+ def reject_users(users, level)
+ level = level.to_s
+
+ unless NotificationSetting.levels.keys.include?(level)
+ raise 'Invalid notification level'
+ end
+
+ users = users.to_a.compact.uniq
+ users = users.select { |u| u.can?(:receive_notifications) }
+
+ users.reject do |user|
+ global_notification_setting = user.global_notification_setting
+
+ next global_notification_setting.level == level unless project
+
+ setting = user.notification_settings_for(project)
+
+ if project.group && (setting.nil? || setting.global?)
+ setting = user.notification_settings_for(project.group)
+ end
+
+ # reject users who globally set mention notification and has no setting per project/group
+ next global_notification_setting.level == level unless setting
+
+ # reject users who set mention notification in project
+ next true if setting.level == level
+
+ # reject users who have mention level in project and disabled in global settings
+ setting.global? && global_notification_setting.level == level
+ end
+ end
+
+ def reject_unsubscribed_users(recipients, target)
+ return recipients unless target.respond_to? :subscriptions
+
+ recipients.reject do |user|
+ subscription = target.subscriptions.find_by_user_id(user.id)
+ subscription && !subscription.subscribed
+ end
+ end
+
+ def reject_users_without_access(recipients, target)
+ ability = case target
+ when Issuable
+ :"read_#{target.to_ability_name}"
+ when Ci::Pipeline
+ :read_build # We have build trace in pipeline emails
+ end
+
+ return recipients unless ability
+
+ recipients.select do |user|
+ user.can?(ability, target)
+ end
+ end
+
+ def add_labels_subscribers(recipients, target, labels: nil)
+ return recipients unless target.respond_to? :labels
+
+ (labels || target.labels).each do |label|
+ recipients += label.subscribers(project)
+ end
+
+ recipients
+ end
+
+ # Build event key to search on custom notification level
+ # Check NotificationSetting::EMAIL_EVENTS
+ def build_custom_key(action, object)
+ "#{action}_#{object.class.model_name.name.underscore}".to_sym
+ end
+end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index d12692ecc90..f9aa2346759 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -150,7 +150,10 @@ class NotificationService
end
def resolve_all_discussions(merge_request, current_user)
- recipients = build_recipients(merge_request, merge_request.target_project, current_user, action: "resolve_all_discussions")
+ recipients = NotificationRecipientService.new(merge_request.target_project).build_recipients(
+ merge_request,
+ current_user,
+ action: "resolve_all_discussions")
recipients.each do |recipient|
mailer.resolved_all_discussions_email(recipient.id, merge_request.id, current_user.id).deliver_later
@@ -164,64 +167,15 @@ class NotificationService
end
# Notify users on new note in system
- #
- # TODO: split on methods and refactor
- #
def new_note(note)
return true unless note.noteable_type.present?
# ignore gitlab service messages
return true if note.cross_reference? && note.system?
- target = note.noteable
-
- recipients = []
-
- mentioned_users = note.mentioned_users
-
- ability, subject = if note.for_personal_snippet?
- [:read_personal_snippet, note.noteable]
- else
- [:read_project, note.project]
- end
-
- mentioned_users.select! do |user|
- user.can?(ability, subject)
- end
-
- # Add all users participating in the thread (author, assignee, comment authors)
- participants =
- if target.respond_to?(:participants)
- target.participants(note.author)
- else
- mentioned_users
- end
-
- recipients = recipients.concat(participants)
-
- unless note.for_personal_snippet?
- # Merge project watchers
- recipients = add_project_watchers(recipients, note.project)
-
- # Merge project with custom notification
- recipients = add_custom_notifications(recipients, note.project, :new_note)
- end
-
- # Reject users with Mention notification level, except those mentioned in _this_ note.
- recipients = reject_mention_users(recipients - mentioned_users, note.project)
- recipients = recipients + mentioned_users
-
- recipients = reject_muted_users(recipients, note.project)
-
- recipients = add_subscribed_users(recipients, note.project, note.noteable)
- recipients = reject_unsubscribed_users(recipients, note.noteable)
- recipients = reject_users_without_access(recipients, note.noteable)
-
- recipients.delete(note.author) unless note.author.notified_of_own_activity?
- recipients = recipients.uniq
-
notify_method = "note_#{note.to_ability_name}_email".to_sym
+ recipients = NotificationRecipientService.new(note.project).build_new_note_recipients(note)
recipients.each do |recipient|
mailer.send(notify_method, recipient.id, note.id).deliver_later
end
@@ -290,7 +244,7 @@ class NotificationService
def project_was_moved(project, old_path_with_namespace)
recipients = project.team.members
- recipients = reject_muted_users(recipients, project)
+ recipients = NotificationRecipientService.new(project).reject_muted_users(recipients)
recipients.each do |recipient|
mailer.project_was_moved_email(
@@ -302,7 +256,7 @@ class NotificationService
end
def issue_moved(issue, new_issue, current_user)
- recipients = build_recipients(issue, issue.project, current_user)
+ recipients = NotificationRecipientService.new(issue.project).build_recipients(issue, current_user)
recipients.map do |recipient|
email = mailer.issue_moved_email(recipient, issue, new_issue, current_user)
@@ -324,12 +278,10 @@ class NotificationService
return unless mailer.respond_to?(email_template)
- recipients ||= build_recipients(
+ recipients ||= NotificationRecipientService.new(pipeline.project).build_recipients(
pipeline,
- pipeline.project,
- pipeline.user,
- action: pipeline.status,
- skip_current_user: false).map(&:notification_email)
+ nil, # The acting user, who won't be added to recipients
+ action: pipeline.status).map(&:notification_email)
if recipients.any?
mailer.public_send(email_template, pipeline, recipients).deliver_later
@@ -338,199 +290,8 @@ class NotificationService
protected
- # Get project/group users with CUSTOM notification level
- def add_custom_notifications(recipients, project, action)
- user_ids = []
-
- # Users with a notification setting on group or project
- user_ids += notification_settings_for(project, :custom, action)
- user_ids += notification_settings_for(project.group, :custom, action)
-
- # Users with global level custom
- users_with_project_level_global = notification_settings_for(project, :global)
- users_with_group_level_global = notification_settings_for(project.group, :global)
-
- global_users_ids = users_with_project_level_global.concat(users_with_group_level_global)
- user_ids += users_with_global_level_custom(global_users_ids, action)
-
- recipients.concat(User.find(user_ids))
- end
-
- # Get project users with WATCH notification level
- def project_watchers(project)
- project_members = notification_settings_for(project)
-
- users_with_project_level_global = notification_settings_for(project, :global)
- users_with_group_level_global = notification_settings_for(project.group, :global)
-
- users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq)
-
- users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users)
- users_with_group_setting = select_group_member_setting(project.group, project_members, users_with_group_level_global, users)
-
- User.where(id: users_with_project_setting.concat(users_with_group_setting).uniq).to_a
- end
-
- def notification_settings_for(resource, notification_level = nil, action = nil)
- return [] unless resource
-
- if notification_level
- settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level])
- settings = settings.select { |setting| setting.events[action] } if action.present?
- settings.map(&:user_id)
- else
- resource.notification_settings.pluck(:user_id)
- end
- end
-
- def users_with_global_level_watch(ids)
- settings_with_global_level_of(:watch, ids).pluck(:user_id)
- end
-
- def users_with_global_level_custom(ids, action)
- settings = settings_with_global_level_of(:custom, ids)
- settings = settings.select { |setting| setting.events[action] }
- settings.map(&:user_id)
- end
-
- def settings_with_global_level_of(level, ids)
- NotificationSetting.where(
- user_id: ids,
- source_type: nil,
- level: NotificationSetting.levels[level]
- )
- end
-
- # Build a list of users based on project notification settings
- def select_project_member_setting(project, global_setting, users_global_level_watch)
- users = notification_settings_for(project, :watch)
-
- # If project setting is global, add to watch list if global setting is watch
- global_setting.each do |user_id|
- if users_global_level_watch.include?(user_id)
- users << user_id
- end
- end
-
- users
- end
-
- # Build a list of users based on group notification settings
- def select_group_member_setting(group, project_members, global_setting, users_global_level_watch)
- uids = notification_settings_for(group, :watch)
-
- # Group setting is watch, add to users list if user is not project member
- users = []
- uids.each do |user_id|
- if project_members.exclude?(user_id)
- users << user_id
- end
- end
-
- # Group setting is global, add to users list if global setting is watch
- global_setting.each do |user_id|
- if project_members.exclude?(user_id) && users_global_level_watch.include?(user_id)
- users << user_id
- end
- end
-
- users
- end
-
- def add_project_watchers(recipients, project)
- recipients.concat(project_watchers(project)).compact
- end
-
- # Remove users with disabled notifications from array
- # Also remove duplications and nil recipients
- def reject_muted_users(users, project = nil)
- reject_users(users, :disabled, project)
- end
-
- # Remove users with notification level 'Mentioned'
- def reject_mention_users(users, project = nil)
- reject_users(users, :mention, project)
- end
-
- # Reject users which has certain notification level
- #
- # Example:
- # reject_users(users, :watch, project)
- #
- def reject_users(users, level, project = nil)
- level = level.to_s
-
- unless NotificationSetting.levels.keys.include?(level)
- raise 'Invalid notification level'
- end
-
- users = users.to_a.compact.uniq
- users = users.select { |u| u.can?(:receive_notifications) }
-
- users.reject do |user|
- global_notification_setting = user.global_notification_setting
-
- next global_notification_setting.level == level unless project
-
- setting = user.notification_settings_for(project)
-
- if project.group && (setting.nil? || setting.global?)
- setting = user.notification_settings_for(project.group)
- end
-
- # reject users who globally set mention notification and has no setting per project/group
- next global_notification_setting.level == level unless setting
-
- # reject users who set mention notification in project
- next true if setting.level == level
-
- # reject users who have mention level in project and disabled in global settings
- setting.global? && global_notification_setting.level == level
- end
- end
-
- def reject_unsubscribed_users(recipients, target)
- return recipients unless target.respond_to? :subscriptions
-
- recipients.reject do |user|
- subscription = target.subscriptions.find_by_user_id(user.id)
- subscription && !subscription.subscribed
- end
- end
-
- def reject_users_without_access(recipients, target)
- ability = case target
- when Issuable
- :"read_#{target.to_ability_name}"
- when Ci::Pipeline
- :read_build # We have build trace in pipeline emails
- end
-
- return recipients unless ability
-
- recipients.select do |user|
- user.can?(ability, target)
- end
- end
-
- def add_subscribed_users(recipients, project, target)
- return recipients unless target.respond_to? :subscribers
-
- recipients + target.subscribers(project)
- end
-
- def add_labels_subscribers(recipients, project, target, labels: nil)
- return recipients unless target.respond_to? :labels
-
- (labels || target.labels).each do |label|
- recipients += label.subscribers(project)
- end
-
- recipients
- end
-
def new_resource_email(target, project, method)
- recipients = build_recipients(target, project, target.author, action: "new")
+ recipients = NotificationRecipientService.new(project).build_recipients(target, target.author, action: "new")
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id).deliver_later
@@ -538,7 +299,7 @@ class NotificationService
end
def new_mentions_in_resource_email(target, project, new_mentioned_users, current_user, method)
- recipients = build_recipients(target, project, current_user, action: "new")
+ recipients = NotificationRecipientService.new(project).build_recipients(target, current_user, action: "new")
recipients = recipients & new_mentioned_users
recipients.each do |recipient|
@@ -549,9 +310,8 @@ class NotificationService
def close_resource_email(target, project, current_user, method, skip_current_user: true)
action = method == :merged_merge_request_email ? "merge" : "close"
- recipients = build_recipients(
+ recipients = NotificationRecipientService.new(project).build_recipients(
target,
- project,
current_user,
action: action,
skip_current_user: skip_current_user
@@ -566,7 +326,12 @@ class NotificationService
previous_assignee_id = previous_record(target, 'assignee_id')
previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- recipients = build_recipients(target, project, current_user, action: "reassign", previous_assignee: previous_assignee)
+ recipients = NotificationRecipientService.new(project).build_recipients(
+ target,
+ current_user,
+ action: "reassign",
+ previous_assignee: previous_assignee
+ )
recipients.each do |recipient|
mailer.send(
@@ -580,7 +345,7 @@ class NotificationService
end
def relabeled_resource_email(target, project, labels, current_user, method)
- recipients = build_relabeled_recipients(target, project, current_user, labels: labels)
+ recipients = NotificationRecipientService.new(project).build_relabeled_recipients(target, current_user, labels: labels)
label_names = labels.map(&:name)
recipients.each do |recipient|
@@ -589,58 +354,13 @@ class NotificationService
end
def reopen_resource_email(target, project, current_user, method, status)
- recipients = build_recipients(target, project, current_user, action: "reopen")
+ recipients = NotificationRecipientService.new(project).build_recipients(target, current_user, action: "reopen")
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, status, current_user.id).deliver_later
end
end
- def build_recipients(target, project, current_user, action: nil, previous_assignee: nil, skip_current_user: true)
- custom_action = build_custom_key(action, target)
-
- recipients = target.participants(current_user)
-
- unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
- recipients = add_project_watchers(recipients, project)
- end
-
- recipients = add_custom_notifications(recipients, project, custom_action)
- recipients = reject_mention_users(recipients, project)
-
- recipients = recipients.uniq
-
- # Re-assign is considered as a mention of the new assignee so we add the
- # new assignee to the list of recipients after we rejected users with
- # the "on mention" notification level
- if [:reassign_merge_request, :reassign_issue].include?(custom_action)
- recipients << previous_assignee if previous_assignee
- recipients << target.assignee
- end
-
- recipients = reject_muted_users(recipients, project)
- recipients = add_subscribed_users(recipients, project, target)
-
- if [:new_issue, :new_merge_request].include?(custom_action)
- recipients = add_labels_subscribers(recipients, project, target)
- end
-
- recipients = reject_unsubscribed_users(recipients, target)
- recipients = reject_users_without_access(recipients, target)
-
- recipients.delete(current_user) if skip_current_user && !current_user.notified_of_own_activity?
-
- recipients.uniq
- end
-
- def build_relabeled_recipients(target, project, current_user, labels:)
- recipients = add_labels_subscribers([], project, target, labels: labels)
- recipients = reject_unsubscribed_users(recipients, target)
- recipients = reject_users_without_access(recipients, target)
- recipients.delete(current_user) unless current_user.notified_of_own_activity?
- recipients.uniq
- end
-
def mailer
Notify
end
@@ -652,10 +372,4 @@ class NotificationService
end
end
end
-
- # Build event key to search on custom notification level
- # Check NotificationSetting::EMAIL_EVENTS
- def build_custom_key(action, object)
- "#{action}_#{object.class.model_name.name.underscore}".to_sym
- end
end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index 1c5a549feb9..d484a96f785 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -33,6 +33,7 @@ module Projects
def import_repository
begin
+ raise Error, "Blocked import URL." if Gitlab::UrlBlocker.blocked_url?(project.import_url)
gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url)
rescue => e
# Expire cache to prevent scenarios such as:
@@ -40,7 +41,7 @@ module Projects
# 2. Retried import, repo is broken or not imported but +exists?+ still returns true
project.repository.before_import if project.repository_exists?
- raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}"
+ raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}"
end
end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index 868fa7b3f21..af0ddbe5934 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -24,10 +24,9 @@ class SystemHooksService
key: model.key,
id: model.id
)
+
if model.user
- data.merge!(
- username: model.user.username
- )
+ data[:username] = model.user.username
end
when Project
data.merge!(project_data(model))
@@ -35,8 +34,6 @@ class SystemHooksService
if event == :rename || event == :transfer
data[:old_path_with_namespace] = model.old_path_with_namespace
end
-
- data
when User
data.merge!({
name: model.name,
@@ -59,6 +56,8 @@ class SystemHooksService
when GroupMember
data.merge!(group_member_data(model))
end
+
+ data
end
def build_event_name(model, event)
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 8787a1c93a9..bf7e76ec59e 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -201,10 +201,12 @@ class TodoService
def update_todos_state_by_ids(ids, current_user, state)
todos = current_user.todos.where(id: ids)
- # Only return those that are not really on that state
- marked_todos = todos.where.not(state: state).update_all(state: state)
+ # Only update those that are not really on that state
+ todos = todos.where.not(state: state)
+ todos_ids = todos.pluck(:id)
+ todos.update_all(state: state)
current_user.update_todos_count_cache
- marked_todos
+ todos_ids
end
def create_todos(users, attributes)
diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb
index d9370bbb598..8f6f5b937c4 100644
--- a/app/services/users/refresh_authorized_projects_service.rb
+++ b/app/services/users/refresh_authorized_projects_service.rb
@@ -93,9 +93,7 @@ module Users
end
def current_authorizations_per_project
- current_authorizations.each_with_object({}) do |row, hash|
- hash[row.project_id] = row
- end
+ current_authorizations.index_by(&:project_id)
end
def current_authorizations
diff --git a/app/validators/importable_url_validator.rb b/app/validators/importable_url_validator.rb
new file mode 100644
index 00000000000..37a314adee6
--- /dev/null
+++ b/app/validators/importable_url_validator.rb
@@ -0,0 +1,11 @@
+# ImportableUrlValidator
+#
+# This validator blocks projects from using dangerous import_urls to help
+# protect against Server-side Request Forgery (SSRF).
+class ImportableUrlValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ if Gitlab::UrlBlocker.blocked_url?(value)
+ record.errors.add(attribute, "imports are not allowed from that URL")
+ end
+ end
+end
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index 9175b3d3f96..e403a9da616 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -48,7 +48,7 @@
.form-actions
= f.submit 'Save', class: 'btn btn-save append-right-10'
- if @appearance.persisted?
- = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank'
+ = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
- if @appearance.updated_at
%span.pull-right
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 00366b0a8c9..3eab065bb9f 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -404,7 +404,7 @@
Enable Sentry
.help-block
Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here:
- %a{ href: 'https://getsentry.com', target: '_blank' } https://getsentry.com
+ %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com
.form-group
= f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2'
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index 388190642aa..d0c12aa57ae 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -36,14 +36,14 @@
- if todo.pending?
.todo-actions
- = link_to [:dashboard, todo], method: :delete, class: 'btn btn-loading js-done-todo' do
+ = link_to dashboard_todo_path(todo), method: :delete, class: 'btn btn-loading js-done-todo', data: { href: dashboard_todo_path(todo) } do
Done
= icon('spinner spin')
- = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-undo-todo hidden' do
+ = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-undo-todo hidden', data: { href: restore_dashboard_todo_path(todo) } do
Undo
= icon('spinner spin')
- else
.todo-actions
- = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-add-todo' do
+ = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'btn btn-loading js-add-todo', data: { href: restore_dashboard_todo_path(todo) } do
Add todo
= icon('spinner spin')
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index d7e0a8e4b2c..d31ced004a0 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -19,9 +19,12 @@
.nav-controls
- if @todos.any?(&:pending?)
- = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do
+ = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete, data: { href: destroy_all_dashboard_todos_path(todos_filter_params) } do
Mark all as done
= icon('spinner spin')
+ = link_to bulk_restore_dashboard_todos_path, class: 'btn btn-loading js-todos-undo-all hidden', method: :patch , data: { href: bulk_restore_dashboard_todos_path(todos_filter_params) } do
+ Undo mark all as done
+ = icon('spinner spin')
.todos-filters
.row-content-block.second-block
@@ -57,8 +60,8 @@
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-sort
%li
- = link_to todos_filter_path(sort: sort_value_priority) do
- = sort_title_priority
+ = link_to todos_filter_path(sort: sort_value_label_priority) do
+ = sort_title_label_priority
= link_to todos_filter_path(sort: sort_value_recently_created) do
= sort_title_recently_created
= link_to todos_filter_path(sort: sort_value_oldest_created) do
@@ -67,12 +70,16 @@
.js-todos-all
- if @todos.any?
- .js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} }
- .panel.panel-default.panel-small.panel-without-border
- %ul.content-list.todos-list
- = render @todos
- = paginate @todos, theme: "gitlab"
-
+ .js-todos-list-container
+ .js-todos-options{ data: { per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages } }
+ .panel.panel-default.panel-small.panel-without-border
+ %ul.content-list.todos-list
+ = render @todos
+ = paginate @todos, theme: "gitlab"
+ .js-nothing-here-container.todos-all-done.hidden
+ = render "shared/empty_states/icons/todos_all_done.svg"
+ %h4.text-center
+ You're all done!
- elsif current_user.todos.any?
.todos-all-done
= render "shared/empty_states/icons/todos_all_done.svg"
diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder
index 43a52cf3002..158061579f6 100644
--- a/app/views/events/_event.atom.builder
+++ b/app/views/events/_event.atom.builder
@@ -9,7 +9,7 @@ xml.entry do
xml.author do
xml.name event.author_name
- xml.email event.author_email
+ xml.email event.author_public_email
end
xml.summary(type: "xhtml") do |summary|
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index f08c96df309..64b5a733b77 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -15,6 +15,6 @@
= link_to note.attachment.url, target: '_blank' do
= image_tag note.attachment.url, class: 'note-image-attach'
- else
- = link_to note.attachment.url, target: "_blank", class: 'note-file-attach' do
+ = link_to note.attachment.url, target: '_blank', class: 'note-file-attach' do
%i.fa.fa-paperclip
= note.attachment_identifier
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index 8374f5a009f..bb2cd0d44c8 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -11,5 +11,3 @@
= render 'groups'
- else
.nothing-here-block No public groups
-
-= paginate @groups, theme: "gitlab"
diff --git a/app/views/groups/_settings_head.html.haml b/app/views/groups/_settings_head.html.haml
new file mode 100644
index 00000000000..2454e7355a7
--- /dev/null
+++ b/app/views/groups/_settings_head.html.haml
@@ -0,0 +1,14 @@
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: container_class }
+ = nav_link(path: 'groups#edit') do
+ = link_to edit_group_path(@group), title: 'General' do
+ %span
+ General
+
+ = nav_link(path: 'groups#projects') do
+ = link_to projects_group_path(@group), title: 'Projects' do
+ %span
+ Projects
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 2706e8692d1..80a77dab97f 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -1,3 +1,4 @@
+= render "groups/settings_head"
.panel.panel-default.prepend-top-default
.panel-heading
Group settings
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index 2e7e5e5c309..83bdd654f27 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -1,4 +1,4 @@
-- page_title "Projects"
+= render "groups/settings_head"
.panel.panel-default.prepend-top-default
.panel-heading
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 31631887317..f93b6b63426 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -17,7 +17,7 @@
%br
Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.
%br
- Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}.
+ Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}.
- if current_application_settings.help_page_text.present?
%hr
= markdown_field(current_application_settings, :help_page_text)
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index e18bd47798b..e6058617ac9 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -33,7 +33,7 @@
- @already_added_projects.each do |project|
%tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
%td
- = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank'
+ = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank', rel: 'noopener noreferrer'
%td
= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
@@ -50,7 +50,7 @@
- @repos.each do |repo|
%tr{ id: "repo_#{repo.owner}___#{repo.slug}" }
%td
- = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: "_blank"
+ = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer'
%td.import-target
%fieldset.row
.input-group
@@ -70,7 +70,7 @@
- @incompatible_repos.each do |repo|
%tr{ id: "repo_#{repo.owner}___#{repo.slug}" }
%td
- = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank'
+ = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer'
%td.import-target
%td.import-actions-job-status
= label_tag 'Incompatible Project', nil, class: 'label label-danger'
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index d5b88709a34..7456799ca0e 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -43,7 +43,7 @@
- @repos.each do |repo|
%tr{ id: "repo_#{repo["id"]}" }
%td
- = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank"
+ = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank", rel: 'noopener noreferrer'
%td.import-target
= import_project_target(repo['namespace']['path'], repo['name'])
%td.import-actions.job-status
diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml
index 336becd229e..c5800a1cca0 100644
--- a/app/views/import/google_code/new.html.haml
+++ b/app/views/import/google_code/new.html.haml
@@ -13,7 +13,7 @@
%li
%p
Go to
- #{link_to "Google Takeout", "https://www.google.com/settings/takeout", target: "_blank"}.
+ #{link_to "Google Takeout", "https://www.google.com/settings/takeout", target: '_blank', rel: 'noopener noreferrer'}.
%li
%p
Make sure you're logged into the account that owns the projects you'd like to import.
diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml
index 5e01af008be..60de6bfe816 100644
--- a/app/views/import/google_code/status.html.haml
+++ b/app/views/import/google_code/status.html.haml
@@ -36,7 +36,7 @@
- @already_added_projects.each do |project|
%tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
%td
- = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank"
+ = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank", rel: 'noopener noreferrer'
%td
= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
@@ -53,7 +53,7 @@
- @repos.each do |repo|
%tr{ id: "repo_#{repo.id}" }
%td
- = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
+ = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank", rel: 'noopener noreferrer'
%td.import-target
#{current_user.username}/#{repo.name}
%td.import-actions.job-status
@@ -63,7 +63,7 @@
- @incompatible_repos.each do |repo|
%tr{ id: "repo_#{repo.id}" }
%td
- = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
+ = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank", rel: 'noopener noreferrer'
%td.import-target
%td.import-actions-job-status
= label_tag "Incompatible Project", nil, class: "label label-danger"
diff --git a/app/views/issues/_issue.atom.builder b/app/views/issues/_issue.atom.builder
index fcd30c8c765..23a88448055 100644
--- a/app/views/issues/_issue.atom.builder
+++ b/app/views/issues/_issue.atom.builder
@@ -7,7 +7,7 @@ xml.entry do
xml.author do
xml.name issue.author_name
- xml.email issue.author_email
+ xml.email issue.author_public_email
end
xml.summary issue.title
@@ -26,7 +26,7 @@ xml.entry do
if issue.assignee
xml.assignee do
xml.name issue.assignee.name
- xml.email issue.assignee.email
+ xml.email issue.assignee_public_email
end
end
end
diff --git a/app/views/koding/index.html.haml b/app/views/koding/index.html.haml
index 65887aacbaf..04e2d4b63e6 100644
--- a/app/views/koding/index.html.haml
+++ b/app/views/koding/index.html.haml
@@ -2,5 +2,5 @@
%p
= icon('circle', class: 'cgreen')
Integration is active for
- = link_to koding_project_url, target: '_blank' do
+ = link_to koding_project_url, target: '_blank', rel: 'noopener noreferrer' do
#{current_application_settings.koding_url}
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 6f4f2dbea3a..5fde5c2613e 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -67,7 +67,7 @@
= link_to root_path, class: 'home', title: 'Dashboard', id: 'logo' do
= brand_header_logo
- %h1.title= title
+ %h1.title{ class: ('initializing' if @has_group_title) }= title
= yield :header_content
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index a6e96942021..8605380848d 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -1,4 +1,3 @@
-= render 'layouts/nav/group_settings'
.scrolling-tabs-container{ class: nav_control_class }
.fade-left
= icon('angle-left')
@@ -25,3 +24,8 @@
= link_to group_group_members_path(@group), title: 'Members' do
%span
Members
+ - if current_user && can?(current_user, :admin_group, @group)
+ = nav_link(path: %w[groups#projects groups#edit]) do
+ = link_to edit_group_path(@group), title: 'Settings' do
+ %span
+ Settings
diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml
deleted file mode 100644
index 30feb6813b4..00000000000
--- a/app/views/layouts/nav/_group_settings.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-- if current_user
- - can_admin_group = can?(current_user, :admin_group, @group)
- - can_edit = can?(current_user, :admin_group, @group)
-
- - if can_admin_group || can_edit
- .controls
- .dropdown.group-settings-dropdown
- %a.dropdown-new.btn.btn-default#group-settings-button{ href: '#', 'data-toggle' => 'dropdown' }
- = icon('cog')
- = icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- - if can_admin_group
- = nav_link(path: 'groups#projects') do
- = link_to 'Projects', projects_group_path(@group), title: 'Projects'
- - if can_edit && can_admin_group
- %li.divider
- %li
- = link_to 'Edit Group', edit_group_path(@group)
diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml
deleted file mode 100644
index 060b50ffc69..00000000000
--- a/app/views/notify/build_fail_email.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-- content_for :header do
- %h1{ style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" }
- GitLab (job failed)
-
-%h3
- Project:
- = link_to namespace_project_url(@project.namespace, @project) do
- = @project.name
-
-%p
- Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)}
-%p
- Author: #{@build.pipeline.git_author_name}
-%p
- Branch: #{@build.ref}
-%p
- Stage: #{@build.stage}
-%p
- Job: #{@build.name}
-%p
- Message: #{@build.pipeline.git_commit_message}
-
-%p
- Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
diff --git a/app/views/notify/build_fail_email.text.erb b/app/views/notify/build_fail_email.text.erb
deleted file mode 100644
index 2a94688a6b0..00000000000
--- a/app/views/notify/build_fail_email.text.erb
+++ /dev/null
@@ -1,11 +0,0 @@
-Job failed for <%= @project.name %>
-
-Status: <%= @build.status %>
-Commit: <%= @build.pipeline.short_sha %>
-Author: <%= @build.pipeline.git_author_name %>
-Branch: <%= @build.ref %>
-Stage: <%= @build.stage %>
-Job: <%= @build.name %>
-Message: <%= @build.pipeline.git_commit_message %>
-
-Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %>
diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml
deleted file mode 100644
index ca0eaa96a9d..00000000000
--- a/app/views/notify/build_success_email.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-- content_for :header do
- %h1{ style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" }
- GitLab (job successful)
-
-%h3
- Project:
- = link_to namespace_project_url(@project.namespace, @project) do
- = @project.name
-
-%p
- Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)}
-%p
- Author: #{@build.pipeline.git_author_name}
-%p
- Branch: #{@build.ref}
-%p
- Stage: #{@build.stage}
-%p
- Job: #{@build.name}
-%p
- Message: #{@build.pipeline.git_commit_message}
-
-%p
- Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
diff --git a/app/views/notify/build_success_email.text.erb b/app/views/notify/build_success_email.text.erb
deleted file mode 100644
index 445cd46e64f..00000000000
--- a/app/views/notify/build_success_email.text.erb
+++ /dev/null
@@ -1,11 +0,0 @@
-Job successful for <%= @project.name %>
-
-Status: <%= @build.status %>
-Commit: <%= @build.pipeline.short_sha %>
-Author: <%= @build.pipeline.git_author_name %>
-Branch: <%= @build.ref %>
-Stage: <%= @build.stage %>
-Job: <%= @build.name %>
-Message: <%= @build.pipeline.git_commit_message %>
-
-Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %>
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 51c4e8e5a73..5c5e5940365 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -34,11 +34,6 @@
.clearfix
- = form_for @user, url: profile_notifications_path, method: :put do |f|
- %label{ for: 'user_notified_of_own_activity' }
- = f.check_box :notified_of_own_activity
- %span Receive notifications about your own activity
-
%hr
%h5
Groups (#{@group_notifications.count})
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index d551754a2e5..c74b3249a13 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -18,7 +18,7 @@
or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
.col-lg-9
.clearfix.avatar-image.append-bottom-default
- = link_to avatar_icon(@user, 400), target: '_blank' do
+ = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
%h5.prepend-top-0
Upload new avatar
diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml
index 188198c47d5..61420fd0fb6 100644
--- a/app/views/projects/_merge_request_merge_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_settings.html.haml
@@ -13,3 +13,7 @@
= form.label :only_allow_merge_if_all_discussions_are_resolved do
= form.check_box :only_allow_merge_if_all_discussions_are_resolved
%strong Only allow merge requests to be merged if all discussions are resolved
+ .checkbox
+ = form.label :printing_merge_request_link_enabled do
+ = form.check_box :printing_merge_request_link_enabled
+ %strong Show link to create/view merge request when pushing from the command line
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index 8a40281e28c..4ad77b6266d 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -7,13 +7,8 @@
#blob-content-holder.tree-holder
.file-holder
- .js-file-title.file-title
- = blob_icon @blob.mode, @blob.name
- %strong
- = @path
- %small= number_to_human_size @blob.size
- .file-actions
- = render "projects/blob/actions"
+ = render "projects/blob/header", blob: @blob
+
.table-responsive.file-content.blame.code.js-syntax-highlight
%table
- current_line = 1
diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml
deleted file mode 100644
index 14d42f7d9ec..00000000000
--- a/app/views/projects/blob/_actions.html.haml
+++ /dev/null
@@ -1,26 +0,0 @@
-- if @environment
- .btn-group<
- = view_on_environment_button(@commit.sha, @path, @environment)
-
-.btn-group{ role: "group" }<
- = link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id),
- class: 'btn btn-sm', target: '_blank'
- -# only show normal/blame view links for text files
- - if blob_text_viewable?(@blob)
- - if current_page? namespace_project_blame_path(@project.namespace, @project, @id)
- = link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id),
- class: 'btn btn-sm'
- - else
- = link_to 'Blame', namespace_project_blame_path(@project.namespace, @project, @id),
- class: 'btn btn-sm js-blob-blame-link' unless @blob.empty?
- = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id),
- class: 'btn btn-sm'
- = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project,
- tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url'
-
-- if current_user
- .btn-group{ role: "group" }<
- - if blob_text_viewable?(@blob)
- = edit_blob_link
- = replace_blob_link
- = delete_blob_link
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index bf8801bb1e3..2b2ee6ed987 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -24,13 +24,5 @@
#blob-content-holder.blob-content-holder
%article.file-holder
- .js-file-title.file-title-flex-parent
- .file-header-content
- = blob_icon blob.mode, blob.name
- %strong.file-title-name
- = blob.name
- %small
- = number_to_human_size(blob_size(blob))
- .file-actions.hidden-xs
- = render "actions"
+ = render "projects/blob/header", blob: blob
= render blob.to_partial_path(@project), blob: blob
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
new file mode 100644
index 00000000000..deeeae3d64a
--- /dev/null
+++ b/app/views/projects/blob/_header.html.haml
@@ -0,0 +1,39 @@
+.js-file-title.file-title-flex-parent
+ .file-header-content
+ = blob_icon blob.mode, blob.name
+
+ %strong.file-title-name
+ = blob.name
+
+ = copy_file_path_button(blob.path)
+
+ %small
+ = number_to_human_size(blob_size(blob))
+
+ .file-actions.hidden-xs
+ .btn-group{ role: "group" }<
+ = copy_blob_content_button(blob) if blob_text_viewable?(blob)
+ = open_raw_file_button(namespace_project_raw_path(@project.namespace, @project, @id))
+ = view_on_environment_button(@commit.sha, @path, @environment) if @environment
+
+ .btn-group{ role: "group" }<
+ -# only show normal/blame view links for text files
+ - if blob_text_viewable?(blob)
+ - if current_page? namespace_project_blame_path(@project.namespace, @project, @id)
+ = link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id),
+ class: 'btn btn-sm'
+ - else
+ = link_to 'Blame', namespace_project_blame_path(@project.namespace, @project, @id),
+ class: 'btn btn-sm js-blob-blame-link' unless blob.empty?
+
+ = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id),
+ class: 'btn btn-sm'
+
+ = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project,
+ tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url'
+
+ - if current_user
+ .btn-group{ role: "group" }<
+ = edit_blob_link if blob_text_viewable?(blob)
+ = replace_blob_link
+ = delete_blob_link
diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml
index f864702d862..ea3cecb86a9 100644
--- a/app/views/projects/blob/_image.html.haml
+++ b/app/views/projects/blob/_image.html.haml
@@ -9,7 +9,7 @@
- else
.nothing-here-block
The SVG could not be displayed as it is too large, you can
- #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank')}
+ #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank', rel: 'noopener noreferrer')}
instead.
- else
%img{ src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path)), alt: "#{blob.name}" }
diff --git a/app/views/projects/blob/_text.html.haml b/app/views/projects/blob/_text.html.haml
index 58524418a67..7b16d266982 100644
--- a/app/views/projects/blob/_text.html.haml
+++ b/app/views/projects/blob/_text.html.haml
@@ -3,17 +3,17 @@
.nothing-here-block
File too large, you can
= succeed '.' do
- = link_to 'view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank'
+ = link_to 'view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank', rel: 'noopener noreferrer'
- else
- blob.load_all_data!(@repository)
- - if markup?(blob.name)
- .file-content.wiki
- = render_markup(blob.name, blob.data)
+ - if blob.empty?
+ .file-content.code
+ .nothing-here-block Empty file
- else
- - if blob.empty?
- .file-content.code
- .nothing-here-block Empty file
+ - if markup?(blob.name)
+ .file-content.wiki
+ = render_markup(blob.name, blob.data)
- else
= render 'shared/file_highlight', blob: blob, repository: @repository
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 8853801016b..3bcddcb37f1 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -9,7 +9,7 @@
- if @conflict
.alert.alert-danger
Someone edited the file the same time you did. Please check out
- = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank"
+ = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank", rel: 'noopener noreferrer'
and make sure your changes will not unintentionally remove theirs.
.file-editor
diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml
index fa463edd526..added3f669b 100644
--- a/app/views/projects/boards/_show.html.haml
+++ b/app/views/projects/boards/_show.html.haml
@@ -10,6 +10,7 @@
%script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
%script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list"
+ %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
= render "projects/issues/head"
diff --git a/app/views/projects/boards/components/_blank_state.html.haml b/app/views/projects/boards/components/_blank_state.html.haml
deleted file mode 100644
index 0af40ddf8fe..00000000000
--- a/app/views/projects/boards/components/_blank_state.html.haml
+++ /dev/null
@@ -1,15 +0,0 @@
-%board-blank-state{ "inline-template" => true,
- "v-if" => 'list.id == "blank"' }
- .board-blank-state
- %p
- Add the following default lists to your Issue Board with one click:
- %ul.board-blank-state-list
- %li{ "v-for" => "label in predefinedLabels" }
- %span.label-color{ ":style" => "{ backgroundColor: label.color } " }
- {{ label.title }}
- %p
- Starting out with the default set of lists will get you right on the way to making the most of your board.
- %button.btn.btn-create.btn-inverted.btn-block{ type: "button", "@click.stop" => "addDefaultLists" }
- Add default lists
- %button.btn.btn-default.btn-block{ type: "button", "@click.stop" => "clearBlankState" }
- Nevermind, I'll use my own
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index 72bce4049de..0bca6a786cb 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -32,4 +32,4 @@
":root-path" => "rootPath",
"ref" => "board-list" }
- if can?(current_user, :admin_list, @project)
- = render "projects/boards/components/blank_state"
+ %board-blank-state{ "v-if" => 'list.id == "blank"' }
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index ae63f8184df..9eb610ba9c0 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -27,7 +27,7 @@
= link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: "btn btn-default #{'prepend-left-10' unless merge_project}", method: :post, title: "Compare" do
Compare
- = render 'projects/buttons/download', project: @project, ref: branch.name
+ = render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name]
- if can?(current_user, :push_code, @project)
= link_to namespace_project_branch_path(@project.namespace, @project, branch.name),
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index b560ed21f1d..d90d4a27cd6 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,3 +1,5 @@
+- pipeline = local_assigns.fetch(:pipeline) { project.pipelines.latest_successful_for(ref) }
+
- if !project.empty_repo? && can?(current_user, :download_code, project)
.project-action-button.dropdown.inline>
%button.btn{ 'data-toggle' => 'dropdown' }
@@ -24,7 +26,6 @@
%i.fa.fa-download
%span Download tar
- - pipeline = project.pipelines.latest_successful_for(ref)
- if pipeline
- artifacts = pipeline.builds.latest.with_artifacts
- if artifacts.any?
diff --git a/app/views/projects/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml
index 5d9a776da89..a5a9e4d0621 100644
--- a/app/views/projects/buttons/_koding.html.haml
+++ b/app/views/projects/buttons/_koding.html.haml
@@ -1,3 +1,3 @@
- if koding_enabled? && current_user && @repository.koding_yml && can_push_branch?(@project, @project.default_branch)
- = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank' do
+ = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank', rel: 'noopener noreferrer' do
Run in IDE (Koding)
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index d001e01609a..a0a292d0508 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -63,15 +63,15 @@
- if @commit.status
.well-segment.pipeline-info
- %div{ class: "icon-container ci-status-icon-#{@commit.status}" }
- = link_to namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id) do
+ .status-icon-container{ class: "ci-status-icon-#{@commit.status}" }
+ = link_to namespace_project_pipeline_path(@project.namespace, @project, @commit.latest_pipeline.id) do
= ci_icon_for_status(@commit.status)
Pipeline
- = link_to "##{@commit.pipelines.last.id}", namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id), class: "monospace"
- for
- = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
- %span.ci-status-label
- = ci_label_for_status(@commit.status)
+ = link_to "##{@commit.latest_pipeline.id}", namespace_project_pipeline_path(@project.namespace, @project, @commit.latest_pipeline.id), class: "monospace"
+ = ci_label_for_status(@commit.status)
+ - if @commit.latest_pipeline.stages.any?
+ .mr-widget-pipeline-graph
+ = render 'shared/mini_pipeline_graph', pipeline: @commit.latest_pipeline, klass: 'js-commit-pipeline-graph'
in
= time_interval_in_words @commit.pipelines.total_duration
diff --git a/app/views/projects/cycle_analytics/_overview.html.haml b/app/views/projects/cycle_analytics/_overview.html.haml
index c8f0b547f80..9007f2c24ba 100644
--- a/app/views/projects/cycle_analytics/_overview.html.haml
+++ b/app/views/projects/cycle_analytics/_overview.html.haml
@@ -9,7 +9,7 @@
Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
To set up CA, you must first define a production environment by setting up your CI and then deploy to production.
%p
- %a.btn{ href: help_page_path('user/project/cycle_analytics'), target: "_blank" } Read more
+ %a.btn{ href: help_page_path('user/project/cycle_analytics'), target: '_blank' } Read more
.col-md-6.overview-image
%span.overview-icon
= custom_icon ('icon_cycle_analytics_overview')
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
index f809c52c367..7d6b3701f95 100644
--- a/app/views/projects/diffs/_file_header.html.haml
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -2,8 +2,11 @@
- if defined?(blob) && blob && diff_file.submodule?
%span
= icon('archive fw')
- %span
+
+ %strong.file-title-name
= submodule_link(blob, diff_commit.id, project.repository)
+
+ = copy_file_path_button(blob.path)
- else
= conditional_link_to url.present?, url do
= blob_icon diff_file.b_mode, diff_file.file_path
@@ -21,7 +24,7 @@
- if diff_file.deleted_file
deleted
- = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard')
+ = copy_file_path_button(diff_file.new_path)
- if diff_file.mode_changed?
%small
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 58c085cdb9d..85e442e115c 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -5,6 +5,7 @@
= render 'shared/no_ssh'
= render 'shared/no_password'
+= render "projects/head"
= render "home_panel"
.row-content-block.second-block.center
diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml
index 4c8fe1c271b..bf0f1819073 100644
--- a/app/views/projects/environments/_external_url.html.haml
+++ b/app/views/projects/environments/_external_url.html.haml
@@ -1,3 +1,3 @@
- if environment.external_url && can?(current_user, :read_environment, environment)
- = link_to environment.external_url, target: '_blank', class: 'btn external-url' do
+ = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do
= icon('external-link')
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index f8e94ca98ae..b8c1782f050 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -1,5 +1,8 @@
- @no_container = true
- page_title "Metrics for environment", @environment.name
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('common_d3')
+ = page_specific_javascript_bundle_tag('monitoring')
= render "projects/pipelines/head"
%div{ class: container_class }
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 7b7d7b1e00e..f3a429d12d9 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -19,15 +19,14 @@
.nav-controls
= link_to params.merge(rss_url_options), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do
= icon('rss')
- - if can? current_user, :create_issue, @project
- = link_to new_namespace_project_issue_path(@project.namespace,
- @project,
- issue: { assignee_id: issues_finder.assignee.try(:id),
- milestone_id: issues_finder.milestones.first.try(:id) }),
- class: "btn btn-new",
- title: "New Issue",
- id: "new_issue_link" do
- New Issue
+ = link_to new_namespace_project_issue_path(@project.namespace,
+ @project,
+ issue: { assignee_id: issues_finder.assignee.try(:id),
+ milestone_id: issues_finder.milestones.first.try(:id) }),
+ class: "btn btn-new",
+ title: "New Issue",
+ id: "new_issue_link" do
+ New Issue
= render 'shared/issuable/search_bar', type: :issues
.issues-holder
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index e6a4cf929d2..d665c9125b3 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -20,37 +20,34 @@
= confidential_icon(@issue)
= issuable_meta(@issue, @project, "Issue")
- - if can?(current_user, :create_issue, @project) || can?(current_user, :update_issue, @issue)
- .issuable-actions
- .clearfix.issue-btn-group.dropdown
- %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
- Options
- = icon('caret-down')
- .dropdown-menu.dropdown-menu-align-right.hidden-lg
- %ul
- - if can?(current_user, :create_issue, @project)
- %li
- = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link'
- - if can?(current_user, :update_issue, @issue)
- %li
- = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- %li
- = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
- %li
- = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
- - if @issue.submittable_as_spam_by?(current_user)
- %li
- = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
-
- - if can?(current_user, :create_issue, @project)
- = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
- New issue
- - if can?(current_user, :update_issue, @issue)
- = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ .issuable-actions
+ .clearfix.issue-btn-group.dropdown
+ %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
+ Options
+ = icon('caret-down')
+ .dropdown-menu.dropdown-menu-align-right.hidden-lg
+ %ul
+ %li
+ = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link'
+ - if can?(current_user, :update_issue, @issue)
+ %li
+ = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ %li
+ = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ %li
+ = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
- if @issue.submittable_as_spam_by?(current_user)
- = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
- = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
+ %li
+ = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
+
+ = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
+ New issue
+ - if can?(current_user, :update_issue, @issue)
+ = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ - if @issue.submittable_as_spam_by?(current_user)
+ = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
+ = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
.issue-details.issuable-details
.detail-page-description.content-block{ class: ('hide-bottom-border' unless @issue.description.present? ) }
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index c8f097c69da..6682a85ffa6 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -16,7 +16,7 @@
.pull-right
- if @merge_request.source_branch_exists?
- if koding_enabled? && @repository.koding_yml
- = link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank' do
+ = link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank', rel: 'noopener noreferrer' do
Run in IDE (Koding)
= link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do
Check out branch
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index 93ed4b68e0e..cde0ce08e14 100644
--- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
@@ -49,7 +49,7 @@
%strong Tip:
= succeed '.' do
You can also checkout merge requests locally by
- = link_to 'following these guidelines', help_page_path('user/project/merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank'
+ = link_to 'following these guidelines', help_page_path('user/project/merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank', rel: 'noopener noreferrer'
:javascript
$(function(){
diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml
index 11f41e75e63..55b0b837c6d 100644
--- a/app/views/projects/milestones/edit.html.haml
+++ b/app/views/projects/milestones/edit.html.haml
@@ -5,7 +5,7 @@
%div{ class: container_class }
%h3.page-title
- Edit Milestone ##{@milestone.iid}
+ Edit Milestone #{@milestone.to_reference}
%hr
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index b4dde2c86c9..d16f49bd33a 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -20,7 +20,7 @@
.header-text-content
%span.identifier
%strong
- Milestone %#{@milestone.iid}
+ Milestone #{@milestone.to_reference}
- if @milestone.due_date || @milestone.start_date
= milestone_date_range(@milestone)
.milestone-buttons
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 2a98bba05ee..d129da943f8 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -1,5 +1,6 @@
- page_title 'New Project'
- header_title "Projects", dashboard_projects_path
+- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility
.project-edit-container
.project-edit-errors
@@ -95,7 +96,7 @@
= f.label :visibility_level, class: 'label-light' do
Visibility Level
= link_to icon('question-circle'), help_page_path("public_access/public_access")
- = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project, with_label: false
+ = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false
= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel'
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
index 3a323d94cc2..2fb88297fb3 100644
--- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -4,13 +4,13 @@
%ul.list-unstyled.indent-list
%li
1.
- = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noopener noreferrer nofollow' do
Enable custom slash commands
= icon('external-link')
on your Mattermost installation
%li
2.
- = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noreferrer noopener nofollow' do
+ = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noopener noreferrer nofollow' do
Add a slash command
= icon('external-link')
in your Mattermost team with these options:
diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
index a04fd5035a6..2a1b9d4c465 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -4,7 +4,7 @@
%p
This service allows users to perform common operations on this
project by entering slash commands in Mattermost.
- = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do
+ = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do
View documentation
= icon('external-link')
%p.inline
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index 0d973a20d4c..078b7be6865 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -5,7 +5,7 @@
%p
This service allows users to perform common operations on this
project by entering slash commands in Slack.
- = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do
+ = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do
View documentation
= icon('external-link')
%p.inline
@@ -57,7 +57,7 @@
= label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block
= image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36)
- = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank')
+ = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer')
.form-group
= label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 6b3d7d4008b..e35385f4cab 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -4,13 +4,7 @@
.project-snippets
%article.file-holder.snippet-file-content
- .js-file-title.file-title
- = blob_icon 0, @snippet.file_name
- = @snippet.file_name
- .file-actions
- = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm")
- = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
- = render 'shared/snippets/blob'
+ = render 'shared/snippets/blob', raw_path: raw_namespace_project_snippet_path(@project.namespace, @project, @snippet)
.row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 8ef069b9e05..dffe908e85a 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -23,7 +23,7 @@
= markdown_field(release, :description)
.row-fixed-content.controls
- = render 'projects/buttons/download', project: @project, ref: tag.name
+ = render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name]
- if can?(current_user, :push_code, @project)
= link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 763c2fea39b..5211ade1a5f 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -4,6 +4,6 @@
New Page
= link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do
Page History
- - if can?(current_user, :create_wiki, @project)
+ - if can?(current_user, :create_wiki, @project) && @page.latest?
= link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn" do
Edit
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index 0ce0d759e86..a212c714826 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -1,6 +1,5 @@
.dropdown.inline.prepend-left-10
%button.dropdown-toggle{ type: 'button', data: {toggle: 'dropdown' } }
- %span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
@@ -10,6 +9,8 @@
%li
= link_to page_filter_path(sort: sort_value_priority, label: true) do
= sort_title_priority
+ = link_to page_filter_path(sort: sort_value_label_priority, label: true) do
+ = sort_title_label_priority
= link_to page_filter_path(sort: sort_value_recently_created, label: true) do
= sort_title_recently_created
= link_to page_filter_path(sort: sort_value_oldest_created, label: true) do
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index e2033654018..7a7e3d46796 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -16,7 +16,6 @@
Also, issues are searchable and filterable.
- if project_select_button
= render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue'
- - else
- = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
- else
- %h4.text-center There are no issues to show.
+ %h4 There are no issues to show.
+ = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml
index ba5c2dae09d..00fb77bdb3b 100644
--- a/app/views/shared/empty_states/_labels.html.haml
+++ b/app/views/shared/empty_states/_labels.html.haml
@@ -5,7 +5,7 @@
.col-xs-12.col-sm-6
.text-content
%h4 Labels can be applied to issues and merge requests to categorize them.
- %p You can also star label to make it a priority label.
+ %p You can also star a label to make it a priority label.
- if can?(current_user, :admin_label, @project)
= link_to 'New label', new_namespace_project_label_path(@project.namespace, @project), class: 'btn btn-new', title: 'New label', id: 'new_label_link'
= link_to 'Generate a default set of labels', generate_namespace_project_labels_path(@project.namespace, @project), method: :post, class: 'btn btn-success btn-inverted', title: 'Generate a default set of labels', id: 'generate_labels_link'
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 60ca23ef680..a95020a9be8 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -1,5 +1,6 @@
- group_member = local_assigns[:group_member]
- full_name = true unless local_assigns[:full_name] == false
+- group_name = full_name ? group.full_name : group.name
- css_class = '' unless local_assigns[:css_class]
- css_class += " no-description" if group.description.blank?
@@ -28,11 +29,7 @@
.avatar-container.s40
= image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
- = link_to group, class: 'group-name' do
- - if full_name
- = group.full_name
- - else
- = group.name
+ = link_to group_name, group, class: 'group-name'
- if group_member
as
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index f0bad69a989..847a86e2e68 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -24,7 +24,7 @@
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), 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
+ = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true, show_started: true
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 0b0f2c9cd1a..17107f55a2d 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -8,7 +8,7 @@
.alert.alert-danger
Someone edited the #{issuable.class.model_name.human.downcase} the same time you did.
Please check out
- = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank"
+ = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank", rel: 'noopener noreferrer'
and make sure your changes will not unintentionally remove theirs
.form-group
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index 415361f8fbf..f0d50828e2a 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -6,7 +6,7 @@
- if selected.present? || params[:milestone_title].present?
= hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id)
= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone",
- placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
+ placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
- if project
%ul.dropdown-footer-list
- if can? current_user, :admin_milestone, project
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index f1730b1791c..b58640c3ef0 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -1,7 +1,8 @@
- type = local_assigns.fetch(:type)
+- block_css_class = type != :boards_modal ? 'row-content-block second-block' : ''
.issues-filters
- .issues-details-filters.row-content-block.second-block.filtered-search-block
+ .issues-details-filters.filtered-search-block{ class: block_css_class, "v-pre" => type == :boards_modal }
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
- if params[:search].present?
= hidden_field_tag :search, params[:search]
@@ -14,7 +15,7 @@
.scroll-container
%ul.tokens-container.list-unstyled
%li.input-token
- %input.form-control.filtered-search{ placeholder: 'Search or filter results...', data: { id: 'filtered-search', 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } }
+ %input.form-control.filtered-search{ placeholder: 'Search or filter results...', data: { id: "filtered-search-#{type.to_s}", 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } }
= icon('filter')
%button.clear-search.hidden{ type: 'button' }
= icon('times')
@@ -68,12 +69,15 @@
%li.filter-dropdown-item{ data: { value: 'upcoming' } }
%button.btn.btn-link
Upcoming
+ %li.filter-dropdown-item{ 'data-value' => 'started' }
+ %button.btn.btn-link
+ Started
%li.divider
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link.js-data-value
{{title}}
- #js-dropdown-label.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label' } }
+ #js-dropdown-label.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label', type: 'array' } }
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link
@@ -97,7 +101,7 @@
= render partial: "shared/issuable/label_page_create"
= dropdown_loading
#js-add-issues-btn.prepend-left-10
- - else
+ - elsif type != :boards_modal
= render 'shared/sort_dropdown'
- if @bulk_edit
@@ -130,19 +134,20 @@
.filter-item.inline.update-issues-btn
= button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
-:javascript
- new UsersSelect();
- new LabelsSelect();
- new MilestoneSelect();
- new IssueStatusSelect();
- new SubscriptionSelect();
+- unless type === :boards_modal
+ :javascript
+ new UsersSelect();
+ new LabelsSelect();
+ new MilestoneSelect();
+ new IssueStatusSelect();
+ new SubscriptionSelect();
- $(document).off('page:restore').on('page:restore', function (event) {
- if (gl.FilteredSearchManager) {
- new gl.FilteredSearchManager();
- }
- Issuable.init();
- new gl.IssuableBulkActions({
- prefixId: 'issue_',
+ $(document).off('page:restore').on('page:restore', function (event) {
+ if (gl.FilteredSearchManager) {
+ new gl.FilteredSearchManager();
+ }
+ Issuable.init();
+ new gl.IssuableBulkActions({
+ prefixId: 'issue_',
+ });
});
- });
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 048fc488207..25a4aec0a38 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -30,7 +30,7 @@
= icon('user', 'aria-hidden': 'true')
.title.hide-collapsed
Assignee
- = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true')
+ = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.hide-collapsed
@@ -64,7 +64,7 @@
None
.title.hide-collapsed
Milestone
- = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true')
+ = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.hide-collapsed
@@ -91,7 +91,7 @@
= issuable.due_date.try(:to_s, :medium) || 'None'
.title.hide-collapsed
Due date
- = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true')
+ = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.hide-collapsed
@@ -121,12 +121,12 @@
- selected_labels = issuable.labels
.block.labels
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
- = icon('tags', 'aria-hidden': 'true')
+ = icon('tags', class: 'hidden', 'aria-hidden': 'true')
%span
= selected_labels.size
.title.hide-collapsed
Labels
- = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true')
+ = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
index 7a21f19ded4..9dbfedb84f1 100644
--- a/app/views/shared/issuable/form/_metadata.html.haml
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -21,7 +21,7 @@
= form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder
- = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
+ = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone"
.form-group
- has_labels = @labels && @labels.any?
= form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 7e9fb7bb4d3..df21857e1ad 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -6,17 +6,16 @@
- css_class = '' unless local_assigns[:css_class]
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
-- cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.3']
-- cache_key.push(project.commit&.sha, project.commit&.status)
+- cache_key = project_list_cache_key(project)
%li.project-row{ class: css_class }
= cache(cache_key) do
.controls
- if project.archived
%span.label.label-warning archived
- - if project.commit.try(:status)
+ - if project.pipeline_status.has_status?
%span
- = render_commit_status(project.commit)
+ = render_project_pipeline_status(project.pipeline_status)
- if forks
%span
= icon('code-fork')
diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml
index ad5c0c2d8c8..74f71e6cbd1 100644
--- a/app/views/shared/snippets/_blob.html.haml
+++ b/app/views/shared/snippets/_blob.html.haml
@@ -1,7 +1,25 @@
-- unless @snippet.content.empty?
+.js-file-title.file-title-flex-parent
+ .file-header-content
+ = blob_icon @snippet.mode, @snippet.path
+
+ %strong.file-title-name
+ = @snippet.path
+
+ = copy_file_path_button(@snippet.path)
+
+ .file-actions.hidden-xs
+ .btn-group{ role: "group" }<
+ = copy_blob_content_button(@snippet)
+ = open_raw_file_button(raw_path)
+
+ - if defined?(download_path) && download_path
+ = link_to icon('download'), download_path, class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' }
+
+- if @snippet.content.empty?
+ .file-content.code
+ .nothing-here-block Empty file
+- else
- if markup?(@snippet.file_name)
- %textarea.markdown-snippet-copy.blob-content{ data: { blob_id: @snippet.id } }
- = @snippet.content
.file-content.wiki
- if gitlab_markdown?(@snippet.file_name)
= preserve(markdown_field(@snippet, :content))
@@ -9,6 +27,3 @@
= render_markup(@snippet.file_name, @snippet.content)
- else
= render 'shared/file_highlight', blob: @snippet
-- else
- .file-content.code
- .nothing-here-block Empty file
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index e7f7db73223..0296597b294 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -3,7 +3,7 @@
= page_specific_javascript_bundle_tag('snippet')
.snippet-form-holder
- = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f|
+ = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input js-quick-submit" } do |f|
= form_errors(@snippet)
.form-group
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 970afbe6b64..da9fb755a36 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -3,13 +3,7 @@
= render 'shared/snippets/header'
%article.file-holder.snippet-file-content
- .js-file-title.file-title
- = blob_icon 0, @snippet.file_name
- = @snippet.file_name
- .file-actions
- = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm")
- = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
- = link_to 'Download', download_snippet_path(@snippet), class: "btn btn-sm"
- = render 'shared/snippets/blob'
+ = render 'shared/snippets/blob', raw_path: raw_snippet_path(@snippet), download_path: download_snippet_path(@snippet)
-= render 'award_emoji/awards_block', awardable: @snippet, inline: true
+.row-content-block.top-block.content-component-block
+ = render 'award_emoji/awards_block', awardable: @snippet, inline: true
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 76cd330e80a..dc9a3b0d0df 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -33,7 +33,7 @@
.profile-header
.avatar-holder
- = link_to avatar_icon(@user, 400), target: '_blank' do
+ = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
= image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
.user-info
diff --git a/app/workers/build_email_worker.rb b/app/workers/build_email_worker.rb
deleted file mode 100644
index 5fdb1f2baa0..00000000000
--- a/app/workers/build_email_worker.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-class BuildEmailWorker
- include Sidekiq::Worker
- include BuildQueue
-
- def perform(build_id, recipients, push_data)
- recipients.each do |recipient|
- begin
- case push_data['build_status']
- when 'success'
- Notify.build_success_email(build_id, recipient).deliver_now
- when 'failed'
- Notify.build_fail_email(build_id, recipient).deliver_now
- end
- # These are input errors and won't be corrected even if Sidekiq retries
- rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e
- logger.info("Failed to send e-mail for project '#{push_data['project_name']}' to #{recipient}: #{e}")
- end
- end
- end
-end
diff --git a/changelogs/unreleased/21451-allow-disable-mr-link.yml b/changelogs/unreleased/21451-allow-disable-mr-link.yml
new file mode 100644
index 00000000000..ef99970a7a2
--- /dev/null
+++ b/changelogs/unreleased/21451-allow-disable-mr-link.yml
@@ -0,0 +1,4 @@
+---
+title: Add ability to disable Merge Request URL on push
+merge_request: 9663
+author: Alex Sanford
diff --git a/changelogs/unreleased/23993-drop-ci_projects.yml b/changelogs/unreleased/23993-drop-ci_projects.yml
new file mode 100644
index 00000000000..ee9cf774e37
--- /dev/null
+++ b/changelogs/unreleased/23993-drop-ci_projects.yml
@@ -0,0 +1,6 @@
+---
+title: Drop unused ci_projects table and some unused project_id columns,
+ then rename gl_project_id to project_id. Stop exporting job trace when
+ exporting projects.
+merge_request: 9378
+author: David Wagner
diff --git a/changelogs/unreleased/24683-sidebar-spinners.yml b/changelogs/unreleased/24683-sidebar-spinners.yml
new file mode 100644
index 00000000000..3fec273152f
--- /dev/null
+++ b/changelogs/unreleased/24683-sidebar-spinners.yml
@@ -0,0 +1,4 @@
+---
+title: hide loading spinners for server-rendered sidebar fields
+merge_request:
+author:
diff --git a/changelogs/unreleased/26236-monospace-gfm.yml b/changelogs/unreleased/26236-monospace-gfm.yml
new file mode 100644
index 00000000000..c44f3d4d3dc
--- /dev/null
+++ b/changelogs/unreleased/26236-monospace-gfm.yml
@@ -0,0 +1,4 @@
+---
+title: Change gfm textarea to use monospace font
+merge_request:
+author:
diff --git a/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml b/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml
new file mode 100644
index 00000000000..e82cbf00cfb
--- /dev/null
+++ b/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml
@@ -0,0 +1,4 @@
+---
+title: Strip reference prefixes on branch creation
+merge_request: 8498
+author: Matthieu Tardy
diff --git a/changelogs/unreleased/27114-add-undo-mark-all-as-done-to-todos.yml b/changelogs/unreleased/27114-add-undo-mark-all-as-done-to-todos.yml
new file mode 100644
index 00000000000..44aae486574
--- /dev/null
+++ b/changelogs/unreleased/27114-add-undo-mark-all-as-done-to-todos.yml
@@ -0,0 +1,4 @@
+---
+title: Add Undo mark all as done to Todos
+merge_request: 9890
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/27142-api-replace-destroy-with-stop-environment.yml b/changelogs/unreleased/27142-api-replace-destroy-with-stop-environment.yml
index ee236310a71..dd649dcde7e 100644
--- a/changelogs/unreleased/27142-api-replace-destroy-with-stop-environment.yml
+++ b/changelogs/unreleased/27142-api-replace-destroy-with-stop-environment.yml
@@ -1,4 +1,4 @@
---
-title: API: Add environment stop action
+title: 'API: Add environment stop action'
merge_request: 8808
author:
diff --git a/changelogs/unreleased/27174-filter-filters.yml b/changelogs/unreleased/27174-filter-filters.yml
new file mode 100644
index 00000000000..0da1e4d5d3b
--- /dev/null
+++ b/changelogs/unreleased/27174-filter-filters.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent filtering issues by multiple Milestones or Authors
+merge_request:
+author:
diff --git a/changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml b/changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml
new file mode 100644
index 00000000000..4ea52a70e89
--- /dev/null
+++ b/changelogs/unreleased/27271-missing-time-spent-in-issue-webhook.yml
@@ -0,0 +1,4 @@
+---
+title: Include time tracking attributes in webhooks payload
+merge_request: 9942
+author:
diff --git a/changelogs/unreleased/27376-cache-default-branch-pipeline-on-project.yml b/changelogs/unreleased/27376-cache-default-branch-pipeline-on-project.yml
new file mode 100644
index 00000000000..a116c68ad87
--- /dev/null
+++ b/changelogs/unreleased/27376-cache-default-branch-pipeline-on-project.yml
@@ -0,0 +1,4 @@
+---
+title: Speed up project dashboard by caching pipeline status and eager loading routes
+merge_request: 9903
+author:
diff --git a/changelogs/unreleased/27988-fix-transient-failure-in-commits-api.yml b/changelogs/unreleased/27988-fix-transient-failure-in-commits-api.yml
new file mode 100644
index 00000000000..c6ba9572f26
--- /dev/null
+++ b/changelogs/unreleased/27988-fix-transient-failure-in-commits-api.yml
@@ -0,0 +1,5 @@
+---
+title: 'Add `requirements: { id: /.+/ }` for all projects and groups namespaced API
+ routes'
+merge_request: 9944
+author:
diff --git a/changelogs/unreleased/28058-hide-emails-in-atom-feeds.yml b/changelogs/unreleased/28058-hide-emails-in-atom-feeds.yml
new file mode 100644
index 00000000000..e0e826a67f8
--- /dev/null
+++ b/changelogs/unreleased/28058-hide-emails-in-atom-feeds.yml
@@ -0,0 +1,4 @@
+---
+title: Only show public emails in atom feeds
+merge_request:
+author:
diff --git a/changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml b/changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml
new file mode 100644
index 00000000000..feca38ff083
--- /dev/null
+++ b/changelogs/unreleased/28187-project-name-cut-off-with-nested-groups.yml
@@ -0,0 +1,4 @@
+---
+title: Use toggle button to expand / collapse mulit-nested groups
+merge_request: 9501
+author:
diff --git a/changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml b/changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml
new file mode 100644
index 00000000000..6e3cd8a60d8
--- /dev/null
+++ b/changelogs/unreleased/28277-document-u2f-limitations-with-multiple-urls.yml
@@ -0,0 +1,4 @@
+---
+title: Document U2F limitations with multiple URLs
+merge_request: 9300
+author:
diff --git a/changelogs/unreleased/28494-mini-pipeline-graph-commit-view.yml b/changelogs/unreleased/28494-mini-pipeline-graph-commit-view.yml
new file mode 100644
index 00000000000..67dbc30e760
--- /dev/null
+++ b/changelogs/unreleased/28494-mini-pipeline-graph-commit-view.yml
@@ -0,0 +1,4 @@
+---
+title: Adds pipeline mini-graph to system information box in Commit View
+merge_request:
+author:
diff --git a/changelogs/unreleased/28499-fix-large-text-tooltip-in-diff-file-name.yml b/changelogs/unreleased/28499-fix-large-text-tooltip-in-diff-file-name.yml
new file mode 100644
index 00000000000..660a881e094
--- /dev/null
+++ b/changelogs/unreleased/28499-fix-large-text-tooltip-in-diff-file-name.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes large file name tooltip cutoff in diff header
+merge_request: 9529
+author:
diff --git a/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml b/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml
new file mode 100644
index 00000000000..8b592766bf3
--- /dev/null
+++ b/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes dismissable error close is not visible enough
+merge_request: 9516
+author:
diff --git a/changelogs/unreleased/28890-allow-creating-mr-without-target-branch-in-url.yml b/changelogs/unreleased/28890-allow-creating-mr-without-target-branch-in-url.yml
new file mode 100644
index 00000000000..114a14ec2df
--- /dev/null
+++ b/changelogs/unreleased/28890-allow-creating-mr-without-target-branch-in-url.yml
@@ -0,0 +1,5 @@
+---
+title: Allow creating merge request even if target branch is not specified in query
+ params
+merge_request: 9968
+author:
diff --git a/changelogs/unreleased/28991-viewing-old-wiki-page-version-edit-button-exists.yml b/changelogs/unreleased/28991-viewing-old-wiki-page-version-edit-button-exists.yml
new file mode 100644
index 00000000000..26989c14958
--- /dev/null
+++ b/changelogs/unreleased/28991-viewing-old-wiki-page-version-edit-button-exists.yml
@@ -0,0 +1,4 @@
+---
+title: When viewing old wiki page version, edit button should be disabled
+merge_request: 9966
+author: TM Lee
diff --git a/changelogs/unreleased/29405-fix-project-wiki-update.yml b/changelogs/unreleased/29405-fix-project-wiki-update.yml
new file mode 100644
index 00000000000..85be36f7902
--- /dev/null
+++ b/changelogs/unreleased/29405-fix-project-wiki-update.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Project Wiki update
+merge_request: 9990
+author: Dongqing Hu
diff --git a/changelogs/unreleased/29428-new-directory-from-existing-branch.yml b/changelogs/unreleased/29428-new-directory-from-existing-branch.yml
new file mode 100644
index 00000000000..b3f7cd1f8f8
--- /dev/null
+++ b/changelogs/unreleased/29428-new-directory-from-existing-branch.yml
@@ -0,0 +1,4 @@
+---
+title: New directory from interface on existing branch
+merge_request: 9921
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml b/changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml
new file mode 100644
index 00000000000..61ffb64fa8f
--- /dev/null
+++ b/changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml
@@ -0,0 +1,4 @@
+---
+title: Fix trigger webhook for ref with a dot
+merge_request: 10001
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/29469-message-for-project-x-will-be-deleted-should-include-namespace.yml b/changelogs/unreleased/29469-message-for-project-x-will-be-deleted-should-include-namespace.yml
new file mode 100644
index 00000000000..23a32d2c11a
--- /dev/null
+++ b/changelogs/unreleased/29469-message-for-project-x-will-be-deleted-should-include-namespace.yml
@@ -0,0 +1,4 @@
+---
+title: Display full project name with namespace upon deletion
+merge_request:
+author:
diff --git a/changelogs/unreleased/29565-name-of-the-uncompressed-folder-of-a-tag-archive-changed.yml b/changelogs/unreleased/29565-name-of-the-uncompressed-folder-of-a-tag-archive-changed.yml
new file mode 100644
index 00000000000..d0a04b0a130
--- /dev/null
+++ b/changelogs/unreleased/29565-name-of-the-uncompressed-folder-of-a-tag-archive-changed.yml
@@ -0,0 +1,4 @@
+---
+title: Fix archive prefix bug for refs containing dots
+merge_request:
+author:
diff --git a/changelogs/unreleased/29604-v3-fix-branch-creation.yml b/changelogs/unreleased/29604-v3-fix-branch-creation.yml
new file mode 100644
index 00000000000..25687e8be97
--- /dev/null
+++ b/changelogs/unreleased/29604-v3-fix-branch-creation.yml
@@ -0,0 +1,4 @@
+---
+title: Use "branch_name" instead "branch" on V3 branch creation API
+merge_request:
+author:
diff --git a/changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml b/changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml
new file mode 100644
index 00000000000..15d7b9dcafb
--- /dev/null
+++ b/changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml
@@ -0,0 +1,4 @@
+---
+title: Allow unauthenticated access to some Branch API GET endpoints
+merge_request:
+author:
diff --git a/changelogs/unreleased/add-blob-copy-button.yml b/changelogs/unreleased/add-blob-copy-button.yml
new file mode 100644
index 00000000000..946723e523b
--- /dev/null
+++ b/changelogs/unreleased/add-blob-copy-button.yml
@@ -0,0 +1,4 @@
+---
+title: Add copy button to blob header and use icon for Raw button
+merge_request:
+author:
diff --git a/changelogs/unreleased/add-labels-to-issue-hook.yml b/changelogs/unreleased/add-labels-to-issue-hook.yml
new file mode 100644
index 00000000000..967430ee09f
--- /dev/null
+++ b/changelogs/unreleased/add-labels-to-issue-hook.yml
@@ -0,0 +1,4 @@
+---
+title: Added labels array to the issue web hook returned object
+merge_request: 9972
+author:
diff --git a/changelogs/unreleased/add_quick_submit_for_snippets_form.yml b/changelogs/unreleased/add_quick_submit_for_snippets_form.yml
new file mode 100644
index 00000000000..088f1335796
--- /dev/null
+++ b/changelogs/unreleased/add_quick_submit_for_snippets_form.yml
@@ -0,0 +1,4 @@
+---
+title: Add quick submit for snippet forms
+merge_request: 9911
+author: blackst0ne
diff --git a/changelogs/unreleased/allow-resolving-conflicts-in-utf-8.yml b/changelogs/unreleased/allow-resolving-conflicts-in-utf-8.yml
new file mode 100644
index 00000000000..c3c877423ff
--- /dev/null
+++ b/changelogs/unreleased/allow-resolving-conflicts-in-utf-8.yml
@@ -0,0 +1,4 @@
+---
+title: Fix conflict resolution when files contain valid UTF-8 characters
+merge_request:
+author:
diff --git a/changelogs/unreleased/api-project-issues-404.yml b/changelogs/unreleased/api-project-issues-404.yml
new file mode 100644
index 00000000000..ce40395c99e
--- /dev/null
+++ b/changelogs/unreleased/api-project-issues-404.yml
@@ -0,0 +1,4 @@
+---
+title: Return 404 in project issues API endpoint when project cannot be found
+merge_request: 10093
+author:
diff --git a/changelogs/unreleased/better-priority-sorting-2.yml b/changelogs/unreleased/better-priority-sorting-2.yml
new file mode 100644
index 00000000000..ca0d14718dc
--- /dev/null
+++ b/changelogs/unreleased/better-priority-sorting-2.yml
@@ -0,0 +1,4 @@
+---
+title: Allow filtering by all started milestones
+merge_request:
+author:
diff --git a/changelogs/unreleased/better-priority-sorting.yml b/changelogs/unreleased/better-priority-sorting.yml
new file mode 100644
index 00000000000..a44cd090ceb
--- /dev/null
+++ b/changelogs/unreleased/better-priority-sorting.yml
@@ -0,0 +1,4 @@
+---
+title: Allow sorting by due date and priority
+merge_request:
+author:
diff --git a/changelogs/unreleased/bugfix-systemhook.yml b/changelogs/unreleased/bugfix-systemhook.yml
new file mode 100644
index 00000000000..4c4d0dcc7a2
--- /dev/null
+++ b/changelogs/unreleased/bugfix-systemhook.yml
@@ -0,0 +1,4 @@
+---
+title: Fix bug when system hook for deploy key
+merge_request: 9796
+author: billy.lb
diff --git a/changelogs/unreleased/chore-23493-remaining-time-tooltip.yml b/changelogs/unreleased/chore-23493-remaining-time-tooltip.yml
new file mode 100644
index 00000000000..dc315ca2367
--- /dev/null
+++ b/changelogs/unreleased/chore-23493-remaining-time-tooltip.yml
@@ -0,0 +1,5 @@
+---
+title: Added remaining_time method to milestoneish, specs and updated the milestone_helper
+ milestone_remaining_days method to correctly return the correct remaining time.
+merge_request:
+author: Michael Robinson
diff --git a/changelogs/unreleased/dm-copy-code-as-gfm.yml b/changelogs/unreleased/dm-copy-code-as-gfm.yml
new file mode 100644
index 00000000000..15ae2da44a3
--- /dev/null
+++ b/changelogs/unreleased/dm-copy-code-as-gfm.yml
@@ -0,0 +1,4 @@
+---
+title: Copy code as GFM from diffs, blobs and GFM code blocks
+merge_request:
+author:
diff --git a/changelogs/unreleased/dz-hide-share-group-ancestors.yml b/changelogs/unreleased/dz-hide-share-group-ancestors.yml
new file mode 100644
index 00000000000..743c4fd56ca
--- /dev/null
+++ b/changelogs/unreleased/dz-hide-share-group-ancestors.yml
@@ -0,0 +1,4 @@
+---
+title: Hide ancestor groups in the share group dropdown list
+merge_request: 9965
+author:
diff --git a/changelogs/unreleased/feature-tokens-rake-task.yml b/changelogs/unreleased/feature-tokens-rake-task.yml
new file mode 100644
index 00000000000..6c3845757db
--- /dev/null
+++ b/changelogs/unreleased/feature-tokens-rake-task.yml
@@ -0,0 +1,4 @@
+---
+title: New rake task to reset all email and private tokens
+merge_request:
+author:
diff --git a/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml b/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml
new file mode 100644
index 00000000000..4b668d994a1
--- /dev/null
+++ b/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml
@@ -0,0 +1,4 @@
+---
+title: Use Gitaly for CommitController#show
+merge_request: 9629
+author:
diff --git a/changelogs/unreleased/fix-milestone-name-on-show.yml b/changelogs/unreleased/fix-milestone-name-on-show.yml
new file mode 100644
index 00000000000..bf17a758c80
--- /dev/null
+++ b/changelogs/unreleased/fix-milestone-name-on-show.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Milestone name on show page
+merge_request:
+author: Raveesh
diff --git a/changelogs/unreleased/fix-prometheus-including-d3-main-bundle.yml b/changelogs/unreleased/fix-prometheus-including-d3-main-bundle.yml
new file mode 100644
index 00000000000..a42b0db3cfc
--- /dev/null
+++ b/changelogs/unreleased/fix-prometheus-including-d3-main-bundle.yml
@@ -0,0 +1,4 @@
+---
+title: Removed d3 from the main application.js bundle
+merge_request: 10062
+author:
diff --git a/changelogs/unreleased/fix-slow-queries-for-branches-index.yml b/changelogs/unreleased/fix-slow-queries-for-branches-index.yml
new file mode 100644
index 00000000000..f5bd7003615
--- /dev/null
+++ b/changelogs/unreleased/fix-slow-queries-for-branches-index.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes n+1 query for tags and branches index page
+merge_request: 9905
+author:
diff --git a/changelogs/unreleased/fix_visibility_level.yml b/changelogs/unreleased/fix_visibility_level.yml
new file mode 100644
index 00000000000..4cf649124ca
--- /dev/null
+++ b/changelogs/unreleased/fix_visibility_level.yml
@@ -0,0 +1,4 @@
+---
+title: Fix visibility level on new project page
+merge_request: 9885
+author: blackst0ne
diff --git a/changelogs/unreleased/fl-remove-ujs-pipelines.yml b/changelogs/unreleased/fl-remove-ujs-pipelines.yml
new file mode 100644
index 00000000000..f353400753a
--- /dev/null
+++ b/changelogs/unreleased/fl-remove-ujs-pipelines.yml
@@ -0,0 +1,4 @@
+---
+title: 'Removes UJS from pipelines tables'
+merge_request: 9929
+author:
diff --git a/changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml b/changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml
new file mode 100644
index 00000000000..aff1bdd957c
--- /dev/null
+++ b/changelogs/unreleased/group-gear-setting-dropdown-to-tab.yml
@@ -0,0 +1,4 @@
+---
+title: Moved the gear settings dropdown to a tab in the groups view
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_27212.yml b/changelogs/unreleased/issue_27212.yml
new file mode 100644
index 00000000000..7a7e04f7ca7
--- /dev/null
+++ b/changelogs/unreleased/issue_27212.yml
@@ -0,0 +1,4 @@
+---
+title: Add closed_at field to issues
+merge_request:
+author:
diff --git a/changelogs/unreleased/issue_29449.yml b/changelogs/unreleased/issue_29449.yml
new file mode 100644
index 00000000000..3556f22b080
--- /dev/null
+++ b/changelogs/unreleased/issue_29449.yml
@@ -0,0 +1,4 @@
+---
+title: Remove whitespace in group links
+merge_request: 9947
+author: Xurxo Méndez Pérez
diff --git a/changelogs/unreleased/make-karma-fast-again.yml b/changelogs/unreleased/make-karma-fast-again.yml
new file mode 100644
index 00000000000..9b95e06954a
--- /dev/null
+++ b/changelogs/unreleased/make-karma-fast-again.yml
@@ -0,0 +1,4 @@
+---
+title: Only add code coverage instrumentation when generating coverage report
+merge_request: 9987
+author:
diff --git a/changelogs/unreleased/migrate-pipeline-events-and-email-service.yml b/changelogs/unreleased/migrate-pipeline-events-and-email-service.yml
new file mode 100644
index 00000000000..ce4d5092c17
--- /dev/null
+++ b/changelogs/unreleased/migrate-pipeline-events-and-email-service.yml
@@ -0,0 +1,4 @@
+---
+title: Migrate SlackService and MattermostService from build_events to pipeline_events, and migrate BuildsEmailService to PipelinesEmailService. Update Hipchat to use pipeline events rather than build events.
+merge_request: 8196
+author:
diff --git a/changelogs/unreleased/option-to-be-notified-of-own-activity.yml b/changelogs/unreleased/option-to-be-notified-of-own-activity.yml
deleted file mode 100644
index c2e0410cc33..00000000000
--- a/changelogs/unreleased/option-to-be-notified-of-own-activity.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Add option to receive email notifications about your own activity
-merge_request: 8836
-author: Richard Macklin
diff --git a/changelogs/unreleased/pipeline-tooltips-overflow.yml b/changelogs/unreleased/pipeline-tooltips-overflow.yml
new file mode 100644
index 00000000000..184da8049f3
--- /dev/null
+++ b/changelogs/unreleased/pipeline-tooltips-overflow.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed pipeline actions tooltips overflowing
+merge_request:
+author:
diff --git a/changelogs/unreleased/remove-unused-ci-tables.yml b/changelogs/unreleased/remove-unused-ci-tables.yml
new file mode 100644
index 00000000000..fccfb882bd9
--- /dev/null
+++ b/changelogs/unreleased/remove-unused-ci-tables.yml
@@ -0,0 +1,4 @@
+---
+title: Remove various unused CI tables and columns
+merge_request: 9639
+author:
diff --git a/changelogs/unreleased/rename-ci_commits-to-ci_pipeline.yml b/changelogs/unreleased/rename-ci_commits-to-ci_pipeline.yml
new file mode 100644
index 00000000000..4067b3de00c
--- /dev/null
+++ b/changelogs/unreleased/rename-ci_commits-to-ci_pipeline.yml
@@ -0,0 +1,4 @@
+---
+title: Rename table ci_commits to ci_pipelines
+merge_request: 9638
+author:
diff --git a/changelogs/unreleased/rename_all_issues.yml b/changelogs/unreleased/rename_all_issues.yml
new file mode 100644
index 00000000000..d3109bdb17e
--- /dev/null
+++ b/changelogs/unreleased/rename_all_issues.yml
@@ -0,0 +1,4 @@
+---
+title: Rename 'All issues' to 'Open issues' in Add issues modal
+merge_request: 10042
+author: blackst0ne
diff --git a/changelogs/unreleased/routes-lower-case.yml b/changelogs/unreleased/routes-lower-case.yml
new file mode 100644
index 00000000000..2110956680c
--- /dev/null
+++ b/changelogs/unreleased/routes-lower-case.yml
@@ -0,0 +1,4 @@
+---
+title: Remove repeated routes.path check for postgresql database
+merge_request:
+author: mhasbini
diff --git a/changelogs/unreleased/simplify-docs-trigger.yml b/changelogs/unreleased/simplify-docs-trigger.yml
new file mode 100644
index 00000000000..062626359ef
--- /dev/null
+++ b/changelogs/unreleased/simplify-docs-trigger.yml
@@ -0,0 +1,4 @@
+---
+title: Simplify trigger_docs build job for CE and EE
+merge_request: 9820
+author: winniehell
diff --git a/changelogs/unreleased/ssrf-protections.yml b/changelogs/unreleased/ssrf-protections.yml
new file mode 100644
index 00000000000..8d803738009
--- /dev/null
+++ b/changelogs/unreleased/ssrf-protections.yml
@@ -0,0 +1,4 @@
+---
+title: To protect against Server-side Request Forgery project import URLs are now prohibited against localhost or the server IP except for the assigned instance URL and port. Imports are also prohibited from ports below 1024 with the exception of ports 22, 80, and 443.
+merge_request:
+author:
diff --git a/config/application.rb b/config/application.rb
index 98b2759a8a7..f9f01b66473 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -92,6 +92,8 @@ module Gitlab
# Enable the asset pipeline
config.assets.enabled = true
+ # Support legacy unicode file named img emojis, `1F939.png`
+ config.assets.paths << Gemojione.images_path
config.assets.paths << "vendor/assets/fonts"
config.assets.precompile << "*.png"
config.assets.precompile << "print.css"
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 954809a882c..ba7f6773985 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -157,8 +157,8 @@ production: &base
host: example.com
port: 80 # Set to 443 if you serve the pages with HTTPS
https: false # Set to true if you serve the pages with HTTPS
- # external_http: "1.1.1.1:80" # If defined, enables custom domain support in GitLab Pages
- # external_https: "1.1.1.1:443" # If defined, enables custom domain and certificate support in GitLab Pages
+ # external_http: ["1.1.1.1:80", "[2001::1]:80"] # If defined, enables custom domain support in GitLab Pages
+ # external_https: ["1.1.1.1:443", "[2001::1]:443"] # If defined, enables custom domain and certificate support in GitLab Pages
## Mattermost
## For enabling Add to Mattermost button
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index d049ae9476f..62020fa9a75 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -278,8 +278,8 @@ Settings.pages['host'] ||= "example.com"
Settings.pages['port'] ||= Settings.pages.https ? 443 : 80
Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http"
Settings.pages['url'] ||= Settings.send(:build_pages_url)
-Settings.pages['external_http'] ||= false if Settings.pages['external_http'].nil?
-Settings.pages['external_https'] ||= false if Settings.pages['external_https'].nil?
+Settings.pages['external_http'] ||= false unless Settings.pages['external_http'].present?
+Settings.pages['external_https'] ||= false unless Settings.pages['external_https'].present?
#
# Git LFS
diff --git a/config/initializers/rspec_profiling.rb b/config/initializers/rspec_profiling.rb
index ac353d14499..70177995356 100644
--- a/config/initializers/rspec_profiling.rb
+++ b/config/initializers/rspec_profiling.rb
@@ -7,7 +7,7 @@ module RspecProfilingExt
module Git
def branch
- ENV['CI_BUILD_REF_NAME'] || super
+ ENV['CI_COMMIT_REF_NAME'] || super
end
end
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 2b018c68703..ecd73956488 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -42,7 +42,7 @@ Sidekiq.configure_server do |config|
Gitlab::SidekiqThrottler.execute!
- config = ActiveRecord::Base.configurations[Rails.env] ||
+ config = Gitlab::Database.config ||
Rails.application.config.database_configuration[Rails.env]
config['pool'] = Sidekiq.options[:concurrency]
ActiveRecord::Base.establish_connection(config)
diff --git a/config/karma.config.js b/config/karma.config.js
index a23e62f5022..eb082dd28bf 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -3,17 +3,6 @@ var webpack = require('webpack');
var webpackConfig = require('./webpack.config.js');
var ROOT_PATH = path.resolve(__dirname, '..');
-// add coverage instrumentation to babel config
-if (webpackConfig.module && webpackConfig.module.rules) {
- var babelConfig = webpackConfig.module.rules.find(function (rule) {
- return rule.loader === 'babel-loader';
- });
-
- babelConfig.options = babelConfig.options || {};
- babelConfig.options.plugins = babelConfig.options.plugins || [];
- babelConfig.options.plugins.push('istanbul');
-}
-
// remove problematic plugins
if (webpackConfig.plugins) {
webpackConfig.plugins = webpackConfig.plugins.filter(function (plugin) {
@@ -27,7 +16,8 @@ if (webpackConfig.plugins) {
// Karma configuration
module.exports = function(config) {
var progressReporter = process.env.CI ? 'mocha' : 'progress';
- config.set({
+
+ var karmaConfig = {
basePath: ROOT_PATH,
browsers: ['PhantomJS'],
frameworks: ['jasmine'],
@@ -36,16 +26,22 @@ module.exports = function(config) {
{ pattern: 'spec/javascripts/fixtures/**/*@(.json|.html|.html.raw)', included: false },
],
preprocessors: {
- 'spec/javascripts/**/*.js?(.es6)': ['webpack', 'sourcemap'],
+ 'spec/javascripts/**/*.js': ['webpack', 'sourcemap'],
},
- reporters: [progressReporter, 'coverage-istanbul'],
- coverageIstanbulReporter: {
+ reporters: [progressReporter],
+ webpack: webpackConfig,
+ webpackMiddleware: { stats: 'errors-only' },
+ };
+
+ if (process.env.BABEL_ENV === 'coverage' || process.env.NODE_ENV === 'coverage') {
+ karmaConfig.reporters.push('coverage-istanbul');
+ karmaConfig.coverageIstanbulReporter = {
reports: ['html', 'text-summary'],
dir: 'coverage-javascript/',
subdir: '.',
fixWebpackSourcePaths: true
- },
- webpack: webpackConfig,
- webpackMiddleware: { stats: 'errors-only' },
- });
+ };
+ }
+
+ config.set(karmaConfig);
};
diff --git a/config/routes/dashboard.rb b/config/routes/dashboard.rb
index adc3ad207cc..8e380a0b0ac 100644
--- a/config/routes/dashboard.rb
+++ b/config/routes/dashboard.rb
@@ -13,6 +13,7 @@ resource :dashboard, controller: 'dashboard', only: [] do
resources :todos, only: [:index, :destroy] do
collection do
delete :destroy_all
+ patch :bulk_restore
end
member do
patch :restore
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 71d9042712a..9753828fe5a 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -35,6 +35,7 @@ var config = {
issuable: './issuable/issuable_bundle.js',
merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js',
merge_request_widget: './merge_request_widget/ci_bundle.js',
+ monitoring: './monitoring/monitoring_bundle.js',
network: './network/network_bundle.js',
profile: './profile/profile_bundle.js',
protected_branches: './protected_branches/protected_branches_bundle.js',
@@ -57,15 +58,9 @@ var config = {
module: {
rules: [
{
- test: /\.(js|es6)$/,
+ test: /\.js$/,
exclude: /(node_modules|vendor\/assets)/,
- loader: 'babel-loader',
- options: {
- presets: [
- ["es2015", {"modules": false}],
- 'stage-2'
- ]
- }
+ loader: 'babel-loader'
},
{
test: /\.svg$/,
@@ -121,7 +116,7 @@ var config = {
// create cacheable common library bundle for all d3 chunks
new webpack.optimize.CommonsChunkPlugin({
name: 'common_d3',
- chunks: ['graphs', 'users'],
+ chunks: ['graphs', 'users', 'monitoring'],
}),
// create cacheable common library bundles
@@ -131,7 +126,7 @@ var config = {
],
resolve: {
- extensions: ['.js', '.es6', '.js.es6'],
+ extensions: ['.js'],
alias: {
'~': path.join(ROOT_PATH, 'app/assets/javascripts'),
'emojis': path.join(ROOT_PATH, 'fixtures/emojis'),
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index aea0a72b633..4bc735916c1 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -155,7 +155,7 @@ class Gitlab::Seeder::CycleAnalytics
issue.project.repository.add_branch(@user, branch_name, 'master')
- commit_sha = issue.project.repository.create_file(@user, filename, "content", options, message: "Commit for ##{issue.iid}", branch_name: branch_name)
+ commit_sha = issue.project.repository.create_file(@user, filename, "content", message: "Commit for ##{issue.iid}", branch_name: branch_name)
issue.project.repository.commit(commit_sha)
GitPushService.new(issue.project,
diff --git a/db/migrate/20160919145149_add_group_id_to_labels.rb b/db/migrate/20160919145149_add_group_id_to_labels.rb
index 828b6afddb1..e20e693f3aa 100644
--- a/db/migrate/20160919145149_add_group_id_to_labels.rb
+++ b/db/migrate/20160919145149_add_group_id_to_labels.rb
@@ -12,8 +12,8 @@ class AddGroupIdToLabels < ActiveRecord::Migration
end
def down
+ remove_foreign_key :labels, column: :group_id
remove_index :labels, :group_id if index_exists? :labels, :group_id
- remove_foreign_key :labels, :namespaces, column: :group_id
remove_column :labels, :group_id
end
end
diff --git a/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb
index ad3eb4a26f9..35ad22b6c01 100644
--- a/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb
+++ b/db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb
@@ -32,8 +32,8 @@ class AddPipelineIdToMergeRequestMetrics < ActiveRecord::Migration
end
def down
+ remove_foreign_key :merge_request_metrics, column: :pipeline_id
remove_index :merge_request_metrics, :pipeline_id if index_exists? :merge_request_metrics, :pipeline_id
- remove_foreign_key :merge_request_metrics, :ci_commits, column: :pipeline_id
remove_column :merge_request_metrics, :pipeline_id
end
end
diff --git a/db/migrate/20161031171301_add_project_id_to_subscriptions.rb b/db/migrate/20161031171301_add_project_id_to_subscriptions.rb
index d5c343dc527..8b1c10a124f 100644
--- a/db/migrate/20161031171301_add_project_id_to_subscriptions.rb
+++ b/db/migrate/20161031171301_add_project_id_to_subscriptions.rb
@@ -9,6 +9,7 @@ class AddProjectIdToSubscriptions < ActiveRecord::Migration
end
def down
+ remove_foreign_key :subscriptions, column: :project_id
remove_column :subscriptions, :project_id
end
end
diff --git a/db/migrate/20161201160452_migrate_project_statistics.rb b/db/migrate/20161201160452_migrate_project_statistics.rb
index 3ae3f2c159b..82fbdf02444 100644
--- a/db/migrate/20161201160452_migrate_project_statistics.rb
+++ b/db/migrate/20161201160452_migrate_project_statistics.rb
@@ -16,8 +16,9 @@ class MigrateProjectStatistics < ActiveRecord::Migration
remove_column :projects, :commit_count
end
+ # rubocop: disable Migration/AddColumn
def down
- add_column_with_default :projects, :repository_size, :float, default: 0.0
- add_column_with_default :projects, :commit_count, :integer, default: 0
+ add_column :projects, :repository_size, :float, default: 0.0
+ add_column :projects, :commit_count, :integer, default: 0
end
end
diff --git a/db/migrate/20161207231621_create_environment_name_unique_index.rb b/db/migrate/20161207231621_create_environment_name_unique_index.rb
index ac680c8d10f..5ff0f5bae4d 100644
--- a/db/migrate/20161207231621_create_environment_name_unique_index.rb
+++ b/db/migrate/20161207231621_create_environment_name_unique_index.rb
@@ -12,7 +12,7 @@ class CreateEnvironmentNameUniqueIndex < ActiveRecord::Migration
end
def down
- remove_index :environments, [:project_id, :name], unique: true
- add_concurrent_index :environments, [:project_id, :name]
+ remove_index :environments, [:project_id, :name]
+ add_concurrent_index :environments, [:project_id, :name], unique: true
end
end
diff --git a/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
index d7ef1aa83d9..ede0316e860 100644
--- a/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
+++ b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
@@ -14,6 +14,6 @@ class AddUniqueIndexForEnvironmentSlug < ActiveRecord::Migration
end
def down
- remove_index :environments, [:project_id, :slug], unique: true if index_exists? :environments, [:project_id, :slug]
+ remove_index :environments, [:project_id, :slug] if index_exists? :environments, [:project_id, :slug]
end
end
diff --git a/db/migrate/20161212142807_add_lower_path_index_to_routes.rb b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb
index 6958500306f..53f4c6bbb18 100644
--- a/db/migrate/20161212142807_add_lower_path_index_to_routes.rb
+++ b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb
@@ -17,6 +17,6 @@ class AddLowerPathIndexToRoutes < ActiveRecord::Migration
def down
return unless Gitlab::Database.postgresql?
- remove_index :routes, name: :index_on_routes_lower_path
+ remove_index :routes, name: :index_on_routes_lower_path if index_exists?(:routes, name: :index_on_routes_lower_path)
end
end
diff --git a/db/migrate/20170123061730_add_notified_of_own_activity_to_users.rb b/db/migrate/20170123061730_add_notified_of_own_activity_to_users.rb
deleted file mode 100644
index f90637e1e35..00000000000
--- a/db/migrate/20170123061730_add_notified_of_own_activity_to_users.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-class AddNotifiedOfOwnActivityToUsers < ActiveRecord::Migration
- include Gitlab::Database::MigrationHelpers
- disable_ddl_transaction!
-
- DOWNTIME = false
-
- def up
- add_column_with_default :users, :notified_of_own_activity, :boolean, default: false
- end
-
- def down
- remove_column :users, :notified_of_own_activity
- end
-end
diff --git a/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb
index 69bfa2d3fc4..a7d4e141a1a 100644
--- a/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb
+++ b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb
@@ -49,6 +49,9 @@ class AddForeignKeysToTimelogs < ActiveRecord::Migration
Timelog.where('issue_id IS NOT NULL').update_all("trackable_id = issue_id, trackable_type = 'Issue'")
Timelog.where('merge_request_id IS NOT NULL').update_all("trackable_id = merge_request_id, trackable_type = 'MergeRequest'")
+ remove_foreign_key :timelogs, name: 'fk_timelogs_issues_issue_id'
+ remove_foreign_key :timelogs, name: 'fk_timelogs_merge_requests_merge_request_id'
+
remove_columns :timelogs, :issue_id, :merge_request_id
end
end
diff --git a/db/migrate/20170130204620_add_index_to_project_authorizations.rb b/db/migrate/20170130204620_add_index_to_project_authorizations.rb
index e9a0aee4d6a..629b49436e3 100644
--- a/db/migrate/20170130204620_add_index_to_project_authorizations.rb
+++ b/db/migrate/20170130204620_add_index_to_project_authorizations.rb
@@ -8,4 +8,9 @@ class AddIndexToProjectAuthorizations < ActiveRecord::Migration
def up
add_concurrent_index(:project_authorizations, :project_id)
end
+
+ def down
+ remove_index(:project_authorizations, :project_id) if
+ Gitlab::Database.postgresql?
+ end
end
diff --git a/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb
index 65adc90c2c1..8a96a784c97 100644
--- a/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb
+++ b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb
@@ -5,7 +5,7 @@ class AddIndexForLatestSuccessfulPipeline < ActiveRecord::Migration
disable_ddl_transaction!
def up
- add_concurrent_index :ci_commits, [:gl_project_id, :ref, :status]
+ add_concurrent_index(:ci_commits, [:gl_project_id, :ref, :status])
end
def down
diff --git a/db/migrate/20170216141440_drop_index_for_builds_project_status.rb b/db/migrate/20170216141440_drop_index_for_builds_project_status.rb
index 906711b9f3f..a2839f52d89 100644
--- a/db/migrate/20170216141440_drop_index_for_builds_project_status.rb
+++ b/db/migrate/20170216141440_drop_index_for_builds_project_status.rb
@@ -3,6 +3,6 @@ class DropIndexForBuildsProjectStatus < ActiveRecord::Migration
DOWNTIME = false
def change
- remove_index(:ci_commits, [:gl_project_id, :status])
+ remove_index(:ci_commits, column: [:gl_project_id, :status])
end
end
diff --git a/db/migrate/20170222143317_drop_ci_projects.rb b/db/migrate/20170222143317_drop_ci_projects.rb
new file mode 100644
index 00000000000..4db8658f36f
--- /dev/null
+++ b/db/migrate/20170222143317_drop_ci_projects.rb
@@ -0,0 +1,34 @@
+class DropCiProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ drop_table :ci_projects
+ end
+
+ def down
+ create_table "ci_projects", force: :cascade do |t|
+ t.string "name"
+ t.integer "timeout", default: 3600, null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "token"
+ t.string "default_ref"
+ t.string "path"
+ t.boolean "always_build", default: false, null: false
+ t.integer "polling_interval"
+ t.boolean "public", default: false, null: false
+ t.string "ssh_url_to_repo"
+ t.integer "gitlab_id"
+ t.boolean "allow_git_fetch", default: true, null: false
+ t.string "email_recipients", default: "", null: false
+ t.boolean "email_add_pusher", default: true, null: false
+ t.boolean "email_only_broken_builds", default: true, null: false
+ t.string "skip_refs"
+ t.string "coverage_regex"
+ t.boolean "shared_runners_enabled", default: false
+ t.text "generated_yaml_config"
+ end
+ end
+end
diff --git a/db/migrate/20170222143500_remove_old_project_id_columns.rb b/db/migrate/20170222143500_remove_old_project_id_columns.rb
new file mode 100644
index 00000000000..eac93e8e407
--- /dev/null
+++ b/db/migrate/20170222143500_remove_old_project_id_columns.rb
@@ -0,0 +1,28 @@
+class RemoveOldProjectIdColumns < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Unused columns are being removed.'
+
+ def up
+ remove_index :ci_builds, :project_id if
+ index_exists?(:ci_builds, :project_id)
+
+ remove_column :ci_builds, :project_id
+ remove_column :ci_commits, :project_id
+ remove_column :ci_runner_projects, :project_id
+ remove_column :ci_triggers, :project_id
+ remove_column :ci_variables, :project_id
+ end
+
+ def down
+ add_column :ci_builds, :project_id, :integer
+ add_column :ci_commits, :project_id, :integer
+ add_column :ci_runner_projects, :project_id, :integer
+ add_column :ci_triggers, :project_id, :integer
+ add_column :ci_variables, :project_id, :integer
+
+ add_concurrent_index :ci_builds, :project_id
+ end
+end
diff --git a/db/migrate/20170222143603_rename_gl_project_id_to_project_id.rb b/db/migrate/20170222143603_rename_gl_project_id_to_project_id.rb
new file mode 100644
index 00000000000..7c19d471557
--- /dev/null
+++ b/db/migrate/20170222143603_rename_gl_project_id_to_project_id.rb
@@ -0,0 +1,14 @@
+class RenameGlProjectIdToProjectId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Renaming an actively used column.'
+
+ def change
+ rename_column :ci_builds, :gl_project_id, :project_id
+ rename_column :ci_commits, :gl_project_id, :project_id
+ rename_column :ci_runner_projects, :gl_project_id, :project_id
+ rename_column :ci_triggers, :gl_project_id, :project_id
+ rename_column :ci_variables, :gl_project_id, :project_id
+ end
+end
diff --git a/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb b/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb
new file mode 100644
index 00000000000..f54608ecceb
--- /dev/null
+++ b/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPrintingMergeRequestLinkEnabledToProject < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ add_column_with_default(:projects, :printing_merge_request_link_enabled, :boolean, default: true)
+ end
+
+ def down
+ remove_column(:projects, :printing_merge_request_link_enabled)
+ end
+end
diff --git a/db/migrate/20170301195939_rename_ci_commits_to_ci_pipelines.rb b/db/migrate/20170301195939_rename_ci_commits_to_ci_pipelines.rb
new file mode 100644
index 00000000000..4f061d96392
--- /dev/null
+++ b/db/migrate/20170301195939_rename_ci_commits_to_ci_pipelines.rb
@@ -0,0 +1,10 @@
+class RenameCiCommitsToCiPipelines < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Rename table ci_commits to ci_pipelines'
+
+ def change
+ rename_table 'ci_commits', 'ci_pipelines'
+ end
+end
diff --git a/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
new file mode 100644
index 00000000000..1e2abea5254
--- /dev/null
+++ b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
@@ -0,0 +1,83 @@
+class RemoveUnusedCiTablesAndColumns < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON =
+ 'Remove unused columns in used tables.' \
+ ' Downtime required in case Rails caches them'
+
+ def up
+ %w[ci_application_settings
+ ci_events
+ ci_jobs
+ ci_sessions
+ ci_taggings
+ ci_tags].each do |table|
+ drop_table(table)
+ end
+
+ remove_column :ci_pipelines, :push_data, :text
+ remove_column :ci_builds, :job_id, :integer
+ remove_column :ci_builds, :deploy, :boolean
+ end
+
+ def down
+ add_column :ci_builds, :deploy, :boolean
+ add_column :ci_builds, :job_id, :integer
+ add_column :ci_pipelines, :push_data, :text
+
+ create_table "ci_tags", force: :cascade do |t|
+ t.string "name"
+ t.integer "taggings_count", default: 0
+ end
+
+ create_table "ci_taggings", force: :cascade do |t|
+ t.integer "tag_id"
+ t.integer "taggable_id"
+ t.string "taggable_type"
+ t.integer "tagger_id"
+ t.string "tagger_type"
+ t.string "context", limit: 128
+ t.datetime "created_at"
+ end
+
+ add_index "ci_taggings", %w[taggable_id taggable_type context]
+
+ create_table "ci_sessions", force: :cascade do |t|
+ t.string "session_id", null: false
+ t.text "data"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ create_table "ci_jobs", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.text "commands"
+ t.boolean "active", default: true, null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "name"
+ t.boolean "build_branches", default: true, null: false
+ t.boolean "build_tags", default: false, null: false
+ t.string "job_type", default: "parallel"
+ t.string "refs"
+ t.datetime "deleted_at"
+ end
+
+ create_table "ci_events", force: :cascade do |t|
+ t.integer "project_id"
+ t.integer "user_id"
+ t.integer "is_admin"
+ t.text "description"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ create_table "ci_application_settings", force: :cascade do |t|
+ t.boolean "all_broken_builds"
+ t.boolean "add_pusher"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+ end
+end
diff --git a/db/migrate/20170305203726_add_owner_id_foreign_key.rb b/db/migrate/20170305203726_add_owner_id_foreign_key.rb
index 3eece0e2eb5..5fbdc45f1a7 100644
--- a/db/migrate/20170305203726_add_owner_id_foreign_key.rb
+++ b/db/migrate/20170305203726_add_owner_id_foreign_key.rb
@@ -5,7 +5,11 @@ class AddOwnerIdForeignKey < ActiveRecord::Migration
disable_ddl_transaction!
- def change
+ def up
add_concurrent_foreign_key :ci_triggers, :users, column: :owner_id, on_delete: :cascade
end
+
+ def down
+ remove_foreign_key :ci_triggers, column: :owner_id
+ end
end
diff --git a/db/migrate/20170313213916_add_index_to_user_ghost.rb b/db/migrate/20170313213916_add_index_to_user_ghost.rb
new file mode 100644
index 00000000000..c429039c275
--- /dev/null
+++ b/db/migrate/20170313213916_add_index_to_user_ghost.rb
@@ -0,0 +1,24 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexToUserGhost < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :users, :ghost
+ end
+
+ def down
+ remove_index :users, :ghost
+ end
+end
diff --git a/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
new file mode 100644
index 00000000000..b39c0a3be0f
--- /dev/null
+++ b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb
@@ -0,0 +1,24 @@
+class RevertAddNotifiedOfOwnActivityToUsers < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ if our_column_exists?
+ remove_column :users, :notified_of_own_activity
+ end
+ end
+
+ def down
+ unless our_column_exists?
+ add_column_with_default :users, :notified_of_own_activity, :boolean, default: false
+ end
+ end
+
+ private
+
+ def our_column_exists?
+ column_exists?(:users, :notified_of_own_activity)
+ end
+end
diff --git a/db/migrate/20170315194013_add_closed_at_to_issues.rb b/db/migrate/20170315194013_add_closed_at_to_issues.rb
new file mode 100644
index 00000000000..1326118cc8d
--- /dev/null
+++ b/db/migrate/20170315194013_add_closed_at_to_issues.rb
@@ -0,0 +1,7 @@
+class AddClosedAtToIssues < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ add_column :issues, :closed_at, :datetime
+ end
+end
diff --git a/db/migrate/20170317203554_index_routes_path_for_like.rb b/db/migrate/20170317203554_index_routes_path_for_like.rb
new file mode 100644
index 00000000000..264ecd322c3
--- /dev/null
+++ b/db/migrate/20170317203554_index_routes_path_for_like.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class IndexRoutesPathForLike < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ INDEX_NAME = 'index_routes_on_path_text_pattern_ops'
+
+ disable_ddl_transaction!
+
+ def up
+ return unless Gitlab::Database.postgresql?
+
+ unless index_exists?(:routes, name: INDEX_NAME)
+ execute("CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON routes (path varchar_pattern_ops);")
+ end
+ end
+
+ def down
+ return unless Gitlab::Database.postgresql?
+
+ if index_exists?(:routes, name: INDEX_NAME)
+ execute("DROP INDEX CONCURRENTLY #{INDEX_NAME};")
+ end
+ end
+end
diff --git a/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb b/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb
index 89aa753646c..aee0c1b6245 100644
--- a/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb
+++ b/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb
@@ -18,6 +18,7 @@ class RemoveTrackableColumnsFromTimelogs < ActiveRecord::Migration
# disable_ddl_transaction!
def change
- remove_columns :timelogs, :trackable_id, :trackable_type
+ remove_column :timelogs, :trackable_id, :integer
+ remove_column :timelogs, :trackable_type, :string
end
end
diff --git a/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb b/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb
new file mode 100644
index 00000000000..2dd14ee5a78
--- /dev/null
+++ b/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb
@@ -0,0 +1,87 @@
+class MigrateBuildEventsToPipelineEvents < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ include Gitlab::Database
+
+ DOWNTIME = false
+
+ def up
+ Gitlab::Database.with_connection_pool(2) do |pool|
+ threads = []
+
+ threads << Thread.new do
+ pool.with_connection do |connection|
+ Thread.current[:foreign_key_connection] = connection
+
+ execute(<<-SQL.strip_heredoc)
+ UPDATE services
+ SET properties = replace(properties,
+ 'notify_only_broken_builds',
+ 'notify_only_broken_pipelines')
+ , pipeline_events = #{true_value}
+ , build_events = #{false_value}
+ WHERE type IN
+ ('SlackService', 'MattermostService', 'HipchatService')
+ AND build_events = #{true_value};
+ SQL
+ end
+ end
+
+ threads << Thread.new do
+ pool.with_connection do |connection|
+ Thread.current[:foreign_key_connection] = connection
+
+ execute(update_pipeline_services_sql)
+ end
+ end
+
+ threads.each(&:join)
+ end
+ end
+
+ def down
+ # Don't bother to migrate the data back
+ end
+
+ def connection
+ # Rails memoizes connection objects, but this causes them to be shared
+ # amongst threads; we don't want that.
+ Thread.current[:foreign_key_connection] || ActiveRecord::Base.connection
+ end
+
+ private
+
+ def update_pipeline_services_sql
+ if Gitlab::Database.postgresql?
+ <<-SQL
+ UPDATE services
+ SET type = 'PipelinesEmailService'
+ , properties = replace(properties,
+ 'notify_only_broken_builds',
+ 'notify_only_broken_pipelines')
+ , pipeline_events = #{true_value}
+ , build_events = #{false_value}
+ WHERE type = 'BuildsEmailService'
+ AND
+ (SELECT 1 FROM services pipeline_services
+ WHERE pipeline_services.project_id = services.project_id
+ AND pipeline_services.type = 'PipelinesEmailService' LIMIT 1)
+ IS NULL;
+ SQL
+ else
+ <<-SQL
+ UPDATE services build_services
+ LEFT OUTER JOIN services pipeline_services
+ ON build_services.project_id = pipeline_services.project_id
+ AND pipeline_services.type = 'PipelinesEmailService'
+ SET build_services.type = 'PipelinesEmailService'
+ , build_services.properties = replace(build_services.properties,
+ 'notify_only_broken_builds',
+ 'notify_only_broken_pipelines')
+ , build_services.pipeline_events = #{true_value}
+ , build_services.build_events = #{false_value}
+ WHERE build_services.type = 'BuildsEmailService'
+ AND pipeline_services.id IS NULL;
+ SQL
+ end.strip_heredoc
+ end
+end
diff --git a/db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb b/db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb
index 9020e0d054c..ec6e8cdfc45 100644
--- a/db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb
+++ b/db/post_migrate/20170306170512_migrate_legacy_manual_actions.rb
@@ -4,6 +4,8 @@ class MigrateLegacyManualActions < ActiveRecord::Migration
DOWNTIME = false
def up
+ disable_statement_timeout
+
execute <<-EOS
UPDATE ci_builds SET status = 'manual', allow_failure = true
WHERE ci_builds.when = 'manual' AND ci_builds.status = 'skipped';
@@ -11,6 +13,8 @@ class MigrateLegacyManualActions < ActiveRecord::Migration
end
def down
+ disable_statement_timeout
+
execute <<-EOS
UPDATE ci_builds SET status = 'skipped', allow_failure = false
WHERE ci_builds.when = 'manual' AND ci_builds.status = 'manual';
diff --git a/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb b/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb
index 9dfe77bedb7..44c688fa134 100644
--- a/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb
+++ b/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb
@@ -6,41 +6,12 @@ class RenameMoreReservedProjectNames < ActiveRecord::Migration
DOWNTIME = false
- THREAD_COUNT = 8
-
KNOWN_PATHS = %w(artifacts graphs refs badges).freeze
def up
- queues = Array.new(THREAD_COUNT) { Queue.new }
- start = false
-
- threads = Array.new(THREAD_COUNT) do |index|
- Thread.new do
- queue = queues[index]
-
- # Wait until we have input to process.
- until start; end
-
- rename_projects(queue.pop) until queue.empty?
- end
- end
-
- enum = queues.each
-
reserved_projects.each_slice(100) do |slice|
- begin
- queue = enum.next
- rescue StopIteration
- enum.rewind
- retry
- end
-
- queue << slice
+ rename_projects(slice)
end
-
- start = true
-
- threads.each(&:join)
end
def down
diff --git a/db/schema.rb b/db/schema.rb
index ca88198079f..904fef4a381 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: 20170313133418) do
+ActiveRecord::Schema.define(version: 20170317203554) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -185,15 +185,7 @@ ActiveRecord::Schema.define(version: 20170313133418) do
add_index "chat_teams", ["namespace_id"], name: "index_chat_teams_on_namespace_id", unique: true, using: :btree
- create_table "ci_application_settings", force: :cascade do |t|
- t.boolean "all_broken_builds"
- t.boolean "add_pusher"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
create_table "ci_builds", force: :cascade do |t|
- t.integer "project_id"
t.string "status"
t.datetime "finished_at"
t.text "trace"
@@ -204,9 +196,7 @@ ActiveRecord::Schema.define(version: 20170313133418) do
t.float "coverage"
t.integer "commit_id"
t.text "commands"
- t.integer "job_id"
t.string "name"
- t.boolean "deploy", default: false
t.text "options"
t.boolean "allow_failure", default: false, null: false
t.string "stage"
@@ -219,7 +209,7 @@ ActiveRecord::Schema.define(version: 20170313133418) do
t.string "target_url"
t.string "description"
t.text "artifacts_file"
- t.integer "gl_project_id"
+ t.integer "project_id"
t.text "artifacts_metadata"
t.integer "erased_by_id"
t.datetime "erased_at"
@@ -238,25 +228,22 @@ ActiveRecord::Schema.define(version: 20170313133418) 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", ["gl_project_id"], name: "index_ci_builds_on_gl_project_id", using: :btree
add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
add_index "ci_builds", ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree
add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree
add_index "ci_builds", ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree
- create_table "ci_commits", force: :cascade do |t|
- t.integer "project_id"
+ create_table "ci_pipelines", force: :cascade do |t|
t.string "ref"
t.string "sha"
t.string "before_sha"
- t.text "push_data"
t.datetime "created_at"
t.datetime "updated_at"
t.boolean "tag", default: false
t.text "yaml_errors"
t.datetime "committed_at"
- t.integer "gl_project_id"
+ t.integer "project_id"
t.string "status"
t.datetime "started_at"
t.datetime "finished_at"
@@ -265,67 +252,20 @@ ActiveRecord::Schema.define(version: 20170313133418) do
t.integer "lock_version"
end
- add_index "ci_commits", ["gl_project_id", "ref", "status"], name: "index_ci_commits_on_gl_project_id_and_ref_and_status", using: :btree
- add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree
- add_index "ci_commits", ["gl_project_id"], name: "index_ci_commits_on_gl_project_id", using: :btree
- add_index "ci_commits", ["status"], name: "index_ci_commits_on_status", using: :btree
- add_index "ci_commits", ["user_id"], name: "index_ci_commits_on_user_id", using: :btree
-
- create_table "ci_events", force: :cascade do |t|
- t.integer "project_id"
- t.integer "user_id"
- t.integer "is_admin"
- t.text "description"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- create_table "ci_jobs", force: :cascade do |t|
- t.integer "project_id", null: false
- t.text "commands"
- t.boolean "active", default: true, null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.string "name"
- t.boolean "build_branches", default: true, null: false
- t.boolean "build_tags", default: false, null: false
- t.string "job_type", default: "parallel"
- t.string "refs"
- t.datetime "deleted_at"
- end
-
- create_table "ci_projects", force: :cascade do |t|
- t.string "name"
- t.integer "timeout", default: 3600, null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.string "token"
- t.string "default_ref"
- t.string "path"
- t.boolean "always_build", default: false, null: false
- t.integer "polling_interval"
- t.boolean "public", default: false, null: false
- t.string "ssh_url_to_repo"
- t.integer "gitlab_id"
- t.boolean "allow_git_fetch", default: true, null: false
- t.string "email_recipients", default: "", null: false
- t.boolean "email_add_pusher", default: true, null: false
- t.boolean "email_only_broken_builds", default: true, null: false
- t.string "skip_refs"
- t.string "coverage_regex"
- t.boolean "shared_runners_enabled", default: false
- t.text "generated_yaml_config"
- end
+ add_index "ci_pipelines", ["project_id", "ref", "status"], name: "index_ci_pipelines_on_project_id_and_ref_and_status", using: :btree
+ add_index "ci_pipelines", ["project_id", "sha"], name: "index_ci_pipelines_on_project_id_and_sha", using: :btree
+ add_index "ci_pipelines", ["project_id"], name: "index_ci_pipelines_on_project_id", using: :btree
+ add_index "ci_pipelines", ["status"], name: "index_ci_pipelines_on_status", using: :btree
+ add_index "ci_pipelines", ["user_id"], name: "index_ci_pipelines_on_user_id", using: :btree
create_table "ci_runner_projects", force: :cascade do |t|
t.integer "runner_id", null: false
- t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "gl_project_id"
+ t.integer "project_id"
end
- add_index "ci_runner_projects", ["gl_project_id"], name: "index_ci_runner_projects_on_gl_project_id", using: :btree
+ add_index "ci_runner_projects", ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree
add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree
create_table "ci_runners", force: :cascade do |t|
@@ -349,30 +289,6 @@ ActiveRecord::Schema.define(version: 20170313133418) do
add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
- create_table "ci_sessions", force: :cascade do |t|
- t.string "session_id", null: false
- t.text "data"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- create_table "ci_taggings", force: :cascade do |t|
- t.integer "tag_id"
- t.integer "taggable_id"
- t.string "taggable_type"
- t.integer "tagger_id"
- t.string "tagger_type"
- t.string "context", limit: 128
- t.datetime "created_at"
- end
-
- add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
-
- create_table "ci_tags", force: :cascade do |t|
- t.string "name"
- t.integer "taggings_count", default: 0
- end
-
create_table "ci_trigger_requests", force: :cascade do |t|
t.integer "trigger_id", null: false
t.text "variables"
@@ -385,28 +301,26 @@ ActiveRecord::Schema.define(version: 20170313133418) do
create_table "ci_triggers", force: :cascade do |t|
t.string "token"
- t.integer "project_id"
t.datetime "deleted_at"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "gl_project_id"
+ t.integer "project_id"
t.integer "owner_id"
t.string "description"
end
- add_index "ci_triggers", ["gl_project_id"], name: "index_ci_triggers_on_gl_project_id", using: :btree
+ add_index "ci_triggers", ["project_id"], name: "index_ci_triggers_on_project_id", using: :btree
create_table "ci_variables", force: :cascade do |t|
- t.integer "project_id"
t.string "key"
t.text "value"
t.text "encrypted_value"
t.string "encrypted_value_salt"
t.string "encrypted_value_iv"
- t.integer "gl_project_id"
+ t.integer "project_id"
end
- add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree
+ add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree
create_table "deploy_keys_projects", force: :cascade do |t|
t.integer "deploy_key_id", null: false
@@ -531,6 +445,7 @@ ActiveRecord::Schema.define(version: 20170313133418) do
t.text "description_html"
t.integer "time_estimate"
t.integer "relative_position"
+ t.datetime "closed_at"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -1003,6 +918,7 @@ ActiveRecord::Schema.define(version: 20170313133418) do
t.boolean "lfs_enabled"
t.text "description_html"
t.boolean "only_allow_merge_if_all_discussions_are_resolved"
+ t.boolean "printing_merge_request_link_enabled", default: true, null: false
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -1069,6 +985,7 @@ ActiveRecord::Schema.define(version: 20170313133418) do
end
add_index "routes", ["path"], name: "index_routes_on_path", unique: true, using: :btree
+ add_index "routes", ["path"], name: "index_routes_on_path_text_pattern_ops", using: :btree, opclasses: {"path"=>"varchar_pattern_ops"}
add_index "routes", ["source_type", "source_id"], name: "index_routes_on_source_type_and_source_id", unique: true, using: :btree
create_table "sent_notifications", force: :cascade do |t|
@@ -1318,7 +1235,6 @@ ActiveRecord::Schema.define(version: 20170313133418) do
t.string "incoming_email_token"
t.string "organization"
t.boolean "authorized_projects_populated"
- t.boolean "notified_of_own_activity", default: false, null: false
t.boolean "ghost"
end
@@ -1329,6 +1245,7 @@ ActiveRecord::Schema.define(version: 20170313133418) do
add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"}
+ add_index "users", ["ghost"], name: "index_users_on_ghost", using: :btree
add_index "users", ["incoming_email_token"], name: "index_users_on_incoming_email_token", using: :btree
add_index "users", ["name"], name: "index_users_on_name", using: :btree
add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
@@ -1378,7 +1295,7 @@ ActiveRecord::Schema.define(version: 20170313133418) do
add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "lists", "boards"
add_foreign_key "lists", "labels"
- add_foreign_key "merge_request_metrics", "ci_commits", column: "pipeline_id", on_delete: :cascade
+ add_foreign_key "merge_request_metrics", "ci_pipelines", column: "pipeline_id", on_delete: :cascade
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md
index 3a394c561db..b2445d1c0e5 100644
--- a/doc/administration/monitoring/prometheus/index.md
+++ b/doc/administration/monitoring/prometheus/index.md
@@ -3,10 +3,9 @@
>**Notes:**
- Prometheus and the various exporters listed in this page are bundled in the
Omnibus GitLab package. Check each exporter's documentation for the timeline
- they got added. For installations from source you will have to install
- them yourself. Over subsequent releases additional GitLab metrics will be
- captured.
-- Prometheus services are off by default but will be on starting with GitLab 9.0.
+ they got added. For installations from source you will have to install them
+ yourself. Over subsequent releases additional GitLab metrics will be captured.
+- Prometheus services are on by default with GitLab 9.0.
- Prometheus and its exporters do not authenticate users, and will be available
to anyone who can access them.
@@ -26,26 +25,25 @@ dashboard tool like [Grafana].
## Configuring Prometheus
>**Note:**
-Available since Omnibus GitLab 8.16. For installations from source you'll
-have to install and configure it yourself.
+For installations from source you'll have to install and configure it yourself.
-To enable Prometheus:
+Prometheus and it's exporters are on by default, starting with GitLab 9.0.
+Prometheus will run as the `gitlab-prometheus` user and listen on
+`http://localhost:9090`. Each exporter will be automatically be set up as a
+monitoring target for Prometheus, unless individually disabled.
+
+To disable Prometheus and all of its exporters, as well as any added in the future:
1. Edit `/etc/gitlab/gitlab.rb`
-1. Add or find and uncomment the following line, making sure it's set to `true`:
+1. Add or find and uncomment the following line, making sure it's set to `false`:
```ruby
- prometheus['enable'] = true
+ prometheus_monitoring['enable'] = false
```
1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
take effect
-By default, Prometheus will run as the `gitlab-prometheus` user and listen on
-`http://localhost:9090`. If the [node exporter](#node-exporter) service
-has been enabled, it will automatically be set up as a monitoring target for
-Prometheus.
-
## Changing the port Prometheus listens on
>**Note:**
@@ -71,16 +69,14 @@ To change the address/port that Prometheus listens on:
## Viewing performance metrics
-After you have [enabled Prometheus](#configuring-prometheus), you can visit
-`http://localhost:9090` for the dashboard that Prometheus offers by default.
+You can visit `http://localhost:9090` for the dashboard that Prometheus offers by default.
>**Note:**
If SSL has been enabled on your GitLab instance, you may not be able to access
Prometheus on the same browser as GitLab due to [HSTS][hsts]. We plan to
[provide access via GitLab][multi-user-prometheus], but in the interim there are
some workarounds: using a separate browser for Prometheus, resetting HSTS, or
-having [Nginx proxy it][nginx-custom-config]. Follow issue [#27069] for more
-information.
+having [Nginx proxy it][nginx-custom-config].
The performance data collected by Prometheus can be viewed directly in the
Prometheus console or through a compatible dashboard tool.
@@ -96,6 +92,24 @@ Sample Prometheus queries:
- **Data transmitted:** `irate(node_network_transmit_bytes[5m])`
- **Data received:** `irate(node_network_receive_bytes[5m])`
+## Configuring Prometheus to monitor Kubernetes
+
+> Introduced in GitLab 9.0.
+
+If your GitLab server is running within Kubernetes, Prometheus will collect metrics from the Nodes in the cluster including performance data on each container. This is particularly helpful if your CI/CD environments run in the same cluster, as you can use the [Prometheus project integration][] to monitor them.
+
+To disable the monitoring of Kubernetes:
+
+1. Edit `/etc/gitlab/gitlab.rb`
+1. Add or find and uncomment the following line and set it to `false`:
+
+ ```ruby
+ prometheus['monitor_kubernetes'] = false
+ ```
+
+1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
+ take effect
+
## Prometheus exporters
There are a number of libraries and servers which help in exporting existing
@@ -143,5 +157,6 @@ The GitLab monitor exporter allows you to measure various GitLab metrics.
[prom-grafana]: https://prometheus.io/docs/visualization/grafana/
[scrape-config]: https://prometheus.io/docs/operating/configuration/#%3Cscrape_config%3E
[reconfigure]: ../../restart_gitlab.md#omnibus-gitlab-reconfigure
-[#27069]: https://gitlab.com/gitlab-org/gitlab-ce/issues/27069
[1261]: https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/1261
+[prometheus integration]: ../../../user/project/integrations/prometheus.md
+[prometheus-cadvisor-metrics]: https://github.com/google/cadvisor/blob/master/docs/storage/prometheus.md
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 62b0468da79..0c63b0b59a7 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -26,9 +26,9 @@ it works.
---
-In the case of [custom domains](#custom-domains) (but not
-[wildcard domains](#wildcard-domains)), the Pages daemon needs to listen on
-ports `80` and/or `443`. For that reason, there is some flexibility in the way
+In the case of [custom domains](#custom-domains) (but not
+[wildcard domains](#wildcard-domains)), the Pages daemon needs to listen on
+ports `80` and/or `443`. For that reason, there is some flexibility in the way
which you can set it up:
1. Run the Pages daemon in the same server as GitLab, listening on a secondary IP.
@@ -65,11 +65,13 @@ you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the
host that GitLab runs. For example, an entry would look like this:
```
-*.example.io. 1800 IN A 1.1.1.1
+*.example.io. 1800 IN A 1.1.1.1
+*.example.io. 1800 IN AAAA 2001::1
```
where `example.io` is the domain under which GitLab Pages will be served
-and `1.1.1.1` is the IP address of your GitLab instance.
+and `1.1.1.1` is the IPv4 address of your GitLab instance and `2001::1` is the
+IPv6 address. If you don't have IPv6, you can omit the AAAA record.
> **Note:**
You should not use the GitLab domain to serve user pages. For more information
@@ -141,7 +143,8 @@ outside world.
In addition to the wildcard domains, you can also have the option to configure
GitLab Pages to work with custom domains. Again, there are two options here:
support custom domains with and without TLS certificates. The easiest setup is
-that without TLS certificates.
+that without TLS certificates. In either case, you'll need a secondary IP. If
+you have IPv6 as well as IPv4 addresses, you can use them both.
### Custom domains
@@ -163,11 +166,12 @@ world. Custom domains are supported, but no TLS.
pages_external_url "http://example.io"
nginx['listen_addresses'] = ['1.1.1.1']
pages_nginx['enable'] = false
- gitlab_pages['external_http'] = '1.1.1.2:80'
+ gitlab_pages['external_http'] = ['1.1.1.2:80', '[2001::2]:80']
```
where `1.1.1.1` is the primary IP address that GitLab is listening to and
- `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to.
+ `1.1.1.2` and `2001::2` are the secondary IPs the GitLab Pages daemon
+ listens on. If you don't have IPv6, you can omit the IPv6 address.
1. [Reconfigure GitLab][reconfigure]
@@ -194,12 +198,13 @@ world. Custom domains and TLS are supported.
pages_nginx['enable'] = false
gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt"
gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key"
- gitlab_pages['external_http'] = '1.1.1.2:80'
- gitlab_pages['external_https'] = '1.1.1.2:443'
+ gitlab_pages['external_http'] = ['1.1.1.2:80', '[2001::2]:80']
+ gitlab_pages['external_https'] = ['1.1.1.2:443', '[2001::2]:443']
```
where `1.1.1.1` is the primary IP address that GitLab is listening to and
- `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to.
+ `1.1.1.2` and `2001::2` are the secondary IPs where the GitLab Pages daemon
+ listens on. If you don't have IPv6, you can omit the IPv6 address.
1. [Reconfigure GitLab][reconfigure]
diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md
index f6f50e2c571..a45c3306457 100644
--- a/doc/administration/pages/source.md
+++ b/doc/administration/pages/source.md
@@ -1,5 +1,9 @@
# GitLab Pages administration for source installations
+>**Note:**
+Before attempting to enable GitLab Pages, first make sure you have
+[installed GitLab](../../install/installation.md) successfully.
+
This is the documentation for configuring a GitLab Pages when you have installed
GitLab from source and not using the Omnibus packages.
@@ -13,7 +17,33 @@ Pages to the latest supported version.
## Overview
-[Read the Omnibus overview section.](index.md#overview)
+GitLab Pages makes use of the [GitLab Pages daemon], a simple HTTP server
+written in Go that can listen on an external IP address and provide support for
+custom domains and custom certificates. It supports dynamic certificates through
+SNI and exposes pages using HTTP2 by default.
+You are encouraged to read its [README][pages-readme] to fully understand how
+it works.
+
+---
+
+In the case of [custom domains](#custom-domains) (but not
+[wildcard domains](#wildcard-domains)), the Pages daemon needs to listen on
+ports `80` and/or `443`. For that reason, there is some flexibility in the way
+which you can set it up:
+
+1. Run the Pages daemon in the same server as GitLab, listening on a secondary IP.
+1. Run the Pages daemon in a separate server. In that case, the
+ [Pages path](#change-storage-path) must also be present in the server that
+ the Pages daemon is installed, so you will have to share it via network.
+1. Run the Pages daemon in the same server as GitLab, listening on the same IP
+ but on different ports. In that case, you will have to proxy the traffic with
+ a loadbalancer. If you choose that route note that you should use TCP load
+ balancing for HTTPS. If you use TLS-termination (HTTPS-load balancing) the
+ pages will not be able to be served with user provided certificates. For
+ HTTP it's OK to use HTTP or TCP load balancing.
+
+In this document, we will proceed assuming the first option. If you are not
+supporting custom domains a secondary IP is not needed.
## Prerequisites
@@ -75,7 +105,7 @@ The Pages daemon doesn't listen to the outside world.
cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
cd gitlab-pages
- sudo -u git -H git checkout v0.2.4
+ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
sudo -u git -H make
```
@@ -100,14 +130,21 @@ The Pages daemon doesn't listen to the outside world.
https: false
```
-1. Copy the `gitlab-pages-ssl` Nginx configuration file:
+1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in
+ order to enable the pages daemon. In `gitlab_pages_options` the
+ `-pages-domain` must match the `host` setting that you set above.
- ```bash
- sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf
- sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
```
+ gitlab_pages_enabled=true
+ gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090
+ ```
+
+1. Copy the `gitlab-pages` Nginx configuration file:
- Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
+ ```bash
+ sudo cp lib/support/nginx/gitlab-pages /etc/nginx/sites-available/gitlab-pages.conf
+ sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf
+ ```
1. Restart NGINX
1. [Restart GitLab][restart]
@@ -131,7 +168,7 @@ outside world.
cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
cd gitlab-pages
- sudo -u git -H git checkout v0.2.4
+ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
sudo -u git -H make
```
@@ -149,6 +186,17 @@ outside world.
https: true
```
+1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in
+ order to enable the pages daemon. In `gitlab_pages_options` the
+ `-pages-domain` must match the `host` setting that you set above.
+ The `-root-cert` and `-root-key` settings are the wildcard TLS certificates
+ of the `example.io` domain:
+
+ ```
+ gitlab_pages_enabled=true
+ gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key
+ ```
+
1. Copy the `gitlab-pages-ssl` Nginx configuration file:
```bash
@@ -156,12 +204,9 @@ outside world.
sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
```
- Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
-
1. Restart NGINX
1. [Restart GitLab][restart]
-
## Advanced configuration
In addition to the wildcard domains, you can also have the option to configure
@@ -189,7 +234,7 @@ world. Custom domains are supported, but no TLS.
cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
cd gitlab-pages
- sudo -u git -H git checkout v0.2.4
+ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
sudo -u git -H make
```
@@ -224,12 +269,10 @@ world. Custom domains are supported, but no TLS.
1. Copy the `gitlab-pages-ssl` Nginx configuration file:
```bash
- sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf
- sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
+ sudo cp lib/support/nginx/gitlab-pages /etc/nginx/sites-available/gitlab-pages.conf
+ sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf
```
- Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
-
1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace
`0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab
listens to.
@@ -257,7 +300,7 @@ world. Custom domains and TLS are supported.
cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git
cd gitlab-pages
- sudo -u git -H git checkout v0.2.4
+ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION)
sudo -u git -H make
```
@@ -300,8 +343,6 @@ world. Custom domains and TLS are supported.
sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf
```
- Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
-
1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace
`0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab
listens to.
@@ -392,5 +433,6 @@ than GitLab to prevent XSS attacks.
[pages-userguide]: ../../user/project/pages/index.md
[reconfigure]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
[restart]: ../restart_gitlab.md#installations-from-source
-[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4
+[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.4.0
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/init.d/gitlab.default.example
[shared runners]: ../../ci/runners/README.md
diff --git a/doc/api/branches.md b/doc/api/branches.md
index 83705106160..815aabda8e3 100644
--- a/doc/api/branches.md
+++ b/doc/api/branches.md
@@ -3,6 +3,8 @@
## List repository branches
Get a list of repository branches from a project, sorted by name alphabetically.
+This endpoint can be accessed without authentication if the repository is
+publicly accessible.
```
GET /projects/:id/repository/branches
@@ -48,7 +50,8 @@ Example response:
## Get single repository branch
-Get a single project repository branch.
+Get a single project repository branch. This endpoint can be accessed without
+authentication if the repository is publicly accessible.
```
GET /projects/:id/repository/branches/:branch
diff --git a/doc/api/issues.md b/doc/api/issues.md
index cb437ffb174..a19c965a8c3 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -23,7 +23,6 @@ GET /issues?state=closed
GET /issues?labels=foo
GET /issues?labels=foo,bar
GET /issues?labels=foo,bar&state=opened
-GET /projects/:id/issues?labels_name=No+Label
GET /issues?milestone=1.0.0
GET /issues?milestone=1.0.0&state=opened
GET /issues?iids[]=42&iids[]=43
@@ -32,8 +31,7 @@ GET /issues?iids[]=42&iids[]=43
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `state` | string | no | Return all issues or just those that are `opened` or `closed`|
-| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned |
-| `labels_name` | string | no | Return all issues with the mentioned label. `No+Label` lists all issues with no labels |
+| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
| `milestone` | string| no | The milestone title |
| `iids` | Array[integer] | no | Return only the issues having the given `iid` |
| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
@@ -103,7 +101,6 @@ GET /groups/:id/issues?state=closed
GET /groups/:id/issues?labels=foo
GET /groups/:id/issues?labels=foo,bar
GET /groups/:id/issues?labels=foo,bar&state=opened
-GET /projects/:id/issues?labels_name=No+Label
GET /groups/:id/issues?milestone=1.0.0
GET /groups/:id/issues?milestone=1.0.0&state=opened
GET /groups/:id/issues?iids[]=42&iids[]=43
@@ -113,8 +110,7 @@ GET /groups/:id/issues?iids[]=42&iids[]=43
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a group |
| `state` | string | no | Return all issues or just those that are `opened` or `closed`|
-| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned |
-| `labels_name` | string | no | Return all issues with the mentioned label. `No+Label` lists all issues with no labels |
+| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
| `iids` | Array[integer] | no | Return only the issues having the given `iid` |
| `milestone` | string| no | The milestone title |
| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
@@ -185,7 +181,6 @@ GET /projects/:id/issues?state=closed
GET /projects/:id/issues?labels=foo
GET /projects/:id/issues?labels=foo,bar
GET /projects/:id/issues?labels=foo,bar&state=opened
-GET /projects/:id/issues?labels_name=No+Label
GET /projects/:id/issues?milestone=1.0.0
GET /projects/:id/issues?milestone=1.0.0&state=opened
GET /projects/:id/issues?iids[]=42&iids[]=43
@@ -196,8 +191,7 @@ GET /projects/:id/issues?iids[]=42&iids[]=43
| `id` | integer | yes | The ID of a project |
| `iids` | Array[integer] | no | Return only the milestone having the given `iid` |
| `state` | string | no | Return all issues or just those that are `opened` or `closed`|
-| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned |
-| `labels_name` | string | no | Return all issues with the mentioned label. `No+Label` lists all issues with no labels |
+| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
| `milestone` | string| no | The milestone title |
| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
diff --git a/doc/api/services.md b/doc/api/services.md
index 8e7afe41b0c..7d4779f1137 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -139,43 +139,6 @@ Get Buildkite service settings for a project.
GET /projects/:id/services/buildkite
```
-## Build-Emails
-
-Get emails for GitLab CI builds.
-
-### Create/Edit Build-Emails service
-
-Set Build-Emails service for a project.
-
-```
-PUT /projects/:id/services/jobs-email
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `recipients` | string | yes | Comma-separated list of recipient email addresses |
-| `add_pusher` | boolean | no | Add pusher to recipients list |
-| `notify_only_broken_jobs` | boolean | no | Notify only broken jobs |
-
-
-### Delete Job-Emails service
-
-Delete Build-Emails service for a project.
-
-```
-DELETE /projects/:id/services/jobs-email
-```
-
-### Get Job-Emails service settings
-
-Get Build-Emails service settings for a project.
-
-```
-GET /projects/:id/services/jobs-email
-```
-
## Campfire
Simple web-based real-time group chat
@@ -580,8 +543,7 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `recipients` | string | yes | Comma-separated list of recipient email addresses |
| `add_pusher` | boolean | no | Add pusher to recipients list |
-| `notify_only_broken_jobs` | boolean | no | Notify only broken pipelines |
-
+| `notify_only_broken_pipelines` | boolean | no | Notify only broken pipelines |
### Delete Pipeline-Emails service
diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md
index 0794156bc39..7f4426ee85d 100644
--- a/doc/api/v3_to_v4.md
+++ b/doc/api/v3_to_v4.md
@@ -8,16 +8,16 @@ Below are the changes made between V3 and V4.
### 8.17
-- Removed `/projects/:search` (use: `/projects?search=x`) [!8877](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8877)
-- `iid` filter has been removed from `projects/:id/issues` [!8967](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8967)
-- `projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids` [!8793](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8793)
-- Endpoints under `projects/merge_request/:id` have been removed (use: `projects/merge_requests/:id`) [!8793](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8793)
+- Removed `GET /projects/:search` (use: `GET /projects?search=x`) [!8877](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8877)
+- `iid` filter has been removed from `GET /projects/:id/issues` [!8967](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8967)
+- `GET /projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids` [!8793](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8793)
+- Endpoints under `GET /projects/merge_request/:id` have been removed (use: `GET /projects/merge_requests/:id`) [!8793](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8793)
- Project snippets do not return deprecated field `expires_at` [!8723](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8723)
-- Endpoints under `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`) [!8716](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8716)
+- Endpoints under `GET /projects/:id/keys` have been removed (use `GET /projects/:id/deploy_keys`) [!8716](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8716)
### 9.0
-- Status 409 returned for POST `project/:id/members` when a member already exists [!9093](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9093)
+- Status 409 returned for `POST /projects/:id/members` when a member already exists [!9093](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9093)
- Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar` [!9328](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9328)
- Removed the following deprecated Templates endpoints (these are still accessible with `/templates` prefix) [!8853](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8853)
- `/licences`
@@ -28,31 +28,31 @@ Below are the changes made between V3 and V4.
- `/gitignores/:key`
- `/gitlab_ci_ymls/:key`
- `/dockerfiles/:key`
-- Moved `/projects/fork/:id` to `/projects/:id/fork` [!8940](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8940)
+- Moved `POST /projects/fork/:id` to `POST /projects/:id/fork` [!8940](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8940)
- Moved `DELETE /todos` to `POST /todos/mark_as_done` and `DELETE /todos/:todo_id` to `POST /todos/:todo_id/mark_as_done` [!9410](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9410)
- Project filters are no longer available as `GET /projects/foo`, but as `GET /projects?foo=true` instead [!8962](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8962)
- `GET /projects/visible` & `GET /projects/all` are consolidated into `GET /projects` and can be used with or without authorization
- `GET /projects/owned` moved to `GET /projects?owned=true`
- `GET /projects/starred` moved to `GET /projects?starred=true`
- `GET /projects` returns all projects visible to current user, even if the user is not a member [!9674](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9674)
- - To get projects the user is a member of, use `/projects?membership=true`
+ - To get projects the user is a member of, use `GET /projects?membership=true`
- Return pagination headers for all endpoints that return an array [!8606](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8606)
- Added `POST /environments/:environment_id/stop` to stop an environment [!8808](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8808)
-- Removed `DELETE projects/:id/deploy_keys/:key_id/disable`. Use `DELETE projects/:id/deploy_keys/:key_id` instead [!9366](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9366)
+- Removed `DELETE /projects/:id/deploy_keys/:key_id/disable`. Use `DELETE /projects/:id/deploy_keys/:key_id` instead [!9366](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9366)
- Moved `PUT /users/:id/(block|unblock)` to `POST /users/:id/(block|unblock)` [!9371](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9371)
-- Make subscription API more RESTful. Use `post ":project_id/:subscribable_type/:subscribable_id/subscribe"` to subscribe and `post ":project_id/:subscribable_type/:subscribable_id/unsubscribe"` to unsubscribe from a resource. [!9325](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9325)
-- Labels filter on `projects/:id/issues` and `/issues` now matches only issues containing all labels (i.e.: Logical AND, not OR) [!8849](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8849)
+- Make subscription API more RESTful. Use `POST /projects/:id/:subscribable_type/:subscribable_id/subscribe` to subscribe and `POST /projects/:id/:subscribable_type/:subscribable_id/unsubscribe` to unsubscribe from a resource. [!9325](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9325)
+- Labels filter on `GET /projects/:id/issues` and `GET /issues` now matches only issues containing all labels (i.e.: Logical AND, not OR) [!8849](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8849)
- Renamed param `branch_name` to `branch` on the following endpoints [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936)
- - POST `:id/repository/branches`
- - POST `:id/repository/commits`
- - POST/PUT/DELETE `:id/repository/files`
-- Renamed `merge when build succeeds` to merge `when pipeline succeeds parameters` on the following endpoints: [!9335](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/)
- - PUT `projects/:id/merge_requests/:merge_request_id/merge`
- - POST `projects/:id/merge_requests/:merge_request_id/cancel_merge_when_pipeline_succeeds`
- - POST `projects`
- - POST `projects/user/:user_id`
- - PUT `projects/:id`
-- Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936)
+ - `POST /projects/:id/repository/branches`
+ - `POST /projects/:id/repository/commits`
+ - `POST/PUT/DELETE :id/repository/files`
+- Renamed the `merge_when_build_succeeds` parameter to `merge_when_pipeline_succeeds` on the following endpoints: [!9335](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/)
+ - `PUT /projects/:id/merge_requests/:merge_request_id/merge`
+ - `POST /projects/:id/merge_requests/:merge_request_id/cancel_merge_when_pipeline_succeeds`
+ - `POST /projects`
+ - `POST /projects/user/:user_id`
+ - `PUT /projects/:id`
+- Renamed `branch_name` to `branch` on `DELETE /projects/:id/repository/branches/:branch` response [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936)
- Remove `public` param from create and edit actions of projects [!8736](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8736)
- Remove `subscribed` field from responses returning list of issues or merge
requests. Fetch individual issues or merge requests to obtain the value
@@ -62,21 +62,21 @@ Below are the changes made between V3 and V4.
- Notes do not return deprecated field `upvote` and `downvote` [!9384](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9384)
- Return HTTP status code `400` for all validation errors when creating or updating a member instead of sometimes `422` error. [!9523](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9523)
- Remove `GET /groups/owned`. Use `GET /groups?owned=true` instead [!9505](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9505)
-- Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449)
-- `projects/:id/milestones?iid[]=x&iid[]=y` array filter has been renamed to `iids` [!9096](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9096)
+- Return 202 with JSON body on async removals on V4 API (`DELETE /projects/:id/repository/merged_branches` and `DELETE /projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449)
+- `GET /projects/:id/milestones?iid[]=x&iid[]=y` array filter has been renamed to `iids` [!9096](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9096)
- Return basic info about pipeline in `GET /projects/:id/pipelines` [!8875](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8875)
- Renamed all `build` references to `job` [!9463](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9463)
-- Drop GET '/projects/:id/repository/commits/:sha/jobs' [!9463](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9463)
+- Drop `GET /projects/:id/repository/commits/:sha/jobs` [!9463](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9463)
- Rename Build Triggers to be Pipeline Triggers API [!9713](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9713)
- `POST /projects/:id/trigger/builds` to `POST /projects/:id/trigger/pipeline`
- Require description when creating a new trigger `POST /projects/:id/triggers`
- Simplify project payload exposed on Environment endpoints [!9675](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9675)
- API uses merge request `IID`s (internal ID, as in the web UI) rather than `ID`s. This affects the merge requests, award emoji, todos, and time tracking APIs. [!9530](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9530)
- API uses issue `IID`s (internal ID, as in the web UI) rather than `ID`s. This affects the issues, award emoji, todos, and time tracking APIs. [!9530](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9530)
-- Change initial page from `0` to `1` on `GET projects/:id/repository/commits` (like on the rest of the API) [!9679] (https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9679)
-- Return correct `Link` header data for `GET projects/:id/repository/commits` [!9679] (https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9679)
+- Change initial page from `0` to `1` on `GET /projects/:id/repository/commits` (like on the rest of the API) [!9679] (https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9679)
+- Return correct `Link` header data for `GET /projects/:id/repository/commits` [!9679] (https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9679)
- Update endpoints for repository files [!9637](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9637)
- - Moved `/projects/:id/repository/files?file_path=:file_path` to `/projects/:id/repository/files/:file_path` (`:file_path` should be URL-encoded)
- - `/projects/:id/repository/blobs/:sha` now returns JSON attributes for the blob identified by `:sha`, instead of finding the commit identified by `:sha` and returning the raw content of the blob in that commit identified by the required `?filepath=:filepath`
- - Moved `/projects/:id/repository/commits/:sha/blob?file_path=:file_path` and `/projects/:id/repository/blobs/:sha?file_path=:file_path` to `/projects/:id/repository/files/:file_path/raw?ref=:sha`
- - `/projects/:id/repository/tree` parameter `ref_name` has been renamed to `ref` for consistency
+ - Moved `GET /projects/:id/repository/files?file_path=:file_path` to `GET /projects/:id/repository/files/:file_path` (`:file_path` should be URL-encoded)
+ - `GET /projects/:id/repository/blobs/:sha` now returns JSON attributes for the blob identified by `:sha`, instead of finding the commit identified by `:sha` and returning the raw content of the blob in that commit identified by the required `?filepath=:filepath`
+ - Moved `GET /projects/:id/repository/commits/:sha/blob?file_path=:file_path` and `GET /projects/:id/repository/blobs/:sha?file_path=:file_path` to `GET /projects/:id/repository/files/:file_path/raw?ref=:sha`
+ - `GET /projects/:id/repository/tree` parameter `ref_name` has been renamed to `ref` for consistency
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 8620984d40d..b3c9fe275c4 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -298,14 +298,14 @@ could look like:
- docker:dind
stage: build
script:
- - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com
+ - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com
- docker build -t registry.example.com/group/project:latest .
- docker push registry.example.com/group/project:latest
```
You have to use the special `gitlab-ci-token` user created for you in order to
push to the Registry connected to your project. Its password is provided in the
-`$CI_BUILD_TOKEN` variable. This allows you to automate building and deployment
+`$CI_JOB_TOKEN` variable. This allows you to automate building and deployment
of your Docker images.
You can also make use of [other variables](../variables/README.md) to avoid hardcoding:
@@ -315,10 +315,10 @@ services:
- docker:dind
variables:
- IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_BUILD_REF_NAME
+ IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME
before_script:
- - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
+ - docker login -u gitlab-ci-token -p $CI_COMMIT_TOKEN $CI_REGISTRY
build:
stage: build
@@ -328,7 +328,7 @@ build:
```
Here, `$CI_REGISTRY_IMAGE` would be resolved to the address of the registry tied
-to this project, and `$CI_BUILD_REF_NAME` would be resolved to the branch or
+to this project, and `$CI_COMMIT_REF_NAME` would be resolved to the branch or
tag name for this particular job. We also declare our own variable, `$IMAGE_TAG`,
combining the two to save us some typing in the `script` section.
@@ -350,11 +350,11 @@ stages:
- deploy
variables:
- CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project:$CI_BUILD_REF_NAME
+ CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project:$CI_COMMIT_REF_NAME
CONTAINER_RELEASE_IMAGE: registry.example.com/my-group/my-project:latest
before_script:
- - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.example.com
+ - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com
build:
stage: build
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 3c31ba45d3d..b28f3e13eae 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -263,7 +263,7 @@ 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`!
+session - and even a multiplexer like `screen` or `tmux`!
>**Note:**
Container-based deployments often lack basic tools (like an editor), and may
@@ -295,7 +295,7 @@ deploy_review:
script:
- echo "Deploy a review app"
environment:
- name: review/$CI_BUILD_REF_NAME
+ name: review/$CI_COMMIT_REF_NAME
url: https://$CI_ENVIRONMENT_SLUG.example.com
only:
- branches
@@ -306,22 +306,22 @@ deploy_review:
Let's break it down in pieces. The job's name is `deploy_review` and it runs
on the `deploy` stage. The `script` at this point is fictional, you'd have to
use your own based on your deployment. Then, we set the `environment` with the
-`environment:name` being `review/$CI_BUILD_REF_NAME`. Now that's an interesting
+`environment:name` being `review/$CI_COMMIT_REF_NAME`. Now that's an interesting
one. Since the [environment name][env-name] can contain slashes (`/`), we can
use this pattern to distinguish between dynamic environments and the regular
ones.
-So, the first part is `review`, followed by a `/` and then `$CI_BUILD_REF_NAME`
-which takes the value of the branch name. Since `$CI_BUILD_REF_NAME` itself may
+So, the first part is `review`, followed by a `/` and then `$CI_COMMIT_REF_NAME`
+which takes the value of the branch name. Since `$CI_COMMIT_REF_NAME` itself may
also contain `/`, or other characters that would be invalid in a domain name or
URL, we use `$CI_ENVIRONMENT_SLUG` in the `environment:url` so that the
environment can get a specific and distinct URL for each branch. In this case,
-given a `$CI_BUILD_REF_NAME` of `100-Do-The-Thing`, the URL will be something
+given a `$CI_COMMIT_REF_NAME` of `100-Do-The-Thing`, the URL will be something
like `https://100-do-the-4f99a2.example.com`. Again, the way you set up
the web server to serve these requests is based on your setup.
-You could also use `$CI_BUILD_REF_SLUG` in `environment:url`, e.g.:
-`https://$CI_BUILD_REF_SLUG.example.com`. We use `$CI_ENVIRONMENT_SLUG`
+You could also use `$CI_COMMIT_REF_SLUG` in `environment:url`, e.g.:
+`https://$CI_COMMIT_REF_SLUG.example.com`. We use `$CI_ENVIRONMENT_SLUG`
here because it is guaranteed to be unique, but if you're using a workflow like
[GitLab Flow][gitlab-flow], collisions are very unlikely, and you may prefer
environment names to be more closely based on the branch name - the example
@@ -356,7 +356,7 @@ deploy_review:
script:
- echo "Deploy a review app"
environment:
- name: review/$CI_BUILD_REF_NAME
+ name: review/$CI_COMMIT_REF_NAME
url: https://$CI_ENVIRONMENT_SLUG.example.com
only:
- branches
@@ -387,16 +387,16 @@ deploy_prod:
A more realistic example would include copying files to a location where a
webserver (NGINX) could then read and serve. The example below will copy the
-`public` directory to `/srv/nginx/$CI_BUILD_REF_SLUG/public`:
+`public` directory to `/srv/nginx/$CI_COMMIT_REF_SLUG/public`:
```yaml
review_app:
stage: deploy
script:
- - rsync -av --delete public /srv/nginx/$CI_BUILD_REF_SLUG
+ - rsync -av --delete public /srv/nginx/$CI_COMMIT_REF_SLUG
environment:
- name: review/$CI_BUILD_REF_NAME
- url: https://$CI_BUILD_REF_SLUG.example.com
+ name: review/$CI_COMMIT_REF_NAME
+ url: https://$CI_COMMIT_REF_SLUG.example.com
```
It is assumed that the user has already setup NGINX and GitLab Runner in the
@@ -526,7 +526,7 @@ deploy_review:
script:
- echo "Deploy a review app"
environment:
- name: review/$CI_BUILD_REF_NAME
+ name: review/$CI_COMMIT_REF_NAME
url: https://$CI_ENVIRONMENT_SLUG.example.com
on_stop: stop_review
only:
@@ -542,7 +542,7 @@ stop_review:
- echo "Remove review app"
when: manual
environment:
- name: review/$CI_BUILD_REF_NAME
+ name: review/$CI_COMMIT_REF_NAME
action: stop
```
@@ -568,13 +568,13 @@ You can read more in the [`.gitlab-ci.yml` reference][onstop].
As we've seen in the [dynamic environments](#dynamic-environments), you can
prepend their name with a word, then followed by a `/` and finally the branch
-name which is automatically defined by the `CI_BUILD_REF_NAME` variable.
+name which is automatically defined by the `CI_COMMIT_REF_NAME` variable.
In short, environments that are named like `type/foo` are presented under a
group named `type`.
-In our minimal example, we name the environments `review/$CI_BUILD_REF_NAME`
-where `$CI_BUILD_REF_NAME` is the branch name:
+In our minimal example, we name the environments `review/$CI_COMMIT_REF_NAME`
+where `$CI_COMMIT_REF_NAME` is the branch name:
```yaml
deploy_review:
@@ -582,7 +582,7 @@ deploy_review:
script:
- echo "Deploy a review app"
environment:
- name: review/$CI_BUILD_REF_NAME
+ name: review/$CI_COMMIT_REF_NAME
```
In that case, if you visit the Environments page, and provided the branches
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 76e86f3e3c3..30f209f80eb 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -207,15 +207,6 @@ you expected.
You are also able to view the status of any commit in the various pages in
GitLab, such as **Commits** and **Merge requests**.
-## Enabling build emails
-
-If you want to receive e-mail notifications about the result status of the
-jobs, you should explicitly enable the **Builds Emails** service under your
-project's settings.
-
-For more information read the
-[Builds emails service documentation](../../user/project/integrations/builds_emails.md).
-
## Examples
Visit the [examples README][examples] to see a list of examples using GitLab
diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md
index c679ea4e298..28c484ddbe6 100644
--- a/doc/ci/review_apps/index.md
+++ b/doc/ci/review_apps/index.md
@@ -80,7 +80,7 @@ The process of adding Review Apps in your workflow would look like:
1. [Install][install-runner] and [configure][conf-runner] a Runner that does
the deployment.
1. Set up a job in `.gitlab-ci.yml` that uses the predefined
- [predefined CI environment variable][variables] `${CI_BUILD_REF_NAME}` to
+ [predefined CI environment variable][variables] `${CI_COMMIT_REF_NAME}` to
create dynamic environments and restrict it to run only on branches.
1. Optionally set a job that [manually stops][manual-env] the Review Apps.
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index d00faaadc8b..befaa06e918 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -38,6 +38,15 @@ following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY`
and in the **Value** field paste the content of your _private_ key that you
created earlier.
+It is also good practice to check the server's own public key to make sure you
+are not being targeted by a man-in-the-middle attack. To do this, add another
+variable named `SSH_SERVER_HOSTKEYS`. To find out the hostkeys of your server, run
+the `ssh-keyscan YOUR_SERVER` command from a trusted network (ideally, from the
+server itself), and paste its output into the `SSH_SERVER_HOSTKEY` variable. If
+you need to connect to multiple servers, concatenate all the server public keys
+that you collected into the **Value** of the variable. There must be one key per
+line.
+
Next you need to modify your `.gitlab-ci.yml` with a `before_script` action.
Add it to the top:
@@ -59,6 +68,11 @@ before_script:
# you will overwrite your user's SSH config.
- mkdir -p ~/.ssh
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
+ # In order to properly check the server's host key, assuming you created the
+ # SSH_SERVER_HOSTKEYS variable previously, uncomment the following two lines
+ # instead.
+ # - mkdir -p ~/.ssh
+ # - '[[ -f /.dockerenv ]] && echo "$SSH_SERVER_HOSTKEYS" > ~/.ssh/known_hosts'
```
As a final step, add the _public_ key from the one you created earlier to the
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 03e6b5303c5..4e9094cb0f1 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -236,18 +236,18 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach
++ CI=true
++ export CI_DEBUG_TRACE=false
++ CI_DEBUG_TRACE=false
-++ export CI_BUILD_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ CI_BUILD_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ export CI_BUILD_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ CI_BUILD_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ export CI_BUILD_REF_NAME=master
-++ CI_BUILD_REF_NAME=master
-++ export CI_BUILD_ID=7046507
-++ CI_BUILD_ID=7046507
-++ export CI_BUILD_REPO=https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@example.com/gitlab-examples/ci-debug-trace.git
-++ CI_BUILD_REPO=https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@example.com/gitlab-examples/ci-debug-trace.git
-++ export CI_BUILD_TOKEN=xxxxxxxxxxxxxxxxxxxx
-++ CI_BUILD_TOKEN=xxxxxxxxxxxxxxxxxxxx
+++ export CI_COMMIT_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ CI_COMMIT_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ export CI_COMMIT_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ CI_COMMIT_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ export CI_COMMIT_REF_NAME=master
+++ CI_COMMIT_REF_NAME=master
+++ export CI_JOB_ID=7046507
+++ CI_JOB_ID=7046507
+++ export CI_REPOSITORY_URL=https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@example.com/gitlab-examples/ci-debug-trace.git
+++ CI_REPOSITORY_URL=https://gitlab-ci-token:xxxxxxxxxxxxxxxxxxxx@example.com/gitlab-examples/ci-debug-trace.git
+++ export CI_JOB_TOKEN=xxxxxxxxxxxxxxxxxxxx
+++ CI_JOB_TOKEN=xxxxxxxxxxxxxxxxxxxx
++ export CI_PROJECT_ID=1796893
++ CI_PROJECT_ID=1796893
++ export CI_PROJECT_DIR=/builds/gitlab-examples/ci-debug-trace
@@ -266,20 +266,20 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach
++ CI=true
++ export GITLAB_CI=true
++ GITLAB_CI=true
-++ export CI_BUILD_ID=7046507
-++ CI_BUILD_ID=7046507
-++ export CI_BUILD_TOKEN=xxxxxxxxxxxxxxxxxxxx
-++ CI_BUILD_TOKEN=xxxxxxxxxxxxxxxxxxxx
-++ export CI_BUILD_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ CI_BUILD_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ export CI_BUILD_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ CI_BUILD_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
-++ export CI_BUILD_REF_NAME=master
-++ CI_BUILD_REF_NAME=master
-++ export CI_BUILD_NAME=debug_trace
-++ CI_BUILD_NAME=debug_trace
-++ export CI_BUILD_STAGE=test
-++ CI_BUILD_STAGE=test
+++ export CI_JOB_ID=7046507
+++ CI_JOB_ID=7046507
+++ export CI_JOB_TOKEN=xxxxxxxxxxxxxxxxxxxx
+++ CI_JOB_TOKEN=xxxxxxxxxxxxxxxxxxxx
+++ export CI_COMMIT_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ CI_COMMIT_REF=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ export CI_COMMIT_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ CI_COMMIT_BEFORE_SHA=dd648b2e48ce6518303b0bb580b2ee32fadaf045
+++ export CI_COMMIT_REF_NAME=master
+++ CI_COMMIT_REF_NAME=master
+++ export CI_COMMIT_NAME=debug_trace
+++ CI_JOB_NAME=debug_trace
+++ export CI_JOB_STAGE=test
+++ CI_JOB_STAGE=test
++ export CI_SERVER_NAME=GitLab
++ CI_SERVER_NAME=GitLab
++ export CI_SERVER_VERSION=8.14.3-ee
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 49fa8761e5e..ad3ebd144df 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -153,7 +153,7 @@ thus allowing to fine tune them. Variables can be also defined on a
[job level](#job-variables).
Except for the user defined variables, there are also the ones set up by the
-Runner itself. One example would be `CI_BUILD_REF_NAME` which has the value of
+Runner itself. One example would be `CI_COMMIT_REF_NAME` which has the value of
the branch or tag name for which project is built. Apart from the variables
you can set in `.gitlab-ci.yml`, there are also the so called secret variables
which can be set in GitLab's UI.
@@ -252,7 +252,7 @@ To enable per-job caching:
```yaml
cache:
- key: "$CI_BUILD_NAME"
+ key: "$CI_JOB_NAME"
untracked: true
```
@@ -260,7 +260,7 @@ To enable per-branch caching:
```yaml
cache:
- key: "$CI_BUILD_REF_NAME"
+ key: "$CI_COMMIT_REF_NAME"
untracked: true
```
@@ -268,7 +268,7 @@ To enable per-job and per-branch caching:
```yaml
cache:
- key: "$CI_BUILD_NAME/$CI_BUILD_REF_NAME"
+ key: "$CI_JOB_NAME/$CI_COMMIT_REF_NAME"
untracked: true
```
@@ -276,7 +276,7 @@ To enable per-branch and per-stage caching:
```yaml
cache:
- key: "$CI_BUILD_STAGE/$CI_BUILD_REF_NAME"
+ key: "$CI_JOB_STAGE/$CI_COMMIT_REF_NAME"
untracked: true
```
@@ -285,7 +285,7 @@ If you use **Windows Batch** to run your shell scripts you need to replace
```yaml
cache:
- key: "%CI_BUILD_STAGE%/%CI_BUILD_REF_NAME%"
+ key: "%CI_JOB_STAGE%/%CI_COMMIT_REF_NAME%"
untracked: true
```
@@ -739,12 +739,12 @@ deploy as review app:
stage: deploy
script: make deploy
environment:
- name: review/$CI_BUILD_REF_NAME
+ name: review/$CI_COMMIT_REF_NAME
url: https://$CI_ENVIRONMENT_SLUG.example.com/
```
The `deploy as review app` job will be marked as deployment to dynamically
-create the `review/$CI_BUILD_REF_NAME` environment, where `$CI_BUILD_REF_NAME`
+create the `review/$CI_COMMIT_REF_NAME` environment, where `$CI_COMMIT_REF_NAME`
is an [environment variable][variables] set by the Runner. The
`$CI_ENVIRONMENT_SLUG` variable is based on the environment name, but suitable
for inclusion in URLs. In this case, if the `deploy as review app` job was run
@@ -850,7 +850,7 @@ To create an archive with a name of the current job:
```yaml
job:
artifacts:
- name: "$CI_BUILD_NAME"
+ name: "$CI_JOB_NAME"
```
To create an archive with a name of the current branch or tag including only
@@ -859,7 +859,7 @@ the files that are untracked by Git:
```yaml
job:
artifacts:
- name: "$CI_BUILD_REF_NAME"
+ name: "$CI_COMMIT_REF_NAME"
untracked: true
```
@@ -869,7 +869,7 @@ tag including only the files that are untracked by Git:
```yaml
job:
artifacts:
- name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}"
+ name: "${CI_JOB_NAME}_${CI_COMMIT_REF_NAME}"
untracked: true
```
@@ -878,7 +878,7 @@ To create an archive with a name of the current [stage](#stages) and branch name
```yaml
job:
artifacts:
- name: "${CI_BUILD_STAGE}_${CI_BUILD_REF_NAME}"
+ name: "${CI_JOB_STAGE}_${CI_COMMIT_REF_NAME}"
untracked: true
```
@@ -890,7 +890,7 @@ If you use **Windows Batch** to run your shell scripts you need to replace
```yaml
job:
artifacts:
- name: "%CI_BUILD_STAGE%_%CI_BUILD_REF_NAME%"
+ name: "%CI_JOB_STAGE%_%CI_COMMIT_REF_NAME%"
untracked: true
```
diff --git a/doc/development/changelog.md b/doc/development/changelog.md
index ff9a4fc4fec..ce39a379a0e 100644
--- a/doc/development/changelog.md
+++ b/doc/development/changelog.md
@@ -51,14 +51,34 @@ making it both concise and descriptive, err on the side of descriptive.
- **Bad:** Go to a project order.
- **Good:** Show a user's starred projects at the top of the "Go to project"
dropdown.
+
+The first example provides no context of where the change was made, or why, or
+how it benefits the user.
+
- **Bad:** Copy [some text] to clipboard.
- **Good:** Update the "Copy to clipboard" tooltip to indicate what's being
copied.
+
+Again, the first example is too vague and provides no context.
+
- **Bad:** Fixes and Improves CSS and HTML problems in mini pipeline graph and
builds dropdown.
- **Good:** Fix tooltips and hover states in mini pipeline graph and builds
dropdown.
+The first example is too focused on implementation details. The user doesn't
+care that we changed CSS and HTML, they care about the _end result_ of those
+changes.
+
+- **Bad:** Strip out `nil`s in the Array of Commit objects returned from
+ `find_commits_by_message_with_elastic`
+- **Good:** Fix 500 errors caused by elasticsearch results referencing
+ garbage-collected commits
+
+The first example focuses on _how_ we fixed something, not on _what_ it fixes.
+The rewritten version clearly describes the _end benefit_ to the user (fewer 500
+errors), and _when_ (searching commits with ElasticSearch).
+
Use your best judgement and try to put yourself in the mindset of someone
reading the compiled changelog. Does this entry add value? Does it offer context
about _where_ and _why_ the change was made?
diff --git a/doc/development/frontend.md b/doc/development/frontend.md
index e7add17fe2d..50105a486d0 100644
--- a/doc/development/frontend.md
+++ b/doc/development/frontend.md
@@ -32,6 +32,28 @@ You can find the Frontend Architecture experts on the [team page][team-page].
You can find documentation about the desired architecture for a new feature built with Vue.js in [here][vue-section].
+### Realtime
+
+When writing code for realtime features we have to keep a couple of things in mind:
+1. Do not overload the server with requests.
+1. It should feel realtime.
+
+Thus, we must strike a balance between sending requests and the feeling of realtime.
+Use the following rules when creating realtime solutions.
+
+1. The server will tell you how much to poll by sending `Poll-Interval` in the header.
+Use that as your polling interval. This way it is easy for system administrators to change the
+polling rate.
+A `Poll-Interval: -1` means you should disable polling, and this must be implemented.
+1. A response with HTTP status `4XX` or `5XX` should disable polling as well.
+1. Use a common library for polling.
+1. Poll on active tabs only. Use a common library to find out which tab currently has eyes on it.
+Please use [Focus](https://gitlab.com/andrewn/focus). Specifically [Eyeballs Detector](https://gitlab.com/andrewn/focus/blob/master/lib/eyeballs-detector.js).
+1. Use regular polling intervals, do not use backoff polling, or jitter, as the interval will be
+controlled by the server.
+1. The backend code will most likely be using etags. You do not and should not check for status
+`304 Not Modified`. The browser will transform it for you.
+
### Vue
For more complex frontend features, we recommend using Vue.js. It shares
diff --git a/doc/development/polling.md b/doc/development/polling.md
index a086aca6697..a7f2962acf0 100644
--- a/doc/development/polling.md
+++ b/doc/development/polling.md
@@ -27,13 +27,13 @@ Instead you should use polling mechanism with ETag caching in Redis.
1. When a client makes a request we set the `ETag` response header to the value
from Redis.
1. The client caches the response (client-side caching) and sends the ETag as
- the `If-None-Modified` header with every subsequent request for the same
+ the `If-None-Match` header with every subsequent request for the same
resource.
-1. If the `If-None-Modified` header matches the current value in Redis we know
+1. If the `If-None-Match` header matches the current value in Redis we know
that the resource did not change so we can send 304 response immediately,
without querying the database at all. The client's browser will use the
cached response.
-1. If the `If-None-Modified` header does not match the current value in Redis
+1. If the `If-None-Match` header does not match the current value in Redis
we have to generate a new response, because the resource changed.
For more information see:
diff --git a/doc/development/testing.md b/doc/development/testing.md
index 9b545d7f0f1..5ac7b8dadeb 100644
--- a/doc/development/testing.md
+++ b/doc/development/testing.md
@@ -35,8 +35,8 @@ GitLab uses [Karma] to run its [Jasmine] JavaScript specs. They can be run on
the command line via `bundle exec karma`.
- JavaScript tests live in `spec/javascripts/`, matching the folder structure of
- `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.es6` has a corresponding
- `spec/javascripts/behaviors/autosize_spec.js.es6` file.
+ `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js` has a corresponding
+ `spec/javascripts/behaviors/autosize_spec.js` file.
- Haml fixtures required for JavaScript tests live in
`spec/javascripts/fixtures`. They should contain the bare minimum amount of
markup necessary for the test.
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 3f90597ec80..7b586138f42 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -108,7 +108,7 @@ use the CI features.
## Unicorn Workers
-It's possible to increase the amount of unicorn workers and this will usually help for to reduce the response time of the applications and increase the ability to handle parallel requests.
+It's possible to increase the amount of unicorn workers and this will usually help to reduce the response time of the applications and increase the ability to handle parallel requests.
For most instances we recommend using: CPU cores + 1 = unicorn workers.
So for a machine with 2 cores, 3 unicorn workers is ideal.
@@ -148,8 +148,18 @@ Sidekiq processes the background jobs with a multithreaded process.
This process starts with the entire Rails stack (200MB+) but it can grow over time due to memory leaks.
On a very active server (10,000 active users) the Sidekiq process can use 1GB+ of memory.
+## Prometheus and its exporters
+
+As of Omnibus GitLab 9.0, [Prometheus](https://prometheus.io) and its related
+exporters are enabled by default, to enable easy and in depth monitoring of
+GitLab. Approximately 200MB of memory will be consumed by these processes, with
+default settings.
+
+If you would like to disable Prometheus and it's exporters or read more information
+about it, check the [Prometheus documentation](../administration/monitoring/prometheus/index.md).
+
## Supported web browsers
We support the current and the previous major release of Firefox, Chrome/Chromium, Safari and Microsoft browsers (Microsoft Edge and Internet Explorer 11).
-Each time a new browser version is released, we begin supporting that version and stop supporting the third most recent version.
+Each time a new browser version is released, we begin supporting that version and stop supporting the third most recent version. \ No newline at end of file
diff --git a/doc/project_services/builds_emails.md b/doc/project_services/builds_emails.md
deleted file mode 100644
index ee54d865225..00000000000
--- a/doc/project_services/builds_emails.md
+++ /dev/null
@@ -1 +0,0 @@
-This document was moved to [user/project/integrations/builds_emails.md](../user/project/integrations/builds_emails.md).
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index 678f5199b02..cf28f1a2eca 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -170,12 +170,12 @@ Integration (CI) server. By using deploy keys, you don't have to setup a
dummy user account.
If you are a project master or owner, you can add a deploy key in the
-project settings under the section 'Deploy Keys'. Press the 'New Deploy
-Key' button and upload a public SSH key. After this, the machine that uses
+project settings under the section 'Repository'. Specify a title for the new
+deploy key and paste a public SSH key. After this, the machine that uses
the corresponding private SSH key has read-only or read-write (if enabled)
access to the project.
-You can't add the same deploy key twice with the 'New Deploy Key' option.
+You can't add the same deploy key twice using the form.
If you want to add the same key to another project, please enable it in the
list that says 'Deploy keys from projects available to you'. All the deploy
keys of all the projects you have access to are available. This project
diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md
index 75956aeb360..ed0e668d854 100644
--- a/doc/update/8.12-to-8.13.md
+++ b/doc/update/8.12-to-8.13.md
@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-13-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v3.6.6
+sudo -u git -H git checkout v3.6.7
```
### 6. Update gitlab-workhorse
diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md
index 626507c0482..b7ba970031c 100644
--- a/doc/update/8.17-to-9.0.md
+++ b/doc/update/8.17-to-9.0.md
@@ -115,11 +115,11 @@ sudo -u git -H bundle clean
# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
-# Install/update frontend asset dependencies
-sudo -u git -H npm install --production
+# Update node dependencies and recompile assets
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
-# Clean up assets and cache
-sudo -u git -H bundle exec rake gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production
+# Clean up cache
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
```
**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
diff --git a/doc/user/award_emojis.md b/doc/user/award_emojis.md
new file mode 100644
index 00000000000..acbd2a66d37
--- /dev/null
+++ b/doc/user/award_emojis.md
@@ -0,0 +1,51 @@
+# Award emoji
+
+>**Notes:**
+- First [introduced][1825] in GitLab 8.2.
+- GitLab 9.0 [introduced][ce-9570] the usage of native emojis if the platform
+ supports them and falls back to images or CSS sprites. This change greatly
+ improved the award emoji performance overall.
+
+When you're collaborating online, you get fewer opportunities for high-fives
+and thumbs-ups. Emoji can be awarded to issues, merge requests, snippets, and
+virtually everywhere where you can have a discussion.
+
+![Award emoji](img/award_emoji_select.png)
+
+Award emoji make it much easier to give and receive feedback without a long
+comment thread. Comments that are only emoji will automatically become
+award emoji.
+
+## Sort issues and merge requests on vote count
+
+> [Introduced][2871] in GitLab 8.5.
+
+You can quickly sort issues and merge requests by the number of votes they
+have received. The sort options can be found in the dropdown menu as "Most
+popular" and "Least popular".
+
+![Votes sort options](img/award_emoji_votes_sort_options.png)
+
+The total number of votes is not summed up. An issue with 18 upvotes and 5
+downvotes is considered more popular than an issue with 17 upvotes and no
+downvotes.
+
+## Award emoji for comments
+
+> [Introduced][4291] in GitLab 8.9.
+
+Award emoji can also be applied to individual comments when you want to
+celebrate an accomplishment or agree with an opinion.
+
+To add an award emoji, click the smile in the top right of the comment and pick
+an emoji from the dropdown. If you want to remove an award emoji, just click
+the emoji again and the vote will be removed.
+
+![Picking an emoji for a comment](img/award_emoji_comment_picker.png)
+
+![An award emoji has been applied to a comment](img/award_emoji_comment_awarded.png)
+
+[2871]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2781
+[1825]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1825
+[4291]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4291
+[ce-9570]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9570
diff --git a/doc/user/group/subgroups/img/create_new_group.png b/doc/user/group/subgroups/img/create_new_group.png
new file mode 100644
index 00000000000..9d011ec709a
--- /dev/null
+++ b/doc/user/group/subgroups/img/create_new_group.png
Binary files differ
diff --git a/doc/user/group/subgroups/img/create_subgroup_button.png b/doc/user/group/subgroups/img/create_subgroup_button.png
new file mode 100644
index 00000000000..000b54c2855
--- /dev/null
+++ b/doc/user/group/subgroups/img/create_subgroup_button.png
Binary files differ
diff --git a/doc/user/group/subgroups/img/group_members.png b/doc/user/group/subgroups/img/group_members.png
new file mode 100644
index 00000000000..b95fe6263bf
--- /dev/null
+++ b/doc/user/group/subgroups/img/group_members.png
Binary files differ
diff --git a/doc/user/group/subgroups/img/mention_subgroups.png b/doc/user/group/subgroups/img/mention_subgroups.png
new file mode 100644
index 00000000000..8e6bed0111b
--- /dev/null
+++ b/doc/user/group/subgroups/img/mention_subgroups.png
Binary files differ
diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md
new file mode 100644
index 00000000000..ce5da07c61a
--- /dev/null
+++ b/doc/user/group/subgroups/index.md
@@ -0,0 +1,164 @@
+# Subgroups
+
+> [Introduced][ce-2772] in GitLab 9.0.
+
+With subgroups (aka nested groups or hierarchical groups) you can have
+up to 20 levels of nested groups, which among other things can help you to:
+
+- **Separate internal / external organizations.** Since every group
+ can have its own visibility level, you are able to host groups for different
+ purposes under the same umbrella.
+- **Organize large projects.** For large projects, subgroups makes it
+ potentially easier to separate permissions on parts of the source code.
+- **Make it easier to manage people and control visibility.** Give people
+ different [permissions][] depending on their group [membership](#membership).
+
+## Overview
+
+A group can have many subgroups inside it, and at the same time a group can have
+only 1 parent group. It resembles a directory behavior or a nested items list:
+
+- Group 1
+ - Group 1.1
+ - Group 1.2
+ - Group 1.2.1
+ - Group 1.2.2
+ - Group 1.2.2.1
+
+In a real world example, imagine maintaining a GNU/Linux distribution with the
+first group being the name of the distro and subsequent groups split like:
+
+- Organization Group - GNU/Linux distro
+ - Category Subgroup - Packages
+ - (project) Package01
+ - (project) Package02
+ - Category Subgroup - Software
+ - (project) Core
+ - (project) CLI
+ - (project) Android app
+ - (project) iOS app
+ - Category Subgroup - Infra tools
+ - (project) Ansible playbooks
+
+Another example of GitLab as a company would be the following:
+
+- Organization Group - GitLab
+ - Category Subroup - Marketing
+ - (project) Design
+ - (project) General
+ - Category Subgroup - Software
+ - (project) GitLab CE
+ - (project) GitLab EE
+ - (project) Omnibus GitLab
+ - (project) GitLab Runner
+ - (project) GitLab Pages daemon
+ - Category Subgroup - Infra tools
+ - (project) Chef cookbooks
+ - Category Subgroup - Executive team
+
+---
+
+The maximum nested groups a group can have, including the first one in the
+hierarchy, is 21.
+
+Things like transferring or importing a project inside nested groups, work like
+when performing these actions the traditional way with the `group/project`
+structure.
+
+## Creating a subgroup
+
+>**Notes:**
+- You need to be an Owner of a group in order to be able to create
+ a subgroup. For more information check the [permissions table][permissions].
+- For a list of words that are not allowed to be used as group names see the
+ [`namespace_validator.rb` file][reserved] under the `RESERVED` and
+ `WILDCARD_ROUTES` lists.
+
+To create a subgroup:
+
+1. In the group's dashboard go to the **Subgroups** page and click **Create subgroup**.
+
+ ![Subgroups page](img/create_subgroup_button.png)
+
+1. Create a new group like you would normally do. Notice that the parent group
+ namespace is fixed under **Group path**. The visibility level can differ from
+ the parent group.
+
+ ![Subgroups page](img/create_new_group.png)
+
+1. Click the **Create group** button and you will be taken to the new group's
+ dashboard page.
+
+---
+
+You can follow the same process to create any subsequent groups.
+
+## Membership
+
+When you add a member to a subgroup, they inherit the membership and permission
+level from the parent group. This model allows access to nested groups if you
+have membership in one of its parents.
+
+The group permissions for a member can be changed only by Owners and only on
+the **Members** page of the group the member was added.
+
+You can tell if a member has inherited the permissions from a parent group by
+looking at the group's **Members** page.
+
+![Group members page](img/group_members.png)
+
+From the image above, we can deduct the following things:
+
+- There are 5 members that have access to the group `four`
+- User0 is a Reporter and has inherited their permissions from group `one`
+ which is above the hierarchy of group `four`
+- User1 is a Developer and has inherited their permissions from group
+ `one/two` which is above the hierarchy of group `four`
+- User2 is a Developer and has inherited their permissions from group
+ `one/two/three` which is above the hierarchy of group `four`
+- For User3 there is no indication of a parent group, therefore they belong to
+ group `four`, the one we're inspecting
+- Administrator is the Owner and member of **all** subgroups and for that reason,
+ same as User3, there is no indication of an ancestor group
+
+### Overriding the ancestor group membership
+
+>**Note:**
+You need to be an Owner of a group in order to be able to add members to it.
+
+To override a user's membership of an ancestor group (the first group they were
+added to), simply add the user in the new subgroup again, but with different
+permissions.
+
+For example, if User0 was first added to group `group-1/group-1-1` with Developer
+permissions, then they will inherit those permissions in every other subgroup
+of `group-1/group-1-1`. To give them Master access to `group-1/group-1-1/group1-1-1`,
+you would add them again in that group as Master. Removing them from that group,
+the permissions will fallback to those of the ancestor group.
+
+## Mentioning subgroups
+
+Mentioning groups (`@group`) in issues, commits and merge requests, would
+notify all members of that group. Now with subgroups, there is a more granular
+support if you want to split your group's structure. Mentioning works as before
+and you can choose the group of people to be notified.
+
+![Mentioning subgroups](img/mention_subgroups.png)
+
+## Limitations
+
+Here's a list of what you can't do with subgroups:
+
+- [GitLab Pages](../../project/pages/index.md) are not currently working for
+ projects hosted under a subgroup. That means that only projects hosted under
+ the first parent group will work.
+- Group level labels don't work in subgroups / sub projects
+- It is not possible to share a project with a group that's an ancestor of
+ the group the project is in. That means you can only share as you walk down
+ the hierarchy. For example, `group/subgroup01/project` **cannot** be shared
+ with `group`, but can be shared with `group/subgroup02` or
+ `group/subgroup01/subgroup03`.
+
+[ce-2772]: https://gitlab.com/gitlab-org/gitlab-ce/issues/2772
+[permissions]: ../../permissions.md#group
+[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/validators/namespace_validator.rb
diff --git a/doc/workflow/img/award_emoji_comment_awarded.png b/doc/user/img/award_emoji_comment_awarded.png
index 111793ebf8a..111793ebf8a 100644
--- a/doc/workflow/img/award_emoji_comment_awarded.png
+++ b/doc/user/img/award_emoji_comment_awarded.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_comment_picker.png b/doc/user/img/award_emoji_comment_picker.png
index 3ad1bab3119..3ad1bab3119 100644
--- a/doc/workflow/img/award_emoji_comment_picker.png
+++ b/doc/user/img/award_emoji_comment_picker.png
Binary files differ
diff --git a/doc/user/img/award_emoji_select.png b/doc/user/img/award_emoji_select.png
new file mode 100644
index 00000000000..496acb29eec
--- /dev/null
+++ b/doc/user/img/award_emoji_select.png
Binary files differ
diff --git a/doc/user/img/award_emoji_votes_sort_options.png b/doc/user/img/award_emoji_votes_sort_options.png
new file mode 100644
index 00000000000..dd84b7f4f64
--- /dev/null
+++ b/doc/user/img/award_emoji_votes_sort_options.png
Binary files differ
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index b49a244160a..0ea6d01411f 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -81,6 +81,7 @@ group.
|-------------------------|-------|----------|-----------|--------|-------|
| Browse group | ✓ | ✓ | ✓ | ✓ | ✓ |
| Edit group | | | | | ✓ |
+| Create subgroup | | | | | ✓ |
| Create project in group | | | | ✓ | ✓ |
| Manage group members | | | | | ✓ |
| Remove group | | | | | ✓ |
diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md
index eaa39a0c4ea..63a3d3c472e 100644
--- a/doc/user/profile/account/two_factor_authentication.md
+++ b/doc/user/profile/account/two_factor_authentication.md
@@ -215,3 +215,14 @@ you may have cases where authorization always fails because of time differences.
[Google Authenticator]: https://support.google.com/accounts/answer/1066447?hl=en
[FreeOTP]: https://freeotp.github.io/
[YubiKey]: https://www.yubico.com/products/yubikey-hardware/
+
+- The GitLab U2F implementation does _not_ work when the GitLab instance is accessed from
+multiple hostnames, or FQDNs. Each U2F registration is linked to the _current hostname_ at
+the time of registration, and cannot be used for other hostnames/FQDNs.
+
+ For example, if a user is trying to access a GitLab instance from `first.host.xyz` and `second.host.xyz`:
+
+ - The user logs in via `first.host.xyz` and registers their U2F key.
+ - The user logs out and attempts to log in via `first.host.xyz` - U2F authentication suceeds.
+ - The user logs out and attempts to log in via `second.host.xyz` - U2F authentication fails, because
+ the U2F key has only been registered on `first.host.xyz`.
diff --git a/doc/user/project/integrations/builds_emails.md b/doc/user/project/integrations/builds_emails.md
deleted file mode 100644
index f769dece242..00000000000
--- a/doc/user/project/integrations/builds_emails.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# Enabling build emails
-
-By enabling this service, you will be able to receive e-mail notifications about
-the result status of your builds.
-
-Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
-and select the **Builds emails** service to configure it.
-
-In the _Recipients_ area, provide a list of e-mails separated by comma.
-
-Check the _Add pusher_ checkbox if you want the committer to also receive
-e-mail notifications about each build's status.
-
-If you enable the _Notify only broken builds_ option, e-mail notifications will
-be sent only for failed builds.
diff --git a/doc/user/project/integrations/img/prometheus_environment_detail_with_metrics.png b/doc/user/project/integrations/img/prometheus_environment_detail_with_metrics.png
new file mode 100644
index 00000000000..1f5a44f8820
--- /dev/null
+++ b/doc/user/project/integrations/img/prometheus_environment_detail_with_metrics.png
Binary files differ
diff --git a/doc/user/project/integrations/img/prometheus_gcp_firewall_rule.png b/doc/user/project/integrations/img/prometheus_gcp_firewall_rule.png
new file mode 100644
index 00000000000..e30cba211e6
--- /dev/null
+++ b/doc/user/project/integrations/img/prometheus_gcp_firewall_rule.png
Binary files differ
diff --git a/doc/user/project/integrations/img/prometheus_gcp_node_name.png b/doc/user/project/integrations/img/prometheus_gcp_node_name.png
new file mode 100644
index 00000000000..ea289431454
--- /dev/null
+++ b/doc/user/project/integrations/img/prometheus_gcp_node_name.png
Binary files differ
diff --git a/doc/user/project/integrations/img/prometheus_service_configuration.png b/doc/user/project/integrations/img/prometheus_service_configuration.png
new file mode 100644
index 00000000000..c7dfe874817
--- /dev/null
+++ b/doc/user/project/integrations/img/prometheus_service_configuration.png
Binary files differ
diff --git a/doc/user/project/integrations/img/prometheus_yaml_deploy.png b/doc/user/project/integrations/img/prometheus_yaml_deploy.png
new file mode 100644
index 00000000000..978cd7eaa50
--- /dev/null
+++ b/doc/user/project/integrations/img/prometheus_yaml_deploy.png
Binary files differ
diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md
index cfb0931273d..3e77823a6aa 100644
--- a/doc/user/project/integrations/mattermost.md
+++ b/doc/user/project/integrations/mattermost.md
@@ -28,7 +28,6 @@ There, you will see a checkbox with the following events that can be triggered:
- Merge request
- Note
- Tag push
-- Build
- Pipeline
- Wiki page
@@ -41,7 +40,6 @@ At the end, fill in your Mattermost details:
| ----- | ----------- |
| **Webhook** | The incoming webhook URL which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo… |
| **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. |
-| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
![Mattermost configuration](img/mattermost_configuration.png)
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index a3a163a4c6b..25400633de5 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -32,7 +32,6 @@ Click on the service links to see further configuration instructions and details
| Assembla | Project Management Software (Source Commits Endpoint) |
| [Atlassian Bamboo CI](bamboo.md) | A continuous integration and build server |
| Buildkite | Continuous integration and deployments |
-| [Builds emails](builds_emails.md) | Email the builds status to a list of recipients |
| [Bugzilla](bugzilla.md) | Bugzilla issue tracker |
| Campfire | Simple web-based real-time group chat |
| Custom Issue Tracker | Custom issue tracker |
@@ -48,9 +47,11 @@ Click on the service links to see further configuration instructions and details
| [Kubernetes](kubernetes.md) | A containerized deployment service |
| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
+| Pipelines emails | Email the pipeline status to a list of recipients |
| [Slack Notifications](slack.md) | Receive event notifications in Slack |
| [Slack slash commands](slack_slash_commands.md) | Slack chat and ChatOps slash commands |
| PivotalTracker | Project Management Software (Source Commits Endpoint) |
+| [Prometheus](prometheus.md) | Monitor the performance of your deployed apps |
| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
| [Redmine](redmine.md) | Redmine issue tracker |
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
new file mode 100644
index 00000000000..676a21e85c4
--- /dev/null
+++ b/doc/user/project/integrations/prometheus.md
@@ -0,0 +1,196 @@
+# Prometheus integration
+
+> [Introduced][ce-8935] in GitLab 9.0.
+
+GitLab offers powerful integration with [Prometheus] for monitoring your apps.
+Metrics are retrieved from the configured Prometheus server, and then displayed
+within the GitLab interface.
+
+Each project can be configured with its own specific Prometheus server, see the
+[configuration](#configuration) section for more details. If you have a single
+Prometheus server which monitors all of your infrastructure, you can pre-fill
+the settings page with a default template. To configure the template, see the
+[Services templates](services_templates.md) document.
+
+## Requirements
+
+Integration with Prometheus requires the following:
+
+1. GitLab 9.0 or higher
+1. Your app must be deployed on [Kubernetes][]
+1. Prometheus must be configured to collect Kubernetes metrics
+1. Each metric must be have a label to indicate the environment
+1. GitLab must have network connectivity to the Prometheus sever
+
+There are a few steps necessary to set up integration between Prometheus and
+GitLab.
+
+## Configuring Prometheus to collect Kubernetes metrics
+
+In order for Prometheus to collect Kubernetes metrics, you first must have a
+Prometheus server up and running. You have two options here:
+
+- If you installed Omnibus GitLab inside of Kubernetes, you can simply use the
+ [bundled version of Prometheus][promgldocs]. In that case, follow the info in the
+ [Omnibus GitLab section](#configuring-omnibus-gitlab-prometheus-to-monitor-kubernetes)
+ below.
+- If you are using GitLab.com or installed GitLab outside of Kubernetes, you
+ will likely need to run a Prometheus server within the Kubernetes cluster.
+ Once installed, the easiest way to monitor Kubernetes is to simply use
+ Prometheus' support for [Kubernetes Service Discovery][prometheus-k8s-sd].
+ In that case, follow the instructions on
+ [configuring your own Prometheus server within Kubernetes](#configuring-your-own-prometheus-server-within-kubernetes).
+
+### Configuring Omnibus GitLab Prometheus to monitor Kubernetes
+
+With Omnibus GitLab running inside of Kubernetes, you can leverage the bundled
+version of Prometheus to collect the required metrics.
+
+1. Read how to configure the bundled Prometheus server in the
+ [Administration guide][gitlab-prometheus-k8s-monitor].
+1. Now that Prometheus is configured, proceed on
+ [configuring the Prometheus project service in GitLab](#configuration-in-gitlab).
+
+### Configuring your own Prometheus server within Kubernetes
+
+Setting up and configuring Prometheus within Kubernetes is quick and painless.
+The Prometheus project provides an [official Docker image][prometheus-docker-image]
+which we can use as a starting point.
+
+To get started quickly, we have provided a [sample YML file][prometheus-yml]
+that can be used as a template. This file will create a `prometheus` **Namespace**,
+**Service**, **Deployment**, and **ConfigMap** in Kubernetes. You can upload
+this file to the Kubernetes dashboard using **+ Create** at the top right.
+
+![Deploy Prometheus](img/prometheus_yaml_deploy.png)
+
+Or use `kubectl`:
+
+```bash
+kubectl apply -f path/to/prometheus.yml
+```
+
+Once deployed, you should see the Prometheus service, deployment, and
+pod start within the `prometheus` namespace. The server will begin to collect
+metrics from each Kubernetes Node in the cluster, based on the configuration
+provided in the template.
+
+Since GitLab is not running within Kubernetes, the template provides external
+network access via a `NodePort` running on `30090`. This method allows access
+to be controlled using provider firewall rules, like within Google Compute Engine.
+
+Since a `NodePort` does not automatically have firewall rules created for it,
+one will need to be created manually to allow access. In GCP/GKE, you will want
+to confirm the Node that the Prometheus pod is running on. This can be done
+either by looking at the Pod in the Kubernetes dashboard, or by running:
+
+```bash
+kubectl describe pods -n prometheus
+```
+
+Next on GKE, we need to get the `tag` of the Node or VM Instance, so we can
+create an accurate firewall rule. The easiest way to do this is to go into the
+Google Cloud Platform Compute console and select the VM instance that matches
+the name of the Node gathered from the step above. In this case, the node tag
+needed is `gke-prometheus-demo-5d5ada10-node`. Also make a note of the
+**External IP**, which will be the IP address the Prometheus server is reachable
+on.
+
+![GCP Node Detail](img/prometheus_gcp_node_name.png)
+
+Armed with the proper Node tag, the firewall rule can now be created
+specifically for this node. To create the firewall rule, open the Google Cloud
+Platform Networking console, and select **Firewall Rules**.
+
+Create a new rule:
+
+- Specify the source IP range to match your desired access list, which should
+ include your GitLab server. A sample of GitLab.com's IP address range is
+ available [in this issue][gitlab.com-ip-range], but note that GitLab.com's IPs
+ are subject to change without prior notification.
+- Allowed protocol and port should be `tcp:30090`.
+- The target tags should match the Node tag identified earlier in this step.
+
+![GCP Firewall Rule](img/prometheus_gcp_firewall_rule.png)
+
+---
+
+Now that Prometheus is configured, proceed to
+[configure the Prometheus project service in GitLab](##configuration-in-gitlab).
+
+## Configuration in GitLab
+
+The actual configuration of Prometheus integration within GitLab is very simple.
+All you will need is the DNS or IP address of the Prometheus server you'd like
+to integrate with.
+
+1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
+1. Click the **Prometheus** service
+1. Provide the base URL of the your server, for example `http://prometheus.example.com/`.
+ The **Test Settings** button can be used to confirm connectivity from GitLab
+ to the Prometheus server.
+
+![Configure Prometheus Service](img/prometheus_service_configuration.png)
+
+## Metrics and Labels
+
+GitLab retrieves performance data from two metrics, `container_cpu_usage_seconds_total`
+and `container_memory_usage_bytes`. These metrics are collected from the
+Kubernetes pods via Prometheus, and report CPU and Memory utilization of each
+container or Pod running in the cluster.
+
+In order to isolate and only display relevant metrics for a given environment
+however, GitLab needs a method to detect which pods are associated. To do that,
+GitLab will specifically request metrics that have an `environment` tag that
+matches the [$CI_ENVIRONMENT_SLUG][ci-environment-slug].
+
+If you are using [GitLab Auto-Deploy][autodeploy] and one of the methods of
+configuring Prometheus above, the `environment` will be automatically added.
+
+### GitLab Prometheus queries
+
+The queries utilized by GitLab are shown in the following table.
+
+| Metric | Query |
+| ------ | ----- |
+| Average Memory (MB) | `(sum(container_memory_usage_bytes{container_name="app",environment="$CI_ENVIRONMENT_SLUG"}) / count(container_memory_usage_bytes{container_name="app",environment="$CI_ENVIRONMENT_SLUG"})) /1024/1024` |
+| Average CPU Utilization (%) | `sum(rate(container_cpu_usage_seconds_total{container_name="app",environment="$CI_ENVIRONMENT_SLUG"}[2m])) / count(container_cpu_usage_seconds_total{container_name="app",environment="$CI_ENVIRONMENT_SLUG"}) * 100` |
+
+## Monitoring CI/CD Environments
+
+Once configured, GitLab will attempt to retrieve performance metrics for any
+environment which has had a successful deployment. If monitoring data was
+successfully retrieved, a metrics button will appear on the environment's
+detail page.
+
+![Environment Detail with Metrics](img/prometheus_environment_detail_with_metrics.png)
+
+Clicking on the metrics button will display a new page, showing up to the last
+8 hours of performance data. It may take a minute or two for data to appear
+after initial deployment.
+
+## Troubleshooting
+
+If the metrics button is not appearing, then one of a few issues may be
+occurring:
+
+- GitLab is not able to reach the Prometheus server. A test request can be sent
+ to the Prometheus server from the [Prometheus Service](#configuration-in-gitlab)
+ configuration screen.
+- No successful deployments have occurred to this environment.
+- Prometheus does not have performance data for this environment, or the metrics
+ are not labeled correctly. To test this, connect to the Prometheus server and
+ [run a query](#gitlab-prometheus-queries), replacing `$CI_ENVIRONMENT_SLUG`
+ with the name of your environment.
+
+[autodeploy]: ../../../ci/autodeploy/index.md
+[kubernetes]: https://kubernetes.io
+[prometheus-k8s-sd]: https://prometheus.io/docs/operating/configuration/#<kubernetes_sd_config>
+[prometheus]: https://prometheus.io
+[gitlab-prometheus-k8s-monitor]: ../../../administration/monitoring/prometheus/index.md#configuring-prometheus-to-monitor-kubernetes
+[prometheus-docker-image]: https://hub.docker.com/r/prom/prometheus/
+[prometheus-yml]:samples/prometheus.yml
+[gitlab.com-ip-range]: https://gitlab.com/gitlab-com/infrastructure/issues/434
+[ci-environment-slug]: https://docs.gitlab.com/ce/ci/variables/#predefined-variables-environment-variables
+[ce-8935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935
+[promgldocs]: ../../../administration/monitoring/prometheus/index.md
diff --git a/doc/user/project/integrations/samples/prometheus.yml b/doc/user/project/integrations/samples/prometheus.yml
new file mode 100644
index 00000000000..01bbcaffe1e
--- /dev/null
+++ b/doc/user/project/integrations/samples/prometheus.yml
@@ -0,0 +1,69 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: prometheus
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: prometheus
+ namespace: prometheus
+data:
+ prometheus.yml: |-
+ scrape_configs:
+ - job_name: 'kubernetes-nodes'
+ scheme: https
+ tls_config:
+ ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
+ insecure_skip_verify: true
+ bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
+ kubernetes_sd_configs:
+ - role: node
+ metric_relabel_configs:
+ - source_labels: [pod_name]
+ target_label: environment
+ regex: (.+)-.+-.+
+ replacement: $1
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: prometheus
+ namespace: prometheus
+spec:
+ selector:
+ app: prometheus
+ ports:
+ - name: prometheus
+ protocol: TCP
+ port: 9090
+ nodePort: 30090
+ type: NodePort
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+ name: prometheus
+ namespace: prometheus
+spec:
+ replicas: 1
+ template:
+ metadata:
+ labels:
+ app: prometheus
+ spec:
+ containers:
+ - name: prometheus
+ image: prom/prometheus:latest
+ args:
+ - '-config.file=/prometheus-data/prometheus.yml'
+ ports:
+ - name: prometheus
+ containerPort: 9090
+ volumeMounts:
+ - name: data-volume
+ mountPath: /prometheus-data
+ volumes:
+ - name: data-volume
+ configMap:
+ name: prometheus
diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md
index f27f9a726fc..e8b238351ca 100644
--- a/doc/user/project/integrations/slack.md
+++ b/doc/user/project/integrations/slack.md
@@ -25,7 +25,6 @@ There, you will see a checkbox with the following events that can be triggered:
- Merge request
- Note
- Tag push
-- Build
- Pipeline
- Wiki page
@@ -38,7 +37,6 @@ At the end, fill in your Slack details:
| ----- | ----------- |
| **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. |
| **Username** | Optional username which can be on messages sent to Slack. Fill this in if you want to change the username of the bot. |
-| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
After you are all done, click **Save changes** for the changes to take effect.
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index ed1e867f5fb..dbdc93a77a8 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -250,7 +250,19 @@ X-Gitlab-Event: Issue Hook
"name": "User1",
"username": "user1",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- }
+ },
+ "labels": [{
+ "id": 206,
+ "title": "API",
+ "color": "#ffffff",
+ "project_id": 14,
+ "created_at": "2013-12-03T17:15:43Z",
+ "updated_at": "2013-12-03T17:15:43Z",
+ "template": false,
+ "description": "API related issues",
+ "type": "ProjectLabel",
+ "group_id": 41
+ }]
}
```
### Comment events
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index 3199d370a58..5aa8337b75d 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -28,7 +28,7 @@ Below is a table of the definitions used for GitLab's Issue Board.
| -------------- | ------------- |
| **Issue Board** | It represents a different view for your issues. It can have multiple lists with each list consisting of issues represented by cards. |
| **List** | Each label that exists in the issue tracker can have its own dedicated list. Every list is named after the label it is based on and is represented by a column which contains all the issues associated with that label. You can think of a list like the results you get when you filter the issues by a label in your issue tracker. |
-| **Card** | Every card represents an issue and it is shown under the list for which it has a label. The information you can see on a card consists of the issue number, the issue title, the assignee and the labels associated with it. You can drag cards around from one list to another. Issues inside lists are [ordered by priority](labels.md#prioritize-labels). |
+| **Card** | Every card represents an issue and it is shown under the list for which it has a label. The information you can see on a card consists of the issue number, the issue title, the assignee and the labels associated with it. You can drag cards around from one list to another. You can re-order cards within a list. |
There are two types of lists, the ones you create based on your labels, and
one default:
@@ -45,6 +45,7 @@ In short, here's a list of actions you can take in an Issue Board:
- [Create a new list](#creating-a-new-list).
- [Delete an existing list](#deleting-a-list).
- Drag issues between lists.
+- Re-order issues in lists.
- Drag and reorder the lists themselves.
- Change issue labels on-the-fly while dragging issues between lists.
- Close an issue if you drag it to the **Done** list.
@@ -114,6 +115,13 @@ board itself.
![Remove issue from list](img/issue_boards_remove_issue.png)
+## Re-ordering an issue in a list
+
+> Introduced in GitLab 9.0.
+
+Issues can be re-ordered inside of lists. This is as simple as dragging and dropping
+an issue into the order you want.
+
## Filtering issues
You should be able to use the filters on top of your Issue Board to show only
@@ -176,7 +184,6 @@ A few things to remember:
- Clicking on the issue title inside a card will take you to that issue.
- Clicking on a label inside a card will quickly filter the entire Issue Board
and show only the issues from all lists that have that label.
-- Issues inside lists are [ordered by priority][label-priority].
- For performance and visibility reasons, each list shows the first 20 issues
by default. If you have more than 20 issues start scrolling down and the next
20 will appear.
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index cf1d9cbe69c..8ec7adad172 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -65,7 +65,7 @@ issues and merge requests assigned to each label.
> https://gitlab.com/gitlab-org/gitlab-ce/issues/18554.
Prioritized labels are like any other label, but sorted by priority. This allows
-you to sort issues and merge requests by priority.
+you to sort issues and merge requests by label priority.
To prioritize labels, navigate to your project's **Issues > Labels** and click
on the star icon next to them to put them in the priority list. Click on the
@@ -77,9 +77,13 @@ having their priority set to null.
![Prioritize labels](img/labels_prioritize.png)
-Now that you have labels prioritized, you can use the 'Priority' filter in the
-issues or merge requests tracker. Those with the highest priority label, will
-appear on top.
+Now that you have labels prioritized, you can use the 'Priority' and 'Label
+priority' filters in the issues or merge requests tracker.
+
+The 'Label priority' filter puts issues with the highest priority label on top.
+
+The 'Priority' filter sorts issues by their soonest milestone due date, then by
+label priority.
![Filter labels by priority](img/labels_filter_by_priority.png)
@@ -156,4 +160,3 @@ mouse over the label in the issue tracker or wherever else the label is
rendered.
![Label tooltips](img/labels_description_tooltip.png)
-
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index 5f631f63050..b559d132590 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -119,7 +119,7 @@ And then the users could also use it in their CI jobs all Docker related
commands to interact with GitLab Container Registry. For example:
```
-docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN registry.gitlab.com
+docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com
```
Using single token had multiple security implications:
@@ -208,7 +208,7 @@ This is how an example usage can look like:
```
test:
script:
- - docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $CI_REGISTRY
+ - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
- docker pull $CI_REGISTRY/group/other-project:latest
- docker run $CI_REGISTRY/group/other-project:latest
```
diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md
index c415d566a7c..d47a3acdbe9 100644
--- a/doc/user/project/repository/web_editor.md
+++ b/doc/user/project/repository/web_editor.md
@@ -109,12 +109,19 @@ the title of the issue and as suffix it will have its ID. Thus, the example
screenshot above will yield a branch named
`2-et-cum-et-sed-expedita-repellat-consequatur-ut-assumenda-numquam-rerum`.
+Since GitLab 9.0, when you click the `New branch` in an empty repository project, GitLab automatically creates the master branch, commits a blank `README.md` file to it and creates and redirects you to a new branch based on the issue title.
+If your [project is already configured with a deployment service][project-services-doc] (e.g. Kubernetes), GitLab takes one step further and prompts you to set up [auto deploy][auto-deploy-doc] by helping you create a `.gitlab-ci.yml` file.
+
+
After the branch is created, you can edit files in the repository to fix
the issue. When a merge request is created based on the newly created branch,
the description field will automatically display the [issue closing pattern]
`Closes #ID`, where `ID` the ID of the issue. This will close the issue once the
merge request is merged.
+[project-services-doc]: ../integrations/project_services.md
+[auto-deploy-doc]: ../../../ci/autodeploy/index.md
+
### Create a new branch from a project's dashboard
If you want to make changes to several files before creating a new merge
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index be042ddf623..7a4f9f408f1 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -34,7 +34,7 @@ with all their related data and be moved into a new GitLab instance.
| 8.10.0 | 0.1.2 |
| 8.9.5 | 0.1.1 |
| 8.9.0 | 0.1.0 |
-
+
> The table reflects what GitLab version we updated the Import/Export version at.
> For instance, 8.10.3 and 8.11 will have the same Import/Export version (0.1.3)
> and the exports between them will be compatible.
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 9e7ee47387c..6a8de51a199 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -40,3 +40,4 @@
- [Importing from SVN, GitHub, Bitbucket, etc](importing/README.md)
- [Todos](todos.md)
- [Snippets](../user/snippets.md)
+- [Subgroups](../user/group/subgroups/index.md)
diff --git a/doc/workflow/award_emoji.md b/doc/workflow/award_emoji.md
index 1df0698afd0..d74378cc564 100644
--- a/doc/workflow/award_emoji.md
+++ b/doc/workflow/award_emoji.md
@@ -1,65 +1 @@
-# Award emoji
-
->**Note:**
-[Introduced][1825] in GitLab 8.2.
-
-When you're collaborating online, you get fewer opportunities for high-fives
-and thumbs-ups. Emoji can be awarded to issues and merge requests, making
-virtual celebrations easier.
-
-![Award emoji](img/award_emoji_select.png)
-
-Award emoji make it much easier to give and receive feedback without a long
-comment thread. Comments that are only emoji will automatically become
-award emoji.
-
-## Sort issues and merge requests on vote count
-
->**Note:**
-[Introduced][2871] in GitLab 8.5.
-
-You can quickly sort issues and merge requests by the number of votes they
-have received. The sort options can be found in the dropdown menu as "Most
-popular" and "Least popular".
-
-![Votes sort options](img/award_emoji_votes_sort_options.png)
-
----
-
-Sort by most popular issues/merge requests.
-
-![Votes sort by most popular](img/award_emoji_votes_most_popular.png)
-
----
-
-Sort by least popular issues/merge requests.
-
-![Votes sort by least popular](img/award_emoji_votes_least_popular.png)
-
----
-
-The total number of votes is not summed up. An issue with 18 upvotes and 5
-downvotes is considered more popular than an issue with 17 upvotes and no
-downvotes.
-
-## Award emoji for comments
-
->**Note:**
-[Introduced][4291] in GitLab 8.9.
-
-Award emoji can also be applied to individual comments when you want to
-celebrate an accomplishment or agree with an opinion.
-
-To add an award emoji, click the smile in the top right of the comment and pick
-an emoji from the dropdown.
-
-![Picking an emoji for a comment](img/award_emoji_comment_picker.png)
-
-![An award emoji has been applied to a comment](img/award_emoji_comment_awarded.png)
-
-If you want to remove an award emoji, just click the emoji again and the vote
-will be removed.
-
-[2871]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2781
-[1825]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1825
-[4291]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4291
+This document was moved to [another location](../user/award_emojis.md).
diff --git a/doc/workflow/img/award_emoji_select.png b/doc/workflow/img/award_emoji_select.png
deleted file mode 100644
index e1b37beaf62..00000000000
--- a/doc/workflow/img/award_emoji_select.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/img/award_emoji_votes_least_popular.png b/doc/workflow/img/award_emoji_votes_least_popular.png
deleted file mode 100644
index 86ede4b0c10..00000000000
--- a/doc/workflow/img/award_emoji_votes_least_popular.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/img/award_emoji_votes_most_popular.png b/doc/workflow/img/award_emoji_votes_most_popular.png
deleted file mode 100644
index 1d3e2e57aa0..00000000000
--- a/doc/workflow/img/award_emoji_votes_most_popular.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/img/award_emoji_votes_sort_options.png b/doc/workflow/img/award_emoji_votes_sort_options.png
deleted file mode 100644
index c6dc1b939c1..00000000000
--- a/doc/workflow/img/award_emoji_votes_sort_options.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/milestones.md b/doc/workflow/milestones.md
index dff36899aec..37afe553e55 100644
--- a/doc/workflow/milestones.md
+++ b/doc/workflow/milestones.md
@@ -1,13 +1,28 @@
# Milestones
-Milestones allow you to organize issues and merge requests into a cohesive group, optionally setting a due date.
+Milestones allow you to organize issues and merge requests into a cohesive group, optionally setting a due date.
A common use is keeping track of an upcoming software version. Milestones are created per-project.
![milestone form](milestones/form.png)
## Groups and milestones
-You can create a milestone for several projects in the same group simultaneously.
+You can create a milestone for several projects in the same group simultaneously.
On the group's milestones page, you will be able to see the status of that milestone across all of the selected projects.
![group milestone form](milestones/group_form.png)
+
+## Special milestone filters
+
+In addition to the milestones that exist in the project or group, there are some
+special options available when filtering by milestone:
+
+* **No Milestone** - only show issues or merge requests without a milestone.
+* **Upcoming** - show issues or merge request that belong to the next open
+ milestone with a due date, by project. (For example: if project A has
+ milestone v1 due in three days, and project B has milestone v2 due in a week,
+ then this will show issues or merge requests from milestone v1 in project A
+ and milestone v2 in project B.)
+* **Started** - show issues or merge requests from any milestone with a start
+ date less than today. Note that this can return results from several
+ milestones in the same project.
diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb
index eb906a55a83..9f01dff776f 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -159,7 +159,11 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
end
def should_not_see_todo(title)
- expect(page).not_to have_content title
+ expect(page).not_to have_visible_content title
+ end
+
+ def have_visible_content(text)
+ have_css('*', text: text, visible: true)
end
def john_doe
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index 18e267294e4..cf75fac8ac6 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -163,7 +163,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'I see commit ci info' do
- expect(page).to have_content "Pipeline #1 for 570e7b2a pending"
+ expect(page).to have_content "Pipeline #1 pending"
end
step 'I search "submodules" commits' do
diff --git a/features/steps/project/pages.rb b/features/steps/project/pages.rb
index c80c6273807..4045955a8b9 100644
--- a/features/steps/project/pages.rb
+++ b/features/steps/project/pages.rb
@@ -53,13 +53,13 @@ class Spinach::Features::ProjectPages < Spinach::FeatureSteps
end
step 'pages are exposed on external HTTP address' do
- allow(Gitlab.config.pages).to receive(:external_http).and_return('1.1.1.1:80')
+ allow(Gitlab.config.pages).to receive(:external_http).and_return(['1.1.1.1:80'])
allow(Gitlab.config.pages).to receive(:external_https).and_return(nil)
end
step 'pages are exposed on external HTTPS address' do
- allow(Gitlab.config.pages).to receive(:external_http).and_return('1.1.1.1:80')
- allow(Gitlab.config.pages).to receive(:external_https).and_return('1.1.1.1:443')
+ allow(Gitlab.config.pages).to receive(:external_http).and_return(['1.1.1.1:80'])
+ allow(Gitlab.config.pages).to receive(:external_https).and_return(['1.1.1.1:443'])
end
step 'I should be able to add a New Domain' do
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 6845f75f22f..5c47eaf0279 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -48,7 +48,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I click link "Raw"' do
- click_link 'Raw'
+ click_link 'Open raw'
end
step 'I should see raw file content' do
@@ -356,7 +356,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I should see buttons for allowed commands' do
page.within '.content' do
- expect(page).to have_content 'Raw'
+ expect(page).to have_link 'Open raw'
expect(page).to have_content 'History'
expect(page).to have_content 'Permalink'
expect(page).not_to have_content 'Edit'
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index 789f45489eb..a5c9f0b509c 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -10,7 +10,7 @@ module API
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
- resource source_type.pluralize do
+ resource source_type.pluralize, requirements: { id: %r{[^/]+} } do
desc "Gets a list of access requests for a #{source_type}." do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::AccessRequester
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index f9e0c2c4e16..56f19f89642 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -9,13 +9,15 @@ module API
{ type: 'snippet', find_by: :id }
].freeze
- resource :projects do
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
AWARDABLES.each do |awardable_params|
awardable_string = awardable_params[:type].pluralize
awardable_id_string = "#{awardable_params[:type]}_#{awardable_params[:find_by]}"
params do
- requires :id, type: String, desc: 'The ID of a project'
requires :"#{awardable_id_string}", type: Integer, desc: "The ID of an Issue, Merge Request or Snippet"
end
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
index b6843c1b6af..5a2d7a681e3 100644
--- a/lib/api/boards.rb
+++ b/lib/api/boards.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all project boards' do
detail 'This feature was introduced in 8.13'
success Entities::Board
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 73a7e939627..f35084a582a 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -4,13 +4,12 @@ module API
class Branches < Grape::API
include PaginationParams
- before { authenticate! }
before { authorize! :download_code, user_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository branches' do
success Entities::RepoBranch
end
@@ -102,6 +101,7 @@ module API
end
post ":id/repository/branches" do
authorize_push_project
+
result = CreateBranchService.new(user_project, current_user).
execute(params[:branch], params[:ref])
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 9d9f82fdb83..827a38d33da 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -2,7 +2,10 @@ require 'mime/types'
module API
class CommitStatuses < Grape::API
- resource :projects do
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
include PaginationParams
before { authenticate! }
@@ -11,7 +14,6 @@ module API
success Entities::CommitStatus
end
params do
- requires :id, type: String, desc: 'The ID of a project'
requires :sha, type: String, desc: 'The commit hash'
optional :ref, type: String, desc: 'The ref'
optional :stage, type: String, desc: 'The stage'
@@ -37,7 +39,6 @@ module API
success Entities::CommitStatus
end
params do
- requires :id, type: String, desc: 'The ID of a project'
requires :sha, type: String, desc: 'The commit hash'
requires :state, type: String, desc: 'The state of the status',
values: %w(pending running success failed canceled)
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 42401abfe0f..66b37fd2bcc 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -10,7 +10,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository commits' do
success Entities::RepoCommit
end
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index 69e85c27a65..b888ede6fe8 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -17,7 +17,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of the project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
before { authorize_admin_project }
desc "Get a specific project's deploy keys" do
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
index 2f1ad12c38c..46b936897f6 100644
--- a/lib/api/deployments.rb
+++ b/lib/api/deployments.rb
@@ -8,7 +8,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all deployments of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Deployment
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 0a12ee72d49..5954aea8041 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -768,7 +768,7 @@ module API
end
class Dependency < Grape::Entity
- expose :id, :name
+ expose :id, :name, :token
expose :artifacts_file, using: ArtifactFile, if: ->(job, _) { job.artifacts? }
end
@@ -796,7 +796,7 @@ module API
expose :artifacts, using: Artifacts
expose :cache, using: Cache
expose :credentials, using: Credentials
- expose :depends_on_builds, as: :dependencies, using: Dependency
+ expose :dependencies, using: Dependency
end
end
end
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index ebe8c3a5b2c..945771d46f3 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all environments of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Environment
diff --git a/lib/api/files.rb b/lib/api/files.rb
index bb8f5c3076d..33fc970dc09 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -52,7 +52,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get raw file contents from the repository'
params do
requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index b862ff70b31..8f3799417e3 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -84,7 +84,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups do
+ resource :groups, requirements: { id: %r{[^/]+} } do
desc 'Update a group. Available only for users who can administrate groups.' do
success Entities::Group
end
@@ -154,7 +154,7 @@ module API
params do
requires :project_id, type: String, desc: 'The ID or path of the project'
end
- post ":id/projects/:project_id" do
+ post ":id/projects/:project_id", requirements: { project_id: /.+/ } do
authenticated_as_admin!
group = find_group!(params[:id])
project = find_project!(params[:project_id])
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
index ec2bcaed929..74848a6e144 100644
--- a/lib/api/helpers/runner.rb
+++ b/lib/api/helpers/runner.rb
@@ -41,14 +41,6 @@ module API
(Time.now - current_runner.contacted_at) >= contacted_at_max_age
end
- def job_not_found!
- if headers['User-Agent'].to_s =~ /gitlab(-ci-multi)?-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /
- no_content!
- else
- not_found!
- end
- end
-
def validate_job!(job)
not_found! unless job
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 1abe8639445..fd2674910d2 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -58,7 +58,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups do
+ resource :groups, requirements: { id: %r{[^/]+} } do
desc 'Get a list of group issues' do
success Entities::IssueBasic
end
@@ -79,7 +79,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
include TimeTrackingEndpoints
desc 'Get a list of project issues' do
@@ -91,7 +91,7 @@ module API
use :issues_params
end
get ":id/issues" do
- project = find_project(params[:id])
+ project = find_project!(params[:id])
issues = find_issues(project_id: project.id)
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 44118522abe..ffab0aafe59 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
helpers do
params :optional_scope do
optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show',
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index 59f0e7cb647..d9a3cb7bb6b 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all labels of the project' do
success Entities::Label
end
diff --git a/lib/api/members.rb b/lib/api/members.rb
index baf85e6075a..c200e46a328 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -10,7 +10,7 @@ module API
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
- resource source_type.pluralize do
+ resource source_type.pluralize, requirements: { id: %r{[^/]+} } do
desc 'Gets a list of group or project members viewable by the authenticated user.' do
success Entities::Member
end
diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb
index a59e39cca26..4b79eac2b8b 100644
--- a/lib/api/merge_request_diffs.rb
+++ b/lib/api/merge_request_diffs.rb
@@ -5,14 +5,16 @@ module API
before { authenticate! }
- resource :projects do
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a list of merge request diff versions' do
detail 'This feature was introduced in GitLab 8.12.'
success Entities::MergeRequestDiff
end
params do
- requires :id, type: String, desc: 'The ID of a project'
requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
use :pagination
end
@@ -28,7 +30,6 @@ module API
end
params do
- requires :id, type: String, desc: 'The ID of a project'
requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
requires :version_id, type: Integer, desc: 'The ID of a merge request diff version'
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 7a03955a045..5cc807d5bff 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
include TimeTrackingEndpoints
helpers do
diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb
index abd263c1dfc..e7ab82f08db 100644
--- a/lib/api/milestones.rb
+++ b/lib/api/milestones.rb
@@ -23,7 +23,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a list of project milestones' do
success Entities::Milestone
end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 3b3e45cbd06..29ceffdbd2d 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
NOTEABLE_TYPES.each do |noteable_type|
noteables_str = noteable_type.to_s.underscore.pluralize
diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb
index c5e9b3ad69b..992ea5dc24d 100644
--- a/lib/api/notification_settings.rb
+++ b/lib/api/notification_settings.rb
@@ -48,14 +48,14 @@ module API
end
%w[group project].each do |source_type|
- resource source_type.pluralize do
+ params do
+ requires :id, type: String, desc: "The #{source_type} ID"
+ end
+ resource source_type.pluralize, requirements: { id: %r{[^/]+} } do
desc "Get #{source_type} level notification level settings, defaults to Global" do
detail 'This feature was introduced in GitLab 8.12'
success Entities::NotificationSetting
end
- params do
- requires :id, type: String, desc: 'The group ID or project ID or project NAMESPACE/PROJECT_NAME'
- end
get ":id/notification_settings" do
source = find_source(source_type, params[:id])
@@ -69,7 +69,6 @@ module API
success Entities::NotificationSetting
end
params do
- requires :id, type: String, desc: 'The group ID or project ID or project NAMESPACE/PROJECT_NAME'
optional :level, type: String, desc: "The #{source_type} notification level"
NotificationSetting::EMAIL_EVENTS.each do |event|
optional event, type: Boolean, desc: 'Enable/disable this notification'
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index 0721b975ba4..754c3d85a04 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all Pipelines of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::PipelineBasic
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index 57a5f97dc7f..53791166c33 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -24,7 +24,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get project hooks' do
success Entities::ProjectHook
end
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index f57e7ea4032..cfee38a9baf 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
helpers do
def handle_project_member_errors(errors)
if errors[:project_access].any?
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 63a4cdd5954..0fbe1669d45 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -142,7 +142,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: /[^\/]+/ } do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a single project' do
success Entities::ProjectWithAccess
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 531ef5a63ea..8f16e532ecb 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
helpers do
def handle_project_member_errors(errors)
if errors[:project_access].any?
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index c700d2ef4a1..4c9db2c8716 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -47,11 +47,25 @@ module API
authenticate_runner!
Ci::Runner.find_by_token(params[:token]).destroy
end
+
+ desc 'Validates authentication credentials' do
+ http_codes [[200, 'Credentials are valid'], [403, 'Forbidden']]
+ end
+ params do
+ requires :token, type: String, desc: %q(Runner's authentication token)
+ end
+ post '/verify' do
+ authenticate_runner!
+ status 200
+ end
end
resource :jobs do
desc 'Request a job' do
success Entities::JobRequest::Response
+ http_codes [[201, 'Job was scheduled'],
+ [204, 'No job for Runner'],
+ [403, 'Forbidden']]
end
params do
requires :token, type: String, desc: %q(Runner's authentication token)
@@ -60,13 +74,13 @@ module API
end
post '/request' do
authenticate_runner!
- not_found! unless current_runner.active?
+ no_content! unless current_runner.active?
update_runner_info
if current_runner.is_runner_queue_value_latest?(params[:last_update])
header 'X-GitLab-Last-Update', params[:last_update]
Gitlab::Metrics.add_event(:build_not_found_cached)
- return job_not_found!
+ return no_content!
end
new_update = current_runner.ensure_runner_queue_value
@@ -80,7 +94,7 @@ module API
else
Gitlab::Metrics.add_event(:build_not_found)
header 'X-GitLab-Last-Update', new_update
- job_not_found!
+ no_content!
end
else
# We received build that is invalid due to concurrency conflict
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index 2e41f16f8c6..a77c876a749 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -86,7 +86,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
before { authorize_admin_project }
desc 'Get runners available for project' do
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 5aa2f5eba7b..4e0c9cb1f63 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -107,26 +107,6 @@ module API
desc: 'Enable SSL verification for communication'
}
],
- 'builds-email' => [
- {
- required: true,
- name: :recipients,
- type: String,
- desc: 'Comma-separated list of recipient email addresses'
- },
- {
- required: false,
- name: :add_pusher,
- type: Boolean,
- desc: 'Add pusher to recipients list'
- },
- {
- required: false,
- name: :notify_only_broken_jobs,
- type: Boolean,
- desc: 'Notify only broken jobs'
- }
- ],
'campfire' => [
{
required: true,
@@ -403,9 +383,9 @@ module API
},
{
required: false,
- name: :notify_only_broken_jobs,
+ name: :notify_only_broken_pipelines,
type: Boolean,
- desc: 'Notify only broken jobs'
+ desc: 'Notify only broken pipelines'
}
],
'pivotaltracker' => [
@@ -550,7 +530,6 @@ module API
BambooService,
BugzillaService,
BuildkiteService,
- BuildsEmailService,
CampfireService,
CustomIssueTrackerService,
DroneCiService,
@@ -604,7 +583,10 @@ module API
]
}.freeze
- resource :projects do
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
before { authenticate! }
before { authorize_admin_project }
@@ -692,7 +674,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc "Trigger a slash command for #{service_slug}" do
detail 'Added in GitLab 8.13'
end
diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb
index 772b5cca017..dbe54d3cd31 100644
--- a/lib/api/subscriptions.rb
+++ b/lib/api/subscriptions.rb
@@ -12,7 +12,7 @@ module API
requires :id, type: String, desc: 'The ID of a project'
requires :subscribable_id, type: String, desc: 'The ID of a resource'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
subscribable_types.each do |type, finder|
type_singularized = type.singularize
entity_class = Entities.const_get(type_singularized.camelcase)
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index d31ef9de26b..c7b1efe0bfa 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -7,7 +7,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository tags' do
success Entities::RepoTag
end
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index d9b8837a5bb..d1f7e364029 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -12,7 +12,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
ISSUABLE_TYPES.each do |type, finder|
type_id_str = "#{type.singularize}_iid".to_sym
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index 119e9024712..a9f2ca2608e 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -5,7 +5,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Trigger a GitLab project pipeline' do
success Entities::Pipeline
end
@@ -14,7 +14,7 @@ module API
requires :token, type: String, desc: 'The unique token of trigger'
optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
end
- post ":id/(ref/:ref/)trigger/pipeline" do
+ post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do
project = find_project(params[:id])
trigger = Ci::Trigger.find_by_token(params[:token].to_s)
not_found! unless project && trigger
diff --git a/lib/api/v3/award_emoji.rb b/lib/api/v3/award_emoji.rb
index cf9e1551f60..b96b2d70b12 100644
--- a/lib/api/v3/award_emoji.rb
+++ b/lib/api/v3/award_emoji.rb
@@ -6,7 +6,7 @@ module API
before { authenticate! }
AWARDABLES = %w[issue merge_request snippet].freeze
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
AWARDABLES.each do |awardable_type|
awardable_string = awardable_type.pluralize
awardable_id_string = "#{awardable_type}_id"
diff --git a/lib/api/v3/boards.rb b/lib/api/v3/boards.rb
index b1c2a3c59f2..94acc67171e 100644
--- a/lib/api/v3/boards.rb
+++ b/lib/api/v3/boards.rb
@@ -6,7 +6,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all project boards' do
detail 'This feature was introduced in 8.13'
success ::API::Entities::Board
diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb
index 699e41b5537..0a877b960f6 100644
--- a/lib/api/v3/branches.rb
+++ b/lib/api/v3/branches.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository branches' do
success ::API::Entities::RepoBranch
end
@@ -45,6 +45,27 @@ module API
status(200)
end
+
+ desc 'Create branch' do
+ success ::API::Entities::RepoBranch
+ end
+ params do
+ requires :branch_name, type: String, desc: 'The name of the branch'
+ requires :ref, type: String, desc: 'Create branch from commit sha or existing branch'
+ end
+ post ":id/repository/branches" do
+ authorize_push_project
+ result = CreateBranchService.new(user_project, current_user).
+ execute(params[:branch_name], params[:ref])
+
+ if result[:status] == :success
+ present result[:branch],
+ with: ::API::Entities::RepoBranch,
+ project: user_project
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
end
end
end
diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb
index d254d247042..3414a2883e5 100644
--- a/lib/api/v3/commits.rb
+++ b/lib/api/v3/commits.rb
@@ -11,7 +11,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository commits' do
success ::API::Entities::RepoCommit
end
diff --git a/lib/api/v3/deploy_keys.rb b/lib/api/v3/deploy_keys.rb
index 5bbb167755c..bbb174b6003 100644
--- a/lib/api/v3/deploy_keys.rb
+++ b/lib/api/v3/deploy_keys.rb
@@ -13,7 +13,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of the project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
before { authorize_admin_project }
%w(keys deploy_keys).each do |path|
diff --git a/lib/api/v3/deployments.rb b/lib/api/v3/deployments.rb
index 95114ad1fe1..1d4972eda26 100644
--- a/lib/api/v3/deployments.rb
+++ b/lib/api/v3/deployments.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all deployments of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success ::API::V3::Deployments
diff --git a/lib/api/v3/environments.rb b/lib/api/v3/environments.rb
index 3056b70e6ef..6bb4e016a01 100644
--- a/lib/api/v3/environments.rb
+++ b/lib/api/v3/environments.rb
@@ -9,7 +9,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all environments of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success Entities::Environment
diff --git a/lib/api/v3/files.rb b/lib/api/v3/files.rb
index 4f8d58d37c8..13542b0c71c 100644
--- a/lib/api/v3/files.rb
+++ b/lib/api/v3/files.rb
@@ -40,7 +40,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a file from repository'
params do
requires :file_path, type: String, desc: 'The path to the file. Ex. lib/class.rb'
diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb
index 0aad87a3f58..c5b37622d79 100644
--- a/lib/api/v3/groups.rb
+++ b/lib/api/v3/groups.rb
@@ -93,7 +93,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups do
+ resource :groups, requirements: { id: %r{[^/]+} } do
desc 'Update a group. Available only for users who can administrate groups.' do
success Entities::Group
end
@@ -163,7 +163,7 @@ module API
params do
requires :project_id, type: String, desc: 'The ID or path of the project'
end
- post ":id/projects/:project_id" do
+ post ":id/projects/:project_id", requirements: { project_id: /.+/ } do
authenticated_as_admin!
group = find_group!(params[:id])
project = find_project!(params[:project_id])
diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb
index 258cbfed022..54c6a8060b8 100644
--- a/lib/api/v3/issues.rb
+++ b/lib/api/v3/issues.rb
@@ -68,7 +68,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a group'
end
- resource :groups do
+ resource :groups, requirements: { id: %r{[^/]+} } do
desc 'Get a list of group issues' do
success ::API::Entities::Issue
end
@@ -89,7 +89,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
include TimeTrackingEndpoints
desc 'Get a list of project issues' do
@@ -103,7 +103,7 @@ module API
use :issues_params
end
get ":id/issues" do
- project = find_project(params[:id])
+ project = find_project!(params[:id])
issues = find_issues(project_id: project.id)
diff --git a/lib/api/v3/labels.rb b/lib/api/v3/labels.rb
index 41f45d244e3..bd5eb2175e8 100644
--- a/lib/api/v3/labels.rb
+++ b/lib/api/v3/labels.rb
@@ -6,7 +6,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all labels of the project' do
success ::API::Entities::Label
end
diff --git a/lib/api/v3/members.rb b/lib/api/v3/members.rb
index 3d4972afd9d..684860b553e 100644
--- a/lib/api/v3/members.rb
+++ b/lib/api/v3/members.rb
@@ -11,7 +11,7 @@ module API
params do
requires :id, type: String, desc: "The #{source_type} ID"
end
- resource source_type.pluralize do
+ resource source_type.pluralize, requirements: { id: %r{[^/]+} } do
desc 'Gets a list of group or project members viewable by the authenticated user.' do
success ::API::Entities::Member
end
diff --git a/lib/api/v3/merge_request_diffs.rb b/lib/api/v3/merge_request_diffs.rb
index a462803e26c..35f462e907b 100644
--- a/lib/api/v3/merge_request_diffs.rb
+++ b/lib/api/v3/merge_request_diffs.rb
@@ -4,14 +4,16 @@ module API
class MergeRequestDiffs < Grape::API
before { authenticate! }
- resource :projects do
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a list of merge request diff versions' do
detail 'This feature was introduced in GitLab 8.12.'
success ::API::Entities::MergeRequestDiff
end
params do
- requires :id, type: String, desc: 'The ID of a project'
requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
end
@@ -27,7 +29,6 @@ module API
end
params do
- requires :id, type: String, desc: 'The ID of a project'
requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
requires :version_id, type: Integer, desc: 'The ID of a merge request diff version'
end
diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb
index 7dbd4691a94..3077240e650 100644
--- a/lib/api/v3/merge_requests.rb
+++ b/lib/api/v3/merge_requests.rb
@@ -10,7 +10,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
include TimeTrackingEndpoints
helpers do
diff --git a/lib/api/v3/milestones.rb b/lib/api/v3/milestones.rb
index 2a850a08a8a..be90cec4afc 100644
--- a/lib/api/v3/milestones.rb
+++ b/lib/api/v3/milestones.rb
@@ -18,7 +18,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a list of project milestones' do
success ::API::Entities::Milestone
end
diff --git a/lib/api/v3/notes.rb b/lib/api/v3/notes.rb
index 0796bb62e68..4f8e0eff4ff 100644
--- a/lib/api/v3/notes.rb
+++ b/lib/api/v3/notes.rb
@@ -10,7 +10,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
NOTEABLE_TYPES.each do |noteable_type|
noteables_str = noteable_type.to_s.underscore.pluralize
diff --git a/lib/api/v3/pipelines.rb b/lib/api/v3/pipelines.rb
index 2c26a5f7d35..82827249244 100644
--- a/lib/api/v3/pipelines.rb
+++ b/lib/api/v3/pipelines.rb
@@ -8,7 +8,7 @@ module API
params do
requires :id, type: String, desc: 'The project ID'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get all Pipelines of the project' do
detail 'This feature was introduced in GitLab 8.11.'
success ::API::Entities::Pipeline
diff --git a/lib/api/v3/project_hooks.rb b/lib/api/v3/project_hooks.rb
index 861b991b8e1..94614bfc8b6 100644
--- a/lib/api/v3/project_hooks.rb
+++ b/lib/api/v3/project_hooks.rb
@@ -25,7 +25,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get project hooks' do
success ::API::V3::Entities::ProjectHook
end
diff --git a/lib/api/v3/project_snippets.rb b/lib/api/v3/project_snippets.rb
index 809ca4f37ba..fc065a22d74 100644
--- a/lib/api/v3/project_snippets.rb
+++ b/lib/api/v3/project_snippets.rb
@@ -8,7 +8,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
helpers do
def handle_project_member_errors(errors)
if errors[:project_access].any?
diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb
index 47bfc12035a..b753dbab381 100644
--- a/lib/api/v3/projects.rb
+++ b/lib/api/v3/projects.rb
@@ -234,7 +234,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: /[^\/]+/ } do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a single project' do
success ::API::V3::Entities::ProjectWithAccess
end
diff --git a/lib/api/v3/repositories.rb b/lib/api/v3/repositories.rb
index 44584e2eb70..e4d14bc8168 100644
--- a/lib/api/v3/repositories.rb
+++ b/lib/api/v3/repositories.rb
@@ -8,7 +8,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
helpers do
def handle_project_member_errors(errors)
if errors[:project_access].any?
diff --git a/lib/api/v3/runners.rb b/lib/api/v3/runners.rb
index 8967141fe3d..1934d6e578c 100644
--- a/lib/api/v3/runners.rb
+++ b/lib/api/v3/runners.rb
@@ -26,7 +26,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
before { authorize_admin_project }
desc "Disable project's runner" do
diff --git a/lib/api/v3/services.rb b/lib/api/v3/services.rb
index d77185ffe5a..3bacaeee032 100644
--- a/lib/api/v3/services.rb
+++ b/lib/api/v3/services.rb
@@ -554,7 +554,10 @@ module API
]
}.freeze
- resource :projects do
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
before { authenticate! }
before { authorize_admin_project }
@@ -609,7 +612,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc "Trigger a slash command for #{service_slug}" do
detail 'Added in GitLab 8.13'
end
diff --git a/lib/api/v3/subscriptions.rb b/lib/api/v3/subscriptions.rb
index 02a4157c26e..068750ec077 100644
--- a/lib/api/v3/subscriptions.rb
+++ b/lib/api/v3/subscriptions.rb
@@ -14,7 +14,7 @@ module API
requires :id, type: String, desc: 'The ID of a project'
requires :subscribable_id, type: String, desc: 'The ID of a resource'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
subscribable_types.each do |type, finder|
type_singularized = type.singularize
entity_class = ::API::Entities.const_get(type_singularized.camelcase)
diff --git a/lib/api/v3/tags.rb b/lib/api/v3/tags.rb
index 6913720d9c5..c2541de2f50 100644
--- a/lib/api/v3/tags.rb
+++ b/lib/api/v3/tags.rb
@@ -6,7 +6,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get a project repository tags' do
success ::API::Entities::RepoTag
end
diff --git a/lib/api/v3/todos.rb b/lib/api/v3/todos.rb
index e60cb25e57b..e3b311d61cd 100644
--- a/lib/api/v3/todos.rb
+++ b/lib/api/v3/todos.rb
@@ -20,9 +20,9 @@ module API
desc 'Mark all todos as done'
delete do
status(200)
-
+
todos = TodosFinder.new(current_user, params).execute
- TodoService.new.mark_todos_as_done(todos, current_user)
+ TodoService.new.mark_todos_as_done(todos, current_user).size
end
end
end
diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb
index 1dfdb6a5956..a23d6b6b48c 100644
--- a/lib/api/v3/triggers.rb
+++ b/lib/api/v3/triggers.rb
@@ -6,7 +6,7 @@ module API
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Trigger a GitLab project build' do
success ::API::V3::Entities::TriggerRequest
end
@@ -15,7 +15,7 @@ module API
requires :token, type: String, desc: 'The unique token of trigger'
optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
end
- post ":id/(ref/:ref/)trigger/builds" do
+ post ":id/(ref/:ref/)trigger/builds", requirements: { ref: /.+/ } do
project = find_project(params[:id])
trigger = Ci::Trigger.find_by_token(params[:token].to_s)
not_found! unless project && trigger
diff --git a/lib/api/v3/variables.rb b/lib/api/v3/variables.rb
index 0f55a14fb28..83972b1e7ce 100644
--- a/lib/api/v3/variables.rb
+++ b/lib/api/v3/variables.rb
@@ -10,7 +10,7 @@ module API
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Delete an existing variable from a project' do
success ::API::Entities::Variable
end
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index 77e5d54c225..5acde41551b 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -9,7 +9,7 @@ module API
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects do
+ resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get project variables' do
success Entities::Variable
end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index 02d5ad70fa7..8bc2dd18bda 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -239,18 +239,13 @@ module Banzai
# path.
def projects_per_reference
@projects_per_reference ||= begin
- hash = {}
refs = Set.new
references_per_project.each do |project_ref, _|
refs << project_ref
end
- find_projects_for_paths(refs.to_a).each do |project|
- hash[project.path_with_namespace] = project
- end
-
- hash
+ find_projects_for_paths(refs.to_a).index_by(&:full_path)
end
end
diff --git a/lib/banzai/filter/image_link_filter.rb b/lib/banzai/filter/image_link_filter.rb
index 651b55523c0..123c92fd250 100644
--- a/lib/banzai/filter/image_link_filter.rb
+++ b/lib/banzai/filter/image_link_filter.rb
@@ -2,7 +2,6 @@ module Banzai
module Filter
# HTML filter that wraps links around inline images.
class ImageLinkFilter < HTML::Pipeline::Filter
-
# Find every image that isn't already wrapped in an `a` tag, create
# a new node (a link to the image source), copy the image as a child
# of the anchor, and then replace the img with the link-wrapped version.
@@ -12,7 +11,8 @@ module Banzai
'a',
class: 'no-attachment-icon',
href: img['src'],
- target: '_blank'
+ target: '_blank',
+ rel: 'noopener noreferrer'
)
link.children = img.clone
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index a447e2b8bff..9f09ca90697 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -5,8 +5,6 @@ module Banzai
# HTML Filter to highlight fenced code blocks
#
class SyntaxHighlightFilter < HTML::Pipeline::Filter
- include Rouge::Plugins::Redcarpet
-
def call
doc.search('pre > code').each do |node|
highlight_node(node)
@@ -23,7 +21,7 @@ module Banzai
lang = lexer.tag
begin
- code = format(lex(lexer, code))
+ code = Rouge::Formatters::HTMLGitlab.format(lex(lexer, code), tag: lang)
css_classes << " js-syntax-highlight #{lang}"
rescue
@@ -45,10 +43,6 @@ module Banzai
lexer.lex(code)
end
- def format(tokens)
- rouge_formatter.format(tokens)
- end
-
def lexer_for(language)
(Rouge::Lexer.find(language) || Rouge::Lexers::PlainText).new
end
@@ -57,11 +51,6 @@ module Banzai
# Replace the parent `pre` element with the entire highlighted block
node.parent.replace(highlighted)
end
-
- # Override Rouge::Plugins::Redcarpet#rouge_formatter
- def rouge_formatter(lexer = nil)
- @rouge_formatter ||= Rouge::Formatters::HTML.new
- end
end
end
end
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index 849e1142841..fe1f0923136 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -74,10 +74,7 @@ module Banzai
# The keys of this Hash are the namespace paths, the values the
# corresponding Namespace objects.
def namespaces
- @namespaces ||=
- Namespace.where_full_path_in(usernames).each_with_object({}) do |row, hash|
- hash[row.full_path] = row
- end
+ @namespaces ||= Namespace.where_full_path_in(usernames).index_by(&:full_path)
end
# Returns all usernames referenced in the current document.
diff --git a/lib/banzai/filter/video_link_filter.rb b/lib/banzai/filter/video_link_filter.rb
index b64a1287d4d..35cb10eae5d 100644
--- a/lib/banzai/filter/video_link_filter.rb
+++ b/lib/banzai/filter/video_link_filter.rb
@@ -43,6 +43,7 @@ module Banzai
element['title'] || element['alt'],
href: element['src'],
target: '_blank',
+ rel: 'noopener noreferrer',
title: "Download '#{element['title'] || element['alt']}'")
download_paragraph = doc.document.create_element('p')
download_paragraph.children = link
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index b25d6f18d59..fd4a6a107c2 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -2,10 +2,10 @@ module Banzai
module Pipeline
class GfmPipeline < BasePipeline
# These filters convert GitLab Flavored Markdown (GFM) to HTML.
- # The handlers defined in app/assets/javascripts/copy_as_gfm.js.es6
+ # The handlers defined in app/assets/javascripts/copy_as_gfm.js
# consequently convert that same HTML to GFM to be copied to the clipboard.
# Every filter that generates HTML from GFM should have a handler in
- # app/assets/javascripts/copy_as_gfm.js.es6, in reverse order.
+ # app/assets/javascripts/copy_as_gfm.js, in reverse order.
# The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
def self.filters
@filters ||= FilterArray[
diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb
index b121c37c5d0..52fdb9a2140 100644
--- a/lib/banzai/reference_parser/base_parser.rb
+++ b/lib/banzai/reference_parser/base_parser.rb
@@ -134,9 +134,7 @@ module Banzai
ids = unique_attribute_values(nodes, attribute)
rows = collection_objects_for_ids(collection, ids)
- rows.each_with_object({}) do |row, hash|
- hash[row.id] = row
- end
+ rows.index_by(&:id)
end
# Returns an Array containing all unique values of an attribute of the
diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb
index d3524c338ee..84f9ecd3d23 100644
--- a/lib/gitlab/conflict/parser.rb
+++ b/lib/gitlab/conflict/parser.rb
@@ -15,11 +15,9 @@ module Gitlab
raise UnmergeableFile if text.blank? # Typically a binary file
raise UnmergeableFile if text.length > 200.kilobytes
- begin
- text.to_json
- rescue Encoding::UndefinedConversionError
- raise UnsupportedEncoding
- end
+ text.force_encoding('UTF-8')
+
+ raise UnsupportedEncoding unless text.valid_encoding?
line_obj_index = 0
line_old = 1
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index f3f417c1a63..63b8d0d3b9d 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -5,8 +5,12 @@ module Gitlab
# http://dev.mysql.com/doc/refman/5.7/en/integer-types.html
MAX_INT_VALUE = 2147483647
+ def self.config
+ ActiveRecord::Base.configurations[Rails.env]
+ end
+
def self.adapter_name
- ActiveRecord::Base.configurations[Rails.env]['adapter']
+ config['adapter']
end
def self.mysql?
diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb
index 35871fd1b7b..a16d9fc2265 100644
--- a/lib/gitlab/emoji.rb
+++ b/lib/gitlab/emoji.rb
@@ -44,27 +44,17 @@ module Gitlab
end
# CSS sprite fallback takes precedence over image fallback
- def gl_emoji_tag(name, image: false, sprite: false, force_fallback: false)
+ def gl_emoji_tag(name)
emoji_name = emojis_aliases[name] || name
emoji_info = emojis[emoji_name]
- emoji_fallback_image_source = ActionController::Base.helpers.url_to_image("emoji/#{emoji_info['name']}.png")
- emoji_fallback_sprite_class = "emoji-#{emoji_name}"
+ return unless emoji_info
data = {
name: emoji_name,
unicode_version: emoji_unicode_version(emoji_name)
}
- data[:fallback_src] = emoji_fallback_image_source if image
- data[:fallback_sprite_class] = emoji_fallback_sprite_class if sprite
- ActionController::Base.helpers.content_tag 'gl-emoji',
- class: ("emoji-icon #{emoji_fallback_sprite_class}" if force_fallback && sprite),
- data: data do
- if force_fallback && !sprite
- emoji_image_tag(emoji_name, emoji_fallback_image_source)
- else
- emoji_info['moji']
- end
- end
+
+ ActionController::Base.helpers.content_tag('gl-emoji', emoji_info['moji'], data: data)
end
end
end
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index 2a017c93f57..019be151353 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -176,9 +176,13 @@ module Gitlab
def initialize(raw_diff, collapse: false)
case raw_diff
when Hash
- init_from_hash(raw_diff, collapse: collapse)
+ init_from_hash(raw_diff)
+ prune_diff_if_eligible(collapse)
when Rugged::Patch, Rugged::Diff::Delta
init_from_rugged(raw_diff, collapse: collapse)
+ when Gitaly::CommitDiffResponse
+ init_from_gitaly(raw_diff)
+ prune_diff_if_eligible(collapse)
when nil
raise "Nil as raw diff passed"
else
@@ -266,13 +270,26 @@ module Gitlab
@diff = encode!(strip_diff_headers(patch.to_s))
end
- def init_from_hash(hash, collapse: false)
+ def init_from_hash(hash)
raw_diff = hash.symbolize_keys
serialize_keys.each do |key|
send(:"#{key}=", raw_diff[key.to_sym])
end
+ end
+
+ def init_from_gitaly(diff_msg)
+ @diff = diff_msg.raw_chunks.join
+ @new_path = encode!(diff_msg.to_path.dup)
+ @old_path = encode!(diff_msg.from_path.dup)
+ @a_mode = diff_msg.old_mode.to_s(8)
+ @b_mode = diff_msg.new_mode.to_s(8)
+ @new_file = diff_msg.from_id == BLANK_SHA
+ @renamed_file = diff_msg.from_path != diff_msg.to_path
+ @deleted_file = diff_msg.to_id == BLANK_SHA
+ end
+ def prune_diff_if_eligible(collapse = false)
prune_large_diff! if too_large?
prune_collapsed_diff! if collapse && collapsible?
end
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index 65e06f5065d..4e45ec7c174 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -30,7 +30,9 @@ module Gitlab
elsif @deltas_only
each_delta(&block)
else
- each_patch(&block)
+ Gitlab::GitalyClient.migrate(:commit_raw_diffs) do
+ each_patch(&block)
+ end
end
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 228ef7bb7a9..2187dd70ff4 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -201,7 +201,7 @@ module Gitlab
def archive_prefix(ref, sha)
project_name = self.name.chomp('.git')
- "#{project_name}-#{ref.parameterize}-#{sha}"
+ "#{project_name}-#{ref.tr('/', '-')}-#{sha}"
end
def archive_metadata(ref, storage_path, format = "tar.gz")
diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb
index 4d83d8e72a8..0e87ee30c98 100644
--- a/lib/gitlab/git_ref_validator.rb
+++ b/lib/gitlab/git_ref_validator.rb
@@ -5,6 +5,9 @@ module Gitlab
#
# Returns true for a valid reference name, false otherwise
def validate(ref_name)
+ return false if ref_name.start_with?('refs/heads/')
+ return false if ref_name.start_with?('refs/remotes/')
+
Gitlab::Utils.system_silent(
%W(#{Gitlab.config.git.bin_path} check-ref-format refs/#{ref_name}))
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index b981a629fb0..5534d4af439 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -25,5 +25,19 @@ module Gitlab
def self.enabled?
gitaly_address.present?
end
+
+ def self.feature_enabled?(feature)
+ enabled? && ENV["GITALY_#{feature.upcase}"] == '1'
+ end
+
+ def self.migrate(feature)
+ is_enabled = feature_enabled?(feature)
+ metric_name = feature.to_s
+ metric_name += "_gitaly" if is_enabled
+
+ Gitlab::Metrics.measure(metric_name) do
+ yield is_enabled
+ end
+ end
end
end
diff --git a/lib/gitlab/gitaly_client/commit.rb b/lib/gitlab/gitaly_client/commit.rb
new file mode 100644
index 00000000000..525b8d680e9
--- /dev/null
+++ b/lib/gitlab/gitaly_client/commit.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module GitalyClient
+ class Commit
+ # The ID of empty tree.
+ # See http://stackoverflow.com/a/40884093/1856239 and https://github.com/git/git/blob/3ad8b5bf26362ac67c9020bf8c30eee54a84f56d/cache.h#L1011-L1012
+ EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'.freeze
+
+ class << self
+ def diff_from_parent(commit, options = {})
+ stub = Gitaly::Diff::Stub.new(nil, nil, channel_override: GitalyClient.channel)
+ repo = Gitaly::Repository.new(path: commit.project.repository.path_to_repo)
+ parent = commit.parents[0]
+ parent_id = parent ? parent.id : EMPTY_TREE_ID
+ request = Gitaly::CommitDiffRequest.new(
+ repository: repo,
+ left_commit_id: parent_id,
+ right_commit_id: commit.id
+ )
+
+ Gitlab::Git::DiffCollection.new(stub.commit_diff(request), options)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/highlight.rb b/lib/gitlab/highlight.rb
index 9360afedfcb..d787d5db4a0 100644
--- a/lib/gitlab/highlight.rb
+++ b/lib/gitlab/highlight.rb
@@ -14,7 +14,7 @@ module Gitlab
end
def initialize(blob_name, blob_content, repository: nil)
- @formatter = Rouge::Formatters::HTMLGitlab.new
+ @formatter = Rouge::Formatters::HTMLGitlab
@repository = repository
@blob_name = blob_name
@blob_content = blob_content
@@ -28,7 +28,7 @@ module Gitlab
hl_lexer = self.lexer
end
- @formatter.format(hl_lexer.lex(text, continue: continue)).html_safe
+ @formatter.format(hl_lexer.lex(text, continue: continue), tag: hl_lexer.tag).html_safe
rescue
@formatter.format(Rouge::Lexers::PlainText.lex(text)).html_safe
end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 416194e57d7..ab74c8782f6 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -73,6 +73,9 @@ excluded_attributes:
- :milestone_id
award_emoji:
- :awardable_id
+ statuses:
+ - :trace
+ - :token
methods:
labels:
@@ -81,6 +84,7 @@ methods:
- :type
statuses:
- :type
+ - :gl_project_id
services:
- :type
merge_request_diff:
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index fae792237d9..d44563333a5 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -15,7 +15,7 @@ module Gitlab
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id merge_user_id resolved_by_id].freeze
- PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze
+ PROJECT_REFERENCES = %w[project_id source_project_id target_project_id].freeze
BUILD_MODELS = %w[Ci::Build commit_status].freeze
@@ -98,12 +98,11 @@ module Gitlab
end
def generate_imported_object
- if BUILD_MODELS.include?(@relation_name) # call #trace= method after assigning the other attributes
- trace = @relation_hash.delete('trace')
+ if BUILD_MODELS.include?(@relation_name)
+ @relation_hash.delete('trace') # old export files have trace
@relation_hash.delete('token')
imported_object do |object|
- object.trace = trace
object.commit_id = nil
end
else
@@ -121,7 +120,6 @@ module Gitlab
# project_id may not be part of the export, but we always need to populate it if required.
@relation_hash['project_id'] = project_id
- @relation_hash['gl_project_id'] = project_id if @relation_hash['gl_project_id']
@relation_hash['target_project_id'] = project_id if @relation_hash['target_project_id']
end
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 62dbd429156..bc5370de32a 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -1,6 +1,7 @@
# This file should not have any direct dependency on Rails environment
# please require all dependencies below:
require 'active_support/core_ext/hash/keys'
+require 'active_support/core_ext/module/delegation'
module Gitlab
class Redis
@@ -9,7 +10,6 @@ module Gitlab
SIDEKIQ_NAMESPACE = 'resque:gitlab'.freeze
MAILROOM_NAMESPACE = 'mail_room:gitlab'.freeze
DEFAULT_REDIS_URL = 'redis://localhost:6379'.freeze
- CONFIG_FILE = File.expand_path('../../config/resque.yml', __dir__)
class << self
delegate :params, :url, to: :new
@@ -33,13 +33,17 @@ module Gitlab
return @_raw_config if defined?(@_raw_config)
begin
- @_raw_config = ERB.new(File.read(CONFIG_FILE)).result.freeze
+ @_raw_config = ERB.new(File.read(config_file)).result.freeze
rescue Errno::ENOENT
@_raw_config = false
end
@_raw_config
end
+
+ def config_file
+ ENV['GITLAB_REDIS_CONFIG_FILE'] || File.expand_path('../../config/resque.yml', __dir__)
+ end
end
def initialize(rails_env = nil)
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
new file mode 100644
index 00000000000..7e14a566696
--- /dev/null
+++ b/lib/gitlab/url_blocker.rb
@@ -0,0 +1,59 @@
+require 'resolv'
+
+module Gitlab
+ class UrlBlocker
+ class << self
+ # Used to specify what hosts and port numbers should be prohibited for project
+ # imports.
+ VALID_PORTS = [22, 80, 443].freeze
+
+ def blocked_url?(url)
+ return false if url.nil?
+
+ blocked_ips = ["127.0.0.1", "::1", "0.0.0.0"]
+ blocked_ips.concat(Socket.ip_address_list.map(&:ip_address))
+
+ begin
+ uri = Addressable::URI.parse(url)
+ # Allow imports from the GitLab instance itself but only from the configured ports
+ return false if internal?(uri)
+
+ return true if blocked_port?(uri.port)
+
+ server_ips = Resolv.getaddresses(uri.hostname)
+ return true if (blocked_ips & server_ips).any?
+ rescue Addressable::URI::InvalidURIError
+ return true
+ end
+
+ false
+ end
+
+ private
+
+ def blocked_port?(port)
+ return false if port.blank?
+
+ port < 1024 && !VALID_PORTS.include?(port)
+ end
+
+ def internal?(uri)
+ internal_web?(uri) || internal_shell?(uri)
+ end
+
+ def internal_web?(uri)
+ uri.hostname == config.gitlab.host &&
+ (uri.port.blank? || uri.port == config.gitlab.port)
+ end
+
+ def internal_shell?(uri)
+ uri.hostname == config.gitlab_shell.ssh_host &&
+ (uri.port.blank? || uri.port == config.gitlab_shell.ssh_port)
+ end
+
+ def config
+ Gitlab.config
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb
index c81dc7e30d0..9ce13feb79a 100644
--- a/lib/gitlab/url_sanitizer.rb
+++ b/lib/gitlab/url_sanitizer.rb
@@ -18,6 +18,12 @@ module Gitlab
false
end
+ def self.http_credentials_for_user(user)
+ return {} unless user.respond_to?(:username)
+
+ { user: user.username }
+ end
+
def initialize(url, credentials: nil)
@url = Addressable::URI.parse(url.strip)
@credentials = credentials
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index 2248763c106..8f1d1fdc02e 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -96,8 +96,8 @@ module Gitlab
end
def level_value(level)
- return string_options[level] if level.is_a? String
- level
+ return level.to_i if level.to_i.to_s == level.to_s && string_options.key(level.to_i)
+ string_options[level] || PRIVATE
end
def string_level(level)
diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb
index 4edfd015074..be0d97370d0 100644
--- a/lib/rouge/formatters/html_gitlab.rb
+++ b/lib/rouge/formatters/html_gitlab.rb
@@ -5,10 +5,10 @@ module Rouge
# Creates a new <tt>Rouge::Formatter::HTMLGitlab</tt> instance.
#
- # [+linenostart+] The line number for the first line (default: 1).
- def initialize(linenostart: 1)
- @linenostart = linenostart
- @line_number = linenostart
+ # [+tag+] The tag (language) of the lexer used to generate the formatted tokens
+ def initialize(tag: nil)
+ @line_number = 1
+ @tag = tag
end
def stream(tokens, &b)
@@ -17,7 +17,7 @@ module Rouge
yield "\n" unless is_first
is_first = false
- yield %(<span id="LC#{@line_number}" class="line">)
+ yield %(<span id="LC#{@line_number}" class="line" lang="#{@tag}">)
line.each { |token, value| yield span(token, value.chomp) }
yield %(</span>)
diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example
index e5797d8fe3c..f6642527639 100644
--- a/lib/support/init.d/gitlab.default.example
+++ b/lib/support/init.d/gitlab.default.example
@@ -56,14 +56,14 @@ gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log"
# The value of -listen-http must be set to `gitlab.yml > pages > external_http`
# as well. For example:
#
-# -listen-http 1.1.1.1:80
+# -listen-http 1.1.1.1:80 -listen-http [2001::1]:80
#
# To enable HTTPS support for custom domains add the `-listen-https`,
# `-root-cert` and `-root-key` directives in `gitlab_pages_options` below.
# The value of -listen-https must be set to `gitlab.yml > pages > external_https`
# as well. For example:
#
-# -listen-https 1.1.1.1:443 -root-cert /path/to/example.com.crt -root-key /path/to/example.com.key
+# -listen-https 1.1.1.1:443 -listen-http [2001::1]:443 -root-cert /path/to/example.com.crt -root-key /path/to/example.com.key
#
# The -pages-domain must be specified the same as in `gitlab.yml > pages > host`.
# Set `gitlab_pages_enabled=true` if you want to enable the Pages feature.
diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab
index 2f7c34a3f31..f25e66d54c8 100644
--- a/lib/support/nginx/gitlab
+++ b/lib/support/nginx/gitlab
@@ -38,6 +38,13 @@ server {
## See app/controllers/application_controller.rb for headers set
+ ## Real IP Module Config
+ ## http://nginx.org/en/docs/http/ngx_http_realip_module.html
+ real_ip_header X-Real-IP; ## X-Real-IP or X-Forwarded-For or proxy_protocol
+ real_ip_recursive off; ## If you enable 'on'
+ ## If you have a trusted IP address, uncomment it and set it
+ # set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24
+
## Individual nginx logs for this GitLab vhost
access_log /var/log/nginx/gitlab_access.log;
error_log /var/log/nginx/gitlab_error.log;
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index 330031aaddc..2b40da18bab 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -85,6 +85,13 @@ server {
## [Optional] Enable HTTP Strict Transport Security
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+ ## Real IP Module Config
+ ## http://nginx.org/en/docs/http/ngx_http_realip_module.html
+ real_ip_header X-Real-IP; ## X-Real-IP or X-Forwarded-For or proxy_protocol
+ real_ip_recursive off; ## If you enable 'on'
+ ## If you have a trusted IP address, uncomment it and set it
+ # set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24
+
## Individual nginx logs for this GitLab vhost
access_log /var/log/nginx/gitlab_access.log;
error_log /var/log/nginx/gitlab_error.log;
diff --git a/lib/tasks/gitlab/dev.rake b/lib/tasks/gitlab/dev.rake
index 7db0779def8..7ccda04a35f 100644
--- a/lib/tasks/gitlab/dev.rake
+++ b/lib/tasks/gitlab/dev.rake
@@ -4,7 +4,7 @@ namespace :gitlab do
task :ee_compat_check, [:branch] => :environment do |_, args|
opts =
if ENV['CI']
- { branch: ENV['CI_BUILD_REF_NAME'] }
+ { branch: ENV['CI_COMMIT_REF_NAME'] }
else
unless args[:branch]
puts "Must specify a branch as an argument".color(:red)
diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake
index f5caca3ddbf..8938bc515f5 100644
--- a/lib/tasks/migrate/setup_postgresql.rake
+++ b/lib/tasks/migrate/setup_postgresql.rake
@@ -3,10 +3,12 @@ require Rails.root.join('lib/gitlab/database/migration_helpers')
require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lower_indexes')
require Rails.root.join('db/migrate/20151008110232_add_users_lower_username_email_indexes')
require Rails.root.join('db/migrate/20161212142807_add_lower_path_index_to_routes')
+require Rails.root.join('db/migrate/20170317203554_index_routes_path_for_like')
desc 'GitLab | Sets up PostgreSQL'
task setup_postgresql: :environment do
NamespacesProjectsPathLowerIndexes.new.up
AddUsersLowerUsernameEmailIndexes.new.up
AddLowerPathIndexToRoutes.new.up
+ IndexRoutesPathForLike.new.up
end
diff --git a/lib/tasks/tokens.rake b/lib/tasks/tokens.rake
new file mode 100644
index 00000000000..95735f43802
--- /dev/null
+++ b/lib/tasks/tokens.rake
@@ -0,0 +1,38 @@
+require_relative '../../app/models/concerns/token_authenticatable.rb'
+
+namespace :tokens do
+ desc "Reset all GitLab user auth tokens"
+ task reset_all_auth: :environment do
+ reset_all_users_token(:reset_authentication_token!)
+ end
+
+ desc "Reset all GitLab email tokens"
+ task reset_all_email: :environment do
+ reset_all_users_token(:reset_incoming_email_token!)
+ end
+
+ def reset_all_users_token(reset_token_method)
+ TmpUser.find_in_batches do |batch|
+ puts "Processing batch starting with user ID: #{batch.first.id}"
+ STDOUT.flush
+
+ batch.each(&reset_token_method)
+ end
+ end
+end
+
+class TmpUser < ActiveRecord::Base
+ include TokenAuthenticatable
+
+ self.table_name = 'users'
+
+ def reset_authentication_token!
+ write_new_token(:authentication_token)
+ save!(validate: false)
+ end
+
+ def reset_incoming_email_token!
+ write_new_token(:incoming_email_token)
+ save!(validate: false)
+ end
+end
diff --git a/package.json b/package.json
index 9652dd8f972..b3d038bd3d1 100644
--- a/package.json
+++ b/package.json
@@ -2,10 +2,11 @@
"private": true,
"scripts": {
"dev-server": "webpack-dev-server --config config/webpack.config.js",
- "eslint": "eslint --max-warnings 0 --ext .js,.js.es6 .",
- "eslint-fix": "eslint --max-warnings 0 --ext .js,.js.es6 --fix .",
- "eslint-report": "eslint --max-warnings 0 --ext .js,.js.es6 --format html --output-file ./eslint-report.html .",
+ "eslint": "eslint --max-warnings 0 --ext .js .",
+ "eslint-fix": "eslint --max-warnings 0 --ext .js --fix .",
+ "eslint-report": "eslint --max-warnings 0 --ext .js --format html --output-file ./eslint-report.html .",
"karma": "karma start config/karma.config.js --single-run",
+ "karma-coverage": "BABEL_ENV=coverage karma start config/karma.config.js --single-run",
"karma-start": "karma start config/karma.config.js",
"webpack": "webpack --config config/webpack.config.js",
"webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
@@ -13,7 +14,8 @@
"dependencies": {
"babel-core": "^6.22.1",
"babel-loader": "^6.2.10",
- "babel-preset-es2015": "^6.22.0",
+ "babel-plugin-transform-define": "^1.2.0",
+ "babel-preset-latest": "^6.24.0",
"babel-preset-stage-2": "^6.22.0",
"bootstrap-sass": "^3.3.6",
"compression-webpack-plugin": "^0.3.2",
@@ -57,13 +59,5 @@
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^2.0.2",
"webpack-dev-server": "^2.3.0"
- },
- "nyc": {
- "exclude": [
- "spec/javascripts/test_bundle.js",
- "spec/javascripts/**/*_spec.js",
- "spec/javascripts/**/*_spec.js.es6",
- "app/assets/javascripts/droplab/**/*"
- ]
}
}
diff --git a/qa/.gitignore b/qa/.gitignore
new file mode 100644
index 00000000000..3fec32c8427
--- /dev/null
+++ b/qa/.gitignore
@@ -0,0 +1 @@
+tmp/
diff --git a/qa/Dockerfile b/qa/Dockerfile
index 2814a7bdef0..72c82503542 100644
--- a/qa/Dockerfile
+++ b/qa/Dockerfile
@@ -8,7 +8,10 @@ RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list &&
WORKDIR /home/qa
-COPY ./ ./
+COPY ./Gemfile* ./
+
RUN bundle install
+COPY ./ ./
+
ENTRYPOINT ["bin/test"]
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
new file mode 100644
index 00000000000..6de2abff198
--- /dev/null
+++ b/qa/Gemfile.lock
@@ -0,0 +1,61 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ addressable (2.5.0)
+ public_suffix (~> 2.0, >= 2.0.2)
+ capybara (2.12.1)
+ addressable
+ mime-types (>= 1.16)
+ nokogiri (>= 1.3.3)
+ rack (>= 1.0.0)
+ rack-test (>= 0.5.4)
+ xpath (~> 2.0)
+ capybara-screenshot (1.0.14)
+ capybara (>= 1.0, < 3)
+ launchy
+ capybara-webkit (1.12.0)
+ capybara (>= 2.3.0, < 2.13.0)
+ json
+ diff-lcs (1.3)
+ json (2.0.3)
+ launchy (2.4.3)
+ addressable (~> 2.3)
+ mime-types (3.1)
+ mime-types-data (~> 3.2015)
+ mime-types-data (3.2016.0521)
+ mini_portile2 (2.1.0)
+ nokogiri (1.7.0.1)
+ mini_portile2 (~> 2.1.0)
+ public_suffix (2.0.5)
+ rack (2.0.1)
+ rack-test (0.6.3)
+ rack (>= 1.0)
+ rake (12.0.0)
+ rspec (3.5.0)
+ rspec-core (~> 3.5.0)
+ rspec-expectations (~> 3.5.0)
+ rspec-mocks (~> 3.5.0)
+ rspec-core (3.5.4)
+ rspec-support (~> 3.5.0)
+ rspec-expectations (3.5.0)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.5.0)
+ rspec-mocks (3.5.0)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.5.0)
+ rspec-support (3.5.0)
+ xpath (2.0.0)
+ nokogiri (~> 1.3)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ capybara (~> 2.12.1)
+ capybara-screenshot (~> 1.0.14)
+ capybara-webkit (~> 1.12.0)
+ rake (~> 12.0.0)
+ rspec (~> 3.5)
+
+BUNDLED WITH
+ 1.14.6
diff --git a/qa/qa/page/main/entry.rb b/qa/qa/page/main/entry.rb
index fe80deb6429..a9810beeb29 100644
--- a/qa/qa/page/main/entry.rb
+++ b/qa/qa/page/main/entry.rb
@@ -5,8 +5,14 @@ module QA
def initialize
visit('/')
- # This resolves cold boot problems with login page
- find('.application', wait: 120)
+ # This resolves cold boot / background tasks problems
+ #
+ start = Time.now
+
+ while Time.now - start < 240
+ break if page.has_css?('.application', wait: 10)
+ refresh
+ end
end
def sign_in_using_credentials
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb
index 0b8ff006d22..092048a6259 100644
--- a/spec/config/mail_room_spec.rb
+++ b/spec/config/mail_room_spec.rb
@@ -1,20 +1,36 @@
require 'spec_helper'
describe 'mail_room.yml' do
- let(:config_path) { 'config/mail_room.yml' }
- let(:configuration) { YAML.load(ERB.new(File.read(config_path)).result) }
- before(:each) { clear_raw_config }
- after(:each) { clear_raw_config }
+ include StubENV
- context 'when incoming email is disabled' do
- before do
- ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/config/mail_room_disabled.yml').to_s
- Gitlab::MailRoom.reset_config!
- end
+ let(:mailroom_config_path) { 'config/mail_room.yml' }
+ let(:gitlab_config_path) { 'config/mail_room.yml' }
+ let(:redis_config_path) { 'config/resque.yml' }
- after do
- ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = nil
- end
+ let(:configuration) do
+ vars = {
+ 'MAIL_ROOM_GITLAB_CONFIG_FILE' => absolute_path(gitlab_config_path),
+ 'GITLAB_REDIS_CONFIG_FILE' => absolute_path(redis_config_path)
+ }
+ cmd = "puts ERB.new(File.read(#{absolute_path(mailroom_config_path).inspect})).result"
+
+ output, status = Gitlab::Popen.popen(%W(ruby -rerb -e #{cmd}), absolute_path('config'), vars)
+ raise "Error interpreting #{mailroom_config_path}: #{output}" unless status.zero?
+
+ YAML.load(output)
+ end
+
+ before(:each) do
+ stub_env('GITLAB_REDIS_CONFIG_FILE', absolute_path(redis_config_path))
+ clear_redis_raw_config
+ end
+
+ after(:each) do
+ clear_redis_raw_config
+ end
+
+ context 'when incoming email is disabled' do
+ let(:gitlab_config_path) { 'spec/fixtures/config/mail_room_disabled.yml' }
it 'contains no configuration' do
expect(configuration[:mailboxes]).to be_nil
@@ -22,21 +38,12 @@ describe 'mail_room.yml' do
end
context 'when incoming email is enabled' do
- let(:redis_config) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') }
- let(:gitlab_redis) { Gitlab::Redis.new(Rails.env) }
-
- before do
- ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/config/mail_room_enabled.yml').to_s
- Gitlab::MailRoom.reset_config!
- end
+ let(:gitlab_config_path) { 'spec/fixtures/config/mail_room_enabled.yml' }
+ let(:redis_config_path) { 'spec/fixtures/config/redis_new_format_host.yml' }
- after do
- ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = nil
- end
+ let(:gitlab_redis) { Gitlab::Redis.new(Rails.env) }
it 'contains the intended configuration' do
- stub_const('Gitlab::Redis::CONFIG_FILE', redis_config)
-
expect(configuration[:mailboxes].length).to eq(1)
mailbox = configuration[:mailboxes].first
@@ -66,9 +73,13 @@ describe 'mail_room.yml' do
end
end
- def clear_raw_config
+ def clear_redis_raw_config
Gitlab::Redis.remove_instance_variable(:@_raw_config)
rescue NameError
# raised if @_raw_config was not set; ignore
end
+
+ def absolute_path(path)
+ Rails.root.join(path).to_s
+ end
end
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
new file mode 100644
index 00000000000..84a1ce773a1
--- /dev/null
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Admin::ApplicationSettingsController do
+ include StubENV
+
+ let(:admin) { create(:admin) }
+
+ before do
+ sign_in(admin)
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ end
+
+ describe 'PATCH #update' do
+ it 'updates the default_project_visibility for string value' do
+ patch :update, application_setting: { default_project_visibility: "20" }
+
+ expect(response).to redirect_to(admin_application_settings_path)
+ expect(ApplicationSetting.current.default_project_visibility).to eq Gitlab::VisibilityLevel::PUBLIC
+ end
+
+ it 'falls back to default with default_project_visibility setting is omitted' do
+ patch :update, application_setting: {}
+
+ expect(response).to redirect_to(admin_application_settings_path)
+ expect(ApplicationSetting.current.default_project_visibility).to eq Gitlab::VisibilityLevel::PRIVATE
+ end
+ end
+end
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index 7072bd5e87c..71a4a2c43c7 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -49,4 +49,18 @@ describe Dashboard::TodosController do
expect(json_response).to eq({ "count" => "1", "done_count" => "0" })
end
end
+
+ describe 'PATCH #bulk_restore' do
+ let(:todos) { create_list(:todo, 2, :done, user: user, project: project, author: author) }
+
+ it 'restores the todos to pending state' do
+ patch :bulk_restore, ids: todos.map(&:id)
+
+ todos.each do |todo|
+ expect(todo.reload).to be_pending
+ end
+ expect(response).to have_http_status(200)
+ expect(json_response).to eq({ 'count' => '2', 'done_count' => '0' })
+ end
+ end
end
diff --git a/spec/controllers/profiles/notifications_controller_spec.rb b/spec/controllers/profiles/notifications_controller_spec.rb
deleted file mode 100644
index 58caf7999cf..00000000000
--- a/spec/controllers/profiles/notifications_controller_spec.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-require 'spec_helper'
-
-describe Profiles::NotificationsController do
- let(:user) do
- create(:user) do |user|
- user.emails.create(email: 'original@example.com')
- user.emails.create(email: 'new@example.com')
- user.update(notification_email: 'original@example.com')
- user.save!
- end
- end
-
- describe 'GET show' do
- it 'renders' do
- sign_in(user)
-
- get :show
-
- expect(response).to render_template :show
- end
- end
-
- describe 'POST update' do
- it 'updates only permitted attributes' do
- sign_in(user)
-
- put :update, user: { notification_email: 'new@example.com', notified_of_own_activity: true, admin: true }
-
- user.reload
- expect(user.notification_email).to eq('new@example.com')
- expect(user.notified_of_own_activity).to eq(true)
- expect(user.admin).to eq(false)
- expect(controller).to set_flash[:notice].to('Notification settings saved')
- end
-
- it 'shows an error message if the params are invalid' do
- sign_in(user)
-
- put :update, user: { notification_email: '' }
-
- expect(user.reload.notification_email).to eq('original@example.com')
- expect(controller).to set_flash[:alert].to('Failed to save new settings')
- end
- end
-end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 6ceaf96f78f..57a921e3676 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -87,6 +87,12 @@ describe Projects::IssuesController do
end
describe 'GET #new' do
+ it 'redirects to signin if not logged in' do
+ get :new, namespace_id: project.namespace, project_id: project
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+
context 'internal issue tracker' do
before do
sign_in(user)
@@ -121,6 +127,11 @@ describe Projects::IssuesController do
end
context 'external issue tracker' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
it 'redirects to the external issue tracker' do
external = double(new_issue_path: 'https://example.com/issues/new')
allow(project).to receive(:external_issue_tracker).and_return(external)
@@ -141,6 +152,24 @@ describe Projects::IssuesController do
it_behaves_like 'update invalid issuable', Issue
+ context 'changing the assignee' do
+ it 'limits the attributes exposed on the assignee' do
+ assignee = create(:user)
+ project.add_developer(assignee)
+
+ put :update,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: issue.iid,
+ issue: { assignee_id: assignee.id },
+ format: :json
+ body = JSON.parse(response.body)
+
+ expect(body['assignee'].keys)
+ .to match_array(%w(name username avatar_url))
+ end
+ end
+
context 'when moving issue to another private project' do
let(:another_project) { create(:empty_project, :private) }
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 250d64f7055..c310d830e81 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -203,6 +203,24 @@ describe Projects::MergeRequestsController do
end
describe 'PUT update' do
+ context 'changing the assignee' do
+ it 'limits the attributes exposed on the assignee' do
+ assignee = create(:user)
+ project.add_developer(assignee)
+
+ put :update,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid,
+ merge_request: { assignee_id: assignee.id },
+ format: :json
+ body = JSON.parse(response.body)
+
+ expect(body['assignee'].keys)
+ .to match_array(%w(name username avatar_url))
+ end
+ end
+
context 'there is no source project' do
let(:project) { create(:project) }
let(:fork_project) { create(:forked_project_with_submodules) }
diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb
index e3f3b4fe8eb..1ecfe48475c 100644
--- a/spec/controllers/projects/variables_controller_spec.rb
+++ b/spec/controllers/projects/variables_controller_spec.rb
@@ -35,7 +35,7 @@ describe Projects::VariablesController do
context 'updating a variable with valid characters' do
before do
- variable.gl_project_id = project.id
+ variable.project_id = project.id
project.variables << variable
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index a1ec41322ad..a88ffc1ea6a 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -78,10 +78,12 @@ describe ProjectsController do
it 'shows issues list page if wiki is disabled' do
project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
+ create(:issue, project: project)
get :show, namespace_id: project.namespace, id: project
expect(response).to render_template('projects/issues/_issues')
+ expect(assigns(:issuable_meta_data)).not_to be_nil
end
it 'shows customize workflow page if wiki and issues are disabled' do
diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb
index 3372e5ab685..6712dd5d82e 100644
--- a/spec/factories/ci/runner_projects.rb
+++ b/spec/factories/ci/runner_projects.rb
@@ -1,6 +1,6 @@
FactoryGirl.define do
factory :ci_runner_project, class: Ci::RunnerProject do
runner_id 1
- gl_project_id 1
+ project_id 1
end
end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index ae0bbbd6aeb..21487541507 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -4,6 +4,7 @@ FactoryGirl.define do
author
association :source_project, :repository, factory: :project
target_project { source_project }
+ project { target_project }
# $ git log --pretty=oneline feature..master
# 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index de42ab81fac..03daab12c8f 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -9,6 +9,13 @@ feature 'Admin updates settings', feature: true do
visit admin_application_settings_path
end
+ scenario 'Change visibility settings' do
+ choose "application_setting_default_project_visibility_20"
+ click_button 'Save'
+
+ expect(page).to have_content "Application settings saved successfully"
+ end
+
scenario 'Change application settings' do
uncheck 'Gravatar enabled'
fill_in 'Home page URL', with: 'https://about.gitlab.com/'
@@ -26,7 +33,7 @@ feature 'Admin updates settings', feature: true do
fill_in 'Webhook', with: 'http://localhost'
fill_in 'Username', with: 'test_user'
fill_in 'service_push_channel', with: '#test_channel'
- page.check('Notify only broken builds')
+ page.check('Notify only broken pipelines')
check_all_events
click_on 'Save'
@@ -50,7 +57,6 @@ feature 'Admin updates settings', feature: true do
page.check('Note')
page.check('Issue')
page.check('Merge request')
- page.check('Build')
page.check('Pipeline')
end
end
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index a7c22615b89..58b14e09740 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -2,7 +2,8 @@ require 'spec_helper'
describe "Dashboard Issues Feed", feature: true do
describe "GET /issues" do
- let!(:user) { create(:user) }
+ let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
+ let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
let!(:project1) { create(:project) }
let!(:project2) { create(:project) }
@@ -31,7 +32,7 @@ describe "Dashboard Issues Feed", feature: true do
end
context "issue with basic fields" do
- let!(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'test desc') }
+ let!(:issue2) { create(:issue, author: user, assignee: assignee, project: project2, description: 'test desc') }
it "renders issue fields" do
visit issues_dashboard_path(:atom, private_token: user.private_token)
@@ -39,8 +40,8 @@ describe "Dashboard Issues Feed", feature: true do
entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue2.title}')]")
expect(entry).to be_present
- expect(entry).to have_selector('author email', text: issue2.author_email)
- expect(entry).to have_selector('assignee email', text: issue2.author_email)
+ expect(entry).to have_selector('author email', text: issue2.author_public_email)
+ expect(entry).to have_selector('assignee email', text: issue2.assignee_public_email)
expect(entry).not_to have_selector('labels')
expect(entry).not_to have_selector('milestone')
expect(entry).to have_selector('description', text: issue2.description)
@@ -50,7 +51,7 @@ describe "Dashboard Issues Feed", feature: true do
context "issue with label and milestone" do
let!(:milestone1) { create(:milestone, project: project1, title: 'v1') }
let!(:label1) { create(:label, project: project1, title: 'label1') }
- let!(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone1) }
+ let!(:issue1) { create(:issue, author: user, assignee: assignee, project: project1, milestone: milestone1) }
before do
issue1.labels << label1
@@ -62,8 +63,8 @@ describe "Dashboard Issues Feed", feature: true do
entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue1.title}')]")
expect(entry).to be_present
- expect(entry).to have_selector('author email', text: issue1.author_email)
- expect(entry).to have_selector('assignee email', text: issue1.author_email)
+ expect(entry).to have_selector('author email', text: issue1.author_public_email)
+ expect(entry).to have_selector('assignee email', text: issue1.assignee_public_email)
expect(entry).to have_selector('labels label', text: label1.title)
expect(entry).to have_selector('milestone', text: milestone1.title)
expect(entry).not_to have_selector('description')
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index a01a050a013..b3903ec2faf 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -2,10 +2,11 @@ require 'spec_helper'
describe 'Issues Feed', feature: true do
describe 'GET /issues' do
- let!(:user) { create(:user) }
+ let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
+ let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
let!(:group) { create(:group) }
let!(:project) { create(:project) }
- let!(:issue) { create(:issue, author: user, project: project) }
+ let!(:issue) { create(:issue, author: user, assignee: assignee, project: project) }
before do
project.team << [user, :developer]
@@ -20,7 +21,8 @@ describe 'Issues Feed', feature: true do
expect(response_headers['Content-Type']).
to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{project.name} issues")
- expect(body).to have_selector('author email', text: issue.author_email)
+ expect(body).to have_selector('author email', text: issue.author_public_email)
+ expect(body).to have_selector('assignee email', text: issue.author_public_email)
expect(body).to have_selector('entry summary', text: issue.title)
end
end
@@ -33,7 +35,8 @@ describe 'Issues Feed', feature: true do
expect(response_headers['Content-Type']).
to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{project.name} issues")
- expect(body).to have_selector('author email', text: issue.author_email)
+ expect(body).to have_selector('author email', text: issue.author_public_email)
+ expect(body).to have_selector('assignee email', text: issue.author_public_email)
expect(body).to have_selector('entry summary', text: issue.title)
end
end
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index f7f2d883d2f..d17a418b8c3 100644
--- a/spec/features/boards/add_issues_modal_spec.rb
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -107,6 +107,9 @@ describe 'Issue Boards add issue modal', :feature, :js do
it 'returns issues' do
page.within('.add-issues-modal') do
find('.form-control').native.send_keys(issue.title)
+ find('.form-control').native.send_keys(:enter)
+
+ wait_for_vue_resource
expect(page).to have_selector('.card', count: 1)
end
@@ -115,6 +118,9 @@ describe 'Issue Boards add issue modal', :feature, :js do
it 'returns no issues' do
page.within('.add-issues-modal') do
find('.form-control').native.send_keys('testing search')
+ find('.form-control').native.send_keys(:enter)
+
+ wait_for_vue_resource
expect(page).not_to have_selector('.card')
expect(page).not_to have_content("You haven't added any issues to your project yet")
diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb
index 1cf0d11d448..e2281a7da55 100644
--- a/spec/features/boards/modal_filter_spec.rb
+++ b/spec/features/boards/modal_filter_spec.rb
@@ -1,7 +1,6 @@
require 'rails_helper'
describe 'Issue Boards add issue modal filtering', :feature, :js do
- include WaitForAjax
include WaitForVueResource
let(:project) { create(:empty_project, :public) }
@@ -23,6 +22,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
page.within('.add-issues-modal') do
find('.form-control').native.send_keys('testing empty state')
+ find('.form-control').native.send_keys(:enter)
wait_for_vue_resource
@@ -33,13 +33,11 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
it 'restores filters when closing' do
visit_board
- page.within('.add-issues-modal') do
- click_button 'Milestone'
-
- wait_for_ajax
-
- click_link 'Upcoming'
+ set_filter('milestone')
+ click_filter_link('Upcoming')
+ submit_filter
+ page.within('.add-issues-modal') do
wait_for_vue_resource
expect(page).to have_selector('.card', count: 0)
@@ -56,39 +54,44 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
end
end
- context 'author' do
- let!(:issue) { create(:issue, project: project, author: user2) }
-
- before do
- project.team << [user2, :developer]
+ it 'resotres filters after clicking clear button' do
+ visit_board
- visit_board
- end
+ set_filter('milestone')
+ click_filter_link('Upcoming')
+ submit_filter
- it 'filters by any author' do
- page.within('.add-issues-modal') do
- click_button 'Author'
+ page.within('.add-issues-modal') do
+ wait_for_vue_resource
- wait_for_ajax
+ expect(page).to have_selector('.card', count: 0)
- click_link 'Any Author'
+ find('.clear-search').click
- wait_for_vue_resource
+ wait_for_vue_resource
- expect(page).to have_selector('.card', count: 2)
- end
+ expect(page).to have_selector('.card', count: 1)
end
+ end
- it 'filters by selected user' do
- page.within('.add-issues-modal') do
- click_button 'Author'
+ context 'author' do
+ let!(:issue) { create(:issue, project: project, author: user2) }
+
+ before do
+ project.team << [user2, :developer]
- wait_for_ajax
+ visit_board
+ end
- click_link user2.name
+ it 'filters by selected user' do
+ set_filter('author')
+ click_filter_link(user2.name)
+ submit_filter
+ page.within('.add-issues-modal') do
wait_for_vue_resource
+ expect(page).to have_selector('.js-visual-token', text: user2.username)
expect(page).to have_selector('.card', count: 1)
end
end
@@ -103,46 +106,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
visit_board
end
- it 'filters by any assignee' do
- page.within('.add-issues-modal') do
- click_button 'Assignee'
-
- wait_for_ajax
-
- click_link 'Any Assignee'
-
- wait_for_vue_resource
-
- expect(page).to have_selector('.card', count: 2)
- end
- end
-
it 'filters by unassigned' do
- page.within('.add-issues-modal') do
- click_button 'Assignee'
-
- wait_for_ajax
-
- click_link 'Unassigned'
+ set_filter('assignee')
+ click_filter_link('No Assignee')
+ submit_filter
+ page.within('.add-issues-modal') do
wait_for_vue_resource
+ expect(page).to have_selector('.js-visual-token', text: 'none')
expect(page).to have_selector('.card', count: 1)
end
end
it 'filters by selected user' do
- page.within('.add-issues-modal') do
- click_button 'Assignee'
-
- wait_for_ajax
-
- page.within '.dropdown-menu-user' do
- click_link user2.name
- end
+ set_filter('assignee')
+ click_filter_link(user2.name)
+ submit_filter
+ page.within('.add-issues-modal') do
wait_for_vue_resource
+ expect(page).to have_selector('.js-visual-token', text: user2.username)
expect(page).to have_selector('.card', count: 1)
end
end
@@ -156,44 +141,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
visit_board
end
- it 'filters by any milestone' do
- page.within('.add-issues-modal') do
- click_button 'Milestone'
-
- wait_for_ajax
-
- click_link 'Any Milestone'
-
- wait_for_vue_resource
-
- expect(page).to have_selector('.card', count: 2)
- end
- end
-
it 'filters by upcoming milestone' do
- page.within('.add-issues-modal') do
- click_button 'Milestone'
-
- wait_for_ajax
-
- click_link 'Upcoming'
+ set_filter('milestone')
+ click_filter_link('Upcoming')
+ submit_filter
+ page.within('.add-issues-modal') do
wait_for_vue_resource
+ expect(page).to have_selector('.js-visual-token', text: 'upcoming')
expect(page).to have_selector('.card', count: 0)
end
end
it 'filters by selected milestone' do
- page.within('.add-issues-modal') do
- click_button 'Milestone'
-
- wait_for_ajax
-
- click_link milestone.name
+ set_filter('milestone')
+ click_filter_link(milestone.name)
+ submit_filter
+ page.within('.add-issues-modal') do
wait_for_vue_resource
+ expect(page).to have_selector('.js-visual-token', text: milestone.name)
expect(page).to have_selector('.card', count: 1)
end
end
@@ -207,44 +176,28 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
visit_board
end
- it 'filters by any label' do
- page.within('.add-issues-modal') do
- click_button 'Label'
-
- wait_for_ajax
-
- click_link 'Any Label'
-
- wait_for_vue_resource
-
- expect(page).to have_selector('.card', count: 2)
- end
- end
-
it 'filters by no label' do
- page.within('.add-issues-modal') do
- click_button 'Label'
-
- wait_for_ajax
-
- click_link 'No Label'
+ set_filter('label')
+ click_filter_link('No Label')
+ submit_filter
+ page.within('.add-issues-modal') do
wait_for_vue_resource
+ expect(page).to have_selector('.js-visual-token', text: 'none')
expect(page).to have_selector('.card', count: 1)
end
end
it 'filters by label' do
- page.within('.add-issues-modal') do
- click_button 'Label'
-
- wait_for_ajax
-
- click_link label.title
+ set_filter('label')
+ click_filter_link(label.title)
+ submit_filter
+ page.within('.add-issues-modal') do
wait_for_vue_resource
+ expect(page).to have_selector('.js-visual-token', text: label.title)
expect(page).to have_selector('.card', count: 1)
end
end
@@ -256,4 +209,20 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
click_button('Add issues')
end
+
+ def set_filter(type, text = '')
+ find('.add-issues-modal .filtered-search').native.send_keys("#{type}:#{text}")
+ end
+
+ def submit_filter
+ find('.add-issues-modal .filtered-search').native.send_keys(:enter)
+ end
+
+ def click_filter_link(link_text)
+ page.within('.add-issues-modal .filtered-search-input-container') do
+ expect(page).to have_button(link_text)
+
+ click_button(link_text)
+ end
+ end
end
diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb
index 4638812b2d9..55df7e45f79 100644
--- a/spec/features/copy_as_gfm_spec.rb
+++ b/spec/features/copy_as_gfm_spec.rb
@@ -2,437 +2,594 @@ require 'spec_helper'
describe 'Copy as GFM', feature: true, js: true do
include GitlabMarkdownHelper
+ include RepoHelpers
include ActionView::Helpers::JavaScriptHelper
before do
- @feat = MarkdownFeature.new
+ login_as :admin
+ end
- # `markdown` helper expects a `@project` variable
- @project = @feat.project
+ describe 'Copying rendered GFM' do
+ before do
+ @feat = MarkdownFeature.new
- visit namespace_project_issue_path(@project.namespace, @project, @feat.issue)
- end
+ # `markdown` helper expects a `@project` variable
+ @project = @feat.project
- # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML.
- # The handlers defined in app/assets/javascripts/copy_as_gfm.js.es6 consequently convert that same HTML to GFM.
- # To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle
- # by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper.
+ visit namespace_project_issue_path(@project.namespace, @project, @feat.issue)
+ end
- # These are all in a single `it` for performance reasons.
- it 'works', :aggregate_failures do
- verify(
- 'nesting',
+ # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML.
+ # The handlers defined in app/assets/javascripts/copy_as_gfm.js consequently convert that same HTML to GFM.
+ # To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle
+ # by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper.
- '> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**'
- )
+ # These are all in a single `it` for performance reasons.
+ it 'works', :aggregate_failures do
+ verify(
+ 'nesting',
- verify(
- 'a real world example from the gitlab-ce README',
+ '> 1. [x] **[$`2 + 2`$ {-=-}{+=+} 2^2 ~~:thumbsup:~~](http://google.com)**'
+ )
- <<-GFM.strip_heredoc
- # GitLab
+ verify(
+ 'a real world example from the gitlab-ce README',
- [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
- [![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
- [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
- [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
+ <<-GFM.strip_heredoc
+ # GitLab
- ## Canonical source
+ [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
+ [![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
+ [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
+ [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
- The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
+ ## Canonical source
- ## Open source software to collaborate on code
+ The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
- To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/).
+ ## Open source software to collaborate on code
+ To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/).
- - Manage Git repositories with fine grained access controls that keep your code secure
- - Perform code reviews and enhance collaboration with merge requests
+ - Manage Git repositories with fine grained access controls that keep your code secure
- - Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications
+ - Perform code reviews and enhance collaboration with merge requests
- - Each project can also have an issue tracker, issue board, and a wiki
+ - Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications
- - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
+ - Each project can also have an issue tracker, issue board, and a wiki
- - Completely free and open source (MIT Expat license)
- GFM
- )
+ - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
- verify(
- 'InlineDiffFilter',
+ - Completely free and open source (MIT Expat license)
+ GFM
+ )
- '{-Deleted text-}',
- '{+Added text+}'
- )
+ verify(
+ 'InlineDiffFilter',
- verify(
- 'TaskListFilter',
+ '{-Deleted text-}',
+ '{+Added text+}'
+ )
- '- [ ] Unchecked task',
- '- [x] Checked task',
- '1. [ ] Unchecked numbered task',
- '1. [x] Checked numbered task'
- )
+ verify(
+ 'TaskListFilter',
- verify(
- 'ReferenceFilter',
+ '- [ ] Unchecked task',
+ '- [x] Checked task',
+ '1. [ ] Unchecked numbered task',
+ '1. [x] Checked numbered task'
+ )
- # issue reference
- @feat.issue.to_reference,
- # full issue reference
- @feat.issue.to_reference(full: true),
- # issue URL
- namespace_project_issue_url(@project.namespace, @project, @feat.issue),
- # issue URL with note anchor
- namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123'),
- # issue link
- "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue)})",
- # issue link with note anchor
- "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123')})",
- )
+ verify(
+ 'ReferenceFilter',
- verify(
- 'AutolinkFilter',
+ # issue reference
+ @feat.issue.to_reference,
+ # full issue reference
+ @feat.issue.to_reference(full: true),
+ # issue URL
+ namespace_project_issue_url(@project.namespace, @project, @feat.issue),
+ # issue URL with note anchor
+ namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123'),
+ # issue link
+ "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue)})",
+ # issue link with note anchor
+ "[Issue](#{namespace_project_issue_url(@project.namespace, @project, @feat.issue, anchor: 'note_123')})",
+ )
- 'https://example.com'
- )
+ verify(
+ 'AutolinkFilter',
- verify(
- 'TableOfContentsFilter',
+ 'https://example.com'
+ )
- '[[_TOC_]]'
- )
+ verify(
+ 'TableOfContentsFilter',
- verify(
- 'EmojiFilter',
+ '[[_TOC_]]'
+ )
- ':thumbsup:'
- )
+ verify(
+ 'EmojiFilter',
- verify(
- 'ImageLinkFilter',
-
- '![Image](https://example.com/image.png)'
- )
+ ':thumbsup:'
+ )
- verify(
- 'VideoLinkFilter',
+ verify(
+ 'ImageLinkFilter',
+
+ '![Image](https://example.com/image.png)'
+ )
- '![Video](https://example.com/video.mp4)'
- )
+ verify(
+ 'VideoLinkFilter',
- verify(
- 'MathFilter: math as converted from GFM to HTML',
+ '![Video](https://example.com/video.mp4)'
+ )
- '$`c = \pm\sqrt{a^2 + b^2}`$',
+ verify(
+ 'MathFilter: math as converted from GFM to HTML',
- # math block
- <<-GFM.strip_heredoc
- ```math
- c = \pm\sqrt{a^2 + b^2}
- ```
- GFM
- )
+ '$`c = \pm\sqrt{a^2 + b^2}`$',
- aggregate_failures('MathFilter: math as transformed from HTML to KaTeX') do
- gfm = '$`c = \pm\sqrt{a^2 + b^2}`$'
+ # math block
+ <<-GFM.strip_heredoc
+ ```math
+ c = \pm\sqrt{a^2 + b^2}
+ ```
+ GFM
+ )
- html = <<-HTML.strip_heredoc
- <span class="katex">
- <span class="katex-mathml">
- <math>
- <semantics>
- <mrow>
- <mi>c</mi>
- <mo>=</mo>
- <mo>±</mo>
- <msqrt>
- <mrow>
- <msup>
- <mi>a</mi>
- <mn>2</mn>
- </msup>
- <mo>+</mo>
- <msup>
- <mi>b</mi>
- <mn>2</mn>
- </msup>
- </mrow>
- </msqrt>
- </mrow>
- <annotation encoding="application/x-tex">c = \\pm\\sqrt{a^2 + b^2}</annotation>
- </semantics>
- </math>
- </span>
- <span class="katex-html" aria-hidden="true">
- <span class="strut" style="height: 0.913389em;"></span>
- <span class="strut bottom" style="height: 1.04em; vertical-align: -0.126611em;"></span>
- <span class="base textstyle uncramped">
- <span class="mord mathit">c</span>
- <span class="mrel">=</span>
- <span class="mord">±</span>
- <span class="sqrt mord"><span class="sqrt-sign" style="top: -0.073389em;">
- <span class="style-wrap reset-textstyle textstyle uncramped">√</span>
- </span>
- <span class="vlist">
- <span class="" style="top: 0em;">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 1em;">​</span>
- </span>
- <span class="mord textstyle cramped">
- <span class="mord">
- <span class="mord mathit">a</span>
- <span class="msupsub">
- <span class="vlist">
- <span class="" style="top: -0.289em; margin-right: 0.05em;">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 0em;">​</span>
- </span>
- <span class="reset-textstyle scriptstyle cramped">
- <span class="mord mathrm">2</span>
+ aggregate_failures('MathFilter: math as transformed from HTML to KaTeX') do
+ gfm = '$`c = \pm\sqrt{a^2 + b^2}`$'
+
+ html = <<-HTML.strip_heredoc
+ <span class="katex">
+ <span class="katex-mathml">
+ <math>
+ <semantics>
+ <mrow>
+ <mi>c</mi>
+ <mo>=</mo>
+ <mo>±</mo>
+ <msqrt>
+ <mrow>
+ <msup>
+ <mi>a</mi>
+ <mn>2</mn>
+ </msup>
+ <mo>+</mo>
+ <msup>
+ <mi>b</mi>
+ <mn>2</mn>
+ </msup>
+ </mrow>
+ </msqrt>
+ </mrow>
+ <annotation encoding="application/x-tex">c = \\pm\\sqrt{a^2 + b^2}</annotation>
+ </semantics>
+ </math>
+ </span>
+ <span class="katex-html" aria-hidden="true">
+ <span class="strut" style="height: 0.913389em;"></span>
+ <span class="strut bottom" style="height: 1.04em; vertical-align: -0.126611em;"></span>
+ <span class="base textstyle uncramped">
+ <span class="mord mathit">c</span>
+ <span class="mrel">=</span>
+ <span class="mord">±</span>
+ <span class="sqrt mord"><span class="sqrt-sign" style="top: -0.073389em;">
+ <span class="style-wrap reset-textstyle textstyle uncramped">√</span>
+ </span>
+ <span class="vlist">
+ <span class="" style="top: 0em;">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 1em;">​</span>
+ </span>
+ <span class="mord textstyle cramped">
+ <span class="mord">
+ <span class="mord mathit">a</span>
+ <span class="msupsub">
+ <span class="vlist">
+ <span class="" style="top: -0.289em; margin-right: 0.05em;">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 0em;">​</span>
+ </span>
+ <span class="reset-textstyle scriptstyle cramped">
+ <span class="mord mathrm">2</span>
+ </span>
</span>
+ <span class="baseline-fix">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 0em;">​</span>
+ </span>
+ ​</span>
</span>
- <span class="baseline-fix">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 0em;">​</span>
- </span>
- ​</span>
</span>
</span>
- </span>
- <span class="mbin">+</span>
- <span class="mord">
- <span class="mord mathit">b</span>
- <span class="msupsub">
- <span class="vlist">
- <span class="" style="top: -0.289em; margin-right: 0.05em;">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 0em;">​</span>
- </span>
- <span class="reset-textstyle scriptstyle cramped">
- <span class="mord mathrm">2</span>
+ <span class="mbin">+</span>
+ <span class="mord">
+ <span class="mord mathit">b</span>
+ <span class="msupsub">
+ <span class="vlist">
+ <span class="" style="top: -0.289em; margin-right: 0.05em;">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 0em;">​</span>
+ </span>
+ <span class="reset-textstyle scriptstyle cramped">
+ <span class="mord mathrm">2</span>
+ </span>
</span>
+ <span class="baseline-fix">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 0em;">​</span>
+ </span>
+ ​</span>
</span>
- <span class="baseline-fix">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 0em;">​</span>
- </span>
- ​</span>
</span>
</span>
</span>
</span>
- </span>
- <span class="" style="top: -0.833389em;">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 1em;">​</span>
+ <span class="" style="top: -0.833389em;">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 1em;">​</span>
+ </span>
+ <span class="reset-textstyle textstyle uncramped sqrt-line"></span>
</span>
- <span class="reset-textstyle textstyle uncramped sqrt-line"></span>
+ <span class="baseline-fix">
+ <span class="fontsize-ensurer reset-size5 size5">
+ <span class="" style="font-size: 1em;">​</span>
+ </span>
+ ​</span>
</span>
- <span class="baseline-fix">
- <span class="fontsize-ensurer reset-size5 size5">
- <span class="" style="font-size: 1em;">​</span>
- </span>
- ​</span>
</span>
</span>
</span>
</span>
- </span>
- HTML
+ HTML
- output_gfm = html_to_gfm(html)
- expect(output_gfm.strip).to eq(gfm.strip)
- end
+ output_gfm = html_to_gfm(html)
+ expect(output_gfm.strip).to eq(gfm.strip)
+ end
- verify(
- 'SanitizationFilter',
+ verify(
+ 'SanitizationFilter',
- <<-GFM.strip_heredoc
- <a name="named-anchor"></a>
+ <<-GFM.strip_heredoc
+ <a name="named-anchor"></a>
- <sub>sub</sub>
+ <sub>sub</sub>
- <dl>
- <dt>dt</dt>
- <dd>dd</dd>
- </dl>
+ <dl>
+ <dt>dt</dt>
+ <dd>dd</dd>
+ </dl>
- <kbd>kbd</kbd>
+ <kbd>kbd</kbd>
- <q>q</q>
+ <q>q</q>
- <samp>samp</samp>
+ <samp>samp</samp>
- <var>var</var>
+ <var>var</var>
- <ruby>ruby</ruby>
+ <ruby>ruby</ruby>
- <rt>rt</rt>
+ <rt>rt</rt>
- <rp>rp</rp>
+ <rp>rp</rp>
- <abbr>abbr</abbr>
+ <abbr>abbr</abbr>
- <summary>summary</summary>
+ <summary>summary</summary>
- <details>details</details>
- GFM
- )
+ <details>details</details>
+ GFM
+ )
- verify(
- 'SanitizationFilter',
+ verify(
+ 'SanitizationFilter',
- <<-GFM.strip_heredoc,
- ```
- Plain text
- ```
- GFM
+ <<-GFM.strip_heredoc,
+ ```
+ Plain text
+ ```
+ GFM
- <<-GFM.strip_heredoc,
- ```ruby
- def foo
- bar
- end
- ```
- GFM
+ <<-GFM.strip_heredoc,
+ ```ruby
+ def foo
+ bar
+ end
+ ```
+ GFM
+
+ <<-GFM.strip_heredoc
+ Foo
+
+ This is an example of GFM
- <<-GFM.strip_heredoc
- Foo
+ ```js
+ Code goes here
+ ```
+ GFM
+ )
- This is an example of GFM
+ verify(
+ 'MarkdownFilter',
- ```js
- Code goes here
- ```
- GFM
- )
+ "Line with two spaces at the end \nto insert a linebreak",
- verify(
- 'MarkdownFilter',
+ '`code`',
+ '`` code with ` ticks ``',
- "Line with two spaces at the end \nto insert a linebreak",
+ '> Quote',
- '`code`',
- '`` code with ` ticks ``',
+ # multiline quote
+ <<-GFM.strip_heredoc,
+ > Multiline
+ > Quote
+ >
+ > With multiple paragraphs
+ GFM
- '> Quote',
+ '![Image](https://example.com/image.png)',
- # multiline quote
- <<-GFM.strip_heredoc,
- > Multiline
- > Quote
- >
- > With multiple paragraphs
- GFM
+ '# Heading with no anchor link',
- '![Image](https://example.com/image.png)',
+ '[Link](https://example.com)',
- '# Heading with no anchor link',
+ '- List item',
- '[Link](https://example.com)',
+ # multiline list item
+ <<-GFM.strip_heredoc,
+ - Multiline
+ List item
+ GFM
- '- List item',
+ # nested lists
+ <<-GFM.strip_heredoc,
+ - Nested
- # multiline list item
- <<-GFM.strip_heredoc,
- - Multiline
- List item
- GFM
- # nested lists
- <<-GFM.strip_heredoc,
- - Nested
+ - Lists
+ GFM
+ # list with blockquote
+ <<-GFM.strip_heredoc,
+ - List
- - Lists
- GFM
+ > Blockquote
+ GFM
- # list with blockquote
- <<-GFM.strip_heredoc,
- - List
+ '1. Numbered list item',
- > Blockquote
- GFM
+ # multiline numbered list item
+ <<-GFM.strip_heredoc,
+ 1. Multiline
+ Numbered list item
+ GFM
- '1. Numbered list item',
+ # nested numbered list
+ <<-GFM.strip_heredoc,
+ 1. Nested
- # multiline numbered list item
- <<-GFM.strip_heredoc,
- 1. Multiline
- Numbered list item
- GFM
- # nested numbered list
- <<-GFM.strip_heredoc,
- 1. Nested
+ 1. Numbered lists
+ GFM
+ '# Heading',
+ '## Heading',
+ '### Heading',
+ '#### Heading',
+ '##### Heading',
+ '###### Heading',
- 1. Numbered lists
- GFM
+ '**Bold**',
- '# Heading',
- '## Heading',
- '### Heading',
- '#### Heading',
- '##### Heading',
- '###### Heading',
+ '_Italics_',
- '**Bold**',
+ '~~Strikethrough~~',
- '_Italics_',
+ '2^2',
- '~~Strikethrough~~',
+ '-----',
- '2^2',
+ # table
+ <<-GFM.strip_heredoc,
+ | Centered | Right | Left |
+ |:--------:|------:|------|
+ | Foo | Bar | **Baz** |
+ | Foo | Bar | **Baz** |
+ GFM
- '-----',
+ # table with empty heading
+ <<-GFM.strip_heredoc,
+ | | x | y |
+ |---|---|---|
+ | a | 1 | 0 |
+ | b | 0 | 1 |
+ GFM
+ )
+ end
+
+ alias_method :gfm_to_html, :markdown
- # table
- <<-GFM.strip_heredoc,
- | Centered | Right | Left |
- |:--------:|------:|------|
- | Foo | Bar | **Baz** |
- | Foo | Bar | **Baz** |
- GFM
+ def verify(label, *gfms)
+ aggregate_failures(label) do
+ gfms.each do |gfm|
+ html = gfm_to_html(gfm)
+ output_gfm = html_to_gfm(html)
+ expect(output_gfm.strip).to eq(gfm.strip)
+ end
+ end
+ end
- # table with empty heading
- <<-GFM.strip_heredoc,
- | | x | y |
- |---|---|---|
- | a | 1 | 0 |
- | b | 0 | 1 |
- GFM
- )
+ # Fake a `current_user` helper
+ def current_user
+ @feat.user
+ end
end
- alias_method :gfm_to_html, :markdown
+ describe 'Copying code' do
+ let(:project) { create(:project) }
+
+ context 'from a diff' do
+ before do
+ visit namespace_project_commit_path(project.namespace, project, sample_commit.id)
+ end
+
+ context 'selecting one word of text' do
+ it 'copies as inline code' do
+ verify(
+ '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line .no',
- def html_to_gfm(html)
+ '`RuntimeError`'
+ )
+ end
+ end
+
+ context 'selecting one line of text' do
+ it 'copies as inline code' do
+ verify(
+ '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"] .line',
+
+ '`raise RuntimeError, "System commands must be given as an array of strings"`'
+ )
+ end
+ end
+
+ context 'selecting multiple lines of text' do
+ it 'copies as a code block' do
+ verify(
+ '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]',
+
+ <<-GFM.strip_heredoc,
+ ```ruby
+ raise RuntimeError, "System commands must be given as an array of strings"
+ end
+ ```
+ GFM
+ )
+ end
+ end
+ end
+
+ context 'from a blob' do
+ before do
+ visit namespace_project_blob_path(project.namespace, project, File.join('master', 'files/ruby/popen.rb'))
+ end
+
+ context 'selecting one word of text' do
+ it 'copies as inline code' do
+ verify(
+ '.line[id="LC9"] .no',
+
+ '`RuntimeError`'
+ )
+ end
+ end
+
+ context 'selecting one line of text' do
+ it 'copies as inline code' do
+ verify(
+ '.line[id="LC9"]',
+
+ '`raise RuntimeError, "System commands must be given as an array of strings"`'
+ )
+ end
+ end
+
+ context 'selecting multiple lines of text' do
+ it 'copies as a code block' do
+ verify(
+ '.line[id="LC9"], .line[id="LC10"]',
+
+ <<-GFM.strip_heredoc,
+ ```ruby
+ raise RuntimeError, "System commands must be given as an array of strings"
+ end
+ ```
+ GFM
+ )
+ end
+ end
+ end
+
+ context 'from a GFM code block' do
+ before do
+ visit namespace_project_blob_path(project.namespace, project, File.join('markdown', 'doc/api/users.md'))
+ end
+
+ context 'selecting one word of text' do
+ it 'copies as inline code' do
+ verify(
+ '.line[id="LC27"] .s2',
+
+ '`"bio"`'
+ )
+ end
+ end
+
+ context 'selecting one line of text' do
+ it 'copies as inline code' do
+ verify(
+ '.line[id="LC27"]',
+
+ '`"bio": null,`'
+ )
+ end
+ end
+
+ context 'selecting multiple lines of text' do
+ it 'copies as a code block with the correct language' do
+ verify(
+ '.line[id="LC27"], .line[id="LC28"]',
+
+ <<-GFM.strip_heredoc,
+ ```json
+ "bio": null,
+ "skype": "",
+ ```
+ GFM
+ )
+ end
+ end
+ end
+
+ def verify(selector, gfm)
+ html = html_for_selector(selector)
+ output_gfm = html_to_gfm(html, 'transformCodeSelection')
+ expect(output_gfm.strip).to eq(gfm.strip)
+ end
+ end
+
+ def html_for_selector(selector)
+ js = <<-JS.strip_heredoc
+ (function(selector) {
+ var els = document.querySelectorAll(selector);
+ var htmls = _.map(els, function(el) { return el.outerHTML; });
+ return htmls.join("\\n");
+ })("#{escape_javascript(selector)}")
+ JS
+ page.evaluate_script(js)
+ end
+
+ def html_to_gfm(html, transformer = 'transformGFMSelection')
js = <<-JS.strip_heredoc
(function(html) {
+ var transformer = window.gl.CopyAsGFM[#{transformer.inspect}];
+
var node = document.createElement('div');
node.innerHTML = html;
+
+ node = transformer(node);
+ if (!node) return null;
+
return window.gl.CopyAsGFM.nodeToGFM(node);
})("#{escape_javascript(html)}")
JS
page.evaluate_script(js)
end
-
- def verify(label, *gfms)
- aggregate_failures(label) do
- gfms.each do |gfm|
- html = gfm_to_html(gfm)
- output_gfm = html_to_gfm(html)
- expect(output_gfm.strip).to eq(gfm.strip)
- end
- end
- end
-
- # Fake a `current_user` helper
- def current_user
- @feat.user
- end
end
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 63eb5c697c2..c4e58d14f75 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -1,10 +1,32 @@
require 'spec_helper'
RSpec.describe 'Dashboard Projects', feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, name: "awesome stuff") }
+
before do
- login_as(create :user)
+ project.team << [user, :developer]
+ login_as user
visit dashboard_projects_path
end
-
+
+ it 'shows the project the user in a member of in the list' do
+ visit dashboard_projects_path
+ expect(page).to have_content('awesome stuff')
+ end
+
+ describe "with a pipeline" do
+ let(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.sha) }
+
+ before do
+ pipeline
+ end
+
+ it 'shows that the last pipeline passed' do
+ visit dashboard_projects_path
+ expect(page).to have_xpath("//a[@href='#{pipelines_namespace_project_commit_path(project.namespace, project, project.commit)}']")
+ end
+ end
+
it_behaves_like "an autodiscoverable RSS feed with current_user's private token"
end
diff --git a/spec/features/groups/group_name_toggle_spec.rb b/spec/features/groups/group_name_toggle_spec.rb
new file mode 100644
index 00000000000..8528718a2f7
--- /dev/null
+++ b/spec/features/groups/group_name_toggle_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+feature 'Group name toggle', feature: true, js: true do
+ let(:group) { create(:group) }
+ let(:nested_group_1) { create(:group, parent: group) }
+ let(:nested_group_2) { create(:group, parent: nested_group_1) }
+ let(:nested_group_3) { create(:group, parent: nested_group_2) }
+
+ before do
+ login_as :user
+ end
+
+ it 'is not present for less than 3 groups' do
+ visit group_path(group)
+ expect(page).not_to have_css('.group-name-toggle')
+
+ visit group_path(nested_group_1)
+ expect(page).not_to have_css('.group-name-toggle')
+ end
+
+ it 'is present for nested group of 3 or more in the namespace' do
+ visit group_path(nested_group_2)
+ expect(page).to have_css('.group-name-toggle')
+
+ visit group_path(nested_group_3)
+ expect(page).to have_css('.group-name-toggle')
+ end
+
+ context 'for group with at least 3 groups' do
+ before do
+ visit group_path(nested_group_2)
+ end
+
+ it 'should show the full group namespace when toggled' do
+ expect(page).not_to have_content(group.name)
+ expect(page).to have_css('.group-path.hidable', visible: false)
+
+ click_button '...'
+
+ expect(page).to have_content(group.name)
+ expect(page).to have_css('.group-path.hidable', visible: true)
+ end
+ end
+end
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index f424186cf30..16e453bc328 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -17,8 +17,21 @@ describe 'Awards Emoji', feature: true do
login_as(user)
end
+ describe 'visiting an issue with a legacy award emoji that is not valid anymore' do
+ before do
+ # The `heart_tip` emoji is not valid anymore so we need to skip validation
+ issue.award_emoji.build(user: user, name: 'heart_tip').save!(validate: false)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ # Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529
+ it 'does not shows a 500 page' do
+ expect(page).to have_text(issue.title)
+ end
+ end
+
describe 'Click award emoji from issue#show' do
- let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") }
+ let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") }
before do
visit namespace_project_issue_path(project.namespace, project, issue)
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index 19a00618b12..1772a120045 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -14,9 +14,10 @@ describe 'Dropdown author', js: true, feature: true do
def send_keys_to_filtered_search(input)
input.split("").each do |i|
filtered_search.send_keys(i)
- sleep 5
- wait_for_ajax
end
+
+ sleep 0.5
+ wait_for_ajax
end
def dropdown_author_size
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index 85ffffe4b6d..ce96a420699 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -202,6 +202,14 @@ describe 'Dropdown milestone', :feature, :js do
expect_tokens([{ name: 'milestone', value: 'upcoming' }])
expect_filtered_search_input_empty
end
+
+ it 'selects `started milestones`' do
+ click_static_milestone('Started')
+
+ expect(page).to have_css(js_dropdown_milestone, visible: false)
+ expect_tokens([{ name: 'milestone', value: 'started' }])
+ expect_filtered_search_input_empty
+ end
end
describe 'input has existing content' do
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index f079a9627e4..f463312bf57 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -8,13 +8,12 @@ describe 'Filter issues', js: true, feature: true do
let!(:project) { create(:project, group: group) }
let!(:user) { create(:user) }
let!(:user2) { create(:user) }
- let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) }
let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
let!(:bug_label) { create(:label, project: project, title: 'bug') }
let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') }
- let!(:milestone) { create(:milestone, title: "8", project: project) }
+ let!(:milestone) { create(:milestone, title: "8", project: project, start_date: 2.days.ago) }
let!(:multiple_words_label) { create(:label, project: project, title: "Two words") }
let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) }
@@ -505,6 +504,14 @@ describe 'Filter issues', js: true, feature: true do
expect_filtered_search_input_empty
end
+ it 'filters issues by started milestones' do
+ input_filtered_search("milestone:started")
+
+ expect_tokens([{ name: 'milestone', value: 'started' }])
+ expect_issues_list_count(5)
+ expect_filtered_search_input_empty
+ end
+
it 'filters issues by invalid milestones' do
skip('to be tested, issue #26546')
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 8c54259f475..6f831ea4b67 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -6,7 +6,7 @@ describe 'Issues', feature: true do
include SortingHelper
include WaitForAjax
- let(:project) { create(:project) }
+ let(:project) { create(:project, :public) }
before do
login_as :user
@@ -565,6 +565,24 @@ describe 'Issues', feature: true do
end
describe 'new issue' do
+ context 'by unauthenticated user' do
+ before do
+ logout
+ end
+
+ it 'redirects to signin then back to new issue after signin' do
+ visit namespace_project_issues_path(project.namespace, project)
+
+ click_link 'New issue'
+
+ expect(current_path).to eq new_user_session_path
+
+ login_as :user
+
+ expect(current_path).to eq new_namespace_project_issue_path(project.namespace, project)
+ end
+ end
+
context 'dropzone upload file', js: true do
before do
visit new_namespace_project_issue_path(project.namespace, project)
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
index 73c5ef31edc..18833ba7266 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -60,9 +60,6 @@ feature 'Merge request created from fork' do
expect(page).to have_content pipeline.status
expect(page).to have_content pipeline.id
end
-
- expect(page.find('a.btn-remove')[:href])
- .to include fork_project.path_with_namespace
end
end
diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb
index 6fed1568fcf..14511707af4 100644
--- a/spec/features/merge_requests/reset_filters_spec.rb
+++ b/spec/features/merge_requests/reset_filters_spec.rb
@@ -49,6 +49,26 @@ feature 'Merge requests filter clear button', feature: true, js: true do
end
end
+ context 'when multiple label filters have been applied' do
+ let!(:label) { create(:label, project: project, name: 'Frontend') }
+ let(:filter_dropdown) { find("#js-dropdown-label .filter-dropdown") }
+
+ before do
+ visit_merge_requests(project)
+ init_label_search
+ end
+
+ it 'filters bug label' do
+ filtered_search.set('~bug')
+
+ filter_dropdown.find('.filter-dropdown-item', text: bug.title).click
+ init_label_search
+
+ expect(filter_dropdown.find('.filter-dropdown-item', text: bug.title)).to be_visible
+ expect(filter_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
+ end
+ end
+
context 'when a text search has been conducted' do
it 'resets the text search filter' do
visit_merge_requests(project, search: 'Bug')
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
index 2f3c3e45ae6..a1f4eb2688b 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -133,7 +133,6 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
it 'changes target_branch in new merge_request' do
visit new_namespace_project_merge_request_path(another_project.namespace, another_project, new_url_opts)
- click_button "Compare branches and continue"
fill_in "merge_request_title", with: 'My brand new feature'
fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:"
diff --git a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
deleted file mode 100644
index e05fbb3715c..00000000000
--- a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'spec_helper'
-
-feature 'Profile > Notifications > User changes notified_of_own_activity setting', feature: true, js: true do
- let(:user) { create(:user) }
-
- before do
- login_as(user)
- end
-
- scenario 'User opts into receiving notifications about their own activity' do
- visit profile_notifications_path
-
- expect(page).not_to have_checked_field('user[notified_of_own_activity]')
-
- check 'user[notified_of_own_activity]'
-
- expect(page).to have_content('Notification settings saved')
- expect(page).to have_checked_field('user[notified_of_own_activity]')
- end
-
- scenario 'User opts out of receiving notifications about their own activity' do
- user.update!(notified_of_own_activity: true)
- visit profile_notifications_path
-
- expect(page).to have_checked_field('user[notified_of_own_activity]')
-
- uncheck 'user[notified_of_own_activity]'
-
- expect(page).to have_content('Notification settings saved')
- expect(page).not_to have_checked_field('user[notified_of_own_activity]')
- end
-end
diff --git a/spec/features/projects/blobs/user_create_spec.rb b/spec/features/projects/blobs/user_create_spec.rb
index 03d08c12612..5686868a0c4 100644
--- a/spec/features/projects/blobs/user_create_spec.rb
+++ b/spec/features/projects/blobs/user_create_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
feature 'New blob creation', feature: true, js: true do
include WaitForAjax
+ include TargetBranchHelpers
given(:user) { create(:user) }
given(:role) { :developer }
@@ -20,19 +21,6 @@ feature 'New blob creation', feature: true, js: true do
execute_script("ace.edit('editor').setValue('#{content}')")
end
- def select_branch_index(index)
- first('button.js-target-branch').click
- wait_for_ajax
- all('a[data-group="Branches"]')[index].click
- end
-
- def create_new_branch(name)
- first('button.js-target-branch').click
- click_link 'Create new branch'
- fill_in 'new_branch_name', with: name
- click_button 'Create'
- end
-
def commit_file
click_button 'Commit Changes'
end
@@ -53,12 +41,12 @@ feature 'New blob creation', feature: true, js: true do
context 'with different target branch' do
background do
edit_file
- select_branch_index(0)
+ select_branch('feature')
commit_file
end
scenario 'creates the blob in the different branch' do
- expect(page).to have_content 'test'
+ expect(page).to have_content 'feature'
expect(page).to have_content 'successfully created'
end
end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index d26a0caf036..8e0306ce83b 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -17,6 +17,14 @@ describe 'Branches', feature: true do
repository.branches { |branch| expect(page).to have_content("#{branch.name}") }
expect(page).to have_content("Protected branches can be managed in project settings")
end
+
+ it 'avoids a N+1 query in branches index' do
+ control_count = ActiveRecord::QueryRecorder.new { visit namespace_project_branches_path(project.namespace, project) }.count
+
+ %w(one two three four five).each { |ref| repository.add_branch(@user, ref, 'master') }
+
+ expect { visit namespace_project_branches_path(project.namespace, project) }.not_to exceed_query_limit(control_count)
+ end
end
describe 'Find branches' do
diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
new file mode 100644
index 00000000000..30a2b2bcf8c
--- /dev/null
+++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
@@ -0,0 +1,55 @@
+require 'rails_helper'
+
+feature 'Mini Pipeline Graph in Commit View', :js, :feature do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ before do
+ login_as(user)
+ end
+
+ context 'when commit has pipelines' do
+ let(:pipeline) do
+ create(:ci_empty_pipeline,
+ project: project,
+ ref: project.default_branch,
+ sha: project.commit.sha)
+ end
+
+ let(:build) do
+ create(:ci_build, pipeline: pipeline)
+ end
+
+ before do
+ build.run
+ visit namespace_project_commit_path(project.namespace, project, project.commit.id)
+ end
+
+ it 'should display a mini pipeline graph' do
+ expect(page).to have_selector('.mr-widget-pipeline-graph')
+ end
+
+ it 'should show the builds list when stage is clicked' do
+ first('.mini-pipeline-graph-dropdown-toggle').click
+
+ wait_for_ajax
+
+ page.within '.js-builds-dropdown-list' do
+ expect(page).to have_selector('.ci-status-icon-running')
+ expect(page).to have_content(build.stage)
+ end
+ end
+ end
+
+ context 'when commit does not have pipelines' do
+ before do
+ visit namespace_project_commit_path(project.namespace, project, project.commit.id)
+ end
+
+ it 'should not display a mini pipeline graph' do
+ expect(page).not_to have_selector('.mr-widget-pipeline-graph')
+ end
+ end
+end
diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb
index 43eb4000e58..030043d14aa 100644
--- a/spec/features/projects/compare_spec.rb
+++ b/spec/features/projects/compare_spec.rb
@@ -26,6 +26,14 @@ describe "Compare", js: true do
click_button "Compare"
expect(page).to have_content "Commits"
end
+
+ it "filters branches" do
+ select_using_dropdown("from", "wip")
+
+ find(".js-compare-from-dropdown .compare-dropdown-toggle").click
+
+ expect(find(".js-compare-from-dropdown .dropdown-content")).to have_selector("li", count: 3)
+ end
end
describe "tags" do
diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb
index 8b302a6aa23..4c28205da9b 100644
--- a/spec/features/projects/group_links_spec.rb
+++ b/spec/features/projects/group_links_spec.rb
@@ -8,7 +8,7 @@ feature 'Project group links', feature: true, js: true do
let!(:group) { create(:group) }
background do
- project.team << [master, :master]
+ project.add_master(master)
login_as(master)
end
@@ -29,4 +29,26 @@ feature 'Project group links', feature: true, js: true do
end
end
end
+
+ context 'nested group project' do
+ let!(:nested_group) { create(:group, parent: group) }
+ let!(:another_group) { create(:group) }
+ let!(:project) { create(:project, namespace: nested_group) }
+
+ background do
+ group.add_master(master)
+ another_group.add_master(master)
+ end
+
+ it 'does not show ancestors' do
+ visit namespace_project_settings_members_path(project.namespace, project)
+
+ click_link 'Search for a group'
+
+ page.within '.select2-drop' do
+ expect(page).to have_content(another_group.name)
+ expect(page).not_to have_content(group.name)
+ end
+ end
+ end
end
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index 20cdfbae24f..399c1d478c5 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
index de3c6eceb82..e2911a37e40 100644
--- a/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
+++ b/spec/features/projects/labels/issues_sorted_by_priority_spec.rb
@@ -29,7 +29,7 @@ feature 'Issue prioritization', feature: true do
issue_1.labels << label_5
login_as user
- visit namespace_project_issues_path(project.namespace, project, sort: 'priority')
+ visit namespace_project_issues_path(project.namespace, project, sort: 'label_priority')
# Ensure we are indicating that issues are sorted by priority
expect(page).to have_selector('.dropdown-toggle', text: 'Label priority')
@@ -68,7 +68,7 @@ feature 'Issue prioritization', feature: true do
issue_6.labels << label_5 # 8 - No priority
login_as user
- visit namespace_project_issues_path(project.namespace, project, sort: 'priority')
+ visit namespace_project_issues_path(project.namespace, project, sort: 'label_priority')
expect(page).to have_selector('.dropdown-toggle', text: 'Label priority')
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index 45185f2dd1f..52196ce49bd 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -16,6 +16,15 @@ feature "New project", feature: true do
expect(find_field("project_visibility_level_#{level}")).to be_checked
end
+
+ it 'saves visibility level on validation error' do
+ visit new_project_path
+
+ choose(key)
+ click_button('Create project')
+
+ expect(find_field("project_visibility_level_#{level}")).to be_checked
+ end
end
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 22bf1bfbdf0..162056671e0 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -99,15 +99,18 @@ describe 'Pipelines', :feature, :js do
end
it 'indicates that pipeline can be canceled' do
- expect(page).to have_link('Cancel')
+ expect(page).to have_selector('.js-pipelines-cancel-button')
expect(page).to have_selector('.ci-running')
end
context 'when canceling' do
- before { click_link('Cancel') }
+ before do
+ find('.js-pipelines-cancel-button').click
+ wait_for_vue_resource
+ end
it 'indicated that pipelines was canceled' do
- expect(page).not_to have_link('Cancel')
+ expect(page).not_to have_selector('.js-pipelines-cancel-button')
expect(page).to have_selector('.ci-canceled')
end
end
@@ -126,15 +129,18 @@ describe 'Pipelines', :feature, :js do
end
it 'indicates that pipeline can be retried' do
- expect(page).to have_link('Retry')
+ expect(page).to have_selector('.js-pipelines-retry-button')
expect(page).to have_selector('.ci-failed')
end
context 'when retrying' do
- before { click_link('Retry') }
+ before do
+ find('.js-pipelines-retry-button').click
+ wait_for_vue_resource
+ end
it 'shows running pipeline that is not retryable' do
- expect(page).not_to have_link('Retry')
+ expect(page).not_to have_selector('.js-pipelines-retry-button')
expect(page).to have_selector('.ci-running')
end
end
@@ -176,17 +182,17 @@ describe 'Pipelines', :feature, :js do
it 'has link to the manual action' do
find('.js-pipeline-dropdown-manual-actions').click
- expect(page).to have_link('manual build')
+ expect(page).to have_button('manual build')
end
context 'when manual action was played' do
before do
find('.js-pipeline-dropdown-manual-actions').click
- click_link('manual build')
+ click_button('manual build')
end
it 'enqueues manual action job' do
- expect(manual.reload).to be_pending
+ expect(page).to have_selector('.js-pipeline-dropdown-manual-actions:disabled')
end
end
end
@@ -203,7 +209,7 @@ describe 'Pipelines', :feature, :js do
before { visit_project_pipelines }
it 'is cancelable' do
- expect(page).to have_link('Cancel')
+ expect(page).to have_selector('.js-pipelines-cancel-button')
end
it 'has pipeline running' do
@@ -211,10 +217,10 @@ describe 'Pipelines', :feature, :js do
end
context 'when canceling' do
- before { click_link('Cancel') }
+ before { find('.js-pipelines-cancel-button').trigger('click') }
it 'indicates that pipeline was canceled' do
- expect(page).not_to have_link('Cancel')
+ expect(page).not_to have_selector('.js-pipelines-cancel-button')
expect(page).to have_selector('.ci-canceled')
end
end
@@ -233,7 +239,7 @@ describe 'Pipelines', :feature, :js do
end
it 'is not retryable' do
- expect(page).not_to have_link('Retry')
+ expect(page).not_to have_selector('.js-pipelines-retry-button')
end
it 'has failed pipeline' do
diff --git a/spec/features/projects/services/slack_service_spec.rb b/spec/features/projects/services/slack_service_spec.rb
index 16541f51d98..c0a4a1e4bf5 100644
--- a/spec/features/projects/services/slack_service_spec.rb
+++ b/spec/features/projects/services/slack_service_spec.rb
@@ -7,7 +7,7 @@ feature 'Projects > Slack service > Setup events', feature: true do
background do
service.fields
- service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, build_channel: 6, wiki_page_channel: 7)
+ service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, pipeline_channel: 6, wiki_page_channel: 7)
project.team << [user, :master]
login_as(user)
end
@@ -20,7 +20,7 @@ feature 'Projects > Slack service > Setup events', feature: true do
expect(page.find_field("service_merge_request_channel").value).to have_content '3'
expect(page.find_field("service_note_channel").value).to have_content '4'
expect(page.find_field("service_tag_push_channel").value).to have_content '5'
- expect(page.find_field("service_build_channel").value).to have_content '6'
+ expect(page.find_field("service_pipeline_channel").value).to have_content '6'
expect(page.find_field("service_wiki_page_channel").value).to have_content '7'
end
end
diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb
index 6815039d5ed..321af416c91 100644
--- a/spec/features/projects/settings/merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/merge_requests_settings_spec.rb
@@ -62,4 +62,27 @@ feature 'Project settings > Merge Requests', feature: true, js: true do
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
end
end
+
+ describe 'Checkbox to enable merge request link' do
+ before do
+ visit edit_project_path(project)
+ end
+
+ scenario 'is initially checked' do
+ checkbox = find_field('project_printing_merge_request_link_enabled')
+ expect(checkbox).to be_checked
+ end
+
+ scenario 'when unchecked sets :printing_merge_request_link_enabled to false' do
+ uncheck('project_printing_merge_request_link_enabled')
+ click_on('Save')
+
+ # Wait for save to complete and page to reload
+ checkbox = find_field('project_printing_merge_request_link_enabled')
+ expect(checkbox).not_to be_checked
+
+ project.reload
+ expect(project.printing_merge_request_link_enabled).to be(false)
+ end
+ end
end
diff --git a/spec/features/projects/user_create_dir_spec.rb b/spec/features/projects/user_create_dir_spec.rb
new file mode 100644
index 00000000000..2065abfb248
--- /dev/null
+++ b/spec/features/projects/user_create_dir_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+feature 'New directory creation', feature: true, js: true do
+ include WaitForAjax
+ include TargetBranchHelpers
+
+ given(:user) { create(:user) }
+ given(:role) { :developer }
+ given(:project) { create(:project) }
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ visit namespace_project_tree_path(project.namespace, project, 'master')
+ open_new_directory_modal
+ fill_in 'dir_name', with: 'new_directory'
+ end
+
+ def open_new_directory_modal
+ first('.add-to-tree').click
+ click_link 'New directory'
+ end
+
+ def create_directory
+ click_button 'Create directory'
+ end
+
+ context 'with default target branch' do
+ background do
+ create_directory
+ end
+
+ scenario 'creates the directory in the default branch' do
+ expect(page).to have_content 'master'
+ expect(page).to have_content 'The directory has been successfully created'
+ expect(page).to have_content 'new_directory'
+ end
+ end
+
+ context 'with different target branch' do
+ background do
+ select_branch('feature')
+ create_directory
+ end
+
+ scenario 'creates the directory in the different branch' do
+ expect(page).to have_content 'feature'
+ expect(page).to have_content 'The directory has been successfully created'
+ end
+ end
+
+ context 'with a new target branch' do
+ given(:new_branch_name) { 'new-feature' }
+
+ background do
+ create_new_branch(new_branch_name)
+ create_directory
+ end
+
+ scenario 'creates the directory in the new branch' do
+ expect(page).to have_content new_branch_name
+ expect(page).to have_content 'The directory has been successfully created'
+ end
+
+ scenario 'redirects to the merge request' do
+ expect(page).to have_content 'New Merge Request'
+ expect(page).to have_content "From #{new_branch_name} into master"
+ expect(page).to have_content 'Add new directory'
+ expect(current_path).to eq(new_namespace_project_merge_request_path(project.namespace, project))
+ end
+ end
+end
diff --git a/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb
new file mode 100644
index 00000000000..6825b95c8aa
--- /dev/null
+++ b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe 'Projects > Wiki > User views Git access wiki page', :feature do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:wiki_page) do
+ WikiPages::CreateService.new(
+ project,
+ user,
+ title: 'home',
+ content: '[some link](other-page)'
+ ).execute
+ end
+
+ before do
+ login_as(user)
+ end
+
+ scenario 'Visit Wiki Page Current Commit' do
+ visit namespace_project_wiki_path(project.namespace, project, wiki_page)
+
+ click_link 'Clone repository'
+ expect(page).to have_text("Clone repository #{project.wiki.path_with_namespace}")
+ expect(page).to have_text(project.wiki.http_url_to_repo(user))
+ end
+end
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index f842d14fa96..aedc0333cb9 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -15,15 +15,30 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do
context 'in the user namespace' do
let(:project) { create(:project, namespace: user.namespace) }
- scenario 'the home page' do
- click_link 'Edit'
-
- fill_in :wiki_content, with: 'My awesome wiki!'
- click_button 'Save changes'
-
- expect(page).to have_content('Home')
- expect(page).to have_content("Last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
+ context 'the home page' do
+ scenario 'success when the wiki content is not empty' do
+ click_link 'Edit'
+
+ fill_in :wiki_content, with: 'My awesome wiki!'
+ click_button 'Save changes'
+
+ expect(page).to have_content('Home')
+ expect(page).to have_content("Last edited by #{user.name}")
+ expect(page).to have_content('My awesome wiki!')
+ end
+
+ scenario 'failure when the wiki content is empty' do
+ click_link 'Edit'
+
+ fill_in :wiki_content, with: ''
+ click_button 'Save changes'
+
+ expect(page).to have_selector('.wiki-form')
+ expect(page).to have_content('Edit Page')
+ expect(page).to have_content('The form contains the following error:')
+ expect(page).to have_content('Content can\'t be blank')
+ expect(find('textarea#wiki_content').value).to eq ''
+ end
end
end
diff --git a/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb
new file mode 100644
index 00000000000..c17e06612de
--- /dev/null
+++ b/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+feature 'Projects > Wiki > User views the wiki page', feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:old_page_version_id) { wiki_page.versions.last.id }
+ let(:wiki_page) do
+ WikiPages::CreateService.new(
+ project,
+ user,
+ title: 'home',
+ content: '[some link](other-page)'
+ ).execute
+ end
+
+ background do
+ project.team << [user, :master]
+ login_as(user)
+ WikiPages::UpdateService.new(
+ project,
+ user,
+ message: 'updated home',
+ content: 'updated [some link](other-page)',
+ format: :markdown
+ ).execute(wiki_page)
+ end
+
+ scenario 'Visit Wiki Page Current Commit' do
+ visit namespace_project_wiki_path(project.namespace, project, wiki_page)
+
+ expect(page).to have_selector('a.btn', text: 'Edit')
+ end
+
+ scenario 'Visit Wiki Page Historical Commit' do
+ visit namespace_project_wiki_path(
+ project.namespace,
+ project,
+ wiki_page,
+ version_id: old_page_version_id
+ )
+
+ expect(page).not_to have_selector('a.btn', text: 'Edit')
+ end
+end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 3a1240f95b5..ba56030e28d 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -56,7 +56,7 @@ feature 'Project', feature: true do
end
describe 'removal', js: true do
- let(:user) { create(:user) }
+ let(:user) { create(:user, username: 'test', name: 'test') }
let(:project) { create(:project, namespace: user.namespace, name: 'project1') }
before do
@@ -67,7 +67,7 @@ feature 'Project', feature: true do
it 'removes a project' do
expect { remove_with_confirm('Remove project', project.path) }.to change {Project.count}.by(-1)
- expect(page).to have_content "Project 'project1' will be deleted."
+ expect(page).to have_content "Project 'test / project1' will be deleted."
expect(Project.all.count).to be_zero
expect(project.issues).to be_empty
expect(project.merge_requests).to be_empty
diff --git a/spec/features/tags/master_views_tags_spec.rb b/spec/features/tags/master_views_tags_spec.rb
index 29d2c244720..555f84c4772 100644
--- a/spec/features/tags/master_views_tags_spec.rb
+++ b/spec/features/tags/master_views_tags_spec.rb
@@ -27,10 +27,20 @@ feature 'Master views tags', feature: true do
context 'when project has tags' do
let(:project) { create(:project, namespace: user.namespace) }
+ let(:repository) { project.repository }
+
before do
visit namespace_project_tags_path(project.namespace, project)
end
+ scenario 'avoids a N+1 query in branches index' do
+ control_count = ActiveRecord::QueryRecorder.new { visit namespace_project_tags_path(project.namespace, project) }.count
+
+ %w(one two three four five).each { |tag| repository.add_tag(user, tag, 'master', 'foo') }
+
+ expect { visit namespace_project_tags_path(project.namespace, project) }.not_to exceed_query_limit(control_count)
+ end
+
scenario 'views the tags list page' do
expect(page).to have_content 'v1.0.0'
end
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 5c2df949ac5..850020109d4 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -31,7 +31,7 @@ describe 'Dashboard Todos', feature: true do
end
it 'shows due date as today' do
- page.within first('.todo') do
+ within first('.todo') do
expect(page).to have_content 'Due today'
end
end
@@ -184,6 +184,60 @@ describe 'Dashboard Todos', feature: true do
expect(page).to have_content "You're all done!"
expect(page).not_to have_selector('.gl-pagination')
end
+
+ it 'shows "Undo mark all as done" button' do
+ expect(page).to have_selector('.js-todos-mark-all', visible: false)
+ expect(page).to have_selector('.js-todos-undo-all', visible: true)
+ end
+ end
+
+ describe 'undo mark all as done', js: true do
+ before do
+ visit dashboard_todos_path
+ end
+
+ it 'shows the restored todo list' do
+ mark_all_and_undo
+
+ expect(page).to have_selector('.todos-list .todo', count: 1)
+ expect(page).to have_selector('.gl-pagination')
+ expect(page).not_to have_content "You're all done!"
+ end
+
+ it 'updates todo count' do
+ mark_all_and_undo
+
+ expect(page).to have_content 'To do 2'
+ expect(page).to have_content 'Done 0'
+ end
+
+ it 'shows "Mark all as done" button' do
+ mark_all_and_undo
+
+ expect(page).to have_selector('.js-todos-mark-all', visible: true)
+ expect(page).to have_selector('.js-todos-undo-all', visible: false)
+ end
+
+ context 'User has deleted a todo' do
+ before do
+ within first('.todo') do
+ click_link 'Done'
+ end
+ end
+
+ it 'shows the restored todo list with the deleted todo' do
+ mark_all_and_undo
+
+ expect(page).to have_selector('.todos-list .todo.todo-pending', count: 1)
+ end
+ end
+
+ def mark_all_and_undo
+ click_link 'Mark all as done'
+ wait_for_ajax
+ click_link 'Undo mark all as done'
+ wait_for_ajax
+ end
end
end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 2a008427478..ee52dc65175 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -101,6 +101,41 @@ describe IssuesFinder do
end
end
+ context 'filtering by started milestone' do
+ let(:params) { { milestone_title: Milestone::Started.name } }
+
+ let(:project_no_started_milestones) { create(:empty_project, :public) }
+ let(:project_started_1_and_2) { create(:empty_project, :public) }
+ let(:project_started_8) { create(:empty_project, :public) }
+
+ let(:yesterday) { Date.today - 1.day }
+ let(:tomorrow) { Date.today + 1.day }
+ let(:two_days_ago) { Date.today - 2.days }
+
+ let(:milestones) do
+ [
+ create(:milestone, project: project_no_started_milestones, start_date: tomorrow),
+ create(:milestone, project: project_started_1_and_2, title: '1.0', start_date: two_days_ago),
+ create(:milestone, project: project_started_1_and_2, title: '2.0', start_date: yesterday),
+ create(:milestone, project: project_started_1_and_2, title: '3.0', start_date: tomorrow),
+ create(:milestone, project: project_started_8, title: '7.0'),
+ create(:milestone, project: project_started_8, title: '8.0', start_date: yesterday),
+ create(:milestone, project: project_started_8, title: '9.0', start_date: tomorrow)
+ ]
+ end
+
+ before do
+ milestones.each do |milestone|
+ create(:issue, project: milestone.project, milestone: milestone, author: user, assignee: user)
+ end
+ end
+
+ it 'returns issues in the started milestones for each project' do
+ expect(issues.map { |issue| issue.milestone.title }).to contain_exactly('1.0', '2.0', '8.0')
+ expect(issues.map { |issue| issue.milestone.start_date }).to contain_exactly(two_days_ago, yesterday, yesterday)
+ end
+ end
+
context 'filtering by label' do
let(:params) { { label_name: label.title } }
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index fa516f9903e..bead7948486 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -19,12 +19,12 @@ describe BlobHelper do
describe '#highlight' do
it 'returns plaintext for unknown lexer context' do
result = helper.highlight(blob_name, no_context_content)
- expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line">:type "assem"))</span></code></pre>])
+ expect(result).to eq(%[<pre class="code highlight"><code><span id="LC1" class="line" lang="">:type "assem"))</span></code></pre>])
end
it 'highlights single block' do
- expected = %Q[<pre class="code highlight"><code><span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
-<span id="LC2" class="line"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>]
+ expected = %Q[<pre class="code highlight"><code><span id="LC1" class="line" lang="common_lisp"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>
+<span id="LC2" class="line" lang="common_lisp"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span></code></pre>]
expect(helper.highlight(blob_name, blob_content)).to eq(expected)
end
@@ -43,10 +43,10 @@ describe BlobHelper do
let(:blob_name) { 'test.diff' }
let(:blob_content) { "+aaa\n+bbb\n- ccc\n ddd\n"}
let(:expected) do
- %q(<pre class="code highlight"><code><span id="LC1" class="line"><span class="gi">+aaa</span></span>
-<span id="LC2" class="line"><span class="gi">+bbb</span></span>
-<span id="LC3" class="line"><span class="gd">- ccc</span></span>
-<span id="LC4" class="line"> ddd</span></code></pre>)
+ %q(<pre class="code highlight"><code><span id="LC1" class="line" lang="diff"><span class="gi">+aaa</span></span>
+<span id="LC2" class="line" lang="diff"><span class="gi">+bbb</span></span>
+<span id="LC3" class="line" lang="diff"><span class="gd">- ccc</span></span>
+<span id="LC4" class="line" lang="diff"> ddd</span></code></pre>)
end
it 'highlights each line properly' do
diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb
index 637b02d9388..174cc84a97b 100644
--- a/spec/helpers/ci_status_helper_spec.rb
+++ b/spec/helpers/ci_status_helper_spec.rb
@@ -16,4 +16,11 @@ describe CiStatusHelper do
helper.ci_icon_for_status(failed_commit.status)
end
end
+
+ describe "#pipeline_status_cache_key" do
+ it "builds a cache key for pipeline status" do
+ pipeline_status = Ci::PipelineStatus.new(build(:project), sha: "123abc", status: "success")
+ expect(helper.pipeline_status_cache_key(pipeline_status)).to eq("pipeline-status/123abc-success")
+ end
+ end
end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index 81ba693f2f3..70443d27f33 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -28,7 +28,7 @@ describe EventsHelper do
it 'displays the first line of a code block' do
input = "```\nCode block\nwith two lines\n```"
- expected = %r{<pre.+><code>Code block\.\.\.</code></pre>}
+ expected = %r{<pre.+><code><span class="line">Code block\.\.\.</span>\n</code></pre>}
expect(helper.event_note(input)).to match(expected)
end
@@ -55,10 +55,8 @@ describe EventsHelper do
it 'preserves code color scheme' do
input = "```ruby\ndef test\n 'hello world'\nend\n```"
expected = '<pre class="code highlight js-syntax-highlight ruby">' \
- "<code><span class=\"k\">def</span> <span class=\"nf\">test</span>\n" \
- " <span class=\"s1\">\'hello world\'</span>\n" \
- "<span class=\"k\">end</span>\n" \
- '</code></pre>'
+ "<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \
+ "</code></pre>"
expect(helper.event_note(input)).to eq(expected)
end
diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb
index 68b20a1e4fc..3cb809d42b5 100644
--- a/spec/helpers/milestones_helper_spec.rb
+++ b/spec/helpers/milestones_helper_spec.rb
@@ -47,4 +47,58 @@ describe MilestonesHelper do
end
end
end
+
+ describe '#milestone_remaining_days' do
+ around do |example|
+ Timecop.freeze(Time.utc(2017, 3, 17)) { example.run }
+ end
+
+ context 'when less than 31 days remaining' do
+ let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) }
+
+ it 'returns days remaining' do
+ expect(milestone_remaining).to eq("<strong>12</strong> days remaining")
+ end
+ end
+
+ context 'when less than 1 year and more than 30 days remaining' do
+ let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.months.from_now.utc)) }
+
+ it 'returns months remaining' do
+ expect(milestone_remaining).to eq("<strong>2</strong> months remaining")
+ end
+ end
+
+ context 'when more than 1 year remaining' do
+ let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: (1.year.from_now + 2.days).utc)) }
+
+ it 'returns years remaining' do
+ expect(milestone_remaining).to eq("<strong>1</strong> year remaining")
+ end
+ end
+
+ context 'when milestone is expired' do
+ let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.days.ago.utc)) }
+
+ it 'returns "Past due"' do
+ expect(milestone_remaining).to eq("<strong>Past due</strong>")
+ end
+ end
+
+ context 'when milestone has start_date in the future' do
+ let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.from_now.utc)) }
+
+ it 'returns "Upcoming"' do
+ expect(milestone_remaining).to eq("<strong>Upcoming</strong>")
+ end
+ end
+
+ context 'when milestone has start_date in the past' do
+ let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.ago.utc)) }
+
+ it 'returns days elapsed' do
+ expect(milestone_remaining).to eq("<strong>2</strong> days elapsed")
+ end
+ end
+ end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index aca0bb1d794..fc6ad6419ac 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -63,6 +63,46 @@ describe ProjectsHelper do
end
end
+ describe "#project_list_cache_key" do
+ let(:project) { create(:project) }
+
+ it "includes the namespace" do
+ expect(helper.project_list_cache_key(project)).to include(project.namespace.cache_key)
+ end
+
+ it "includes the project" do
+ expect(helper.project_list_cache_key(project)).to include(project.cache_key)
+ end
+
+ it "includes the controller name" do
+ expect(helper.controller).to receive(:controller_name).and_return("testcontroller")
+
+ expect(helper.project_list_cache_key(project)).to include("testcontroller")
+ end
+
+ it "includes the controller action" do
+ expect(helper.controller).to receive(:action_name).and_return("testaction")
+
+ expect(helper.project_list_cache_key(project)).to include("testaction")
+ end
+
+ it "includes the application settings" do
+ settings = Gitlab::CurrentSettings.current_application_settings
+
+ expect(helper.project_list_cache_key(project)).to include(settings.cache_key)
+ end
+
+ it "includes a version" do
+ expect(helper.project_list_cache_key(project)).to include("v2.3")
+ end
+
+ it "includes the pipeline status when there is a status" do
+ create(:ci_pipeline, :success, project: project, sha: project.commit.sha)
+
+ expect(helper.project_list_cache_key(project)).to include("pipeline-status/#{project.commit.sha}-success")
+ end
+ end
+
describe 'link_to_member' do
let(:group) { create(:group) }
let(:project) { create(:empty_project, group: group) }
diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb
new file mode 100644
index 00000000000..21e0e74e008
--- /dev/null
+++ b/spec/helpers/todos_helper_spec.rb
@@ -0,0 +1,57 @@
+require "spec_helper"
+
+describe TodosHelper do
+ include GitlabRoutingHelper
+
+ describe '#todo_target_path' do
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, target_project: project, source_project: project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:note) { create(:note_on_issue, noteable: issue, project: project) }
+
+ let(:mr_todo) { build(:todo, project: project, target: merge_request) }
+ let(:issue_todo) { build(:todo, project: project, target: issue) }
+ let(:note_todo) { build(:todo, project: project, target: issue, note: note) }
+ let(:build_failed_todo) { build(:todo, :build_failed, project: project, target: merge_request) }
+
+ it 'returns correct path to the todo MR' do
+ expect(todo_target_path(mr_todo)).
+ to eq("/#{project.full_path}/merge_requests/#{merge_request.iid}")
+ end
+
+ it 'returns correct path to the todo issue' do
+ expect(todo_target_path(issue_todo)).
+ to eq("/#{project.full_path}/issues/#{issue.iid}")
+ end
+
+ it 'returns correct path to the todo note' do
+ expect(todo_target_path(note_todo)).
+ to eq("/#{project.full_path}/issues/#{issue.iid}#note_#{note.id}")
+ end
+
+ it 'returns correct path to build_todo MR when pipeline failed' do
+ expect(todo_target_path(build_failed_todo)).
+ to eq("/#{project.full_path}/merge_requests/#{merge_request.iid}/pipelines")
+ end
+ end
+
+ describe '#todo_projects_options' do
+ let(:projects) { create_list(:empty_project, 3) }
+ let(:user) { create(:user) }
+
+ it 'returns users authorised projects in json format' do
+ projects.first.add_developer(user)
+ projects.second.add_developer(user)
+
+ allow(helper).to receive(:current_user).and_return(user)
+
+ expected_results = [
+ { 'id' => '', 'text' => 'Any Project' },
+ { 'id' => projects.second.id, 'text' => projects.second.name_with_namespace },
+ { 'id' => projects.first.id, 'text' => projects.first.name_with_namespace }
+ ]
+
+ expect(JSON.parse(helper.todo_projects_options)).to match_array(expected_results)
+ end
+ end
+end
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index 0a6e042b700..ea7753c7a1d 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -287,6 +287,20 @@ import AwardsHandler from '~/awards_handler';
done.fail(`Failed to open and build emoji menu: ${err.message}`);
});
});
+
+ it('should disregard invalid frequently used emoji that are being attempted to be added', function() {
+ awardsHandler.addEmojiToFrequentlyUsedList('8ball');
+ awardsHandler.addEmojiToFrequentlyUsedList('invalid_emoji');
+ awardsHandler.addEmojiToFrequentlyUsedList('grinning');
+
+ expect(awardsHandler.getFrequentlyUsedEmojis()).toEqual(['8ball', 'grinning']);
+ });
+
+ it('should disregard invalid frequently used emoji already set in cookie', function() {
+ Cookies.set('frequently_used_emojis', '8ball,invalid_emoji,grinning');
+
+ expect(awardsHandler.getFrequentlyUsedEmojis()).toEqual(['8ball', 'grinning']);
+ });
});
});
}).call(window);
diff --git a/spec/javascripts/boards/board_blank_state_spec.js b/spec/javascripts/boards/board_blank_state_spec.js
new file mode 100644
index 00000000000..47baf83512f
--- /dev/null
+++ b/spec/javascripts/boards/board_blank_state_spec.js
@@ -0,0 +1,93 @@
+/* global BoardService */
+import Vue from 'vue';
+import '~/boards/stores/boards_store';
+import boardBlankState from '~/boards/components/board_blank_state';
+import './mock_data';
+
+describe('Boards blank state', () => {
+ let vm;
+ let fail = false;
+
+ beforeEach((done) => {
+ const Comp = Vue.extend(boardBlankState);
+
+ gl.issueBoards.BoardsStore.create();
+ gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
+
+ spyOn(gl.boardService, 'generateDefaultLists').and.callFake(() => new Promise((resolve, reject) => {
+ if (fail) {
+ reject();
+ } else {
+ resolve({
+ json() {
+ return [{
+ id: 1,
+ title: 'To Do',
+ label: { id: 1 },
+ }, {
+ id: 2,
+ title: 'Doing',
+ label: { id: 2 },
+ }];
+ },
+ });
+ }
+ }));
+
+ vm = new Comp();
+
+ setTimeout(() => {
+ vm.$mount();
+ done();
+ });
+ });
+
+ it('renders pre-defined labels', () => {
+ expect(
+ vm.$el.querySelectorAll('.board-blank-state-list li').length,
+ ).toBe(2);
+
+ expect(
+ vm.$el.querySelectorAll('.board-blank-state-list li')[0].textContent.trim(),
+ ).toEqual('To Do');
+
+ expect(
+ vm.$el.querySelectorAll('.board-blank-state-list li')[1].textContent.trim(),
+ ).toEqual('Doing');
+ });
+
+ it('clears blank state', (done) => {
+ vm.$el.querySelector('.btn-default').click();
+
+ setTimeout(() => {
+ expect(gl.issueBoards.BoardsStore.welcomeIsHidden()).toBeTruthy();
+
+ done();
+ });
+ });
+
+ it('creates pre-defined labels', (done) => {
+ vm.$el.querySelector('.btn-create').click();
+
+ setTimeout(() => {
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+ expect(gl.issueBoards.BoardsStore.state.lists[0].title).toEqual('To Do');
+ expect(gl.issueBoards.BoardsStore.state.lists[1].title).toEqual('Doing');
+
+ done();
+ });
+ });
+
+ it('resets the store if request fails', (done) => {
+ fail = true;
+
+ vm.$el.querySelector('.btn-create').click();
+
+ setTimeout(() => {
+ expect(gl.issueBoards.BoardsStore.welcomeIsHidden()).toBeFalsy();
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js
index fe7f3d2e9c4..549c7af8ea8 100644
--- a/spec/javascripts/build_spec.js
+++ b/spec/javascripts/build_spec.js
@@ -17,7 +17,7 @@ describe('Build', () => {
spyOn($, 'ajax');
});
- describe('constructor', () => {
+ describe('class constructor', () => {
beforeEach(() => {
jasmine.clock().install();
});
diff --git a/spec/javascripts/commit/pipelines/mock_data.js b/spec/javascripts/commit/pipelines/mock_data.js
index 188908d66bd..82b00b4c1ec 100644
--- a/spec/javascripts/commit/pipelines/mock_data.js
+++ b/spec/javascripts/commit/pipelines/mock_data.js
@@ -1,5 +1,4 @@
-/* eslint-disable no-unused-vars */
-const pipeline = {
+export default {
id: 73,
user: {
name: 'Administrator',
@@ -88,5 +87,3 @@ const pipeline = {
created_at: '2017-01-16T17:13:59.800Z',
updated_at: '2017-01-25T00:00:17.132Z',
};
-
-module.exports = pipeline;
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index f09c57978a1..75efcc06585 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -1,11 +1,6 @@
-/* global pipeline, Vue */
-
-require('~/flash');
-require('~/commit/pipelines/pipelines_store');
-require('~/commit/pipelines/pipelines_service');
-require('~/commit/pipelines/pipelines_table');
-require('~/vue_shared/vue_resource_interceptor');
-const pipeline = require('./mock_data');
+import Vue from 'vue';
+import PipelinesTable from '~/commit/pipelines/pipelines_table';
+import pipeline from './mock_data';
describe('Pipelines table in Commits and Merge requests', () => {
preloadFixtures('static/pipelines_table.html.raw');
@@ -33,7 +28,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
});
it('should render the empty state', (done) => {
- const component = new gl.commits.pipelines.PipelinesTableView({
+ const component = new PipelinesTable({
el: document.querySelector('#commit-pipeline-table-view'),
});
@@ -62,7 +57,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
});
it('should render a table with the received pipelines', (done) => {
- const component = new gl.commits.pipelines.PipelinesTableView({
+ const component = new PipelinesTable({
el: document.querySelector('#commit-pipeline-table-view'),
});
@@ -92,7 +87,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
});
it('should render empty state', (done) => {
- const component = new gl.commits.pipelines.PipelinesTableView({
+ const component = new PipelinesTable({
el: document.querySelector('#commit-pipeline-table-view'),
});
diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js b/spec/javascripts/commit/pipelines/pipelines_store_spec.js
deleted file mode 100644
index 4182a4b28ff..00000000000
--- a/spec/javascripts/commit/pipelines/pipelines_store_spec.js
+++ /dev/null
@@ -1,30 +0,0 @@
-const PipelinesStore = require('~/commit/pipelines/pipelines_store');
-
-describe('Store', () => {
- let store;
-
- beforeEach(() => {
- store = new PipelinesStore();
- });
-
- it('should start with a blank state', () => {
- expect(store.state.pipelines.length).toBe(0);
- });
-
- it('should store an array of pipelines', () => {
- const pipelines = [
- {
- id: '1',
- name: 'pipeline',
- },
- {
- id: '2',
- name: 'pipeline_2',
- },
- ];
-
- store.storePipelines(pipelines);
-
- expect(store.state.pipelines.length).toBe(pipelines.length);
- });
-});
diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js
index 5c65903701b..e6538020896 100644
--- a/spec/javascripts/filtered_search/dropdown_utils_spec.js
+++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js
@@ -126,7 +126,11 @@ require('~/filtered_search/filtered_search_dropdown_manager');
beforeEach(() => {
setFixtures(`
- <input type="text" id="test" />
+ <ul class="tokens-container">
+ <li class="input-token">
+ <input class="filtered-search" type="text" id="test" />
+ </li>
+ </ul>
`);
input = document.getElementById('test');
@@ -142,7 +146,7 @@ require('~/filtered_search/filtered_search_dropdown_manager');
input.value = 'o';
updatedItem = gl.DropdownUtils.filterHint(input, {
hint: 'label',
- }, 'o');
+ });
expect(updatedItem.droplab_hidden).toBe(true);
});
@@ -150,6 +154,29 @@ require('~/filtered_search/filtered_search_dropdown_manager');
const updatedItem = gl.DropdownUtils.filterHint(input, {}, '');
expect(updatedItem.droplab_hidden).toBe(false);
});
+
+ it('should allow multiple if item.type is array', () => {
+ input.value = 'label:~first la';
+ const updatedItem = gl.DropdownUtils.filterHint(input, {
+ hint: 'label',
+ type: 'array',
+ });
+ expect(updatedItem.droplab_hidden).toBe(false);
+ });
+
+ it('should prevent multiple if item.type is not array', () => {
+ input.value = 'milestone:~first mile';
+ let updatedItem = gl.DropdownUtils.filterHint(input, {
+ hint: 'milestone',
+ });
+ expect(updatedItem.droplab_hidden).toBe(true);
+
+ updatedItem = gl.DropdownUtils.filterHint(input, {
+ hint: 'milestone',
+ type: 'string',
+ });
+ expect(updatedItem.droplab_hidden).toBe(true);
+ });
});
describe('setDataValueIfSelected', () => {
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
index ae9c263d1d7..113161c21c6 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -246,5 +246,17 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
});
});
+
+ describe('toggleInputContainerFocus', () => {
+ it('toggles on focus', () => {
+ input.focus();
+ expect(document.querySelector('.filtered-search-input-container').classList.contains('focus')).toEqual(true);
+ });
+
+ it('toggles on blur', () => {
+ input.blur();
+ expect(document.querySelector('.filtered-search-input-container').classList.contains('focus')).toEqual(false);
+ });
+ });
});
})();
diff --git a/spec/javascripts/gl_emoji_spec.js b/spec/javascripts/gl_emoji_spec.js
index 9b44b25980c..b2b46640e5b 100644
--- a/spec/javascripts/gl_emoji_spec.js
+++ b/spec/javascripts/gl_emoji_spec.js
@@ -43,6 +43,11 @@ const emojiFixtureMap = {
moji: '5️⃣',
unicodeVersion: '3.0',
},
+ grey_question: {
+ name: 'grey_question',
+ moji: '❔',
+ unicodeVersion: '6.0',
+ },
};
function markupToDomElement(markup) {
@@ -153,6 +158,37 @@ describe('gl_emoji', () => {
},
);
});
+
+ it('question mark when invalid emoji name given', () => {
+ const name = 'invalid_emoji';
+ const emojiKey = 'grey_question';
+ const markup = glEmojiTag(name);
+ const glEmojiElement = markupToDomElement(markup);
+ testGlEmojiElement(
+ glEmojiElement,
+ emojiFixtureMap[emojiKey].name,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ emojiFixtureMap[emojiKey].moji,
+ );
+ });
+
+ it('question mark with image fallback when invalid emoji name given', () => {
+ const name = 'invalid_emoji';
+ const emojiKey = 'grey_question';
+ const markup = glEmojiTag(name, {
+ forceFallback: true,
+ });
+ const glEmojiElement = markupToDomElement(markup);
+ testGlEmojiElement(
+ glEmojiElement,
+ emojiFixtureMap[emojiKey].name,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ emojiFixtureMap[emojiKey].moji,
+ {
+ forceFallback: true,
+ },
+ );
+ });
});
describe('isFlagEmoji', () => {
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 8d25500b9fd..aabc8bea12f 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -136,6 +136,21 @@ describe('Issue', function() {
expectErrorMessage();
expect($('.issue_counter')).toHaveText(1);
});
+
+ it('updates counter', () => {
+ spyOn(jQuery, 'ajax').and.callFake(function(req) {
+ expectPendingRequest(req, $btnClose);
+ req.success({
+ id: 34
+ });
+ });
+
+ expect($('.issue_counter')).toHaveText(1);
+ $('.issue_counter').text('1,001');
+ expect($('.issue_counter').text()).toEqual('1,001');
+ $btnClose.trigger('click');
+ expect($('.issue_counter').text()).toEqual('1,000');
+ });
});
describe('reopen issue', function() {
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index fae462561e9..5cdb6473eda 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -32,10 +32,11 @@ testsContext.keys().forEach(function (path) {
}
});
-// workaround: include all source files to find files with 0% coverage
-// see also https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15
-describe('Uncovered files', function () {
- // the following files throw errors because of undefined variables
+// if we're generating coverage reports, make sure to include all files so
+// that we can catch files with 0% coverage
+// see: https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15
+if (process.env.BABEL_ENV === 'coverage') {
+ // exempt these files from the coverage report
const troubleMakers = [
'./blob_edit/blob_edit_bundle.js',
'./cycle_analytics/components/stage_plan_component.js',
@@ -48,21 +49,23 @@ describe('Uncovered files', function () {
'./network/branch_graph.js',
];
- const sourceFiles = require.context('~', true, /^\.\/(?!application\.js).*\.(js|es6)$/);
- sourceFiles.keys().forEach(function (path) {
- // ignore if there is a matching spec file
- if (testsContext.keys().indexOf(`${path.replace(/\.js(\.es6)?$/, '')}_spec`) > -1) {
- return;
- }
+ describe('Uncovered files', function () {
+ const sourceFiles = require.context('~', true, /\.js$/);
+ sourceFiles.keys().forEach(function (path) {
+ // ignore if there is a matching spec file
+ if (testsContext.keys().indexOf(`${path.replace(/\.js$/, '')}_spec`) > -1) {
+ return;
+ }
- it(`includes '${path}'`, function () {
- try {
- sourceFiles(path);
- } catch (err) {
- if (troubleMakers.indexOf(path) === -1) {
- expect(err).toBeNull();
+ it(`includes '${path}'`, function () {
+ try {
+ sourceFiles(path);
+ } catch (err) {
+ if (troubleMakers.indexOf(path) === -1) {
+ expect(err).toBeNull();
+ }
}
- }
+ });
});
});
-});
+}
diff --git a/spec/javascripts/vue_pipelines_index/async_button_spec.js b/spec/javascripts/vue_pipelines_index/async_button_spec.js
new file mode 100644
index 00000000000..bc8e504c413
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/async_button_spec.js
@@ -0,0 +1,93 @@
+import Vue from 'vue';
+import asyncButtonComp from '~/vue_pipelines_index/components/async_button';
+
+describe('Pipelines Async Button', () => {
+ let component;
+ let spy;
+ let AsyncButtonComponent;
+
+ beforeEach(() => {
+ AsyncButtonComponent = Vue.extend(asyncButtonComp);
+
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+
+ component = new AsyncButtonComponent({
+ propsData: {
+ endpoint: '/foo',
+ title: 'Foo',
+ icon: 'fa fa-foo',
+ cssClass: 'bar',
+ service: {
+ postAction: spy,
+ },
+ },
+ }).$mount();
+ });
+
+ it('should render a button', () => {
+ expect(component.$el.tagName).toEqual('BUTTON');
+ });
+
+ it('should render the provided icon', () => {
+ expect(component.$el.querySelector('i').getAttribute('class')).toContain('fa fa-foo');
+ });
+
+ it('should render the provided title', () => {
+ expect(component.$el.getAttribute('title')).toContain('Foo');
+ expect(component.$el.getAttribute('aria-label')).toContain('Foo');
+ });
+
+ it('should render the provided cssClass', () => {
+ expect(component.$el.getAttribute('class')).toContain('bar');
+ });
+
+ it('should call the service when it is clicked with the provided endpoint', () => {
+ component.$el.click();
+ expect(spy).toHaveBeenCalledWith('/foo');
+ });
+
+ it('should hide loading if request fails', () => {
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.reject());
+
+ component = new AsyncButtonComponent({
+ propsData: {
+ endpoint: '/foo',
+ title: 'Foo',
+ icon: 'fa fa-foo',
+ cssClass: 'bar',
+ dataAttributes: {
+ 'data-foo': 'foo',
+ },
+ service: {
+ postAction: spy,
+ },
+ },
+ }).$mount();
+
+ component.$el.click();
+ expect(component.$el.querySelector('.fa-spinner')).toBe(null);
+ });
+
+ describe('With confirm dialog', () => {
+ it('should call the service when confimation is positive', () => {
+ spyOn(window, 'confirm').and.returnValue(true);
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+
+ component = new AsyncButtonComponent({
+ propsData: {
+ endpoint: '/foo',
+ title: 'Foo',
+ icon: 'fa fa-foo',
+ cssClass: 'bar',
+ service: {
+ postAction: spy,
+ },
+ confirmActionMessage: 'bar',
+ },
+ }).$mount();
+
+ component.$el.click();
+ expect(spy).toHaveBeenCalledWith('/foo');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js b/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js
new file mode 100644
index 00000000000..96a2a37b5f7
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js
@@ -0,0 +1,100 @@
+import Vue from 'vue';
+import pipelineUrlComp from '~/vue_pipelines_index/components/pipeline_url';
+
+describe('Pipeline Url Component', () => {
+ let PipelineUrlComponent;
+
+ beforeEach(() => {
+ PipelineUrlComponent = Vue.extend(pipelineUrlComp);
+ });
+
+ it('should render a table cell', () => {
+ const component = new PipelineUrlComponent({
+ propsData: {
+ pipeline: {
+ id: 1,
+ path: 'foo',
+ flags: {},
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.tagName).toEqual('TD');
+ });
+
+ it('should render a link the provided path and id', () => {
+ const component = new PipelineUrlComponent({
+ propsData: {
+ pipeline: {
+ id: 1,
+ path: 'foo',
+ flags: {},
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.js-pipeline-url-link').getAttribute('href')).toEqual('foo');
+ expect(component.$el.querySelector('.js-pipeline-url-link span').textContent).toEqual('#1');
+ });
+
+ it('should render user information when a user is provided', () => {
+ const mockData = {
+ pipeline: {
+ id: 1,
+ path: 'foo',
+ flags: {},
+ user: {
+ web_url: '/',
+ name: 'foo',
+ avatar_url: '/',
+ },
+ },
+ };
+
+ const component = new PipelineUrlComponent({
+ propsData: mockData,
+ }).$mount();
+
+ const image = component.$el.querySelector('.js-pipeline-url-user img');
+
+ expect(
+ component.$el.querySelector('.js-pipeline-url-user').getAttribute('href'),
+ ).toEqual(mockData.pipeline.user.web_url);
+ expect(image.getAttribute('title')).toEqual(mockData.pipeline.user.name);
+ expect(image.getAttribute('src')).toEqual(mockData.pipeline.user.avatar_url);
+ });
+
+ it('should render "API" when no user is provided', () => {
+ const component = new PipelineUrlComponent({
+ propsData: {
+ pipeline: {
+ id: 1,
+ path: 'foo',
+ flags: {},
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.js-pipeline-url-api').textContent).toContain('API');
+ });
+
+ it('should render latest, yaml invalid and stuck flags when provided', () => {
+ const component = new PipelineUrlComponent({
+ propsData: {
+ pipeline: {
+ id: 1,
+ path: 'foo',
+ flags: {
+ latest: true,
+ yaml_errors: true,
+ stuck: true,
+ },
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.js-pipeline-url-lastest').textContent).toContain('latest');
+ expect(component.$el.querySelector('.js-pipeline-url-yaml').textContent).toContain('yaml invalid');
+ expect(component.$el.querySelector('.js-pipeline-url-stuck').textContent).toContain('stuck');
+ });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js
new file mode 100644
index 00000000000..dba998c7688
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js
@@ -0,0 +1,62 @@
+import Vue from 'vue';
+import pipelinesActionsComp from '~/vue_pipelines_index/components/pipelines_actions';
+
+describe('Pipelines Actions dropdown', () => {
+ let component;
+ let spy;
+ let actions;
+ let ActionsComponent;
+
+ beforeEach(() => {
+ ActionsComponent = Vue.extend(pipelinesActionsComp);
+
+ actions = [
+ {
+ name: 'stop_review',
+ path: '/root/review-app/builds/1893/play',
+ },
+ ];
+
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+
+ component = new ActionsComponent({
+ propsData: {
+ actions,
+ service: {
+ postAction: spy,
+ },
+ },
+ }).$mount();
+ });
+
+ it('should render a dropdown with the provided actions', () => {
+ expect(
+ component.$el.querySelectorAll('.dropdown-menu li').length,
+ ).toEqual(actions.length);
+ });
+
+ it('should call the service when an action is clicked', () => {
+ component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click();
+ component.$el.querySelector('.js-pipeline-action-link').click();
+
+ expect(spy).toHaveBeenCalledWith(actions[0].path);
+ });
+
+ it('should hide loading if request fails', () => {
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.reject());
+
+ component = new ActionsComponent({
+ propsData: {
+ actions,
+ service: {
+ postAction: spy,
+ },
+ },
+ }).$mount();
+
+ component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click();
+ component.$el.querySelector('.js-pipeline-action-link').click();
+
+ expect(component.$el.querySelector('.fa-spinner')).toEqual(null);
+ });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js
new file mode 100644
index 00000000000..f7f49649c1c
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js
@@ -0,0 +1,40 @@
+import Vue from 'vue';
+import artifactsComp from '~/vue_pipelines_index/components/pipelines_artifacts';
+
+describe('Pipelines Artifacts dropdown', () => {
+ let component;
+ let artifacts;
+
+ beforeEach(() => {
+ const ArtifactsComponent = Vue.extend(artifactsComp);
+
+ artifacts = [
+ {
+ name: 'artifact',
+ path: '/download/path',
+ },
+ ];
+
+ component = new ArtifactsComponent({
+ propsData: {
+ artifacts,
+ },
+ }).$mount();
+ });
+
+ it('should render a dropdown with the provided artifacts', () => {
+ expect(
+ component.$el.querySelectorAll('.dropdown-menu li').length,
+ ).toEqual(artifacts.length);
+ });
+
+ it('should render a link with the provided path', () => {
+ expect(
+ component.$el.querySelector('.dropdown-menu li a').getAttribute('href'),
+ ).toEqual(artifacts[0].path);
+
+ expect(
+ component.$el.querySelector('.dropdown-menu li a span').textContent,
+ ).toContain(artifacts[0].name);
+ });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js
new file mode 100644
index 00000000000..5c0934404bb
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js
@@ -0,0 +1,72 @@
+import PipelineStore from '~/vue_pipelines_index/stores/pipelines_store';
+
+describe('Pipelines Store', () => {
+ let store;
+
+ beforeEach(() => {
+ store = new PipelineStore();
+ });
+
+ it('should be initialized with an empty state', () => {
+ expect(store.state.pipelines).toEqual([]);
+ expect(store.state.count).toEqual({});
+ expect(store.state.pageInfo).toEqual({});
+ });
+
+ describe('storePipelines', () => {
+ it('should use the default parameter if none is provided', () => {
+ store.storePipelines();
+ expect(store.state.pipelines).toEqual([]);
+ });
+
+ it('should store the provided array', () => {
+ const array = [{ id: 1, status: 'running' }, { id: 2, status: 'success' }];
+ store.storePipelines(array);
+ expect(store.state.pipelines).toEqual(array);
+ });
+ });
+
+ describe('storeCount', () => {
+ it('should use the default parameter if none is provided', () => {
+ store.storeCount();
+ expect(store.state.count).toEqual({});
+ });
+
+ it('should store the provided count', () => {
+ const count = { all: 20, finished: 10 };
+ store.storeCount(count);
+
+ expect(store.state.count).toEqual(count);
+ });
+ });
+
+ describe('storePagination', () => {
+ it('should use the default parameter if none is provided', () => {
+ store.storePagination();
+ expect(store.state.pageInfo).toEqual({});
+ });
+
+ it('should store pagination information normalized and parsed', () => {
+ const pagination = {
+ 'X-nExt-pAge': '2',
+ 'X-page': '1',
+ 'X-Per-Page': '1',
+ 'X-Prev-Page': '2',
+ 'X-TOTAL': '37',
+ 'X-Total-Pages': '2',
+ };
+
+ const expectedResult = {
+ perPage: 1,
+ page: 1,
+ total: 37,
+ totalPages: 2,
+ nextPage: 2,
+ previousPage: 2,
+ };
+
+ store.storePagination(pagination);
+ expect(store.state.pageInfo).toEqual(expectedResult);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js
index 15ab10b9b69..df547299d75 100644
--- a/spec/javascripts/vue_shared/components/commit_spec.js
+++ b/spec/javascripts/vue_shared/components/commit_spec.js
@@ -1,13 +1,17 @@
-require('~/vue_shared/components/commit');
+import Vue from 'vue';
+import commitComp from '~/vue_shared/components/commit';
describe('Commit component', () => {
let props;
let component;
+ let CommitComponent;
+
+ beforeEach(() => {
+ CommitComponent = Vue.extend(commitComp);
+ });
it('should render a code-fork icon if it does not represent a tag', () => {
- setFixtures('<div class="test-commit-container"></div>');
- component = new window.gl.CommitComponent({
- el: document.querySelector('.test-commit-container'),
+ component = new CommitComponent({
propsData: {
tag: false,
commitRef: {
@@ -23,15 +27,13 @@ describe('Commit component', () => {
username: 'jschatz1',
},
},
- });
+ }).$mount();
expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-code-fork');
});
describe('Given all the props', () => {
beforeEach(() => {
- setFixtures('<div class="test-commit-container"></div>');
-
props = {
tag: true,
commitRef: {
@@ -49,10 +51,9 @@ describe('Commit component', () => {
commitIconSvg: '<svg></svg>',
};
- component = new window.gl.CommitComponent({
- el: document.querySelector('.test-commit-container'),
+ component = new CommitComponent({
propsData: props,
- });
+ }).$mount();
});
it('should render a tag icon if it represents a tag', () => {
@@ -105,7 +106,6 @@ describe('Commit component', () => {
describe('When commit title is not provided', () => {
it('should render default message', () => {
- setFixtures('<div class="test-commit-container"></div>');
props = {
tag: false,
commitRef: {
@@ -118,10 +118,9 @@ describe('Commit component', () => {
author: {},
};
- component = new window.gl.CommitComponent({
- el: document.querySelector('.test-commit-container'),
+ component = new CommitComponent({
propsData: props,
- });
+ }).$mount();
expect(
component.$el.querySelector('.commit-title span').textContent,
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
index 412abfd5e41..699625cdbb7 100644
--- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
+++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
@@ -1,20 +1,20 @@
-require('~/vue_shared/components/pipelines_table_row');
-const pipeline = require('../../commit/pipelines/mock_data');
+import Vue from 'vue';
+import tableRowComp from '~/vue_shared/components/pipelines_table_row';
+import pipeline from '../../commit/pipelines/mock_data';
describe('Pipelines Table Row', () => {
let component;
- preloadFixtures('static/environments/element.html.raw');
beforeEach(() => {
- loadFixtures('static/environments/element.html.raw');
+ const PipelinesTableRowComponent = Vue.extend(tableRowComp);
- component = new gl.pipelines.PipelinesTableRowComponent({
+ component = new PipelinesTableRowComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
pipeline,
- svgs: {},
+ service: {},
},
- });
+ }).$mount();
});
it('should render a table row', () => {
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_spec.js
index 54d81e2ea7d..b0b1df5a753 100644
--- a/spec/javascripts/vue_shared/components/pipelines_table_spec.js
+++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js
@@ -1,24 +1,24 @@
-require('~/vue_shared/components/pipelines_table');
-require('~/lib/utils/datetime_utility');
-const pipeline = require('../../commit/pipelines/mock_data');
+import Vue from 'vue';
+import pipelinesTableComp from '~/vue_shared/components/pipelines_table';
+import '~/lib/utils/datetime_utility';
+import pipeline from '../../commit/pipelines/mock_data';
describe('Pipelines Table', () => {
- preloadFixtures('static/environments/element.html.raw');
+ let PipelinesTableComponent;
beforeEach(() => {
- loadFixtures('static/environments/element.html.raw');
+ PipelinesTableComponent = Vue.extend(pipelinesTableComp);
});
describe('table', () => {
let component;
beforeEach(() => {
- component = new gl.pipelines.PipelinesTableComponent({
- el: document.querySelector('.test-dom-element'),
+ component = new PipelinesTableComponent({
propsData: {
pipelines: [],
- svgs: {},
+ service: {},
},
- });
+ }).$mount();
});
it('should render a table', () => {
@@ -37,26 +37,25 @@ describe('Pipelines Table', () => {
describe('without data', () => {
it('should render an empty table', () => {
- const component = new gl.pipelines.PipelinesTableComponent({
- el: document.querySelector('.test-dom-element'),
+ const component = new PipelinesTableComponent({
propsData: {
pipelines: [],
- svgs: {},
+ service: {},
},
- });
+ }).$mount();
expect(component.$el.querySelectorAll('tbody tr').length).toEqual(0);
});
});
describe('with data', () => {
it('should render rows', () => {
- const component = new gl.pipelines.PipelinesTableComponent({
+ const component = new PipelinesTableComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
pipelines: [pipeline],
- svgs: {},
+ service: {},
},
- });
+ }).$mount();
expect(component.$el.querySelectorAll('tbody tr').length).toEqual(1);
});
diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/javascripts/vue_shared/components/table_pagination_spec.js
index 9cb067921a7..a5c3870b3ac 100644
--- a/spec/javascripts/vue_shared/components/table_pagination_spec.js
+++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js
@@ -1,8 +1,10 @@
-require('~/lib/utils/common_utils');
-require('~/vue_shared/components/table_pagination');
+import Vue from 'vue';
+import paginationComp from '~/vue_shared/components/table_pagination';
+import '~/lib/utils/common_utils';
describe('Pagination component', () => {
let component;
+ let PaginationComponent;
const changeChanges = {
one: '',
@@ -12,11 +14,12 @@ describe('Pagination component', () => {
changeChanges.one = one;
};
- it('should render and start at page 1', () => {
- setFixtures('<div class="test-pagination-container"></div>');
+ beforeEach(() => {
+ PaginationComponent = Vue.extend(paginationComp);
+ });
- component = new window.gl.VueGlPagination({
- el: document.querySelector('.test-pagination-container'),
+ it('should render and start at page 1', () => {
+ component = new PaginationComponent({
propsData: {
pageInfo: {
totalPages: 10,
@@ -25,7 +28,7 @@ describe('Pagination component', () => {
},
change,
},
- });
+ }).$mount();
expect(component.$el.classList).toContain('gl-pagination');
@@ -35,10 +38,7 @@ describe('Pagination component', () => {
});
it('should go to the previous page', () => {
- setFixtures('<div class="test-pagination-container"></div>');
-
- component = new window.gl.VueGlPagination({
- el: document.querySelector('.test-pagination-container'),
+ component = new PaginationComponent({
propsData: {
pageInfo: {
totalPages: 10,
@@ -47,7 +47,7 @@ describe('Pagination component', () => {
},
change,
},
- });
+ }).$mount();
component.changePage({ target: { innerText: 'Prev' } });
@@ -55,10 +55,7 @@ describe('Pagination component', () => {
});
it('should go to the next page', () => {
- setFixtures('<div class="test-pagination-container"></div>');
-
- component = new window.gl.VueGlPagination({
- el: document.querySelector('.test-pagination-container'),
+ component = new PaginationComponent({
propsData: {
pageInfo: {
totalPages: 10,
@@ -67,7 +64,7 @@ describe('Pagination component', () => {
},
change,
},
- });
+ }).$mount();
component.changePage({ target: { innerText: 'Next' } });
@@ -75,10 +72,7 @@ describe('Pagination component', () => {
});
it('should go to the last page', () => {
- setFixtures('<div class="test-pagination-container"></div>');
-
- component = new window.gl.VueGlPagination({
- el: document.querySelector('.test-pagination-container'),
+ component = new PaginationComponent({
propsData: {
pageInfo: {
totalPages: 10,
@@ -87,7 +81,7 @@ describe('Pagination component', () => {
},
change,
},
- });
+ }).$mount();
component.changePage({ target: { innerText: 'Last >>' } });
@@ -95,10 +89,7 @@ describe('Pagination component', () => {
});
it('should go to the first page', () => {
- setFixtures('<div class="test-pagination-container"></div>');
-
- component = new window.gl.VueGlPagination({
- el: document.querySelector('.test-pagination-container'),
+ component = new PaginationComponent({
propsData: {
pageInfo: {
totalPages: 10,
@@ -107,7 +98,7 @@ describe('Pagination component', () => {
},
change,
},
- });
+ }).$mount();
component.changePage({ target: { innerText: '<< First' } });
@@ -115,10 +106,7 @@ describe('Pagination component', () => {
});
it('should do nothing', () => {
- setFixtures('<div class="test-pagination-container"></div>');
-
- component = new window.gl.VueGlPagination({
- el: document.querySelector('.test-pagination-container'),
+ component = new PaginationComponent({
propsData: {
pageInfo: {
totalPages: 10,
@@ -127,7 +115,7 @@ describe('Pagination component', () => {
},
change,
},
- });
+ }).$mount();
component.changePage({ target: { innerText: '...' } });
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
index 69e3c52b35a..63fb1bb25c4 100644
--- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -6,21 +6,21 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
context "when no language is specified" do
it "highlights as plaintext" do
result = filter('<pre><code>def fun end</code></pre>')
- expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code>def fun end</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">def fun end</span></code></pre>')
end
end
context "when a valid language is specified" do
it "highlights as that language" do
result = filter('<pre><code class="ruby">def fun end</code></pre>')
- expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" lang="ruby" v-pre="true"><code><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></span></code></pre>')
end
end
context "when an invalid language is specified" do
it "highlights as plaintext" do
result = filter('<pre><code class="gnuplot">This is a test</code></pre>')
- expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code>This is a test</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" lang="plaintext" v-pre="true"><code><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre>')
end
end
diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb
index 730ca1f7c0a..90628917943 100644
--- a/spec/lib/expand_variables_spec.rb
+++ b/spec/lib/expand_variables_spec.rb
@@ -45,10 +45,10 @@ describe ExpandVariables do
{ key: 'variable', value: 'value' },
{ key: 'variable2', value: 'result' }
] },
- { value: 'review/$CI_BUILD_REF_NAME',
+ { value: 'review/$CI_COMMIT_REF_NAME',
result: 'review/feature/add-review-apps',
variables: [
- { key: 'CI_BUILD_REF_NAME', value: 'feature/add-review-apps' }
+ { key: 'CI_COMMIT_REF_NAME', value: 'feature/add-review-apps' }
] },
]
diff --git a/spec/lib/git_ref_validator_spec.rb b/spec/lib/git_ref_validator_spec.rb
index dc57e94f193..cc8daa535d6 100644
--- a/spec/lib/git_ref_validator_spec.rb
+++ b/spec/lib/git_ref_validator_spec.rb
@@ -5,6 +5,7 @@ describe Gitlab::GitRefValidator, lib: true do
it { expect(Gitlab::GitRefValidator.validate('implement_@all')).to be_truthy }
it { expect(Gitlab::GitRefValidator.validate('my_new_feature')).to be_truthy }
it { expect(Gitlab::GitRefValidator.validate('#1')).to be_truthy }
+ it { expect(Gitlab::GitRefValidator.validate('feature/refs/heads/foo')).to be_truthy }
it { expect(Gitlab::GitRefValidator.validate('feature/~new/')).to be_falsey }
it { expect(Gitlab::GitRefValidator.validate('feature/^new/')).to be_falsey }
it { expect(Gitlab::GitRefValidator.validate('feature/:new/')).to be_falsey }
@@ -17,4 +18,8 @@ describe Gitlab::GitRefValidator, lib: true do
it { expect(Gitlab::GitRefValidator.validate('feature\new')).to be_falsey }
it { expect(Gitlab::GitRefValidator.validate('feature//new')).to be_falsey }
it { expect(Gitlab::GitRefValidator.validate('feature new')).to be_falsey }
+ it { expect(Gitlab::GitRefValidator.validate('refs/heads/')).to be_falsey }
+ it { expect(Gitlab::GitRefValidator.validate('refs/remotes/')).to be_falsey }
+ it { expect(Gitlab::GitRefValidator.validate('refs/heads/feature')).to be_falsey }
+ it { expect(Gitlab::GitRefValidator.validate('refs/remotes/origin')).to be_falsey }
end
diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
index 2adbed2154f..c330e609337 100644
--- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
@@ -151,8 +151,8 @@ describe Gitlab::Ci::Config::Entry::Environment do
context 'when variables are used for environment' do
let(:config) do
- { name: 'review/$CI_BUILD_REF_NAME',
- url: 'https://$CI_BUILD_REF_NAME.review.gitlab.com' }
+ { name: 'review/$CI_COMMIT_REF_NAME',
+ url: 'https://$CI_COMMIT_REF_NAME.review.gitlab.com' }
end
describe '#valid?' do
diff --git a/spec/lib/gitlab/conflict/parser_spec.rb b/spec/lib/gitlab/conflict/parser_spec.rb
index 16eb3766356..2570f95dd21 100644
--- a/spec/lib/gitlab/conflict/parser_spec.rb
+++ b/spec/lib/gitlab/conflict/parser_spec.rb
@@ -120,43 +120,61 @@ CONFLICT
end
context 'when the file contents include conflict delimiters' do
- it 'raises UnexpectedDelimiter when there is a non-start delimiter first' do
- expect { parse_text('=======') }.
- to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
-
- expect { parse_text('>>>>>>> README.md') }.
- to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
-
- expect { parse_text('>>>>>>> some-other-path.md') }.
- not_to raise_error
+ context 'when there is a non-start delimiter first' do
+ it 'raises UnexpectedDelimiter when there is a middle delimiter first' do
+ expect { parse_text('=======') }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ end
+
+ it 'raises UnexpectedDelimiter when there is an end delimiter first' do
+ expect { parse_text('>>>>>>> README.md') }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ end
+
+ it 'does not raise when there is an end delimiter for a different path first' do
+ expect { parse_text('>>>>>>> some-other-path.md') }.
+ not_to raise_error
+ end
end
- it 'raises UnexpectedDelimiter when a start delimiter is followed by a non-middle delimiter' do
- start_text = "<<<<<<< README.md\n"
- end_text = "\n=======\n>>>>>>> README.md"
+ context 'when a start delimiter is followed by a non-middle delimiter' do
+ let(:start_text) { "<<<<<<< README.md\n" }
+ let(:end_text) { "\n=======\n>>>>>>> README.md" }
- expect { parse_text(start_text + '>>>>>>> README.md' + end_text) }.
- to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ it 'raises UnexpectedDelimiter when it is followed by an end delimiter' do
+ expect { parse_text(start_text + '>>>>>>> README.md' + end_text) }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ end
- expect { parse_text(start_text + start_text + end_text) }.
- to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ it 'raises UnexpectedDelimiter when it is followed by another start delimiter' do
+ expect { parse_text(start_text + start_text + end_text) }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ end
- expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
- not_to raise_error
+ it 'does not raise when it is followed by a start delimiter for a different path' do
+ expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
+ not_to raise_error
+ end
end
- it 'raises UnexpectedDelimiter when a middle delimiter is followed by a non-end delimiter' do
- start_text = "<<<<<<< README.md\n=======\n"
- end_text = "\n>>>>>>> README.md"
+ context 'when a middle delimiter is followed by a non-end delimiter' do
+ let(:start_text) { "<<<<<<< README.md\n=======\n" }
+ let(:end_text) { "\n>>>>>>> README.md" }
- expect { parse_text(start_text + '=======' + end_text) }.
- to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ it 'raises UnexpectedDelimiter when it is followed by another middle delimiter' do
+ expect { parse_text(start_text + '=======' + end_text) }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ end
- expect { parse_text(start_text + start_text + end_text) }.
- to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ it 'raises UnexpectedDelimiter when it is followed by a start delimiter' do
+ expect { parse_text(start_text + start_text + end_text) }.
+ to raise_error(Gitlab::Conflict::Parser::UnexpectedDelimiter)
+ end
- expect { parse_text(start_text + '>>>>>>> some-other-path.md' + end_text) }.
- not_to raise_error
+ it 'does not raise when it is followed by a start delimiter for another path' do
+ expect { parse_text(start_text + '<<<<<<< some-other-path.md' + end_text) }.
+ not_to raise_error
+ end
end
it 'raises MissingEndDelimiter when there is no end delimiter at the end' do
@@ -184,9 +202,20 @@ CONFLICT
to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
end
- it 'raises UnsupportedEncoding when the file contains non-UTF-8 characters' do
- expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }.
- to raise_error(Gitlab::Conflict::Parser::UnsupportedEncoding)
+ # All text from Rugged has an encoding of ASCII_8BIT, so force that in
+ # these strings.
+ context 'when the file contains UTF-8 characters' do
+ it 'does not raise' do
+ expect { parse_text("Espa\xC3\xB1a".force_encoding(Encoding::ASCII_8BIT)) }.
+ not_to raise_error
+ end
+ end
+
+ context 'when the file contains non-UTF-8 characters' do
+ it 'raises UnsupportedEncoding' do
+ expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }.
+ to raise_error(Gitlab::Conflict::Parser::UnsupportedEncoding)
+ end
end
end
end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index edd01d032c8..4ce4e6e1034 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -1,10 +1,16 @@
require 'spec_helper'
-class MigrationTest
- include Gitlab::Database
-end
-
describe Gitlab::Database, lib: true do
+ before do
+ stub_const('MigrationTest', Class.new { include Gitlab::Database })
+ end
+
+ describe '.config' do
+ it 'returns a Hash' do
+ expect(described_class.config).to be_an_instance_of(Hash)
+ end
+ end
+
describe '.adapter_name' do
it 'returns the name of the adapter' do
expect(described_class.adapter_name).to be_an_instance_of(String)
diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb
index 0e9309d278e..c6bd4e81f4f 100644
--- a/spec/lib/gitlab/diff/highlight_spec.rb
+++ b/spec/lib/gitlab/diff/highlight_spec.rb
@@ -22,19 +22,19 @@ describe Gitlab::Diff::Highlight, lib: true do
end
it 'highlights and marks unchanged lines' do
- code = %Q{ <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>\n}
+ code = %Q{ <span id="LC7" class="line" lang="ruby"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>\n}
expect(subject[2].text).to eq(code)
end
it 'highlights and marks removed lines' do
- code = %Q{-<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n}
+ code = %Q{-<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n}
expect(subject[4].text).to eq(code)
end
it 'highlights and marks added lines' do
- code = %Q{+<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n}
+ code = %Q{+<span id="LC9" class="line" lang="ruby"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n}
expect(subject[5].text).to eq(code)
end
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index 4c55532d165..992126ef153 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -109,6 +109,43 @@ EOT
end
end
end
+
+ context 'using a Gitaly::CommitDiffResponse' do
+ let(:diff) do
+ described_class.new(
+ Gitaly::CommitDiffResponse.new(
+ to_path: ".gitmodules",
+ from_path: ".gitmodules",
+ old_mode: 0100644,
+ new_mode: 0100644,
+ from_id: '357406f3075a57708d0163752905cc1576fceacc',
+ to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0',
+ raw_chunks: raw_chunks,
+ )
+ )
+ end
+
+ context 'with a small diff' do
+ let(:raw_chunks) { [@raw_diff_hash[:diff]] }
+
+ it 'initializes the diff' do
+ expect(diff.to_hash).to eq(@raw_diff_hash)
+ end
+
+ it 'does not prune the diff' do
+ expect(diff).not_to be_too_large
+ end
+ end
+
+ context 'using a diff that is too large' do
+ let(:raw_chunks) { ['a' * 204800] }
+
+ it 'prunes the diff' do
+ expect(diff.diff).to be_empty
+ expect(diff).to be_too_large
+ end
+ end
+ end
end
describe 'straight diffs' do
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index bc139d5ef28..9ed43da1116 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -90,6 +90,12 @@ describe Gitlab::Git::Repository, seed_helper: true do
expect(prefix).to eq("#{project_name}-test-branch-SHA")
end
+
+ it 'returns correct string for a ref containing dots' do
+ prefix = repository.archive_prefix('test.branch', 'SHA')
+
+ expect(prefix).to eq("#{project_name}-test.branch-SHA")
+ end
end
describe '#archive' do
@@ -507,7 +513,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe "#remote_add" do
before(:all) do
@repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
- @repo.remote_add("new_remote", SeedHelper::GITLAB_URL)
+ @repo.remote_add("new_remote", SeedHelper::GITLAB_GIT_TEST_REPO_URL)
end
it "should add the remote" do
diff --git a/spec/lib/gitlab/gitaly_client/commit_spec.rb b/spec/lib/gitlab/gitaly_client/commit_spec.rb
new file mode 100644
index 00000000000..4684b1d1ac0
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/commit_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::Commit do
+ describe '.diff_from_parent' do
+ let(:diff_stub) { double('Gitaly::Diff::Stub') }
+ let(:project) { create(:project, :repository) }
+ let(:repository_message) { Gitaly::Repository.new(path: project.repository.path) }
+ let(:commit) { project.commit('913c66a37b4a45b9769037c55c2d238bd0942d2e') }
+
+ before do
+ allow(Gitaly::Diff::Stub).to receive(:new).and_return(diff_stub)
+ allow(diff_stub).to receive(:commit_diff).and_return([])
+ end
+
+ context 'when a commit has a parent' do
+ it 'sends an RPC request with the parent ID as left commit' do
+ request = Gitaly::CommitDiffRequest.new(
+ repository: repository_message,
+ left_commit_id: 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660',
+ right_commit_id: commit.id,
+ )
+
+ expect(diff_stub).to receive(:commit_diff).with(request)
+
+ described_class.diff_from_parent(commit)
+ end
+ end
+
+ context 'when a commit does not have a parent' do
+ it 'sends an RPC request with empty tree ref as left commit' do
+ initial_commit = project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863')
+ request = Gitaly::CommitDiffRequest.new(
+ repository: repository_message,
+ left_commit_id: '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
+ right_commit_id: initial_commit.id,
+ )
+
+ expect(diff_stub).to receive(:commit_diff).with(request)
+
+ described_class.diff_from_parent(initial_commit)
+ end
+ end
+
+ it 'returns a Gitlab::Git::DiffCollection' do
+ ret = described_class.diff_from_parent(commit)
+
+ expect(ret).to be_kind_of(Gitlab::Git::DiffCollection)
+ end
+
+ it 'passes options to Gitlab::Git::DiffCollection' do
+ options = { max_files: 31, max_lines: 13 }
+
+ expect(Gitlab::Git::DiffCollection).to receive(:new).with([], options)
+
+ described_class.diff_from_parent(commit, options)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb
index e177d883158..e49799ad105 100644
--- a/spec/lib/gitlab/highlight_spec.rb
+++ b/spec/lib/gitlab/highlight_spec.rb
@@ -13,9 +13,9 @@ describe Gitlab::Highlight, lib: true do
end
it 'highlights all the lines properly' do
- expect(lines[4]).to eq(%Q{<span id="LC5" class="line"> <span class="kp">extend</span> <span class="nb">self</span></span>\n})
- expect(lines[21]).to eq(%Q{<span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n})
- expect(lines[26]).to eq(%Q{<span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n})
+ expect(lines[4]).to eq(%Q{<span id="LC5" class="line" lang="ruby"> <span class="kp">extend</span> <span class="nb">self</span></span>\n})
+ expect(lines[21]).to eq(%Q{<span id="LC22" class="line" lang="ruby"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n})
+ expect(lines[26]).to eq(%Q{<span id="LC27" class="line" lang="ruby"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n})
end
describe 'with CRLF' do
@@ -26,7 +26,7 @@ describe Gitlab::Highlight, lib: true do
end
it 'strips extra LFs' do
- expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\">test </span>")
+ expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\" lang=\"plaintext\">test </span>")
end
end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index e47956a365f..ddeb71730e7 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -130,7 +130,6 @@ project:
- campfire_service
- drone_ci_service
- emails_on_push_service
-- builds_email_service
- pipelines_email_service
- mattermost_slash_commands_service
- slack_slash_commands_service
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index c3d5c451a3c..d9b67426818 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -6507,7 +6507,6 @@
"tag": null,
"yaml_errors": null,
"committed_at": null,
- "gl_project_id": 5,
"status": "failed",
"started_at": null,
"finished_at": null,
@@ -6565,7 +6564,6 @@
"artifacts_file": {
"url": null
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": null
},
@@ -6603,7 +6601,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts_metadata.gz"
},
@@ -6624,7 +6621,6 @@
"tag": null,
"yaml_errors": null,
"committed_at": null,
- "gl_project_id": 5,
"status": "failed",
"started_at": null,
"finished_at": null,
@@ -6659,7 +6655,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts_metadata.gz"
},
@@ -6695,7 +6690,6 @@
"artifacts_file": {
"url": null
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": null
},
@@ -6716,7 +6710,6 @@
"tag": null,
"yaml_errors": null,
"committed_at": null,
- "gl_project_id": 5,
"status": "failed",
"started_at": null,
"finished_at": null,
@@ -6751,7 +6744,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts_metadata.gz"
},
@@ -6787,7 +6779,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts_metadata.gz"
},
@@ -6808,7 +6799,6 @@
"tag": null,
"yaml_errors": null,
"committed_at": null,
- "gl_project_id": 5,
"status": "failed",
"started_at": null,
"finished_at": null,
@@ -6843,7 +6833,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts_metadata.gz"
},
@@ -6879,7 +6868,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts_metadata.gz"
},
@@ -6900,7 +6888,6 @@
"tag": null,
"yaml_errors": null,
"committed_at": null,
- "gl_project_id": 5,
"status": "failed",
"started_at": null,
"finished_at": null,
@@ -6935,7 +6922,6 @@
"artifacts_file": {
"url": null
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": null
},
@@ -6971,7 +6957,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts_metadata.gz"
},
@@ -6985,11 +6970,10 @@
{
"id": 123,
"token": "cdbfasdf44a5958c83654733449e585",
- "project_id": null,
+ "project_id": 5,
"deleted_at": null,
"created_at": "2017-01-16T15:25:28.637Z",
- "updated_at": "2017-01-16T15:25:28.637Z",
- "gl_project_id": 123
+ "updated_at": "2017-01-16T15:25:28.637Z"
}
],
"deploy_keys": [
@@ -7047,7 +7031,7 @@
"updated_at": "2016-06-14T15:01:51.303Z",
"active": false,
"properties": {
- "notify_only_broken_builds": true
+ "notify_only_broken_pipelines": true
},
"template": false,
"push_events": true,
@@ -7055,7 +7039,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "pipeline_events": true,
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7174,7 +7158,7 @@
"updated_at": "2016-06-14T15:01:51.219Z",
"active": false,
"properties": {
- "notify_only_broken_builds": true
+ "notify_only_broken_pipelines": true
},
"template": false,
"push_events": true,
@@ -7182,7 +7166,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "pipeline_events": true,
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7335,27 +7319,6 @@
"wiki_page_events": true
},
{
- "id": 85,
- "title": "Builds emails",
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.090Z",
- "updated_at": "2016-06-14T15:01:51.090Z",
- "active": false,
- "properties": {
- "notify_only_broken_builds": true
- },
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "build_events": true,
- "category": "common",
- "default": false,
- "wiki_page_events": true
- },
- {
"id": 84,
"title": "Buildkite",
"project_id": 5,
@@ -7503,4 +7466,4 @@
"updated_at": "2016-09-23T11:58:28.000Z",
"wiki_access_level": 20
}
-} \ No newline at end of file
+}
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index f4a21c24fa1..c36f12dbd82 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -129,6 +129,25 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(Ci::Build.where(token: 'abcd')).to be_empty
end
end
+
+ context 'has restored the correct number of records' do
+ it 'has the correct number of merge requests' do
+ expect(@project.merge_requests.size).to eq(9)
+ end
+
+ it 'has the correct number of triggers' do
+ expect(@project.triggers.size).to eq(1)
+ end
+
+ it 'has the correct number of pipelines and statuses' do
+ expect(@project.pipelines.size).to eq(5)
+
+ @project.pipelines.zip([2, 2, 2, 2, 2])
+ .each do |(pipeline, expected_status_size)|
+ expect(pipeline.statuses.size).to eq(expected_status_size)
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index c718e792461..1ad16a9b57d 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -15,6 +15,7 @@ Issue:
- updated_by_id
- confidential
- deleted_at
+- closed_at
- due_date
- moved_to_id
- lock_version
@@ -176,7 +177,6 @@ Ci::Pipeline:
- tag
- yaml_errors
- committed_at
-- gl_project_id
- status
- started_at
- finished_at
@@ -211,7 +211,6 @@ CommitStatus:
- target_url
- description
- artifacts_file
-- gl_project_id
- artifacts_metadata
- erased_by_id
- erased_at
@@ -232,7 +231,6 @@ Ci::Variable:
- encrypted_value
- encrypted_value_salt
- encrypted_value_iv
-- gl_project_id
Ci::Trigger:
- id
- token
@@ -240,7 +238,6 @@ Ci::Trigger:
- deleted_at
- created_at
- updated_at
-- gl_project_id
- owner_id
- description
DeployKey:
diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb
index 917c5c46db1..8b77c925705 100644
--- a/spec/lib/gitlab/redis_spec.rb
+++ b/spec/lib/gitlab/redis_spec.rb
@@ -3,8 +3,16 @@ require 'spec_helper'
describe Gitlab::Redis do
include StubENV
- before(:each) { clear_raw_config }
- after(:each) { clear_raw_config }
+ let(:config) { 'config/resque.yml' }
+
+ before(:each) do
+ stub_env('GITLAB_REDIS_CONFIG_FILE', Rails.root.join(config).to_s)
+ clear_raw_config
+ end
+
+ after(:each) do
+ clear_raw_config
+ end
describe '.params' do
subject { described_class.params }
@@ -18,22 +26,22 @@ describe Gitlab::Redis do
end
context 'when url contains unix socket reference' do
- let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_socket.yml').to_s }
- let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_socket.yml').to_s }
+ let(:config_old) { 'spec/fixtures/config/redis_old_format_socket.yml' }
+ let(:config_new) { 'spec/fixtures/config/redis_new_format_socket.yml' }
context 'with old format' do
- it 'returns path key instead' do
- stub_const("#{described_class}::CONFIG_FILE", config_old)
+ let(:config) { config_old }
+ it 'returns path key instead' do
is_expected.to include(path: '/path/to/old/redis.sock')
is_expected.not_to have_key(:url)
end
end
context 'with new format' do
- it 'returns path key instead' do
- stub_const("#{described_class}::CONFIG_FILE", config_new)
+ let(:config) { config_new }
+ it 'returns path key instead' do
is_expected.to include(path: '/path/to/redis.sock')
is_expected.not_to have_key(:url)
end
@@ -41,22 +49,22 @@ describe Gitlab::Redis do
end
context 'when url is host based' do
- let(:config_old) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') }
- let(:config_new) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') }
+ let(:config_old) { 'spec/fixtures/config/redis_old_format_host.yml' }
+ let(:config_new) { 'spec/fixtures/config/redis_new_format_host.yml' }
context 'with old format' do
- it 'returns hash with host, port, db, and password' do
- stub_const("#{described_class}::CONFIG_FILE", config_old)
+ let(:config) { config_old }
+ it 'returns hash with host, port, db, and password' do
is_expected.to include(host: 'localhost', password: 'mypassword', port: 6379, db: 99)
is_expected.not_to have_key(:url)
end
end
context 'with new format' do
- it 'returns hash with host, port, db, and password' do
- stub_const("#{described_class}::CONFIG_FILE", config_new)
+ let(:config) { config_new }
+ it 'returns hash with host, port, db, and password' do
is_expected.to include(host: 'localhost', password: 'mynewpassword', port: 6379, db: 99)
is_expected.not_to have_key(:url)
end
@@ -74,15 +82,13 @@ describe Gitlab::Redis do
end
context 'when yml file with env variable' do
- let(:redis_config) { Rails.root.join('spec/fixtures/config/redis_config_with_env.yml') }
+ let(:config) { 'spec/fixtures/config/redis_config_with_env.yml' }
before do
stub_env('TEST_GITLAB_REDIS_URL', 'redis://redishost:6379')
end
it 'reads redis url from env variable' do
- stub_const("#{described_class}::CONFIG_FILE", redis_config)
-
expect(described_class.url).to eq 'redis://redishost:6379'
end
end
@@ -90,14 +96,13 @@ describe Gitlab::Redis do
describe '._raw_config' do
subject { described_class._raw_config }
+ let(:config) { '/var/empty/doesnotexist' }
it 'should be frozen' do
expect(subject).to be_frozen
end
it 'returns false when the file does not exist' do
- stub_const("#{described_class}::CONFIG_FILE", '/var/empty/doesnotexist')
-
expect(subject).to eq(false)
end
end
@@ -134,22 +139,18 @@ describe Gitlab::Redis do
subject { described_class.new(Rails.env).sentinels }
context 'when sentinels are defined' do
- let(:config) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') }
+ let(:config) { 'spec/fixtures/config/redis_new_format_host.yml' }
it 'returns an array of hashes with host and port keys' do
- stub_const("#{described_class}::CONFIG_FILE", config)
-
is_expected.to include(host: 'localhost', port: 26380)
is_expected.to include(host: 'slave2', port: 26381)
end
end
context 'when sentinels are not defined' do
- let(:config) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') }
+ let(:config) { 'spec/fixtures/config/redis_old_format_host.yml' }
it 'returns nil' do
- stub_const("#{described_class}::CONFIG_FILE", config)
-
is_expected.to be_nil
end
end
@@ -159,21 +160,17 @@ describe Gitlab::Redis do
subject { described_class.new(Rails.env).sentinels? }
context 'when sentinels are defined' do
- let(:config) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') }
+ let(:config) { 'spec/fixtures/config/redis_new_format_host.yml' }
it 'returns true' do
- stub_const("#{described_class}::CONFIG_FILE", config)
-
is_expected.to be_truthy
end
end
context 'when sentinels are not defined' do
- let(:config) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') }
+ let(:config) { 'spec/fixtures/config/redis_old_format_host.yml' }
it 'returns false' do
- stub_const("#{described_class}::CONFIG_FILE", config)
-
is_expected.to be_falsey
end
end
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
new file mode 100644
index 00000000000..a504d299307
--- /dev/null
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Gitlab::UrlBlocker, lib: true do
+ describe '#blocked_url?' do
+ it 'allows imports from configured web host and port' do
+ import_url = "http://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}/t.git"
+ expect(described_class.blocked_url?(import_url)).to be false
+ end
+
+ it 'allows imports from configured SSH host and port' do
+ import_url = "http://#{Gitlab.config.gitlab_shell.ssh_host}:#{Gitlab.config.gitlab_shell.ssh_port}/t.git"
+ expect(described_class.blocked_url?(import_url)).to be false
+ end
+
+ it 'returns true for bad localhost hostname' do
+ expect(described_class.blocked_url?('https://localhost:65535/foo/foo.git')).to be true
+ end
+
+ it 'returns true for bad port' do
+ expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git')).to be true
+ end
+
+ it 'returns true for invalid URL' do
+ expect(described_class.blocked_url?('http://:8080')).to be true
+ end
+
+ it 'returns false for legitimate URL' do
+ expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git')).to be false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
index 3fd361de458..fc144a2556a 100644
--- a/spec/lib/gitlab/url_sanitizer_spec.rb
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -5,6 +5,7 @@ describe Gitlab::UrlSanitizer, lib: true do
let(:url_sanitizer) do
described_class.new("https://github.com/me/project.git", credentials: credentials)
end
+ let(:user) { double(:user, username: 'john.doe') }
describe '.sanitize' do
def sanitize_url(url)
@@ -53,12 +54,33 @@ describe Gitlab::UrlSanitizer, lib: true do
end
end
+ describe '.valid?' do
+ it 'validates url strings' do
+ expect(described_class.valid?(nil)).to be(false)
+ expect(described_class.valid?('valid@project:url.git')).to be(true)
+ expect(described_class.valid?('123://invalid:url')).to be(false)
+ end
+ end
+
+ describe '.http_credentials_for_user' do
+ it { expect(described_class.http_credentials_for_user(user)).to eq({ user: 'john.doe' }) }
+ it { expect(described_class.http_credentials_for_user('foo')).to eq({}) }
+ end
+
describe '#sanitized_url' do
it { expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") }
end
describe '#credentials' do
it { expect(url_sanitizer.credentials).to eq(credentials) }
+
+ context 'when user is given to #initialize' do
+ let(:url_sanitizer) do
+ described_class.new("https://github.com/me/project.git", credentials: described_class.http_credentials_for_user(user))
+ end
+
+ it { expect(url_sanitizer.credentials).to eq({ user: 'john.doe' }) }
+ end
end
describe '#full_url' do
@@ -69,13 +91,13 @@ describe Gitlab::UrlSanitizer, lib: true do
expect(sanitizer.full_url).to eq('user@server:project.git')
end
- end
- describe '.valid?' do
- it 'validates url strings' do
- expect(described_class.valid?(nil)).to be(false)
- expect(described_class.valid?('valid@project:url.git')).to be(true)
- expect(described_class.valid?('123://invalid:url')).to be(false)
+ context 'when user is given to #initialize' do
+ let(:url_sanitizer) do
+ described_class.new("https://github.com/me/project.git", credentials: described_class.http_credentials_for_user(user))
+ end
+
+ it { expect(url_sanitizer.full_url).to eq("https://john.doe@github.com/me/project.git") }
end
end
end
diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb
new file mode 100644
index 00000000000..3255c6f1ef7
--- /dev/null
+++ b/spec/lib/gitlab/visibility_level_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Gitlab::VisibilityLevel, lib: true do
+ describe '.level_value' do
+ it 'converts "public" to integer value' do
+ expect(described_class.level_value('public')).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'converts string integer to integer value' do
+ expect(described_class.level_value('20')).to eq(20)
+ end
+
+ it 'defaults to PRIVATE when string value is not valid' do
+ expect(described_class.level_value('invalid')).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'defaults to PRIVATE when integer value is not valid' do
+ expect(described_class.level_value(100)).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+end
diff --git a/spec/mailers/emails/builds_spec.rb b/spec/mailers/emails/builds_spec.rb
deleted file mode 100644
index d968096783c..00000000000
--- a/spec/mailers/emails/builds_spec.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-require 'spec_helper'
-require 'email_spec'
-
-describe Notify do
- include EmailSpec::Matchers
-
- include_context 'gitlab email notification'
-
- describe 'build notification email' do
- let(:build) { create(:ci_build) }
- let(:project) { build.project }
-
- shared_examples 'build email' do
- it 'contains name of project' do
- is_expected.to have_body_text build.project_name
- end
-
- it 'contains link to project' do
- is_expected.to have_body_text namespace_project_path(project.namespace, project)
- end
- end
-
- shared_examples 'an email with X-GitLab headers containing build details' do
- it 'has X-GitLab-Build* headers' do
- is_expected.to have_header 'X-GitLab-Build-Id', /#{build.id}/
- is_expected.to have_header 'X-GitLab-Build-Ref', /#{build.ref}/
- end
- end
-
- describe 'build success' do
- subject { Notify.build_success_email(build.id, 'wow@example.com') }
- before { build.success }
-
- it_behaves_like 'build email'
- it_behaves_like 'an email with X-GitLab headers containing build details'
- it_behaves_like 'an email with X-GitLab headers containing project details'
-
- it 'has header indicating build status' do
- is_expected.to have_header 'X-GitLab-Build-Status', 'success'
- end
-
- it 'has the correct subject' do
- is_expected.to have_subject /Build success for/
- end
- end
-
- describe 'build fail' do
- subject { Notify.build_fail_email(build.id, 'wow@example.com') }
- before { build.drop }
-
- it_behaves_like 'build email'
- it_behaves_like 'an email with X-GitLab headers containing build details'
- it_behaves_like 'an email with X-GitLab headers containing project details'
-
- it 'has header indicating build status' do
- is_expected.to have_header 'X-GitLab-Build-Status', 'failed'
- end
-
- it 'has the correct subject' do
- is_expected.to have_subject /Build failed for/
- end
- end
- end
-end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index e822d7eb348..6ee91576676 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -63,7 +63,7 @@ describe Notify do
end
it 'contains a link to note author' do
- is_expected.to have_body_text issue.author_name
+ is_expected.to have_html_escaped_body_text issue.author_name
is_expected.to have_body_text 'wrote:'
end
end
@@ -75,7 +75,7 @@ describe Notify do
it_behaves_like 'it should show Gmail Actions View Issue link'
it 'contains the description' do
- is_expected.to have_body_text issue_with_description.description
+ is_expected.to have_html_escaped_body_text issue_with_description.description
end
end
@@ -100,11 +100,11 @@ describe Notify do
end
it 'contains the name of the previous assignee' do
- is_expected.to have_body_text previous_assignee.name
+ is_expected.to have_html_escaped_body_text previous_assignee.name
end
it 'contains the name of the new assignee' do
- is_expected.to have_body_text assignee.name
+ is_expected.to have_html_escaped_body_text assignee.name
end
it 'contains a link to the issue' do
@@ -167,7 +167,7 @@ describe Notify do
end
it 'contains the user name' do
- is_expected.to have_body_text current_user.name
+ is_expected.to have_html_escaped_body_text current_user.name
end
it 'contains a link to the issue' do
@@ -242,7 +242,7 @@ describe Notify do
end
it 'contains a link to note author' do
- is_expected.to have_body_text merge_request.author_name
+ is_expected.to have_html_escaped_body_text merge_request.author_name
is_expected.to have_body_text 'wrote:'
end
end
@@ -255,7 +255,7 @@ describe Notify do
it_behaves_like "an unsubscribeable thread"
it 'contains the description' do
- is_expected.to have_body_text merge_request_with_description.description
+ is_expected.to have_html_escaped_body_text merge_request_with_description.description
end
end
@@ -280,11 +280,11 @@ describe Notify do
end
it 'contains the name of the previous assignee' do
- is_expected.to have_body_text previous_assignee.name
+ is_expected.to have_html_escaped_body_text previous_assignee.name
end
it 'contains the name of the new assignee' do
- is_expected.to have_body_text assignee.name
+ is_expected.to have_html_escaped_body_text assignee.name
end
it 'contains a link to the merge request' do
@@ -347,7 +347,7 @@ describe Notify do
end
it 'contains the user name' do
- is_expected.to have_body_text current_user.name
+ is_expected.to have_html_escaped_body_text current_user.name
end
it 'contains a link to the merge request' do
@@ -400,7 +400,7 @@ describe Notify do
end
it 'contains name of project' do
- is_expected.to have_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
end
it 'contains new user role' do
@@ -433,7 +433,7 @@ describe Notify do
expect(to_emails[0].address).to eq(project.members.owners_and_masters.first.user.notification_email)
is_expected.to have_subject "Request to join the #{project.name_with_namespace} project"
- is_expected.to have_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
is_expected.to have_body_text namespace_project_project_members_url(project.namespace, project)
is_expected.to have_body_text project_member.human_access
end
@@ -460,7 +460,7 @@ describe Notify do
expect(to_emails[0].address).to eq(group.members.owners_and_masters.first.user.notification_email)
is_expected.to have_subject "Request to join the #{project.name_with_namespace} project"
- is_expected.to have_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
is_expected.to have_body_text namespace_project_project_members_url(project.namespace, project)
is_expected.to have_body_text project_member.human_access
end
@@ -482,13 +482,14 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{project.name_with_namespace} project was denied"
- is_expected.to have_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
is_expected.to have_body_text project.web_url
end
end
describe 'project access changed' do
- let(:project) { create(:empty_project, :public, :access_requestable) }
+ let(:owner) { create(:user, name: "Chang O'Keefe") }
+ let(:project) { create(:empty_project, :public, :access_requestable, namespace: owner.namespace) }
let(:user) { create(:user) }
let(:project_member) { create(:project_member, project: project, user: user) }
subject { Notify.member_access_granted_email('project', project_member.id) }
@@ -499,7 +500,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{project.name_with_namespace} project was granted"
- is_expected.to have_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project_member.human_access
end
@@ -530,7 +531,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Invitation to join the #{project.name_with_namespace} project"
- is_expected.to have_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project_member.human_access
is_expected.to have_body_text project_member.invite_token
@@ -555,10 +556,10 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation accepted'
- is_expected.to have_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project_member.invite_email
- is_expected.to have_body_text invited_user.name
+ is_expected.to have_html_escaped_body_text invited_user.name
end
end
@@ -579,7 +580,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation declined'
- is_expected.to have_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project_member.invite_email
end
@@ -607,7 +608,7 @@ describe Notify do
end
it 'contains the message from the note' do
- is_expected.to have_body_text note.note
+ is_expected.to have_html_escaped_body_text note.note
end
it 'does not contain note author' do
@@ -620,7 +621,7 @@ describe Notify do
end
it 'contains a link to note author' do
- is_expected.to have_body_text note.author_name
+ is_expected.to have_html_escaped_body_text note.author_name
is_expected.to have_body_text 'wrote:'
end
end
@@ -727,7 +728,7 @@ describe Notify do
end
it 'contains the message from the note' do
- is_expected.to have_body_text note.note
+ is_expected.to have_html_escaped_body_text note.note
end
it 'does not contain note author' do
@@ -740,7 +741,7 @@ describe Notify do
end
it 'contains a link to note author' do
- is_expected.to have_body_text note.author_name
+ is_expected.to have_html_escaped_body_text note.author_name
is_expected.to have_body_text 'wrote:'
end
end
@@ -786,7 +787,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Request to join the #{group.name} group"
- is_expected.to have_body_text group.name
+ is_expected.to have_html_escaped_body_text group.name
is_expected.to have_body_text group_group_members_url(group)
is_expected.to have_body_text group_member.human_access
end
@@ -807,7 +808,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{group.name} group was denied"
- is_expected.to have_body_text group.name
+ is_expected.to have_html_escaped_body_text group.name
is_expected.to have_body_text group.web_url
end
end
@@ -825,7 +826,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{group.name} group was granted"
- is_expected.to have_body_text group.name
+ is_expected.to have_html_escaped_body_text group.name
is_expected.to have_body_text group.web_url
is_expected.to have_body_text group_member.human_access
end
@@ -856,7 +857,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Invitation to join the #{group.name} group"
- is_expected.to have_body_text group.name
+ is_expected.to have_html_escaped_body_text group.name
is_expected.to have_body_text group.web_url
is_expected.to have_body_text group_member.human_access
is_expected.to have_body_text group_member.invite_token
@@ -881,10 +882,10 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation accepted'
- is_expected.to have_body_text group.name
+ is_expected.to have_html_escaped_body_text group.name
is_expected.to have_body_text group.web_url
is_expected.to have_body_text group_member.invite_email
- is_expected.to have_body_text invited_user.name
+ is_expected.to have_html_escaped_body_text invited_user.name
end
end
@@ -905,7 +906,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation declined'
- is_expected.to have_body_text group.name
+ is_expected.to have_html_escaped_body_text group.name
is_expected.to have_body_text group.web_url
is_expected.to have_body_text group_member.invite_email
end
diff --git a/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb b/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb
new file mode 100644
index 00000000000..57eb03e3c80
--- /dev/null
+++ b/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170301205640_migrate_build_events_to_pipeline_events.rb')
+
+# This migration uses multiple threads, and thus different transactions. This
+# means data created in this spec may not be visible to some threads. To work
+# around this we use the TRUNCATE cleaning strategy.
+describe MigrateBuildEventsToPipelineEvents, truncate: true do
+ let(:migration) { described_class.new }
+ let(:project_with_pipeline_service) { create(:empty_project) }
+ let(:project_with_build_service) { create(:empty_project) }
+
+ before do
+ ActiveRecord::Base.connection.execute <<-SQL
+ INSERT INTO services (properties, build_events, pipeline_events, type)
+ VALUES
+ ('{"notify_only_broken_builds":true}', true, false, 'SlackService')
+ , ('{"notify_only_broken_builds":true}', true, false, 'MattermostService')
+ , ('{"notify_only_broken_builds":true}', true, false, 'HipchatService')
+ ;
+ SQL
+
+ ActiveRecord::Base.connection.execute <<-SQL
+ INSERT INTO services
+ (properties, build_events, pipeline_events, type, project_id)
+ VALUES
+ ('{"notify_only_broken_builds":true}', true, false,
+ 'BuildsEmailService', #{project_with_pipeline_service.id})
+ , ('{"notify_only_broken_pipelines":true}', false, true,
+ 'PipelinesEmailService', #{project_with_pipeline_service.id})
+ , ('{"notify_only_broken_builds":true}', true, false,
+ 'BuildsEmailService', #{project_with_build_service.id})
+ ;
+ SQL
+ end
+
+ describe '#up' do
+ before do
+ silence_migration = Module.new do
+ # rubocop:disable Rails/Delegate
+ def execute(query)
+ connection.execute(query)
+ end
+ end
+
+ migration.extend(silence_migration)
+ migration.up
+ end
+
+ it 'migrates chat service properly' do
+ [SlackService, MattermostService, HipchatService].each do |service|
+ expect(service.count).to eq(1)
+
+ verify_service_record(service.first)
+ end
+ end
+
+ it 'migrates pipelines email service only if it has none before' do
+ Project.find_each do |project|
+ pipeline_service_count =
+ project.services.where(type: 'PipelinesEmailService').count
+
+ expect(pipeline_service_count).to eq(1)
+
+ verify_service_record(project.pipelines_email_service)
+ end
+ end
+
+ def verify_service_record(service)
+ expect(service.notify_only_broken_pipelines).to be(true)
+ expect(service.build_events).to be(false)
+ expect(service.pipeline_events).to be(true)
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index fd6ea2d6722..8dbcf50ee0c 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -795,8 +795,8 @@ describe Ci::Build, :models do
describe '#merge_request' do
def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
- create(factory, source_project_id: pipeline.gl_project_id,
- target_project_id: pipeline.gl_project_id,
+ create(factory, source_project: pipeline.project,
+ target_project: pipeline.project,
source_branch: build.ref,
created_at: created_at)
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 9962c987110..53282b999dc 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -532,6 +532,19 @@ describe Ci::Pipeline, models: true do
end
end
+ describe '.latest_successful_for_refs' do
+ include_context 'with some outdated pipelines'
+
+ let!(:latest_successful_pipeline1) { create_pipeline(:success, 'ref1', 'D') }
+ let!(:latest_successful_pipeline2) { create_pipeline(:success, 'ref2', 'D') }
+
+ it 'returns the latest successful pipeline for both refs' do
+ refs = %w(ref1 ref2 ref3)
+
+ expect(described_class.latest_successful_for_refs(refs)).to eq({ 'ref1' => latest_successful_pipeline1, 'ref2' => latest_successful_pipeline2 })
+ end
+ end
+
describe '#status' do
let(:build) do
create(:ci_build, :created, pipeline: pipeline, name: 'test')
@@ -1018,6 +1031,19 @@ describe Ci::Pipeline, models: true do
end
end
+ describe '#update_status' do
+ let(:pipeline) { create(:ci_pipeline, sha: '123456') }
+
+ it 'updates the cached status' do
+ fake_status = double
+ # after updating the status, the status is set to `skipped` for this pipeline's builds
+ expect(Ci::PipelineStatus).to receive(:new).with(pipeline.project, sha: '123456', status: 'skipped').and_return(fake_status)
+ expect(fake_status).to receive(:store_in_cache_if_needed)
+
+ pipeline.update_status
+ end
+ end
+
describe 'notifications when pipeline success or failed' do
let(:project) { create(:project, :repository) }
diff --git a/spec/models/ci/pipeline_status_spec.rb b/spec/models/ci/pipeline_status_spec.rb
new file mode 100644
index 00000000000..bc5b71666c2
--- /dev/null
+++ b/spec/models/ci/pipeline_status_spec.rb
@@ -0,0 +1,173 @@
+require 'spec_helper'
+
+describe Ci::PipelineStatus do
+ let(:project) { create(:project) }
+ let(:pipeline_status) { described_class.new(project) }
+
+ describe '.load_for_project' do
+ it "loads the status" do
+ expect_any_instance_of(described_class).to receive(:load_status)
+
+ described_class.load_for_project(project)
+ end
+ end
+
+ describe '#has_status?' do
+ it "is false when the status wasn't loaded yet" do
+ expect(pipeline_status.has_status?).to be_falsy
+ end
+
+ it 'is true when all status information was loaded' do
+ fake_commit = double
+ allow(fake_commit).to receive(:status).and_return('failed')
+ allow(fake_commit).to receive(:sha).and_return('failed424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6')
+ allow(pipeline_status).to receive(:commit).and_return(fake_commit)
+ allow(pipeline_status).to receive(:has_cache?).and_return(false)
+
+ pipeline_status.load_status
+
+ expect(pipeline_status.has_status?).to be_truthy
+ end
+ end
+
+ describe '#load_status' do
+ it 'loads the status from the cache when there is one' do
+ expect(pipeline_status).to receive(:has_cache?).and_return(true)
+ expect(pipeline_status).to receive(:load_from_cache)
+
+ pipeline_status.load_status
+ end
+
+ it 'loads the status from the project commit when there is no cache' do
+ allow(pipeline_status).to receive(:has_cache?).and_return(false)
+
+ expect(pipeline_status).to receive(:load_from_commit)
+
+ pipeline_status.load_status
+ end
+
+ it 'stores the status in the cache when it loading it from the project' do
+ allow(pipeline_status).to receive(:has_cache?).and_return(false)
+ allow(pipeline_status).to receive(:load_from_commit)
+
+ expect(pipeline_status).to receive(:store_in_cache)
+
+ pipeline_status.load_status
+ end
+
+ it 'sets the state to loaded' do
+ pipeline_status.load_status
+
+ expect(pipeline_status).to be_loaded
+ end
+
+ it 'only loads the status once' do
+ expect(pipeline_status).to receive(:has_cache?).and_return(true).exactly(1)
+ expect(pipeline_status).to receive(:load_from_cache).exactly(1)
+
+ pipeline_status.load_status
+ pipeline_status.load_status
+ end
+ end
+
+ describe "#load_from_commit" do
+ let!(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.sha) }
+
+ it 'reads the status from the pipeline for the commit' do
+ pipeline_status.load_from_commit
+
+ expect(pipeline_status.status).to eq('success')
+ expect(pipeline_status.sha).to eq(project.commit.sha)
+ end
+
+ it "doesn't fail for an empty project" do
+ status_for_empty_commit = described_class.new(create(:empty_project))
+
+ status_for_empty_commit.load_status
+
+ expect(status_for_empty_commit).to be_loaded
+ end
+ end
+
+ describe "#store_in_cache", :redis do
+ it "sets the object in redis" do
+ pipeline_status.sha = '123456'
+ pipeline_status.status = 'failed'
+
+ pipeline_status.store_in_cache
+ read_sha, read_status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) }
+
+ expect(read_sha).to eq('123456')
+ expect(read_status).to eq('failed')
+ end
+ end
+
+ describe '#store_in_cache_if_needed', :redis do
+ it 'stores the state in the cache when the sha is the HEAD of the project' do
+ create(:ci_pipeline, :success, project: project, sha: project.commit.sha)
+ build_status = described_class.load_for_project(project)
+
+ build_status.store_in_cache_if_needed
+ sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) }
+
+ expect(sha).not_to be_nil
+ expect(status).not_to be_nil
+ end
+
+ it "doesn't store the status in redis when the sha is not the head of the project" do
+ other_status = described_class.new(project, sha: "123456", status: "failed")
+
+ other_status.store_in_cache_if_needed
+ sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) }
+
+ expect(sha).to be_nil
+ expect(status).to be_nil
+ end
+
+ it "deletes the cache if the repository doesn't have a head commit" do
+ empty_project = create(:empty_project)
+ Gitlab::Redis.with { |redis| redis.mapped_hmset("projects/#{empty_project.id}/build_status", { sha: "sha", status: "pending" }) }
+ other_status = described_class.new(empty_project, sha: "123456", status: "failed")
+
+ other_status.store_in_cache_if_needed
+ sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{empty_project.id}/build_status", :sha, :status) }
+
+ expect(sha).to be_nil
+ expect(status).to be_nil
+ end
+ end
+
+ describe "with a status in redis", :redis do
+ let(:status) { 'success' }
+ let(:sha) { '424d1b73bc0d3cb726eb7dc4ce17a4d48552f8c6' }
+
+ before do
+ Gitlab::Redis.with { |redis| redis.mapped_hmset("projects/#{project.id}/build_status", { sha: sha, status: status }) }
+ end
+
+ describe '#load_from_cache' do
+ it 'reads the status from redis' do
+ pipeline_status.load_from_cache
+
+ expect(pipeline_status.sha).to eq(sha)
+ expect(pipeline_status.status).to eq(status)
+ end
+ end
+
+ describe '#has_cache?' do
+ it 'knows the status is cached' do
+ expect(pipeline_status.has_cache?).to be_truthy
+ end
+ end
+
+ describe '#delete_from_cache' do
+ it 'deletes values from redis' do
+ pipeline_status.delete_from_cache
+
+ key_exists = Gitlab::Redis.with { |redis| redis.exists("projects/#{project.id}/build_status") }
+
+ expect(key_exists).to be_falsy
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index bee9f714849..048d25869bc 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -6,7 +6,7 @@ describe Ci::Variable, models: true do
let(:secret_value) { 'secret' }
it { is_expected.to validate_presence_of(:key) }
- it { is_expected.to validate_uniqueness_of(:key).scoped_to(:gl_project_id) }
+ it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id) }
it { is_expected.to validate_length_of(:key).is_at_most(255) }
it { is_expected.to allow_value('foo').for(:key) }
it { is_expected.not_to allow_value('foo bar').for(:key) }
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 32f9366a14c..980a1b70ef5 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -212,6 +212,25 @@ eos
end
end
+ describe '#latest_pipeline' do
+ let!(:first_pipeline) do
+ create(:ci_empty_pipeline,
+ project: project,
+ sha: commit.sha,
+ status: 'success')
+ end
+ let!(:second_pipeline) do
+ create(:ci_empty_pipeline,
+ project: project,
+ sha: commit.sha,
+ status: 'success')
+ end
+
+ it 'returns latest pipeline' do
+ expect(commit.latest_pipeline).to eq second_pipeline
+ end
+ end
+
describe '#status' do
context 'without ref argument' do
before do
@@ -369,4 +388,32 @@ eos
expect(described_class.valid_hash?('a' * 41)).to be false
end
end
+
+ describe '#raw_diffs' do
+ context 'Gitaly commit_raw_diffs feature enabled' do
+ before do
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:commit_raw_diffs).and_return(true)
+ end
+
+ context 'when a truthy deltas_only is not passed to args' do
+ it 'fetches diffs from Gitaly server' do
+ expect(Gitlab::GitalyClient::Commit).to receive(:diff_from_parent).
+ with(commit)
+
+ commit.raw_diffs
+ end
+ end
+
+ context 'when a truthy deltas_only is passed to args' do
+ it 'fetches diffs using Rugged' do
+ opts = { deltas_only: true }
+
+ expect(Gitlab::GitalyClient::Commit).not_to receive(:diff_from_parent)
+ expect(commit.raw).to receive(:diffs).with(opts)
+
+ commit.raw_diffs(opts)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb
index f134da441c2..82abad0e2f6 100644
--- a/spec/models/concerns/has_status_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -110,6 +110,24 @@ describe HasStatus do
it { is_expected.to eq 'running' }
end
+ context 'when one status finished and second is still created' do
+ let!(:statuses) do
+ [create(type, status: :success), create(type, status: :created)]
+ end
+
+ it { is_expected.to eq 'running' }
+ end
+
+ context 'when there is a manual status before created status' do
+ let!(:statuses) do
+ [create(type, status: :success),
+ create(type, status: :manual, allow_failure: false),
+ create(type, status: :created)]
+ end
+
+ it { is_expected.to eq 'manual' }
+ end
+
context 'when one status is a blocking manual action' do
let!(:statuses) do
[create(type, status: :failed),
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 545a11912e3..9574796a945 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -278,6 +278,16 @@ describe Issue, "Issuable" do
end
end
+ context 'issue has labels' do
+ let(:labels) { [create(:label), create(:label)] }
+
+ before { issue.update_attribute(:labels, labels)}
+
+ it 'includes labels in the hook data' do
+ expect(data[:labels]).to eq(labels.map(&:hook_attrs))
+ end
+ end
+
include_examples 'project hook data'
include_examples 'deprecated repository hook data'
end
@@ -344,6 +354,46 @@ describe Issue, "Issuable" do
end
end
+ describe '.order_due_date_and_labels_priority' do
+ let(:project) { create(:empty_project) }
+
+ def create_issue(milestone, labels)
+ create(:labeled_issue, milestone: milestone, labels: labels, project: project)
+ end
+
+ it 'sorts issues in order of milestone due date, then label priority' do
+ first_priority = create(:label, project: project, priority: 1)
+ second_priority = create(:label, project: project, priority: 2)
+ no_priority = create(:label, project: project)
+
+ first_milestone = create(:milestone, project: project, due_date: Time.now)
+ second_milestone = create(:milestone, project: project, due_date: Time.now + 1.month)
+ third_milestone = create(:milestone, project: project)
+
+ # The issues here are ordered by label priority, to ensure that we don't
+ # accidentally just sort by creation date.
+ second_milestone_first_priority = create_issue(second_milestone, [first_priority, second_priority, no_priority])
+ third_milestone_first_priority = create_issue(third_milestone, [first_priority, second_priority, no_priority])
+ first_milestone_second_priority = create_issue(first_milestone, [second_priority, no_priority])
+ second_milestone_second_priority = create_issue(second_milestone, [second_priority, no_priority])
+ no_milestone_second_priority = create_issue(nil, [second_priority, no_priority])
+ first_milestone_no_priority = create_issue(first_milestone, [no_priority])
+ second_milestone_no_labels = create_issue(second_milestone, [])
+ third_milestone_no_priority = create_issue(third_milestone, [no_priority])
+
+ result = Issue.order_due_date_and_labels_priority
+
+ expect(result).to eq([first_milestone_second_priority,
+ first_milestone_no_priority,
+ second_milestone_first_priority,
+ second_milestone_second_priority,
+ second_milestone_no_labels,
+ third_milestone_first_priority,
+ no_milestone_second_priority,
+ third_milestone_no_priority])
+ end
+ end
+
describe '.order_labels_priority' do
let(:label_1) { create(:label, title: 'label_1', project: issue.project, priority: 1) }
let(:label_2) { create(:label, title: 'label_2', project: issue.project, priority: 2) }
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index ad703a6c8bb..68e4c0a522b 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -116,21 +116,41 @@ describe Milestone, 'Milestoneish' do
end
end
+ describe '#remaining_days' do
+ it 'shows 0 if no due date' do
+ milestone = build_stubbed(:milestone)
+
+ expect(milestone.remaining_days).to eq(0)
+ end
+
+ it 'shows 0 if expired' do
+ milestone = build_stubbed(:milestone, due_date: 2.days.ago)
+
+ expect(milestone.remaining_days).to eq(0)
+ end
+
+ it 'shows correct remaining days' do
+ milestone = build_stubbed(:milestone, due_date: 2.days.from_now)
+
+ expect(milestone.remaining_days).to eq(2)
+ end
+ end
+
describe '#elapsed_days' do
it 'shows 0 if no start_date set' do
- milestone = build(:milestone)
+ milestone = build_stubbed(:milestone)
expect(milestone.elapsed_days).to eq(0)
end
it 'shows 0 if start_date is a future' do
- milestone = build(:milestone, start_date: Time.now + 2.days)
+ milestone = build_stubbed(:milestone, start_date: Time.now + 2.days)
expect(milestone.elapsed_days).to eq(0)
end
it 'shows correct amount of days' do
- milestone = build(:milestone, start_date: Time.now - 2.days)
+ milestone = build_stubbed(:milestone, start_date: Time.now - 2.days)
expect(milestone.elapsed_days).to eq(2)
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index b4305e92812..9f0e7fbbe26 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -239,7 +239,7 @@ describe Environment, models: true do
describe '#actions_for' do
let(:deployment) { create(:deployment, environment: environment) }
let(:pipeline) { deployment.deployable.pipeline }
- let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_BUILD_REF_NAME' )}
+ let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_COMMIT_REF_NAME' )}
let!(:production_action) { create(:ci_build, :manual, name: 'production', pipeline: pipeline, environment: 'production' )}
it 'returns a list of actions with matching environment' do
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index f67fbe79bde..73977d031f9 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -37,6 +37,30 @@ describe Issue, models: true do
end
end
+ describe '#closed_at' do
+ after do
+ Timecop.return
+ end
+
+ let!(:now) { Timecop.freeze(Time.now) }
+
+ it 'sets closed_at to Time.now when issue is closed' do
+ issue = create(:issue, state: 'opened')
+
+ issue.close
+
+ expect(issue.closed_at).to eq(now)
+ end
+
+ it 'sets closed_at to nil when issue is reopened' do
+ issue = create(:issue, state: 'closed')
+
+ issue.reopen
+
+ expect(issue.closed_at).to be_nil
+ end
+ end
+
describe '#to_reference' do
let(:namespace) { build(:namespace, path: 'sample-namespace') }
let(:project) { build(:empty_project, name: 'sample-project', namespace: namespace) }
@@ -635,4 +659,15 @@ describe Issue, models: true do
end
end
end
+
+ describe '#hook_attrs' do
+ let(:attrs_hash) { subject.hook_attrs }
+
+ it 'includes time tracking attrs' do
+ expect(attrs_hash).to include(:total_time_spent)
+ expect(attrs_hash).to include(:human_time_estimate)
+ expect(attrs_hash).to include(:human_total_time_spent)
+ expect(attrs_hash).to include('time_estimate')
+ end
+ end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index fcaf4c71182..24e7c1b17d9 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -542,7 +542,7 @@ describe MergeRequest, models: true do
end
describe "#hook_attrs" do
- let(:attrs_hash) { subject.hook_attrs.to_h }
+ let(:attrs_hash) { subject.hook_attrs }
[:source, :target].each do |key|
describe "#{key} key" do
@@ -558,6 +558,10 @@ describe MergeRequest, models: true do
expect(attrs_hash).to include(:target)
expect(attrs_hash).to include(:last_commit)
expect(attrs_hash).to include(:work_in_progress)
+ expect(attrs_hash).to include(:total_time_spent)
+ expect(attrs_hash).to include(:human_time_estimate)
+ expect(attrs_hash).to include(:human_total_time_spent)
+ expect(attrs_hash).to include('time_estimate')
end
end
diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb
deleted file mode 100644
index 0194f9e2563..00000000000
--- a/spec/models/project_services/builds_email_service_spec.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-require 'spec_helper'
-
-describe BuildsEmailService do
- let(:data) do
- Gitlab::DataBuilder::Build.build(create(:ci_build))
- end
-
- describe 'Validations' do
- context 'when service is active' do
- before { subject.active = true }
-
- it { is_expected.to validate_presence_of(:recipients) }
-
- context 'when pusher is added' do
- before { subject.add_pusher = true }
-
- it { is_expected.not_to validate_presence_of(:recipients) }
- end
- end
-
- context 'when service is inactive' do
- before { subject.active = false }
-
- it { is_expected.not_to validate_presence_of(:recipients) }
- end
- end
-
- describe '#test_data' do
- let(:build) { create(:ci_build) }
- let(:project) { build.project }
- let(:user) { create(:user) }
-
- before { project.team << [user, :developer] }
-
- it 'builds test data' do
- data = subject.test_data(project)
-
- expect(data[:object_kind]).to eq("build")
- end
- end
-
- describe '#test' do
- it 'sends email' do
- data = Gitlab::DataBuilder::Build.build(create(:ci_build))
- subject.recipients = 'test@gitlab.com'
-
- expect(BuildEmailWorker).to receive(:perform_async)
-
- subject.test(data)
- end
-
- context 'notify only failed builds is true' do
- it 'sends email' do
- data = Gitlab::DataBuilder::Build.build(create(:ci_build))
- data[:build_status] = "success"
- subject.recipients = 'test@gitlab.com'
-
- expect(subject).not_to receive(:notify_only_broken_builds)
- expect(BuildEmailWorker).to receive(:perform_async)
-
- subject.test(data)
- end
- end
- end
-
- describe '#execute' do
- it 'sends email' do
- subject.recipients = 'test@gitlab.com'
- data[:build_status] = 'failed'
-
- expect(BuildEmailWorker).to receive(:perform_async)
-
- subject.execute(data)
- end
-
- it 'does not send email with succeeded build and notify_only_broken_builds on' do
- expect(subject).to receive(:notify_only_broken_builds).and_return(true)
- data[:build_status] = 'success'
-
- expect(BuildEmailWorker).not_to receive(:perform_async)
-
- subject.execute(data)
- end
-
- it 'does not send email with failed build and build_allow_failure is true' do
- data[:build_status] = 'failed'
- data[:build_allow_failure] = true
-
- expect(BuildEmailWorker).not_to receive(:perform_async)
-
- subject.execute(data)
- end
-
- it 'does not send email with unknown build status' do
- data[:build_status] = 'foo'
-
- expect(BuildEmailWorker).not_to receive(:perform_async)
-
- subject.execute(data)
- end
-
- it 'does not send email when recipients list is empty' do
- subject.recipients = ' ,, '
- data[:build_status] = 'failed'
-
- expect(BuildEmailWorker).not_to receive(:perform_async)
-
- subject.execute(data)
- end
- end
-end
diff --git a/spec/models/project_services/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb
deleted file mode 100644
index 3bd7ec18ae0..00000000000
--- a/spec/models/project_services/chat_message/build_message_spec.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-require 'spec_helper'
-
-describe ChatMessage::BuildMessage do
- subject { described_class.new(args) }
-
- let(:args) do
- {
- sha: '97de212e80737a608d939f648d959671fb0a0142',
- ref: 'develop',
- tag: false,
-
- project_name: 'project_name',
- project_url: 'http://example.gitlab.com',
- build_id: 1,
- build_name: build_name,
- build_stage: stage,
-
- commit: {
- status: status,
- author_name: 'hacker',
- author_url: 'http://example.gitlab.com/hacker',
- duration: duration,
- },
- }
- end
-
- let(:message) { build_message }
- let(:stage) { 'test' }
- let(:status) { 'success' }
- let(:build_name) { 'rspec' }
- let(:duration) { 10 }
-
- context 'build succeeded' do
- let(:status) { 'success' }
- let(:color) { 'good' }
- let(:message) { build_message('passed') }
-
- it 'returns a message with information about succeeded build' do
- expect(subject.pretext).to be_empty
- expect(subject.fallback).to eq(message)
- expect(subject.attachments).to eq([text: message, color: color])
- end
- end
-
- context 'build failed' do
- let(:status) { 'failed' }
- let(:color) { 'danger' }
-
- it 'returns a message with information about failed build' do
- expect(subject.pretext).to be_empty
- expect(subject.fallback).to eq(message)
- expect(subject.attachments).to eq([text: message, color: color])
- end
- end
-
- it 'returns a message with information on build' do
- expect(subject.fallback).to include("on build <http://example.gitlab.com/builds/1|#{build_name}>")
- end
-
- it 'returns a message with stage name' do
- expect(subject.fallback).to include("of stage #{stage}")
- end
-
- it 'returns a message with link to author' do
- expect(subject.fallback).to include("by <http://example.gitlab.com/hacker|hacker>")
- end
-
- def build_message(status_text = status, stage_text = stage, build_text = build_name)
- "<http://example.gitlab.com|project_name>:" \
- " Commit <http://example.gitlab.com/commit/" \
- "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \
- " of <http://example.gitlab.com/commits/develop|develop> branch" \
- " by <http://example.gitlab.com/hacker|hacker> #{status_text}" \
- " on build <http://example.gitlab.com/builds/1|#{build_text}>" \
- " of stage #{stage_text} in #{duration} #{'second'.pluralize(duration)}"
- end
-end
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index bf422ac7ce1..1200ae7eb22 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -280,13 +280,14 @@ describe HipchatService, models: true do
end
end
- context 'build events' do
- let(:pipeline) { create(:ci_empty_pipeline) }
- let(:build) { create(:ci_build, pipeline: pipeline) }
- let(:data) { Gitlab::DataBuilder::Build.build(build.reload) }
+ context 'pipeline events' do
+ let(:pipeline) { create(:ci_empty_pipeline, user: create(:user)) }
+ let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
context 'for failed' do
- before { build.drop }
+ before do
+ pipeline.drop
+ end
it "calls Hipchat API" do
hipchat.execute(data)
@@ -295,35 +296,36 @@ describe HipchatService, models: true do
end
it "creates a build message" do
- message = hipchat.send(:create_build_message, data)
+ message = hipchat.__send__(:create_pipeline_message, data)
project_url = project.web_url
project_name = project.name_with_namespace.gsub(/\s/, '')
- sha = data[:sha]
- ref = data[:ref]
- ref_type = data[:tag] ? 'tag' : 'branch'
- duration = data[:commit][:duration]
+ pipeline_attributes = data[:object_attributes]
+ ref = pipeline_attributes[:ref]
+ ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+ duration = pipeline_attributes[:duration]
+ user_name = data[:user][:name]
expect(message).to eq("<a href=\"#{project_url}\">#{project_name}</a>: " \
- "Commit <a href=\"#{project_url}/commit/#{sha}/builds\">#{Commit.truncate_sha(sha)}</a> " \
+ "Pipeline <a href=\"#{project_url}/pipelines/#{pipeline.id}\">##{pipeline.id}</a> " \
"of <a href=\"#{project_url}/commits/#{ref}\">#{ref}</a> #{ref_type} " \
- "by #{data[:commit][:author_name]} failed in #{duration} second(s)")
+ "by #{user_name} failed in #{duration} second(s)")
end
end
context 'for succeeded' do
before do
- build.success
+ pipeline.succeed
end
it "calls Hipchat API" do
- hipchat.notify_only_broken_builds = false
+ hipchat.notify_only_broken_pipelines = false
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
end
it "notifies only broken" do
- hipchat.notify_only_broken_builds = true
+ hipchat.notify_only_broken_pipelines = true
hipchat.execute(data)
expect(WebMock).not_to have_requested(:post, api_url).once
end
@@ -349,17 +351,19 @@ describe HipchatService, models: true do
context 'with a successful build' do
it 'uses the green color' do
- build_data = { object_kind: 'build', commit: { status: 'success' } }
+ data = { object_kind: 'pipeline',
+ object_attributes: { status: 'success' } }
- expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'green' })
+ expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'green' })
end
end
context 'with a failed build' do
it 'uses the red color' do
- build_data = { object_kind: 'build', commit: { status: 'failed' } }
+ data = { object_kind: 'pipeline',
+ object_attributes: { status: 'failed' } }
- expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'red' })
+ expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'red' })
end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index e120e21af06..5e5f690acd4 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -29,8 +29,7 @@ describe Project, models: true do
it { is_expected.to have_one(:campfire_service).dependent(:destroy) }
it { is_expected.to have_one(:drone_ci_service).dependent(:destroy) }
it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) }
- it { is_expected.to have_one(:builds_email_service).dependent(:destroy) }
- it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) }
+ it { is_expected.to have_one(:pipelines_email_service).dependent(:destroy) }
it { is_expected.to have_one(:irker_service).dependent(:destroy) }
it { is_expected.to have_one(:pivotaltracker_service).dependent(:destroy) }
it { is_expected.to have_one(:hipchat_service).dependent(:destroy) }
@@ -219,6 +218,20 @@ describe Project, models: true do
expect(project2.import_data).to be_nil
end
+ it "does not allow blocked import_url localhost" do
+ project2 = build(:empty_project, import_url: 'http://localhost:9000/t.git')
+
+ expect(project2).to be_invalid
+ expect(project2.errors[:import_url]).to include('imports are not allowed from that URL')
+ end
+
+ it "does not allow blocked import_url port" do
+ project2 = build(:empty_project, import_url: 'http://github.com:25/t.git')
+
+ expect(project2).to be_invalid
+ expect(project2.errors[:import_url]).to include('imports are not allowed from that URL')
+ end
+
describe 'project pending deletion' do
let!(:project_pending_deletion) do
create(:empty_project,
@@ -1901,10 +1914,8 @@ describe Project, models: true do
context 'when no user is given' do
it 'returns the url to the repo without a username' do
- url = project.http_url_to_repo
-
- expect(url).to eq(project.http_url_to_repo)
- expect(url).not_to include('@')
+ expect(project.http_url_to_repo).to eq("#{project.web_url}.git")
+ expect(project.http_url_to_repo).not_to include('@')
end
end
@@ -1912,8 +1923,19 @@ describe Project, models: true do
it 'returns the url to the repo with the username' do
user = build_stubbed(:user)
- expect(project.http_url_to_repo(user)).to match(%r{https?:\/\/#{user.username}@})
+ expect(project.http_url_to_repo(user)).to start_with("http://#{user.username}@")
end
end
end
+
+ describe '#pipeline_status' do
+ let(:project) { create(:project) }
+ it 'builds a pipeline status' do
+ expect(project.pipeline_status).to be_a(Ci::PipelineStatus)
+ end
+
+ it 'hase a loaded pipeline status' do
+ expect(project.pipeline_status).to be_loaded
+ end
+ end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 58b57bd4fef..b5b9cd024b0 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -35,10 +35,23 @@ describe ProjectWiki, models: true do
end
describe "#http_url_to_repo" do
- it "provides the full http url to the repo" do
- gitlab_url = Gitlab.config.gitlab.url
- repo_http_url = "#{gitlab_url}/#{subject.path_with_namespace}.git"
- expect(subject.http_url_to_repo).to eq(repo_http_url)
+ let(:project) { create :empty_project }
+
+ context 'when no user is given' do
+ it 'returns the url to the repo without a username' do
+ expected_url = "#{Gitlab.config.gitlab.url}/#{subject.path_with_namespace}.git"
+
+ expect(project_wiki.http_url_to_repo).to eq(expected_url)
+ expect(project_wiki.http_url_to_repo).not_to include('@')
+ end
+ end
+
+ context 'when user is given' do
+ it 'returns the url to the repo with the username' do
+ user = build_stubbed(:user)
+
+ expect(project_wiki.http_url_to_repo(user)).to start_with("http://#{user.username}@")
+ end
end
end
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index 0b222022e62..bc8ae4ae5a8 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -43,14 +43,22 @@ describe Route, models: true do
end
context 'name update' do
- before { route.update_attributes(name: 'bar') }
-
it "updates children routes with new path" do
+ route.update_attributes(name: 'bar')
+
expect(described_class.exists?(name: 'bar')).to be_truthy
expect(described_class.exists?(name: 'bar / test')).to be_truthy
expect(described_class.exists?(name: 'bar / test / foo')).to be_truthy
expect(described_class.exists?(name: 'gitlab-org')).to be_truthy
end
+
+ it 'handles a rename from nil' do
+ # Note: using `update_columns` to skip all validation and callbacks
+ route.update_columns(name: nil)
+
+ expect { route.update_attributes(name: 'bar') }
+ .to change { route.name }.from(nil).to('bar')
+ end
end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 9da4140f3ce..90378179e32 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -699,7 +699,9 @@ describe User, models: true do
let!(:user) { create(:user, name: 'John Doe', username: 'john.doe', email: 'john.doe@example.com' ) }
let!(:another_user) { create(:user, name: 'Albert Smith', username: 'albert.smith', email: 'albert.smith@example.com' ) }
- let!(:email) { create(:email, user: another_user) }
+ let!(:email) do
+ create(:email, user: another_user, email: 'alias@example.com')
+ end
it 'returns users with a matching name' do
expect(search_with_secondary_emails(user.name)).to eq([user])
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index ab5a7e4d3de..a70f7beaae0 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -5,77 +5,146 @@ describe API::Branches, api: true do
include ApiHelpers
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) }
+ let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } }
let!(:branch_name) { 'feature' }
let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
- let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") }
+ let(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master")[:branch] }
describe "GET /projects/:id/repository/branches" do
- it "returns an array of project branches" do
- project.repository.expire_all_method_caches
+ let(:route) { "/projects/#{project.id}/repository/branches" }
- get api("/projects/#{project.id}/repository/branches", user), per_page: 100
+ shared_examples_for 'repository branches' do
+ it 'returns the repository branches' do
+ get api(route, current_user), per_page: 100
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- branch_names = json_response.map { |x| x['name'] }
- expect(branch_names).to match_array(project.repository.branch_names)
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ branch_names = json_response.map { |x| x['name'] }
+ expect(branch_names).to match_array(project.repository.branch_names)
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, current_user) }
+ end
+ end
end
- end
- describe "GET /projects/:id/repository/branches/:branch" do
- it "returns the branch information for a single branch" do
- get api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
- expect(response).to have_http_status(200)
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository branches' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
+ end
- expect(json_response['name']).to eq(branch_name)
- json_commit = json_response['commit']
- expect(json_commit['id']).to eq(branch_sha)
- expect(json_commit).to have_key('short_id')
- expect(json_commit).to have_key('title')
- expect(json_commit).to have_key('message')
- expect(json_commit).to have_key('author_name')
- expect(json_commit).to have_key('author_email')
- expect(json_commit).to have_key('authored_date')
- expect(json_commit).to have_key('committer_name')
- expect(json_commit).to have_key('committer_email')
- expect(json_commit).to have_key('committed_date')
- expect(json_commit).to have_key('parent_ids')
- expect(json_response['merged']).to eq(false)
- expect(json_response['protected']).to eq(false)
- expect(json_response['developers_can_push']).to eq(false)
- expect(json_response['developers_can_merge']).to eq(false)
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
end
- it "returns the branch information for a single branch with dots in the name" do
- get api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository branches' do
+ let(:current_user) { user }
+ end
+ end
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq("with.1.2.3")
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, guest) }
+ end
end
+ end
+
+ describe "GET /projects/:id/repository/branches/:branch" do
+ let(:route) { "/projects/#{project.id}/repository/branches/#{branch_name}" }
- context 'on a merged branch' do
- it "returns the branch information for a single branch" do
- get api("/projects/#{project.id}/repository/branches/merge-test", user)
+ shared_examples_for 'repository branch' do |merged: false|
+ it 'returns the repository branch' do
+ get api(route, current_user)
expect(response).to have_http_status(200)
- expect(json_response['name']).to eq('merge-test')
- expect(json_response['merged']).to eq(true)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['merged']).to eq(merged)
+ expect(json_response['protected']).to eq(false)
+ expect(json_response['developers_can_push']).to eq(false)
+ expect(json_response['developers_can_merge']).to eq(false)
+
+ json_commit = json_response['commit']
+ expect(json_commit['id']).to eq(branch_sha)
+ expect(json_commit).to have_key('short_id')
+ expect(json_commit).to have_key('title')
+ expect(json_commit).to have_key('message')
+ expect(json_commit).to have_key('author_name')
+ expect(json_commit).to have_key('author_email')
+ expect(json_commit).to have_key('authored_date')
+ expect(json_commit).to have_key('committer_name')
+ expect(json_commit).to have_key('committer_email')
+ expect(json_commit).to have_key('committed_date')
+ expect(json_commit).to have_key('parent_ids')
+ end
+
+ context 'when branch does not exist' do
+ let(:branch_name) { 'unknown' }
+
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, current_user) }
+ let(:message) { '404 Branch Not Found' }
+ end
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, current_user) }
+ end
end
end
- it "returns a 403 error if guest" do
- get api("/projects/#{project.id}/repository/branches", user2)
- expect(response).to have_http_status(403)
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository branch' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
end
- it "returns a 404 error if branch is not available" do
- get api("/projects/#{project.id}/repository/branches/unknown", user)
- expect(response).to have_http_status(404)
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+
+ context 'when authenticated', 'as a developer' do
+ let(:current_user) { user }
+ it_behaves_like 'repository branch'
+
+ context 'when branch contains a dot' do
+ let(:branch_name) { branch_with_dot.name }
+ let(:branch_sha) { project.commit('master').sha }
+
+ it_behaves_like 'repository branch'
+ end
+
+ context 'when branch is merged' do
+ let(:branch_name) { 'merge-test' }
+ let(:branch_sha) { project.commit('merge-test').sha }
+
+ it_behaves_like 'repository branch', merged: true
+ end
+ end
+
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, guest) }
+ end
end
end
@@ -93,10 +162,10 @@ describe API::Branches, api: true do
end
it "protects a single branch with dots in the name" do
- put api("/projects/#{project.id}/repository/branches/with.1.2.3/protect", user)
+ put api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}/protect", user)
expect(response).to have_http_status(200)
- expect(json_response['name']).to eq("with.1.2.3")
+ expect(json_response['name']).to eq(branch_with_dot.name)
expect(json_response['protected']).to eq(true)
end
@@ -234,7 +303,7 @@ describe API::Branches, api: true do
end
it "returns a 403 error if guest" do
- put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2)
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", guest)
expect(response).to have_http_status(403)
end
end
@@ -250,10 +319,10 @@ describe API::Branches, api: true do
end
it "update branches with dots in branch name" do
- put api("/projects/#{project.id}/repository/branches/with.1.2.3/unprotect", user)
+ put api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}/unprotect", user)
expect(response).to have_http_status(200)
- expect(json_response['name']).to eq("with.1.2.3")
+ expect(json_response['name']).to eq(branch_with_dot.name)
expect(json_response['protected']).to eq(false)
end
@@ -282,7 +351,7 @@ describe API::Branches, api: true do
end
it "denies for user without push access" do
- post api("/projects/#{project.id}/repository/branches", user2),
+ post api("/projects/#{project.id}/repository/branches", guest),
branch: branch_name,
ref: branch_sha
expect(response).to have_http_status(403)
@@ -330,7 +399,7 @@ describe API::Branches, api: true do
end
it "removes a branch with dots in the branch name" do
- delete api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+ delete api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}", user)
expect(response).to have_http_status(204)
end
@@ -367,7 +436,7 @@ describe API::Branches, api: true do
end
it 'returns a 403 error if guest' do
- delete api("/projects/#{project.id}/repository/merged_branches", user2)
+ delete api("/projects/#{project.id}/repository/merged_branches", guest)
expect(response).to have_http_status(403)
end
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 585449e62b6..a10d876ffad 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -178,7 +178,7 @@ describe API::Commits, api: true do
end
end
- describe "Create a commit with multiple files and actions" do
+ describe "POST /projects/:id/repository/commits" do
let!(:url) { "/projects/#{project.id}/repository/commits" }
it 'returns a 403 unauthorized for user without permissions' do
@@ -193,7 +193,7 @@ describe API::Commits, api: true do
expect(response).to have_http_status(400)
end
- context :create do
+ describe 'create' do
let(:message) { 'Created file' }
let!(:invalid_c_params) do
{
@@ -237,8 +237,8 @@ describe API::Commits, api: true do
expect(response).to have_http_status(400)
end
- context 'with project path in URL' do
- let(:url) { "/projects/#{project.full_path.gsub('/', '%2F')}/repository/commits" }
+ context 'with project path containing a dot in URL' do
+ let(:url) { "/projects/#{CGI.escape(project.full_path)}/repository/commits" }
it 'a new file in project repo' do
post api(url, user), valid_c_params
@@ -248,7 +248,7 @@ describe API::Commits, api: true do
end
end
- context :delete do
+ describe 'delete' do
let(:message) { 'Deleted file' }
let!(:invalid_d_params) do
{
@@ -289,7 +289,7 @@ describe API::Commits, api: true do
end
end
- context :move do
+ describe 'move' do
let(:message) { 'Moved file' }
let!(:invalid_m_params) do
{
@@ -334,7 +334,7 @@ describe API::Commits, api: true do
end
end
- context :update do
+ describe 'update' do
let(:message) { 'Updated file' }
let!(:invalid_u_params) do
{
@@ -377,7 +377,7 @@ describe API::Commits, api: true do
end
end
- context "multiple operations" do
+ describe 'multiple operations' do
let(:message) { 'Multiple actions' }
let!(:invalid_mo_params) do
{
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index f18b8e98707..63ec00cdf04 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -397,16 +397,25 @@ describe API::Internal, api: true do
before do
project.team << [user, :developer]
- get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token
end
it 'returns link to create new merge request' do
+ get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token
+
expect(json_response).to match [{
"branch_name" => "new_branch",
"url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
"new_merge_request" => true
}]
end
+
+ it 'returns empty array if printing_merge_request_link_enabled is false' do
+ project.update!(printing_merge_request_link_enabled: false)
+
+ get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token
+
+ expect(json_response).to eq([])
+ end
end
describe 'POST /notify_post_receive' do
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index de7dbca0b22..52f68fed2cc 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -153,6 +153,16 @@ describe API::Issues, api: true do
expect(json_response.first['state']).to eq('opened')
end
+ it 'returns unlabeled issues for "No Label" label' do
+ get api("/issues", user), labels: 'No Label'
+
+ 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['labels']).to be_empty
+ end
+
it 'returns an empty array if no issue matches labels and state filters' do
get api("/issues?labels=#{label.title}&state=closed", user)
@@ -524,6 +534,12 @@ describe API::Issues, api: true do
describe "GET /projects/:id/issues" do
let(:base_url) { "/projects/#{project.id}" }
+ it 'returns 404 when project does not exist' do
+ get api('/projects/1000/issues', non_member)
+
+ expect(response).to have_http_status(404)
+ end
+
it "returns 404 on private projects for other users" do
private_project = create(:empty_project, :private)
create(:issue, project: private_project)
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index d50fe80b36a..044b989e5ba 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -152,6 +152,34 @@ describe API::Runner do
end
end
end
+
+ describe 'POST /api/v4/runners/verify' do
+ let(:runner) { create(:ci_runner) }
+
+ context 'when no token is provided' do
+ it 'returns 400 error' do
+ post api('/runners/verify')
+
+ expect(response).to have_http_status :bad_request
+ end
+ end
+
+ context 'when invalid token is provided' do
+ it 'returns 403 error' do
+ post api('/runners/verify'), token: 'invalid-token'
+
+ expect(response).to have_http_status 403
+ end
+ end
+
+ context 'when valid token is provided' do
+ it 'verifies Runner credentials' do
+ post api('/runners/verify'), token: runner.token
+
+ expect(response).to have_http_status 200
+ end
+ end
+ end
end
describe '/api/v4/jobs' do
@@ -220,18 +248,6 @@ describe API::Runner do
it { expect(response).to have_http_status(204) }
end
end
-
- context "when runner doesn't send version in User-Agent" do
- let(:user_agent) { 'Go-http-client/1.1' }
-
- it { expect(response).to have_http_status(404) }
- end
-
- context "when runner doesn't have a User-Agent" do
- let(:user_agent) { nil }
-
- it { expect(response).to have_http_status(404) }
- end
end
context 'when no token is provided' do
@@ -254,10 +270,10 @@ describe API::Runner do
context 'when Runner is not active' do
let(:runner) { create(:ci_runner, :inactive) }
- it 'returns 404 error' do
+ it 'returns 204 error' do
request_job
- expect(response).to have_http_status 404
+ expect(response).to have_http_status 204
end
end
@@ -312,8 +328,8 @@ describe API::Runner do
end
let(:expected_variables) do
- [{ 'key' => 'CI_BUILD_NAME', 'value' => 'spinach', 'public' => true },
- { 'key' => 'CI_BUILD_STAGE', 'value' => 'test', 'public' => true },
+ [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true },
+ { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true },
{ 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true }]
end
@@ -401,9 +417,39 @@ describe API::Runner do
end
context 'when project and pipeline have multiple jobs' do
+ let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
- before { job.success }
+ before do
+ job.success
+ job2.success
+ end
+
+ it 'returns dependent jobs' do
+ request_job
+
+ expect(response).to have_http_status(201)
+ expect(json_response['id']).to eq(test_job.id)
+ expect(json_response['dependencies'].count).to eq(2)
+ expect(json_response['dependencies']).to include({ 'id' => job.id, 'name' => job.name, 'token' => job.token },
+ { 'id' => job2.id, 'name' => job2.name, 'token' => job2.token })
+ end
+ end
+
+ context 'when explicit dependencies are defined' do
+ let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
+ let!(:test_job) do
+ create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy',
+ stage: 'deploy', stage_idx: 1,
+ options: { dependencies: [job2.name] })
+ end
+
+ before do
+ job.success
+ job2.success
+ end
it 'returns dependent jobs' do
request_job
@@ -411,7 +457,7 @@ describe API::Runner do
expect(response).to have_http_status(201)
expect(json_response['id']).to eq(test_job.id)
expect(json_response['dependencies'].count).to eq(1)
- expect(json_response['dependencies'][0]).to include('id' => job.id, 'name' => 'spinach')
+ expect(json_response['dependencies'][0]).to include('id' => job2.id, 'name' => job2.name, 'token' => job2.token)
end
end
@@ -437,9 +483,9 @@ describe API::Runner do
context 'when triggered job is available' do
let(:expected_variables) do
- [{ 'key' => 'CI_BUILD_NAME', 'value' => 'spinach', 'public' => true },
- { 'key' => 'CI_BUILD_STAGE', 'value' => 'test', 'public' => true },
- { 'key' => 'CI_BUILD_TRIGGERED', 'value' => 'true', 'public' => true },
+ [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true },
+ { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true },
+ { 'key' => 'CI_PIPELINE_TRIGGERED', 'value' => 'true', 'public' => true },
{ 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true },
{ 'key' => 'SECRET_KEY', 'value' => 'secret_value', 'public' => false },
{ 'key' => 'TRIGGER_KEY_1', 'value' => 'TRIGGER_VALUE_1', 'public' => false }]
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 424c02932ab..d93a734f5b6 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -59,14 +59,6 @@ describe API::Triggers do
expect(pipeline.builds.size).to eq(5)
end
- it 'creates builds on webhook from other gitlab repository and branch' do
- expect do
- post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
- end.to change(project.builds, :count).by(5)
-
- expect(response).to have_http_status(201)
- end
-
it 'returns bad request with no pipeline created if there\'s no commit for that ref' do
post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'other-branch')
@@ -101,6 +93,28 @@ describe API::Triggers do
end
end
end
+
+ context 'when triggering a pipeline from a trigger token' do
+ it 'creates builds from the ref given in the URL, not in the body' do
+ expect do
+ post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+ end.to change(project.builds, :count).by(5)
+
+ expect(response).to have_http_status(201)
+ end
+
+ context 'when ref contains a dot' do
+ it 'creates builds from the ref given in the URL, not in the body' do
+ project.repository.create_file(user, '.gitlab/gitlabhq/new_feature.md', 'something valid', message: 'new_feature', branch_name: 'v.1-branch')
+
+ expect do
+ post api("/projects/#{project.id}/ref/v.1-branch/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+ end.to change(project.builds, :count).by(4)
+
+ expect(response).to have_http_status(201)
+ end
+ end
+ end
end
describe 'GET /projects/:id/triggers' do
diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb
index e4cedf98e64..5dcd4f21f4e 100644
--- a/spec/requests/api/v3/branches_spec.rb
+++ b/spec/requests/api/v3/branches_spec.rb
@@ -10,6 +10,7 @@ describe API::V3::Branches, api: true do
let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(: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") }
describe "GET /projects/:id/repository/branches" do
@@ -80,4 +81,55 @@ describe API::V3::Branches, api: true do
expect(response).to have_http_status(403)
end
end
+
+ describe "POST /projects/:id/repository/branches" do
+ it "creates a new branch" do
+ post v3_api("/projects/#{project.id}/repository/branches", user),
+ branch_name: 'feature1',
+ ref: branch_sha
+
+ expect(response).to have_http_status(201)
+
+ expect(json_response['name']).to eq('feature1')
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ end
+
+ it "denies for user without push access" do
+ post v3_api("/projects/#{project.id}/repository/branches", user2),
+ branch_name: branch_name,
+ ref: branch_sha
+ expect(response).to have_http_status(403)
+ end
+
+ it 'returns 400 if branch name is invalid' do
+ post v3_api("/projects/#{project.id}/repository/branches", user),
+ branch_name: 'new design',
+ ref: branch_sha
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('Branch name is invalid')
+ end
+
+ it 'returns 400 if branch already exists' do
+ post v3_api("/projects/#{project.id}/repository/branches", user),
+ branch_name: 'new_design1',
+ ref: branch_sha
+ expect(response).to have_http_status(201)
+
+ post v3_api("/projects/#{project.id}/repository/branches", user),
+ branch_name: 'new_design1',
+ ref: branch_sha
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('Branch already exists')
+ end
+
+ it 'returns 400 if ref name is invalid' do
+ post v3_api("/projects/#{project.id}/repository/branches", user),
+ branch_name: 'new_design3',
+ ref: 'foo'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('Invalid reference name')
+ end
+ end
end
diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb
index e298ef055e1..adba3a787aa 100644
--- a/spec/requests/api/v3/commits_spec.rb
+++ b/spec/requests/api/v3/commits_spec.rb
@@ -88,7 +88,7 @@ describe API::V3::Commits, api: true do
end
end
- describe "Create a commit with multiple files and actions" do
+ describe "POST /projects/:id/repository/commits" do
let!(:url) { "/projects/#{project.id}/repository/commits" }
it 'returns a 403 unauthorized for user without permissions' do
@@ -103,7 +103,7 @@ describe API::V3::Commits, api: true do
expect(response).to have_http_status(400)
end
- context :create do
+ describe 'create' do
let(:message) { 'Created file' }
let!(:invalid_c_params) do
{
@@ -147,8 +147,9 @@ describe API::V3::Commits, api: true do
expect(response).to have_http_status(400)
end
- context 'with project path in URL' do
- let(:url) { "/projects/#{project.full_path.gsub('/', '%2F')}/repository/commits" }
+ context 'with project path containing a dot in URL' do
+ let!(:user) { create(:user, username: 'foo.bar') }
+ let(:url) { "/projects/#{CGI.escape(project.full_path)}/repository/commits" }
it 'a new file in project repo' do
post v3_api(url, user), valid_c_params
@@ -158,7 +159,7 @@ describe API::V3::Commits, api: true do
end
end
- context :delete do
+ describe 'delete' do
let(:message) { 'Deleted file' }
let!(:invalid_d_params) do
{
@@ -199,7 +200,7 @@ describe API::V3::Commits, api: true do
end
end
- context :move do
+ describe 'move' do
let(:message) { 'Moved file' }
let!(:invalid_m_params) do
{
@@ -244,7 +245,7 @@ describe API::V3::Commits, api: true do
end
end
- context :update do
+ describe 'update' do
let(:message) { 'Updated file' }
let!(:invalid_u_params) do
{
diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb
index 1941ca0d7d8..51021eec63c 100644
--- a/spec/requests/api/v3/issues_spec.rb
+++ b/spec/requests/api/v3/issues_spec.rb
@@ -439,6 +439,12 @@ describe API::V3::Issues, api: true do
describe "GET /projects/:id/issues" do
let(:base_url) { "/projects/#{project.id}" }
+ it 'returns 404 when project does not exist' do
+ get v3_api('/projects/1000/issues', non_member)
+
+ expect(response).to have_http_status(404)
+ end
+
it "returns 404 on private projects for other users" do
private_project = create(:empty_project, :private)
create(:issue, project: private_project)
diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb
index 4819269d69f..9233e9621bf 100644
--- a/spec/requests/api/v3/triggers_spec.rb
+++ b/spec/requests/api/v3/triggers_spec.rb
@@ -51,13 +51,6 @@ describe API::V3::Triggers do
expect(pipeline.builds.size).to eq(5)
end
- it 'creates builds on webhook from other gitlab repository and branch' do
- expect do
- post v3_api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
- end.to change(project.builds, :count).by(5)
- expect(response).to have_http_status(201)
- end
-
it 'returns bad request with no builds created if there\'s no commit for that ref' do
post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch')
expect(response).to have_http_status(400)
@@ -89,6 +82,27 @@ describe API::V3::Triggers do
end
end
end
+
+ context 'when triggering a pipeline from a trigger token' do
+ it 'creates builds from the ref given in the URL, not in the body' do
+ expect do
+ post v3_api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+ end.to change(project.builds, :count).by(5)
+ expect(response).to have_http_status(201)
+ end
+
+ context 'when ref contains a dot' do
+ it 'creates builds from the ref given in the URL, not in the body' do
+ project.repository.create_file(user, '.gitlab/gitlabhq/new_feature.md', 'something valid', message: 'new_feature', branch_name: 'v.1-branch')
+
+ expect do
+ post v3_api("/projects/#{project.id}/ref/v.1-branch/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+ end.to change(project.builds, :count).by(4)
+
+ expect(response).to have_http_status(201)
+ end
+ end
+ end
end
describe 'GET /projects/:id/triggers' do
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index 9948d1a9ea0..c879f37f50d 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -81,8 +81,8 @@ describe Ci::API::Builds do
expect(runner.reload.platform).to eq("darwin")
expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
expect(json_response["variables"]).to include(
- { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
- { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
+ { "key" => "CI_JOB_NAME", "value" => "spinach", "public" => true },
+ { "key" => "CI_JOB_STAGE", "value" => "test", "public" => true },
{ "key" => "DB_NAME", "value" => "postgres", "public" => true }
)
end
@@ -182,9 +182,9 @@ describe Ci::API::Builds do
expect(response).to have_http_status(201)
expect(json_response["variables"]).to include(
- { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
- { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
- { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true },
+ { "key" => "CI_JOB_NAME", "value" => "spinach", "public" => true },
+ { "key" => "CI_JOB_STAGE", "value" => "test", "public" => true },
+ { "key" => "CI_PIPELINE_TRIGGERED", "value" => "true", "public" => true },
{ "key" => "DB_NAME", "value" => "postgres", "public" => true },
{ "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
{ "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false },
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 8459a3d8cfb..a969829a63e 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -200,7 +200,7 @@ describe Ci::CreatePipelineService, services: true do
context 'with environment' do
before do
- config = YAML.dump(deploy: { environment: { name: "review/$CI_BUILD_REF_NAME" }, script: 'ls' })
+ config = YAML.dump(deploy: { environment: { name: "review/$CI_COMMIT_REF_NAME" }, script: 'ls' })
stub_ci_pipeline_yaml_file(config)
end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 65af4e13118..8567817147b 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -19,7 +19,7 @@ describe Ci::RetryBuildService, :services do
erased_at].freeze
IGNORE_ACCESSORS =
- %i[type lock_version target_url gl_project_id deploy job_id base_tags
+ %i[type lock_version target_url base_tags
commit_id deployments erased_by_id last_deployment project_id
runner_id tag_taggings taggings tags trigger_request_id
user_id].freeze
diff --git a/spec/services/create_branch_service_spec.rb b/spec/services/create_branch_service_spec.rb
new file mode 100644
index 00000000000..3f548688c20
--- /dev/null
+++ b/spec/services/create_branch_service_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe CreateBranchService, services: true do
+ let(:user) { create(:user) }
+ let(:service) { described_class.new(project, user) }
+
+ describe '#execute' do
+ context 'when repository is empty' do
+ let(:project) { create(:project_empty_repo) }
+
+ it 'creates master branch' do
+ service.execute('my-feature', 'master')
+
+ expect(project.repository.branch_exists?('master')).to be_truthy
+ end
+
+ it 'creates my-feature branch' do
+ service.execute('my-feature', 'master')
+
+ expect(project.repository.branch_exists?('my-feature')).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
index 18b964e2453..a883705bd45 100644
--- a/spec/services/create_deployment_service_spec.rb
+++ b/spec/services/create_deployment_service_spec.rb
@@ -104,16 +104,16 @@ describe CreateDeploymentService, services: true do
context 'when variables are used' do
let(:params) do
{
- environment: 'review-apps/$CI_BUILD_REF_NAME',
+ environment: 'review-apps/$CI_COMMIT_REF_NAME',
ref: 'master',
tag: false,
sha: '97de212e80737a608d939f648d959671fb0a0142',
options: {
- name: 'review-apps/$CI_BUILD_REF_NAME',
- url: 'http://$CI_BUILD_REF_NAME.review-apps.gitlab.com'
+ name: 'review-apps/$CI_COMMIT_REF_NAME',
+ url: 'http://$CI_COMMIT_REF_NAME.review-apps.gitlab.com'
},
variables: [
- { key: 'CI_BUILD_REF_NAME', value: 'feature-review-apps' }
+ { key: 'CI_COMMIT_REF_NAME', value: 'feature-review-apps' }
]
}
end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 0768f644036..adfa75a524f 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -49,10 +49,13 @@ describe MergeRequests::BuildService, services: true do
let(:commits) { Commit.decorate([commit_1], project) }
it 'creates compare object with target branch as default branch' do
- expect(merge_request.can_be_created).to eq(false)
expect(merge_request.compare).to be_present
expect(merge_request.target_branch).to eq(project.default_branch)
end
+
+ it 'allows the merge request to be created' do
+ expect(merge_request.can_be_created).to eq(true)
+ end
end
context 'same source and target branch' do
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
index 08829e4be70..b7a05907208 100644
--- a/spec/services/merge_requests/get_urls_service_spec.rb
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -130,5 +130,15 @@ describe MergeRequests::GetUrlsService do
}])
end
end
+
+ context 'when printing_merge_request_link_enabled is false' do
+ it 'returns empty array' do
+ project.update!(printing_merge_request_link_enabled: false)
+
+ result = service.execute(existing_branch_changes)
+
+ expect(result).to eq([])
+ end
+ end
end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index ebbaea4e59a..f7240969588 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -146,16 +146,6 @@ describe NotificationService, services: true do
should_not_email(@u_lazy_participant)
end
- it "emails the note author if they've opted into notifications about their activity" do
- add_users_with_subscription(note.project, issue)
- note.author.notified_of_own_activity = true
- reset_delivered_emails!
-
- notification.new_note(note)
-
- should_email(note.author)
- end
-
it 'filters out "mentioned in" notes' do
mentioned_note = SystemNoteService.cross_reference(mentioned_issue, issue, issue.author)
@@ -486,20 +476,6 @@ describe NotificationService, services: true do
should_not_email(issue.assignee)
end
- it "emails the author if they've opted into notifications about their activity" do
- issue.author.notified_of_own_activity = true
-
- notification.new_issue(issue, issue.author)
-
- should_email(issue.author)
- end
-
- it "doesn't email the author if they haven't opted into notifications about their activity" do
- notification.new_issue(issue, issue.author)
-
- should_not_email(issue.author)
- end
-
it "emails subscribers of the issue's labels" do
user_1 = create(:user)
user_2 = create(:user)
@@ -689,19 +665,6 @@ describe NotificationService, services: true do
should_email(subscriber_to_label_2)
end
- it "emails the current user if they've opted into notifications about their activity" do
- subscriber_to_label_2.notified_of_own_activity = true
- notification.relabeled_issue(issue, [group_label_2, label_2], subscriber_to_label_2)
-
- should_email(subscriber_to_label_2)
- end
-
- it "doesn't email the current user if they haven't opted into notifications about their activity" do
- notification.relabeled_issue(issue, [group_label_2, label_2], subscriber_to_label_2)
-
- should_not_email(subscriber_to_label_2)
- end
-
it "doesn't send email to anyone but subscribers of the given labels" do
notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled)
@@ -795,7 +758,7 @@ describe NotificationService, services: true do
update_custom_notification(:reopen_issue, @u_custom_global)
end
- it 'sends email to issue assignee and issue author' do
+ it 'sends email to issue notification recipients' do
notification.reopen_issue(issue, @u_disabled)
should_email(issue.assignee)
@@ -809,6 +772,7 @@ describe NotificationService, services: true do
should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
+ should_not_email(@u_disabled)
should_not_email(@u_lazy_participant)
end
@@ -818,6 +782,32 @@ describe NotificationService, services: true do
let(:notification_trigger) { notification.reopen_issue(issue, @u_disabled) }
end
end
+
+ describe '#issue_moved' do
+ let(:new_issue) { create(:issue) }
+
+ it 'sends email to issue notification recipients' do
+ notification.issue_moved(issue, new_issue, @u_disabled)
+
+ should_email(issue.assignee)
+ should_email(issue.author)
+ should_email(@u_watcher)
+ should_email(@u_guest_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ it_behaves_like 'participating notifications' do
+ let(:participant) { create(:user, username: 'user-participant') }
+ let(:issuable) { issue }
+ let(:notification_trigger) { notification.issue_moved(issue, new_issue, @u_disabled) }
+ end
+ end
end
describe 'Merge Requests' do
@@ -855,20 +845,6 @@ describe NotificationService, services: true do
should_not_email(@u_lazy_participant)
end
- it "emails the author if they've opted into notifications about their activity" do
- merge_request.author.notified_of_own_activity = true
-
- notification.new_merge_request(merge_request, merge_request.author)
-
- should_email(merge_request.author)
- end
-
- it "doesn't email the author if they haven't opted into notifications about their activity" do
- notification.new_merge_request(merge_request, merge_request.author)
-
- should_not_email(merge_request.author)
- end
-
it "emails subscribers of the merge request's labels" do
user_1 = create(:user)
user_2 = create(:user)
@@ -1064,14 +1040,6 @@ describe NotificationService, services: true do
should_not_email(@u_watcher)
end
- it "notifies the merger when the pipeline succeeds is false but they've opted into notifications about their activity" do
- merge_request.merge_when_pipeline_succeeds = false
- @u_watcher.notified_of_own_activity = true
- notification.merge_mr(merge_request, @u_watcher)
-
- should_email(@u_watcher)
- end
-
it_behaves_like 'participating notifications' do
let(:participant) { create(:user, username: 'user-participant') }
let(:issuable) { merge_request }
@@ -1251,6 +1219,48 @@ describe NotificationService, services: true do
end
end
+ describe 'Pipelines' do
+ describe '#pipeline_finished' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { create(:user) }
+ let(:u_member) { create(:user) }
+ let(:u_other) { create(:user) }
+
+ let(:commit) { project.commit }
+ let(:pipeline) do
+ create(:ci_pipeline, :success,
+ project: project,
+ user: current_user,
+ ref: 'refs/heads/master',
+ sha: commit.id,
+ before_sha: '00000000')
+ end
+
+ before do
+ project.add_master(current_user)
+ project.add_master(u_member)
+ reset_delivered_emails!
+ end
+
+ context 'without custom recipients' do
+ it 'notifies the pipeline user' do
+ notification.pipeline_finished(pipeline)
+
+ should_only_email(current_user, kind: :bcc)
+ end
+ end
+
+ context 'with custom recipients' do
+ it 'notifies the custom recipients' do
+ users = [u_member, u_other]
+ notification.pipeline_finished(pipeline, users.map(&:notification_email))
+
+ should_only_email(*users, kind: :bcc)
+ end
+ end
+ end
+ end
+
def build_team(project)
@u_watcher = create_global_setting_for(create(:user), :watch)
@u_participating = create_global_setting_for(create(:user), :participating)
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index ab6e8f537ba..e5917bb0b7a 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -120,6 +120,26 @@ describe Projects::ImportService, services: true do
end
end
+ context 'with blocked import_URL' do
+ it 'fails with localhost' do
+ project.import_url = 'https://localhost:9000/vim/vim.git'
+
+ result = described_class.new(project, user).execute
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to end_with 'Blocked import URL.'
+ end
+
+ it 'fails with port 25' do
+ project.import_url = "https://github.com:25/vim/vim.git"
+
+ result = described_class.new(project, user).execute
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to end_with 'Blocked import URL.'
+ end
+ end
+
def stub_github_omniauth_provider
provider = OpenStruct.new(
'name' => 'github',
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index db9f1231682..11037a4917b 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -5,6 +5,7 @@ describe SystemHooksService, services: true do
let(:project) { create :project }
let(:project_member) { create :project_member }
let(:key) { create(:key, user: user) }
+ let(:deploy_key) { create(:key) }
let(:group) { create(:group) }
let(:group_member) { create(:group_member) }
@@ -18,6 +19,8 @@ describe SystemHooksService, services: true do
it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :user_id, :access_level, :project_visibility) }
it { expect(event_data(key, :create)).to include(:username, :key, :id) }
it { expect(event_data(key, :destroy)).to include(:username, :key, :id) }
+ it { expect(event_data(deploy_key, :create)).to include(:key, :id) }
+ it { expect(event_data(deploy_key, :destroy)).to include(:key, :id) }
it do
project.old_path_with_namespace = 'renamed_from_path'
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index a8395cb48ea..3645b73b039 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -298,6 +298,10 @@ describe TodoService, services: true do
expect(second_todo.reload.state?(new_state)).to be true
end
+ it 'returns the updated ids' do
+ expect(service.send(meth, collection, john_doe)).to match_array([first_todo.id, second_todo.id])
+ end
+
describe 'cached counts' do
it 'updates when todos change' do
expect(john_doe.todos.where(state: new_state).count).to eq(0)
@@ -706,7 +710,7 @@ describe TodoService, services: true do
should_create_todo(user: admin, author: admin, target: mr_unassigned, action: Todo::UNMERGEABLE)
end
end
-
+
describe '#mark_todo' do
it 'creates a todo from a merge request' do
service.mark_todo(mr_unassigned, author)
@@ -779,29 +783,27 @@ describe TodoService, services: true do
.to change { todo.reload.state }.from('pending').to('done')
end
- it 'returns the number of updated todos' do # Needed on API
+ it 'returns the ids of updated todos' do # Needed on API
todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
- expect(TodoService.new.mark_todos_as_done([todo], john_doe)).to eq(1)
+ expect(TodoService.new.mark_todos_as_done([todo], john_doe)).to eq([todo.id])
end
context 'when some of the todos are done already' do
- before do
- create(:todo, :mentioned, user: john_doe, target: issue, project: project)
- create(:todo, :mentioned, user: john_doe, target: another_issue, project: project)
- end
+ let!(:first_todo) { create(:todo, :mentioned, user: john_doe, target: issue, project: project) }
+ let!(:second_todo) { create(:todo, :mentioned, user: john_doe, target: another_issue, project: project) }
- it 'returns the number of those still pending' do
+ it 'returns the ids of those still pending' do
TodoService.new.mark_pending_todos_as_done(issue, john_doe)
- expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(1)
+ expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq([second_todo.id])
end
- it 'returns 0 if all are done' do
+ it 'returns an empty array if all are done' do
TodoService.new.mark_pending_todos_as_done(issue, john_doe)
TodoService.new.mark_pending_todos_as_done(another_issue, john_doe)
- expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(0)
+ expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq([])
end
end
diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb
index b507d38f472..ac2c89b3ff9 100644
--- a/spec/simplecov_env.rb
+++ b/spec/simplecov_env.rb
@@ -15,9 +15,9 @@ module SimpleCovEnv
def configure_job
SimpleCov.configure do
- if ENV['CI_BUILD_NAME']
- coverage_dir "coverage/#{ENV['CI_BUILD_NAME']}"
- command_name ENV['CI_BUILD_NAME']
+ if ENV['CI_JOB_NAME']
+ coverage_dir "coverage/#{ENV['CI_JOB_NAME']}"
+ command_name ENV['CI_JOB_NAME']
end
if ENV['CI']
diff --git a/spec/support/matchers/email_matchers.rb b/spec/support/matchers/email_matchers.rb
new file mode 100644
index 00000000000..d9d59ec12ec
--- /dev/null
+++ b/spec/support/matchers/email_matchers.rb
@@ -0,0 +1,5 @@
+RSpec::Matchers.define :have_html_escaped_body_text do |expected|
+ match do |actual|
+ expect(actual).to have_body_text(ERB::Util.html_escape(expected))
+ end
+end
diff --git a/spec/support/seed_helper.rb b/spec/support/seed_helper.rb
index 07f81e9c4f3..f55fee28ff9 100644
--- a/spec/support/seed_helper.rb
+++ b/spec/support/seed_helper.rb
@@ -7,7 +7,7 @@ TEST_MUTABLE_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "mutable-repo.git")
TEST_BROKEN_REPO_PATH = File.join(SEED_REPOSITORY_PATH, "broken-repo.git")
module SeedHelper
- GITLAB_URL = "https://gitlab.com/gitlab-org/gitlab-git-test.git".freeze
+ GITLAB_GIT_TEST_REPO_URL = ENV.fetch('GITLAB_GIT_TEST_REPO_URL', 'https://gitlab.com/gitlab-org/gitlab-git-test.git').freeze
def ensure_seeds
if File.exist?(SEED_REPOSITORY_PATH)
@@ -25,7 +25,7 @@ module SeedHelper
end
def create_bare_seeds
- system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_URL}),
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_GIT_TEST_REPO_URL}),
chdir: SEED_REPOSITORY_PATH,
out: '/dev/null',
err: '/dev/null')
@@ -45,7 +45,7 @@ module SeedHelper
system(git_env, *%w(git branch -t feature origin/feature),
chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null')
- system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_URL}),
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_GIT_TEST_REPO_URL}),
chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null')
end
diff --git a/spec/support/target_branch_helpers.rb b/spec/support/target_branch_helpers.rb
new file mode 100644
index 00000000000..3ee8f0f657e
--- /dev/null
+++ b/spec/support/target_branch_helpers.rb
@@ -0,0 +1,16 @@
+module TargetBranchHelpers
+ def select_branch(name)
+ first('button.js-target-branch').click
+ wait_for_ajax
+ all('a[data-group="Branches"]').find do |el|
+ el.text == name
+ end.click
+ end
+
+ def create_new_branch(name)
+ first('button.js-target-branch').click
+ click_link 'Create new branch'
+ fill_in 'new_branch_name', with: name
+ click_button 'Create'
+ end
+end
diff --git a/spec/tasks/tokens_spec.rb b/spec/tasks/tokens_spec.rb
new file mode 100644
index 00000000000..19036c7677c
--- /dev/null
+++ b/spec/tasks/tokens_spec.rb
@@ -0,0 +1,21 @@
+require 'rake_helper'
+
+describe 'tokens rake tasks' do
+ let!(:user) { create(:user) }
+
+ before do
+ Rake.application.rake_require 'tasks/tokens'
+ end
+
+ describe 'reset_all task' do
+ it 'invokes create_hooks task' do
+ expect { run_rake_task('tokens:reset_all_auth') }.to change { user.reload.authentication_token }
+ end
+ end
+
+ describe 'reset_all_email task' do
+ it 'invokes create_hooks task' do
+ expect { run_rake_task('tokens:reset_all_email') }.to change { user.reload.incoming_email_token }
+ end
+ end
+end
diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
index f2919f20e85..8bc344bfbf6 100644
--- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb
+++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
@@ -25,7 +25,7 @@ describe 'projects/commit/_commit_box.html.haml' do
render
- expect(rendered).to have_text("Pipeline ##{third_pipeline.id} for #{Commit.truncate_sha(project.commit.sha)} failed")
+ expect(rendered).to have_text("Pipeline ##{third_pipeline.id} failed")
end
context 'viewing a commit' do
diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb
deleted file mode 100644
index 542e674c150..00000000000
--- a/spec/workers/build_email_worker_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-require 'spec_helper'
-
-describe BuildEmailWorker do
- include EmailHelpers
- include RepoHelpers
-
- let(:build) { create(:ci_build) }
- let(:user) { create(:user) }
- let(:data) { Gitlab::DataBuilder::Build.build(build) }
-
- subject { BuildEmailWorker.new }
-
- before do
- allow(build).to receive(:execute_hooks).and_return(false)
- build.success
- end
-
- describe "#perform" do
- it "sends mail" do
- subject.perform(build.id, [user.email], data.stringify_keys)
-
- email = ActionMailer::Base.deliveries.last
- expect(email.subject).to include('Build success for')
- expect(email.to).to eq([user.email])
- end
-
- it "gracefully handles an input SMTP error" do
- reset_delivered_emails!
- allow(Notify).to receive(:build_success_email).and_raise(Net::SMTPFatalError)
-
- subject.perform(build.id, [user.email], data.stringify_keys)
-
- expect(ActionMailer::Base.deliveries.count).to eq(0)
- end
- end
-end
diff --git a/yarn.lock b/yarn.lock
index 391b1c7eccf..2500ddc6f6b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -473,6 +473,13 @@ babel-plugin-transform-decorators@^6.22.0:
babel-template "^6.22.0"
babel-types "^6.22.0"
+babel-plugin-transform-define@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-define/-/babel-plugin-transform-define-1.2.0.tgz#f036bda05162f29a542e434f585da1ccf1e7ec6a"
+ dependencies:
+ lodash.get "4.4.2"
+ traverse "0.6.6"
+
babel-plugin-transform-es2015-arrow-functions@^6.22.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221"
@@ -549,17 +556,17 @@ babel-plugin-transform-es2015-literals@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-modules-amd@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.22.0.tgz#bf69cd34889a41c33d90dfb740e0091ccff52f21"
+babel-plugin-transform-es2015-modules-amd@^6.24.0:
+ version "6.24.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.0.tgz#a1911fb9b7ec7e05a43a63c5995007557bcf6a2e"
dependencies:
- babel-plugin-transform-es2015-modules-commonjs "^6.22.0"
+ babel-plugin-transform-es2015-modules-commonjs "^6.24.0"
babel-runtime "^6.22.0"
babel-template "^6.22.0"
-babel-plugin-transform-es2015-modules-commonjs@^6.22.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.23.0.tgz#cba7aa6379fb7ec99250e6d46de2973aaffa7b92"
+babel-plugin-transform-es2015-modules-commonjs@^6.24.0:
+ version "6.24.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.0.tgz#e921aefb72c2cc26cb03d107626156413222134f"
dependencies:
babel-plugin-transform-strict-mode "^6.22.0"
babel-runtime "^6.22.0"
@@ -574,11 +581,11 @@ babel-plugin-transform-es2015-modules-systemjs@^6.22.0:
babel-runtime "^6.22.0"
babel-template "^6.23.0"
-babel-plugin-transform-es2015-modules-umd@^6.22.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.23.0.tgz#8d284ae2e19ed8fe21d2b1b26d6e7e0fcd94f0f1"
+babel-plugin-transform-es2015-modules-umd@^6.24.0:
+ version "6.24.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.0.tgz#fd5fa63521cae8d273927c3958afd7c067733450"
dependencies:
- babel-plugin-transform-es2015-modules-amd "^6.22.0"
+ babel-plugin-transform-es2015-modules-amd "^6.24.0"
babel-runtime "^6.22.0"
babel-template "^6.23.0"
@@ -669,9 +676,9 @@ babel-plugin-transform-strict-mode@^6.22.0:
babel-runtime "^6.22.0"
babel-types "^6.22.0"
-babel-preset-es2015@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.22.0.tgz#af5a98ecb35eb8af764ad8a5a05eb36dc4386835"
+babel-preset-es2015@^6.24.0:
+ version "6.24.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.0.tgz#c162d68b1932696e036cd3110dc1ccd303d2673a"
dependencies:
babel-plugin-check-es2015-constants "^6.22.0"
babel-plugin-transform-es2015-arrow-functions "^6.22.0"
@@ -684,10 +691,10 @@ babel-preset-es2015@^6.22.0:
babel-plugin-transform-es2015-for-of "^6.22.0"
babel-plugin-transform-es2015-function-name "^6.22.0"
babel-plugin-transform-es2015-literals "^6.22.0"
- babel-plugin-transform-es2015-modules-amd "^6.22.0"
- babel-plugin-transform-es2015-modules-commonjs "^6.22.0"
+ babel-plugin-transform-es2015-modules-amd "^6.24.0"
+ babel-plugin-transform-es2015-modules-commonjs "^6.24.0"
babel-plugin-transform-es2015-modules-systemjs "^6.22.0"
- babel-plugin-transform-es2015-modules-umd "^6.22.0"
+ babel-plugin-transform-es2015-modules-umd "^6.24.0"
babel-plugin-transform-es2015-object-super "^6.22.0"
babel-plugin-transform-es2015-parameters "^6.22.0"
babel-plugin-transform-es2015-shorthand-properties "^6.22.0"
@@ -698,6 +705,27 @@ babel-preset-es2015@^6.22.0:
babel-plugin-transform-es2015-unicode-regex "^6.22.0"
babel-plugin-transform-regenerator "^6.22.0"
+babel-preset-es2016@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-es2016/-/babel-preset-es2016-6.22.0.tgz#b061aaa3983d40c9fbacfa3743b5df37f336156c"
+ dependencies:
+ babel-plugin-transform-exponentiation-operator "^6.22.0"
+
+babel-preset-es2017@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-es2017/-/babel-preset-es2017-6.22.0.tgz#de2f9da5a30c50d293fb54a0ba15d6ddc573f0f2"
+ dependencies:
+ babel-plugin-syntax-trailing-function-commas "^6.22.0"
+ babel-plugin-transform-async-to-generator "^6.22.0"
+
+babel-preset-latest@^6.24.0:
+ version "6.24.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-latest/-/babel-preset-latest-6.24.0.tgz#a68d20f509edcc5d7433a48dfaebf7e4f2cd4cb7"
+ dependencies:
+ babel-preset-es2015 "^6.24.0"
+ babel-preset-es2016 "^6.22.0"
+ babel-preset-es2017 "^6.22.0"
+
babel-preset-stage-2@^6.22.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.22.0.tgz#ccd565f19c245cade394b21216df704a73b27c07"
@@ -2900,6 +2928,10 @@ lodash.deburr@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-4.1.0.tgz#ddb1bbb3ef07458c0177ba07de14422cb033ff9b"
+lodash.get@4.4.2:
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
+
lodash.get@^3.7.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-3.7.0.tgz#3ce68ae2c91683b281cc5394128303cbf75e691f"
@@ -4271,6 +4303,10 @@ tough-cookie@~2.3.0:
dependencies:
punycode "^1.4.1"
+traverse@0.6.6:
+ version "0.6.6"
+ resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137"
+
trim-right@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"