summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKamil Trzciński <ayufan@ayufan.eu>2017-04-06 13:40:30 +0000
committerKamil Trzciński <ayufan@ayufan.eu>2017-04-06 13:40:30 +0000
commit88f33a1266ca51d48272e858c399daedd837575f (patch)
tree9c0da433d01a0cf01cdc0ded774c8b85a949403f
parentb3ea598b1aeaf7f3eb0071b60f97247ad67e69f4 (diff)
parent8199ff7bd71ce91fbc945b2c31010fcf654bda67 (diff)
downloadgitlab-ce-update-trace-handling-code.tar.gz
Merge branch 'master' into 'update-trace-handling-code'update-trace-handling-code
# Conflicts: # app/assets/javascripts/build.js
-rw-r--r--CHANGELOG.md24
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rwxr-xr-xapp/assets/images/ci_favicons/icon_status_canceled.icobin0 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/icon_status_created.icobin0 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/icon_status_failed.icobin0 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/icon_status_manual.icobin0 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/icon_status_not_found.icobin0 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/icon_status_pending.icobin0 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/icon_status_running.icobin0 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/icon_status_skipped.icobin0 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/icon_status_success.icobin0 -> 5430 bytes
-rwxr-xr-xapp/assets/images/ci_favicons/icon_status_warning.icobin0 -> 5430 bytes
-rw-r--r--app/assets/javascripts/awards_handler.js3
-rw-r--r--app/assets/javascripts/blob/3d_viewer/index.js147
-rw-r--r--app/assets/javascripts/blob/3d_viewer/mesh_object.js49
-rw-r--r--app/assets/javascripts/blob/pdf/index.js2
-rw-r--r--app/assets/javascripts/blob/stl_viewer.js19
-rw-r--r--app/assets/javascripts/build.js1
-rw-r--r--app/assets/javascripts/dispatcher.js2
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.js25
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js1
-rw-r--r--app/assets/javascripts/issue_show/index.js26
-rw-r--r--app/assets/javascripts/issue_show/issue_title.js78
-rw-r--r--app/assets/javascripts/issue_show/services/index.js10
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js31
-rw-r--r--app/assets/javascripts/merge_request_widget.js7
-rw-r--r--app/assets/javascripts/monitoring/prometheus_graph.js67
-rw-r--r--app/assets/javascripts/pipelines.js4
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js23
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js4
-rw-r--r--app/assets/javascripts/vue_realtime_listener/index.js38
-rw-r--r--app/assets/stylesheets/framework/awards.scss58
-rw-r--r--app/assets/stylesheets/framework/files.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/assets/stylesheets/pages/environments.scss9
-rw-r--r--app/assets/stylesheets/pages/notes.scss41
-rw-r--r--app/controllers/admin/projects_controller.rb1
-rw-r--r--app/controllers/concerns/continue_params.rb1
-rw-r--r--app/controllers/concerns/filter_projects.rb17
-rw-r--r--app/controllers/concerns/params_backward_compatibility.rb7
-rw-r--r--app/controllers/dashboard/projects_controller.rb27
-rw-r--r--app/controllers/dashboard/todos_controller.rb2
-rw-r--r--app/controllers/explore/projects_controller.rb34
-rw-r--r--app/controllers/groups/application_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb11
-rw-r--r--app/controllers/projects/forks_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb11
-rwxr-xr-xapp/controllers/projects/merge_requests_controller.rb2
-rw-r--r--app/controllers/users_controller.rb2
-rw-r--r--app/finders/group_projects_finder.rb59
-rw-r--r--app/finders/issuable_finder.rb8
-rw-r--r--app/finders/labels_finder.rb2
-rw-r--r--app/finders/projects_finder.rb88
-rw-r--r--app/finders/todos_finder.rb2
-rw-r--r--app/helpers/projects_helper.rb5
-rw-r--r--app/helpers/sorting_helper.rb20
-rw-r--r--app/models/blob.rb6
-rw-r--r--app/models/issue.rb11
-rw-r--r--app/models/project.rb7
-rw-r--r--app/policies/group_policy.rb4
-rw-r--r--app/serializers/build_action_entity.rb2
-rw-r--r--app/serializers/build_entity.rb1
-rw-r--r--app/services/merge_requests/base_service.rb2
-rw-r--r--app/services/merge_requests/build_service.rb4
-rw-r--r--app/services/search/global_service.rb2
-rw-r--r--app/views/award_emoji/_awards_block.html.haml4
-rw-r--r--app/views/dashboard/projects/_zero_authorized_projects.html.haml2
-rw-r--r--app/views/layouts/_head.html.haml2
-rw-r--r--app/views/notify/pipeline_failed_email.html.haml63
-rw-r--r--app/views/notify/pipeline_failed_email.text.erb14
-rw-r--r--app/views/notify/pipeline_success_email.html.haml57
-rw-r--r--app/views/notify/pipeline_success_email.text.erb14
-rw-r--r--app/views/projects/blob/_stl.html.haml12
-rw-r--r--app/views/projects/environments/_metrics_button.html.haml2
-rw-r--r--app/views/projects/environments/metrics.html.haml77
-rw-r--r--app/views/projects/issues/show.html.haml9
-rw-r--r--app/views/projects/merge_requests/widget/_show.html.haml1
-rw-r--r--app/views/projects/notes/_note.html.haml4
-rw-r--r--app/views/shared/empty_states/monitoring/_getting_started.svg1
-rw-r--r--app/views/shared/empty_states/monitoring/_loading.svg1
-rw-r--r--app/views/shared/empty_states/monitoring/_unable_to_connect.svg1
-rw-r--r--app/views/shared/icons/_emoji_slightly_smiling_face.svg1
-rw-r--r--app/views/shared/icons/_emoji_smile.svg1
-rw-r--r--app/views/shared/icons/_emoji_smiley.svg1
-rw-r--r--app/views/shared/projects/_dropdown.html.haml2
-rw-r--r--app/workers/repository_import_worker.rb4
-rw-r--r--app/workers/stuck_import_jobs_worker.rb37
-rw-r--r--changelogs/unreleased/12818-ci-status-as-favicon.yml4
-rw-r--r--changelogs/unreleased/28810-projectfinder-should-handle-more-options.yml4
-rw-r--r--changelogs/unreleased/29364-private-projects-mr-fix.yml4
-rw-r--r--changelogs/unreleased/30021-api-deploy_keys-can_push-is-not-honoured.yml4
-rw-r--r--changelogs/unreleased/30125-markdown-security.yml4
-rw-r--r--changelogs/unreleased/30195-document-search-param-on-api.yml4
-rw-r--r--changelogs/unreleased/30291-reopen-mr.yml4
-rw-r--r--changelogs/unreleased/30493-env-deploy-tooltip.yml5
-rw-r--r--changelogs/unreleased/add-error-empty-states.yml4
-rw-r--r--changelogs/unreleased/add_remove_concurrent_index_to_database_helper.yml4
-rw-r--r--changelogs/unreleased/award-emoji-button-smiley-animation.yml4
-rw-r--r--changelogs/unreleased/feature-gh-rake-task.yml4
-rw-r--r--changelogs/unreleased/file-import-export-path-disclosure.yml5
-rw-r--r--changelogs/unreleased/fix-gh-import-status-check.yml4
-rw-r--r--changelogs/unreleased/open-redirect-continue-params.yml4
-rw-r--r--changelogs/unreleased/open-redirect-host-field.yml4
-rw-r--r--changelogs/unreleased/tc-fix-pipeline-recipient.yml4
-rw-r--r--changelogs/unreleased/tc-fix-unplayable-build-action-404.yml4
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--config/initializers/8_gitaly.rb14
-rw-r--r--config/routes/project.rb1
-rw-r--r--config/webpack.config.js2
-rw-r--r--db/migrate/20160615142710_add_index_on_requested_at_to_members.rb1
-rw-r--r--db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb1
-rw-r--r--db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb1
-rw-r--r--db/migrate/20160620115026_add_index_on_runners_locked.rb1
-rw-r--r--db/migrate/20160715134306_add_index_for_pipeline_user_id.rb1
-rw-r--r--db/migrate/20160725104020_merge_request_diff_remove_uniq.rb1
-rw-r--r--db/migrate/20160725104452_merge_request_diff_add_index.rb1
-rw-r--r--db/migrate/20160802010328_remove_builds_enable_index_on_projects.rb1
-rw-r--r--db/migrate/20160803161903_add_unique_index_to_lists_label_id.rb1
-rw-r--r--db/migrate/20160805041956_add_deleted_at_to_namespaces.rb1
-rw-r--r--db/migrate/20160808085602_add_index_for_build_token.rb1
-rw-r--r--db/migrate/20160810142633_remove_redundant_indexes.rb1
-rw-r--r--db/migrate/20160819221631_add_index_to_note_discussion_id.rb1
-rw-r--r--db/migrate/20160819232256_add_incoming_email_token_to_users.rb1
-rw-r--r--db/migrate/20160919145149_add_group_id_to_labels.rb1
-rw-r--r--db/migrate/20160920160832_add_index_to_labels_title.rb1
-rw-r--r--db/migrate/20161017125927_add_unique_index_to_labels.rb1
-rw-r--r--db/migrate/20161020083353_add_pipeline_id_to_merge_request_metrics.rb1
-rw-r--r--db/migrate/20161031181638_add_unique_index_to_subscriptions.rb1
-rw-r--r--db/migrate/20161106185620_add_project_import_data_project_index.rb1
-rw-r--r--db/migrate/20161124111395_add_index_to_parent_id.rb1
-rw-r--r--db/migrate/20161128142110_remove_unnecessary_indexes.rb1
-rw-r--r--db/migrate/20161202152035_add_index_to_routes.rb1
-rw-r--r--db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb1
-rw-r--r--db/migrate/20161206153751_add_path_index_to_namespace.rb1
-rw-r--r--db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb1
-rw-r--r--db/migrate/20161206153754_add_name_index_to_namespace.rb1
-rw-r--r--db/migrate/20161207231621_create_environment_name_unique_index.rb1
-rw-r--r--db/migrate/20161209153400_add_unique_index_for_environment_slug.rb1
-rw-r--r--db/migrate/20161212142807_add_lower_path_index_to_routes.rb1
-rw-r--r--db/migrate/20170121123724_add_index_to_ci_builds_for_status_runner_id_and_type.rb1
-rw-r--r--db/migrate/20170121130655_add_index_to_ci_runners_for_is_shared.rb1
-rw-r--r--db/migrate/20170130204620_add_index_to_project_authorizations.rb5
-rw-r--r--db/migrate/20170131221752_add_relative_position_to_issues.rb1
-rw-r--r--db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb1
-rw-r--r--db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb1
-rw-r--r--db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb1
-rw-r--r--db/migrate/20170210103609_add_index_to_user_agent_detail.rb1
-rw-r--r--db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb1
-rw-r--r--db/migrate/20170216141440_drop_index_for_builds_project_status.rb1
-rw-r--r--db/migrate/20170222143500_remove_old_project_id_columns.rb1
-rw-r--r--db/migrate/20170313213916_add_index_to_user_ghost.rb1
-rw-r--r--db/migrate/20170405080720_add_import_jid_to_projects.rb9
-rw-r--r--db/schema.rb3
-rw-r--r--doc/administration/raketasks/github_import.md36
-rw-r--r--doc/api/issues.md142
-rw-r--r--doc/ci/README.md149
-rw-r--r--doc/ci/examples/README.md69
-rw-r--r--doc/ci/img/cicd_pipeline_infograph.pngbin0 -> 32493 bytes
-rw-r--r--doc/development/migration_style_guide.md16
-rw-r--r--generator_templates/active_record/migration/create_table_migration.rb14
-rw-r--r--generator_templates/active_record/migration/migration.rb14
-rw-r--r--generator_templates/rails/post_deployment_migration/migration.rb14
-rw-r--r--lib/api/deploy_keys.rb1
-rw-r--r--lib/api/groups.rb2
-rw-r--r--lib/api/issues.rb1
-rw-r--r--lib/api/projects.rb2
-rw-r--r--lib/api/users.rb2
-rw-r--r--lib/api/v3/groups.rb2
-rw-r--r--lib/api/v3/projects.rb2
-rw-r--r--lib/api/v3/users.rb2
-rw-r--r--lib/banzai/filter/markdown_filter.rb2
-rw-r--r--lib/banzai/filter/sanitization_filter.rb22
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb2
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb2
-rw-r--r--lib/banzai/renderer/html.rb13
-rw-r--r--lib/gitlab/database/migration_helpers.rb24
-rw-r--r--lib/gitlab/etag_caching/middleware.rb3
-rw-r--r--lib/gitlab/gitaly_client.rb38
-rw-r--r--lib/gitlab/sidekiq_status.rb13
-rw-r--r--lib/gitlab/sidekiq_status/client_middleware.rb4
-rw-r--r--lib/gitlab/workhorse.rb7
-rw-r--r--lib/tasks/import.rake204
-rw-r--r--package.json3
-rw-r--r--rubocop/cop/migration/add_concurrent_index.rb2
-rw-r--r--rubocop/cop/migration/remove_concurrent_index.rb29
-rw-r--r--rubocop/cop/migration/remove_index.rb26
-rw-r--r--rubocop/rubocop.rb2
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb7
-rw-r--r--spec/controllers/projects/imports_controller_spec.rb9
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb11
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb12
-rw-r--r--spec/factories/ci/builds.rb5
-rw-r--r--spec/factories/environments.rb5
-rw-r--r--spec/factories/keys.rb4
-rw-r--r--spec/features/gitlab_flavored_markdown_spec.rb12
-rw-r--r--spec/features/issues/award_emoji_spec.rb5
-rw-r--r--spec/features/issues/move_spec.rb4
-rw-r--r--spec/features/issues/spam_issues_spec.rb2
-rw-r--r--spec/features/issues_spec.rb17
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb12
-rw-r--r--spec/finders/group_projects_finder_spec.rb70
-rw-r--r--spec/finders/projects_finder_spec.rb128
-rw-r--r--spec/helpers/events_helper_spec.rb27
-rw-r--r--spec/helpers/projects_helper_spec.rb8
-rw-r--r--spec/javascripts/blob/3d_viewer/mesh_object_spec.js42
-rw-r--r--spec/javascripts/blob/pdf/index_spec.js24
-rw-r--r--spec/javascripts/build_spec.js2
-rw-r--r--spec/javascripts/environments/environment_actions_spec.js15
-rw-r--r--spec/javascripts/fixtures/environments/metrics.html.haml64
-rw-r--r--spec/javascripts/issue_show/issue_title_spec.js22
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js51
-rw-r--r--spec/javascripts/merge_request_widget_spec.js7
-rw-r--r--spec/javascripts/monitoring/prometheus_graph_spec.js23
-rw-r--r--spec/javascripts/test_bundle.js1
-rw-r--r--spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js15
-rw-r--r--spec/lib/banzai/filter/markdown_filter_spec.rb19
-rw-r--r--spec/lib/banzai/filter/sanitization_filter_spec.rb7
-rw-r--r--spec/lib/banzai/filter/syntax_highlight_filter_spec.rb6
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb42
-rw-r--r--spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_status_spec.rb13
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb4
-rw-r--r--spec/mailers/previews/notify_preview.rb11
-rw-r--r--spec/models/blob_spec.rb22
-rw-r--r--spec/requests/api/deploy_keys_spec.rb9
-rw-r--r--spec/requests/api/issues_spec.rb376
-rw-r--r--spec/rubocop/cop/migration/remove_concurrent_index_spec.rb41
-rw-r--r--spec/rubocop/cop/migration/remove_index_spec.rb35
-rw-r--r--spec/serializers/build_action_entity_spec.rb4
-rw-r--r--spec/serializers/build_entity_spec.rb4
-rw-r--r--spec/services/merge_requests/build_service_spec.rb42
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb27
-rw-r--r--spec/views/notify/pipeline_failed_email.html.haml_spec.rb54
-rw-r--r--spec/views/notify/pipeline_success_email.html.haml_spec.rb54
-rw-r--r--spec/workers/repository_import_worker_spec.rb2
-rw-r--r--spec/workers/stuck_import_jobs_worker_spec.rb36
-rw-r--r--yarn.lock12
237 files changed, 3077 insertions, 696 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e5475a2296..712a4970a41 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 9.0.4 (2017-04-05)
+
+- Don’t show source project name when user does not have access.
+- Remove the class attribute from the whitelist for HTML generated from Markdown.
+- Fix path disclosure in project import/export.
+- Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
+- Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
+
## 9.0.3 (2017-04-05)
- Fix name colision when importing GitHub pull requests from forked repositories. !9719
@@ -320,6 +328,14 @@ entry.
- Change development tanuki favicon colors to match logo color order.
- API issues - support filtering by iids.
+## 8.17.5 (2017-04-05)
+
+- Don’t show source project name when user does not have access.
+- Remove the class attribute from the whitelist for HTML generated from Markdown.
+- Fix path disclosure in project import/export.
+- Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
+- Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
+
## 8.17.4 (2017-03-19)
- Only show public emails in atom feeds.
@@ -533,6 +549,14 @@ entry.
- Remove deprecated GitlabCiService.
- Requeue pending deletion projects.
+## 8.16.9 (2017-04-05)
+
+- Don’t show source project name when user does not have access.
+- Remove the class attribute from the whitelist for HTML generated from Markdown.
+- Fix path disclosure in project import/export.
+- Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
+- Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
+
## 8.16.8 (2017-03-19)
- Only show public emails in atom feeds.
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 9df886c42a1..428b770e3e2 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-1.4.2
+1.4.3
diff --git a/app/assets/images/ci_favicons/icon_status_canceled.ico b/app/assets/images/ci_favicons/icon_status_canceled.ico
new file mode 100755
index 00000000000..5a19458f2a2
--- /dev/null
+++ b/app/assets/images/ci_favicons/icon_status_canceled.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_created.ico b/app/assets/images/ci_favicons/icon_status_created.ico
new file mode 100755
index 00000000000..4dca9640cb3
--- /dev/null
+++ b/app/assets/images/ci_favicons/icon_status_created.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_failed.ico b/app/assets/images/ci_favicons/icon_status_failed.ico
new file mode 100755
index 00000000000..c961ff9a69b
--- /dev/null
+++ b/app/assets/images/ci_favicons/icon_status_failed.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_manual.ico b/app/assets/images/ci_favicons/icon_status_manual.ico
new file mode 100755
index 00000000000..5fbbc99ea7c
--- /dev/null
+++ b/app/assets/images/ci_favicons/icon_status_manual.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_not_found.ico b/app/assets/images/ci_favicons/icon_status_not_found.ico
new file mode 100755
index 00000000000..21afa9c72e6
--- /dev/null
+++ b/app/assets/images/ci_favicons/icon_status_not_found.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_pending.ico b/app/assets/images/ci_favicons/icon_status_pending.ico
new file mode 100755
index 00000000000..8be32dab85a
--- /dev/null
+++ b/app/assets/images/ci_favicons/icon_status_pending.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_running.ico b/app/assets/images/ci_favicons/icon_status_running.ico
new file mode 100755
index 00000000000..f328ff1a5ed
--- /dev/null
+++ b/app/assets/images/ci_favicons/icon_status_running.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_skipped.ico b/app/assets/images/ci_favicons/icon_status_skipped.ico
new file mode 100755
index 00000000000..b4394e1b4af
--- /dev/null
+++ b/app/assets/images/ci_favicons/icon_status_skipped.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_success.ico b/app/assets/images/ci_favicons/icon_status_success.ico
new file mode 100755
index 00000000000..4f436c95242
--- /dev/null
+++ b/app/assets/images/ci_favicons/icon_status_success.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/icon_status_warning.ico b/app/assets/images/ci_favicons/icon_status_warning.ico
new file mode 100755
index 00000000000..805cc20cdec
--- /dev/null
+++ b/app/assets/images/ci_favicons/icon_status_warning.ico
Binary files differ
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 4f63c7988f5..67106e85a37 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -263,7 +263,8 @@ AwardsHandler.prototype.addAward = function addAward(
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
return typeof callback === 'function' ? callback() : undefined;
});
- return $('.emoji-menu').removeClass('is-visible');
+ $('.emoji-menu').removeClass('is-visible');
+ $('.js-add-award.is-active').removeClass('is-active');
};
AwardsHandler.prototype.addAwardToEmojiBar = function addAwardToEmojiBar(
diff --git a/app/assets/javascripts/blob/3d_viewer/index.js b/app/assets/javascripts/blob/3d_viewer/index.js
new file mode 100644
index 00000000000..68d4ddad551
--- /dev/null
+++ b/app/assets/javascripts/blob/3d_viewer/index.js
@@ -0,0 +1,147 @@
+import * as THREE from 'three/build/three.module';
+import STLLoaderClass from 'three-stl-loader';
+import OrbitControlsClass from 'three-orbit-controls';
+import MeshObject from './mesh_object';
+
+const STLLoader = STLLoaderClass(THREE);
+const OrbitControls = OrbitControlsClass(THREE);
+
+export default class Renderer {
+ constructor(container) {
+ this.renderWrapper = this.render.bind(this);
+ this.objects = [];
+
+ this.container = container;
+ this.width = this.container.offsetWidth;
+ this.height = 500;
+
+ this.loader = new STLLoader();
+
+ this.fov = 45;
+ this.camera = new THREE.PerspectiveCamera(
+ this.fov,
+ this.width / this.height,
+ 1,
+ 1000,
+ );
+
+ this.scene = new THREE.Scene();
+
+ this.scene.add(this.camera);
+
+ // Setup the viewer
+ this.setupRenderer();
+ this.setupGrid();
+ this.setupLight();
+
+ // Setup OrbitControls
+ this.controls = new OrbitControls(
+ this.camera,
+ this.renderer.domElement,
+ );
+ this.controls.minDistance = 5;
+ this.controls.maxDistance = 30;
+ this.controls.enableKeys = false;
+
+ this.loadFile();
+ }
+
+ setupRenderer() {
+ this.renderer = new THREE.WebGLRenderer({
+ antialias: true,
+ });
+
+ this.renderer.setClearColor(0xFFFFFF);
+ this.renderer.setPixelRatio(window.devicePixelRatio);
+ this.renderer.setSize(
+ this.width,
+ this.height,
+ );
+ }
+
+ setupLight() {
+ // Point light illuminates the object
+ const pointLight = new THREE.PointLight(
+ 0xFFFFFF,
+ 2,
+ 0,
+ );
+
+ pointLight.castShadow = true;
+
+ this.camera.add(pointLight);
+
+ // Ambient light illuminates the scene
+ const ambientLight = new THREE.AmbientLight(
+ 0xFFFFFF,
+ 1,
+ );
+ this.scene.add(ambientLight);
+ }
+
+ setupGrid() {
+ this.grid = new THREE.GridHelper(
+ 20,
+ 20,
+ 0x000000,
+ 0x000000,
+ );
+
+ this.scene.add(this.grid);
+ }
+
+ loadFile() {
+ this.loader.load(this.container.dataset.endpoint, (geo) => {
+ const obj = new MeshObject(geo);
+
+ this.objects.push(obj);
+ this.scene.add(obj);
+
+ this.start();
+ this.setDefaultCameraPosition();
+ });
+ }
+
+ start() {
+ // Empty the container first
+ this.container.innerHTML = '';
+
+ // Add to DOM
+ this.container.appendChild(this.renderer.domElement);
+
+ // Make controls visible
+ this.container.parentNode.classList.remove('is-stl-loading');
+
+ this.render();
+ }
+
+ render() {
+ this.renderer.render(
+ this.scene,
+ this.camera,
+ );
+
+ requestAnimationFrame(this.renderWrapper);
+ }
+
+ changeObjectMaterials(type) {
+ this.objects.forEach((obj) => {
+ obj.changeMaterial(type);
+ });
+ }
+
+ setDefaultCameraPosition() {
+ const obj = this.objects[0];
+ const radius = (obj.geometry.boundingSphere.radius / 1.5);
+ const dist = radius / (Math.sin((this.fov * (Math.PI / 180)) / 2));
+
+ this.camera.position.set(
+ 0,
+ dist + 1,
+ dist,
+ );
+
+ this.camera.lookAt(this.grid);
+ this.controls.update();
+ }
+}
diff --git a/app/assets/javascripts/blob/3d_viewer/mesh_object.js b/app/assets/javascripts/blob/3d_viewer/mesh_object.js
new file mode 100644
index 00000000000..96758884abf
--- /dev/null
+++ b/app/assets/javascripts/blob/3d_viewer/mesh_object.js
@@ -0,0 +1,49 @@
+import {
+ Matrix4,
+ MeshLambertMaterial,
+ Mesh,
+} from 'three/build/three.module';
+
+const defaultColor = 0xE24329;
+const materials = {
+ default: new MeshLambertMaterial({
+ color: defaultColor,
+ }),
+ wireframe: new MeshLambertMaterial({
+ color: defaultColor,
+ wireframe: true,
+ }),
+};
+
+export default class MeshObject extends Mesh {
+ constructor(geo) {
+ super(
+ geo,
+ materials.default,
+ );
+
+ this.geometry.computeBoundingSphere();
+
+ this.rotation.set(-Math.PI / 2, 0, 0);
+
+ if (this.geometry.boundingSphere.radius > 4) {
+ const scale = 4 / this.geometry.boundingSphere.radius;
+
+ this.geometry.applyMatrix(
+ new Matrix4().makeScale(
+ scale,
+ scale,
+ scale,
+ ),
+ );
+ this.geometry.computeBoundingSphere();
+
+ this.position.x = -this.geometry.boundingSphere.center.x;
+ this.position.z = this.geometry.boundingSphere.center.y;
+ }
+ }
+
+ changeMaterial(type) {
+ this.material = materials[type];
+ }
+}
diff --git a/app/assets/javascripts/blob/pdf/index.js b/app/assets/javascripts/blob/pdf/index.js
index 5b79717d1e1..a74c2db9a61 100644
--- a/app/assets/javascripts/blob/pdf/index.js
+++ b/app/assets/javascripts/blob/pdf/index.js
@@ -10,7 +10,7 @@ Vue.use(PDFLab, {
export default () => {
const el = document.getElementById('js-pdf-viewer');
- new Vue({
+ return new Vue({
el,
data() {
return {
diff --git a/app/assets/javascripts/blob/stl_viewer.js b/app/assets/javascripts/blob/stl_viewer.js
new file mode 100644
index 00000000000..f611c4fe640
--- /dev/null
+++ b/app/assets/javascripts/blob/stl_viewer.js
@@ -0,0 +1,19 @@
+import Renderer from './3d_viewer';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const viewer = new Renderer(document.getElementById('js-stl-viewer'));
+
+ [].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => {
+ el.addEventListener('click', (e) => {
+ const target = e.target;
+
+ e.preventDefault();
+
+ document.querySelector('.js-material-changer.active').classList.remove('active');
+ target.classList.add('active');
+ target.blur();
+
+ viewer.changeObjectMaterials(target.dataset.type);
+ });
+ });
+});
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index f97fc427198..7cb022c848a 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -88,6 +88,7 @@ window.Build = (function() {
dataType: 'json',
success: function(buildData) {
$('.js-build-output').html(buildData.html);
+ gl.utils.setCiStatusFavicon(`${this.pageUrl}/status.json`);
if (window.location.hash === DOWN_BUILD_TRACE) {
$("html,body").scrollTop(this.$buildTrace.height());
}
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 80490052389..9c7acc903d1 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -226,9 +226,11 @@ const ShortcutsBlob = require('./shortcuts_blob');
case 'projects:pipelines:builds':
case 'projects:pipelines:show':
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
+ const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`;
new gl.Pipelines({
initTabs: true,
+ pipelineStatusUrl,
tabsOptions: {
action: controllerAction,
defaultAction: 'pipelines',
diff --git a/app/assets/javascripts/environments/components/environment_actions.js b/app/assets/javascripts/environments/components/environment_actions.js
index 385085c03e2..1418e8d86ee 100644
--- a/app/assets/javascripts/environments/components/environment_actions.js
+++ b/app/assets/javascripts/environments/components/environment_actions.js
@@ -45,11 +45,20 @@ export default {
new Flash('An error occured while making the request.');
});
},
+
+ isActionDisabled(action) {
+ if (action.playable === undefined) {
+ return false;
+ }
+
+ return !action.playable;
+ },
},
template: `
<div class="btn-group" role="group">
<button
+ type="button"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
data-container="body"
data-toggle="dropdown"
@@ -58,15 +67,24 @@ export default {
:disabled="isLoading">
<span>
<span v-html="playIconSvg"></span>
- <i class="fa fa-caret-down" aria-hidden="true"></i>
- <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+ <i
+ class="fa fa-caret-down"
+ aria-hidden="true"/>
+ <i
+ v-if="isLoading"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true"/>
</span>
+ </button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
<button
+ type="button"
+ class="js-manual-action-link no-btn btn"
@click="onClickAction(action.play_path)"
- class="js-manual-action-link no-btn">
+ :class="{ 'disabled': isActionDisabled(action) }"
+ :disabled="isActionDisabled(action)">
${playIconSvg}
<span>
{{action.name}}
@@ -74,7 +92,6 @@ export default {
</button>
</li>
</ul>
- </button>
</div>
`,
};
diff --git a/app/assets/javascripts/environments/components/environment_item.js b/app/assets/javascripts/environments/components/environment_item.js
index e44d93a30c7..d9b49287dec 100644
--- a/app/assets/javascripts/environments/components/environment_item.js
+++ b/app/assets/javascripts/environments/components/environment_item.js
@@ -142,6 +142,7 @@ export default {
const parsedAction = {
name: gl.text.humanize(action.name),
play_path: action.play_path,
+ playable: action.playable,
};
return parsedAction;
});
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
new file mode 100644
index 00000000000..b6ce8e83729
--- /dev/null
+++ b/app/assets/javascripts/issue_show/index.js
@@ -0,0 +1,26 @@
+import Vue from 'vue';
+import IssueTitle from './issue_title';
+import '../vue_shared/vue_resource_interceptor';
+
+const vueOptions = () => ({
+ el: '.issue-title-entrypoint',
+ components: {
+ IssueTitle,
+ },
+ data() {
+ const issueTitleData = document.querySelector('.issue-title-data').dataset;
+
+ return {
+ initialTitle: issueTitleData.initialTitle,
+ endpoint: issueTitleData.endpoint,
+ };
+ },
+ template: `
+ <IssueTitle
+ :initialTitle="initialTitle"
+ :endpoint="endpoint"
+ />
+ `,
+});
+
+(() => new Vue(vueOptions()))();
diff --git a/app/assets/javascripts/issue_show/issue_title.js b/app/assets/javascripts/issue_show/issue_title.js
new file mode 100644
index 00000000000..1184c8956dc
--- /dev/null
+++ b/app/assets/javascripts/issue_show/issue_title.js
@@ -0,0 +1,78 @@
+import Visibility from 'visibilityjs';
+import Poll from './../lib/utils/poll';
+import Service from './services/index';
+
+export default {
+ props: {
+ initialTitle: { required: true, type: String },
+ endpoint: { required: true, type: String },
+ },
+ data() {
+ const resource = new Service(this.$http, this.endpoint);
+
+ const poll = new Poll({
+ resource,
+ method: 'getTitle',
+ successCallback: (res) => {
+ this.renderResponse(res);
+ },
+ errorCallback: (err) => {
+ if (process.env.NODE_ENV !== 'production') {
+ // eslint-disable-next-line no-console
+ console.error('ISSUE SHOW TITLE REALTIME ERROR', err);
+ } else {
+ throw new Error(err);
+ }
+ },
+ });
+
+ return {
+ poll,
+ timeoutId: null,
+ title: this.initialTitle,
+ };
+ },
+ methods: {
+ fetch() {
+ this.poll.makeRequest();
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ this.poll.restart();
+ } else {
+ this.poll.stop();
+ }
+ });
+ },
+ renderResponse(res) {
+ const body = JSON.parse(res.body);
+ this.triggerAnimation(body);
+ },
+ triggerAnimation(body) {
+ const { title } = body;
+
+ /**
+ * since opacity is changed, even if there is no diff for Vue to update
+ * we must check the title even on a 304 to ensure no visual change
+ */
+ if (this.title === title) return;
+
+ this.$el.style.opacity = 0;
+
+ this.timeoutId = setTimeout(() => {
+ this.title = title;
+
+ this.$el.style.transition = 'opacity 0.2s ease';
+ this.$el.style.opacity = 1;
+
+ clearTimeout(this.timeoutId);
+ }, 100);
+ },
+ },
+ created() {
+ this.fetch();
+ },
+ template: `
+ <h2 class='title' v-html='title'></h2>
+ `,
+};
diff --git a/app/assets/javascripts/issue_show/services/index.js b/app/assets/javascripts/issue_show/services/index.js
new file mode 100644
index 00000000000..c4ab0b1e07a
--- /dev/null
+++ b/app/assets/javascripts/issue_show/services/index.js
@@ -0,0 +1,10 @@
+export default class Service {
+ constructor(resource, endpoint) {
+ this.resource = resource;
+ this.endpoint = endpoint;
+ }
+
+ getTitle() {
+ return this.resource.get(this.endpoint);
+ }
+}
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 46b80c04e20..e1e6ca25446 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -2,6 +2,8 @@
(function() {
(function(w) {
var base;
+ const faviconEl = document.getElementById('favicon');
+ const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null;
w.gl || (w.gl = {});
(base = w.gl).utils || (base.utils = {});
w.gl.utils.isInGroupsPage = function() {
@@ -361,5 +363,34 @@
fn(next, stop);
});
};
+
+ w.gl.utils.setFavicon = (iconName) => {
+ if (faviconEl && iconName) {
+ faviconEl.setAttribute('href', `/assets/${iconName}.ico`);
+ }
+ };
+
+ w.gl.utils.resetFavicon = () => {
+ if (faviconEl) {
+ faviconEl.setAttribute('href', originalFavicon);
+ }
+ };
+
+ w.gl.utils.setCiStatusFavicon = (pageUrl) => {
+ $.ajax({
+ url: pageUrl,
+ dataType: 'json',
+ success: function(data) {
+ if (data && data.icon) {
+ gl.utils.setFavicon(`ci_favicons/${data.icon}`);
+ } else {
+ gl.utils.resetFavicon();
+ }
+ },
+ error: function() {
+ gl.utils.resetFavicon();
+ }
+ });
+ };
})(window);
}).call(window);
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js
index 0e2af3df071..b0254b17dd2 100644
--- a/app/assets/javascripts/merge_request_widget.js
+++ b/app/assets/javascripts/merge_request_widget.js
@@ -38,11 +38,13 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
function MergeRequestWidget(opts) {
// Initialize MergeRequestWidget behavior
//
- // check_enable - Boolean, whether to check automerge status
- // merge_check_url - String, URL to use to check automerge status
+ // check_enable - Boolean, whether to check automerge status
+ // merge_check_url - String, URL to use to check automerge status
// ci_status_url - String, URL to use to check CI status
+ // pipeline_status_url - String, URL to use to get CI status for Favicon
//
this.opts = opts;
+ this.opts.pipeline_status_url = `${this.opts.pipeline_status_url}.json`;
this.$widgetBody = $('.mr-widget-body');
$('#modal_merge_info').modal({
show: false
@@ -159,6 +161,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
_this.status = data.status;
_this.hasCi = data.has_ci;
_this.updateMergeButton(_this.status, _this.hasCi);
+ gl.utils.setCiStatusFavicon(_this.opts.pipeline_status_url);
if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
if (data.status !== _this.opts.ci_status ||
data.sha !== _this.opts.ci_sha ||
diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js
index a6ffa0f59de..d82a4eb9642 100644
--- a/app/assets/javascripts/monitoring/prometheus_graph.js
+++ b/app/assets/javascripts/monitoring/prometheus_graph.js
@@ -6,7 +6,10 @@ import statusCodes from '~/lib/utils/http_status';
import { formatRelevantDigits } from '~/lib/utils/number_utils';
import '../flash';
+const prometheusContainer = '.prometheus-container';
+const prometheusParentGraphContainer = '.prometheus-graphs';
const prometheusGraphsContainer = '.prometheus-graph';
+const prometheusStatesContainer = '.prometheus-state';
const metricsEndpoint = 'metrics.json';
const timeFormat = d3.time.format('%H:%M');
const dayFormat = d3.time.format('%b %e, %a');
@@ -14,19 +17,30 @@ const bisectDate = d3.bisector(d => d.time).left;
const extraAddedWidthParent = 100;
class PrometheusGraph {
-
constructor() {
- this.margin = { top: 80, right: 180, bottom: 80, left: 100 };
- this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 };
- const parentContainerWidth = $(prometheusGraphsContainer).parent().width() +
- extraAddedWidthParent;
- this.originalWidth = parentContainerWidth;
- this.originalHeight = 330;
- this.width = parentContainerWidth - this.margin.left - this.margin.right;
- this.height = this.originalHeight - this.margin.top - this.margin.bottom;
- this.backOffRequestCounter = 0;
- this.configureGraph();
- this.init();
+ const $prometheusContainer = $(prometheusContainer);
+ const hasMetrics = $prometheusContainer.data('has-metrics');
+ this.docLink = $prometheusContainer.data('doc-link');
+ this.integrationLink = $prometheusContainer.data('prometheus-integration');
+
+ $(document).ajaxError(() => {});
+
+ if (hasMetrics) {
+ this.margin = { top: 80, right: 180, bottom: 80, left: 100 };
+ this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 };
+ const parentContainerWidth = $(prometheusGraphsContainer).parent().width() +
+ extraAddedWidthParent;
+ this.originalWidth = parentContainerWidth;
+ this.originalHeight = 330;
+ this.width = parentContainerWidth - this.margin.left - this.margin.right;
+ this.height = this.originalHeight - this.margin.top - this.margin.bottom;
+ this.backOffRequestCounter = 0;
+ this.configureGraph();
+ this.init();
+ } else {
+ this.state = '.js-getting-started';
+ this.updateState();
+ }
}
createGraph() {
@@ -40,8 +54,19 @@ class PrometheusGraph {
init() {
this.getData().then((metricsResponse) => {
- if (Object.keys(metricsResponse).length === 0) {
- new Flash('Empty metrics', 'alert');
+ let enoughData = true;
+ Object.keys(metricsResponse.metrics).forEach((key) => {
+ let currentKey;
+ if (key === 'cpu_values' || key === 'memory_values') {
+ currentKey = metricsResponse.metrics[key];
+ if (Object.keys(currentKey).length === 0) {
+ enoughData = false;
+ }
+ }
+ });
+ if (!enoughData) {
+ this.state = '.js-loading';
+ this.updateState();
} else {
this.transformData(metricsResponse);
this.createGraph();
@@ -345,14 +370,17 @@ class PrometheusGraph {
}
return resp.metrics;
})
- .catch(() => new Flash('An error occurred while fetching metrics.', 'alert'));
+ .catch(() => {
+ this.state = '.js-unable-to-connect';
+ this.updateState();
+ });
}
transformData(metricsResponse) {
Object.keys(metricsResponse.metrics).forEach((key) => {
if (key === 'cpu_values' || key === 'memory_values') {
const metricValues = (metricsResponse.metrics[key])[0];
- if (typeof metricValues !== 'undefined') {
+ if (metricValues !== undefined) {
this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({
time: new Date(metric[0] * 1000),
value: metric[1],
@@ -361,6 +389,13 @@ class PrometheusGraph {
}
});
}
+
+ updateState() {
+ const $statesContainer = $(prometheusStatesContainer);
+ $(prometheusParentGraphContainer).hide();
+ $(`${this.state}`, $statesContainer).removeClass('hidden');
+ $(prometheusStatesContainer).show();
+ }
}
export default PrometheusGraph;
diff --git a/app/assets/javascripts/pipelines.js b/app/assets/javascripts/pipelines.js
index 9203abefbbc..4252b615887 100644
--- a/app/assets/javascripts/pipelines.js
+++ b/app/assets/javascripts/pipelines.js
@@ -9,6 +9,10 @@ require('./lib/utils/bootstrap_linked_tabs');
new global.LinkedTabs(options.tabsOptions);
}
+ if (options.pipelineStatusUrl) {
+ gl.utils.setCiStatusFavicon(options.pipelineStatusUrl);
+ }
+
this.addMarginToBuildColumns();
}
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js
index 4bb2b048884..12d80768646 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js
@@ -38,6 +38,14 @@ export default {
new Flash('An error occured while making the request.');
});
},
+
+ isActionDisabled(action) {
+ if (action.playable === undefined) {
+ return false;
+ }
+
+ return !action.playable;
+ },
},
template: `
@@ -51,16 +59,23 @@ export default {
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>
+ <i
+ class="fa fa-caret-down"
+ aria-hidden="true" />
+ <i
+ v-if="isLoading"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
</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)">
+ class="js-pipeline-action-link no-btn btn"
+ @click="onClickAction(action.path)"
+ :class="{ 'disabled': isActionDisabled(action) }"
+ :disabled="isActionDisabled(action)">
${playIconSvg}
<span>{{action.name}}</span>
</button>
diff --git a/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
index 7ac10086a55..377ec8ba2cc 100644
--- a/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
+++ b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
@@ -1,5 +1,5 @@
/* eslint-disable no-underscore-dangle*/
-import '../../vue_realtime_listener';
+import VueRealtimeListener from '../../vue_realtime_listener';
export default class PipelinesStore {
constructor() {
@@ -56,6 +56,6 @@ export default class PipelinesStore {
const removeIntervals = () => clearInterval(this.timeLoopInterval);
const startIntervals = () => startTimeLoops();
- gl.VueRealtimeListener(removeIntervals, startIntervals);
+ VueRealtimeListener(removeIntervals, startIntervals);
}
}
diff --git a/app/assets/javascripts/vue_realtime_listener/index.js b/app/assets/javascripts/vue_realtime_listener/index.js
index 30f6680a673..4ddb2f975b0 100644
--- a/app/assets/javascripts/vue_realtime_listener/index.js
+++ b/app/assets/javascripts/vue_realtime_listener/index.js
@@ -1,29 +1,9 @@
-/* eslint-disable no-param-reassign */
-
-((gl) => {
- gl.VueRealtimeListener = (removeIntervals, startIntervals) => {
- const removeAll = () => {
- removeIntervals();
- window.removeEventListener('beforeunload', removeIntervals);
- window.removeEventListener('focus', startIntervals);
- window.removeEventListener('blur', removeIntervals);
- document.removeEventListener('beforeunload', removeAll);
- };
-
- window.addEventListener('beforeunload', removeIntervals);
- window.addEventListener('focus', startIntervals);
- window.addEventListener('blur', removeIntervals);
- document.addEventListener('beforeunload', removeAll);
-
- // add removeAll methods to stack
- const stack = gl.VueRealtimeListener.reset;
- gl.VueRealtimeListener.reset = () => {
- gl.VueRealtimeListener.reset = stack;
- removeAll();
- stack();
- };
- };
-
- // remove all event listeners and intervals
- gl.VueRealtimeListener.reset = () => undefined; // noop
-})(window.gl || (window.gl = {}));
+export default (removeIntervals, startIntervals) => {
+ window.removeEventListener('focus', startIntervals);
+ window.removeEventListener('blur', removeIntervals);
+ window.removeEventListener('onbeforeload', removeIntervals);
+
+ window.addEventListener('focus', startIntervals);
+ window.addEventListener('blur', removeIntervals);
+ window.addEventListener('onbeforeload', removeIntervals);
+};
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 1ae144fb471..b849cc2d853 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -91,7 +91,7 @@
.award-menu-holder {
display: inline-block;
- position: relative;
+ position: absolute;
.tooltip {
white-space: nowrap;
@@ -117,11 +117,41 @@
&.active,
&:hover,
- &:active {
+ &:active,
+ &.is-active {
background-color: $row-hover;
border-color: $row-hover-border;
box-shadow: none;
outline: 0;
+
+ .award-control-icon svg {
+ background: $award-emoji-positive-add-bg;
+
+ path {
+ fill: $award-emoji-positive-add-lines;
+ }
+ }
+
+ .award-control-icon-neutral {
+ opacity: 0;
+ }
+
+ .award-control-icon-positive {
+ opacity: 1;
+ transform: scale(1.15);
+ }
+ }
+
+ &.is-active {
+ .award-control-icon-positive {
+ opacity: 0;
+ transform: scale(1);
+ }
+
+ .award-control-icon-super-positive {
+ opacity: 1;
+ transform: scale(1);
+ }
}
&.btn {
@@ -162,9 +192,33 @@
color: $border-gray-normal;
margin-top: 1px;
padding: 0 2px;
+
+ svg {
+ margin-bottom: 1px;
+ height: 18px;
+ width: 18px;
+ border-radius: 50%;
+
+ path {
+ fill: $border-gray-normal;
+ }
+ }
+ }
+
+ .award-control-icon-positive,
+ .award-control-icon-super-positive {
+ position: absolute;
+ left: 7px;
+ bottom: 9px;
+ opacity: 0;
+ @include transition(opacity, transform);
}
.award-control-text {
vertical-align: middle;
}
}
+
+.note-awards .award-control-icon-positive {
+ left: 6px;
+}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index ffece53a093..ddea1cf540b 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -275,3 +275,9 @@ span.idiff {
}
}
}
+
+.is-stl-loading {
+ .stl-controls {
+ display: none;
+ }
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 97794a47df8..712eb7caf33 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -293,6 +293,8 @@ $badge-color: $gl-text-color-secondary;
* Award emoji
*/
$award-emoji-menu-shadow: rgba(0,0,0,.175);
+$award-emoji-positive-add-bg: #fed159;
+$award-emoji-positive-add-lines: #bb9c13;
/*
* Search Box
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 6faa3794c83..72e7d42858d 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -233,6 +233,15 @@
stroke-width: 1;
}
+.prometheus-state {
+ margin-top: 10px;
+ display: none;
+
+ .state-button-section {
+ margin-top: 10px;
+ }
+}
+
.environments-actions {
.external-url,
.monitoring-url,
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 57cf8e136e2..603ef461ffe 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -398,13 +398,50 @@ ul.notes {
font-size: 17px;
}
- &:hover {
+ svg {
+ height: 16px;
+ width: 16px;
+ fill: $gray-darkest;
+ vertical-align: text-top;
+ }
+
+ .award-control-icon-positive,
+ .award-control-icon-super-positive {
+ position: absolute;
+ margin-left: -20px;
+ opacity: 0;
+ }
+
+ &:hover,
+ &.is-active {
.danger-highlight {
color: $gl-text-red;
}
.link-highlight {
color: $gl-link-color;
+
+ svg {
+ fill: $gl-link-color;
+ }
+ }
+
+ .award-control-icon-neutral {
+ opacity: 0;
+ }
+
+ .award-control-icon-positive {
+ opacity: 1;
+ }
+ }
+
+ &.is-active {
+ .award-control-icon-positive {
+ opacity: 0;
+ }
+
+ .award-control-icon-super-positive {
+ opacity: 1;
}
}
}
@@ -508,7 +545,6 @@ ul.notes {
}
.line-resolve-all-container {
-
.btn-group {
margin-left: -4px;
}
@@ -537,7 +573,6 @@ ul.notes {
fill: $gray-darkest;
}
}
-
}
.line-resolve-all {
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index daecfc832bf..a1975c0e341 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -3,6 +3,7 @@ class Admin::ProjectsController < Admin::ApplicationController
before_action :group, only: [:show, :transfer]
def index
+ params[:sort] ||= 'latest_activity_desc'
@projects = Project.with_statistics
@projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present?
@projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
diff --git a/app/controllers/concerns/continue_params.rb b/app/controllers/concerns/continue_params.rb
index 0a995c45bdf..eb3a623acdd 100644
--- a/app/controllers/concerns/continue_params.rb
+++ b/app/controllers/concerns/continue_params.rb
@@ -7,6 +7,7 @@ module ContinueParams
continue_params = continue_params.permit(:to, :notice, :notice_now)
return unless continue_params[:to] && continue_params[:to].start_with?('/')
+ return if continue_params[:to].start_with?('//')
continue_params
end
diff --git a/app/controllers/concerns/filter_projects.rb b/app/controllers/concerns/filter_projects.rb
deleted file mode 100644
index 6014112256a..00000000000
--- a/app/controllers/concerns/filter_projects.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# == FilterProjects
-#
-# Controller concern to handle projects filtering
-# * by name
-# * by archived state
-#
-module FilterProjects
- extend ActiveSupport::Concern
-
- def filter_projects(projects)
- projects = projects.search(params[:name]) if params[:name].present?
- projects = projects.non_archived if params[:archived].blank?
- projects = projects.personal(current_user) if params[:personal].present? && current_user
-
- projects
- end
-end
diff --git a/app/controllers/concerns/params_backward_compatibility.rb b/app/controllers/concerns/params_backward_compatibility.rb
new file mode 100644
index 00000000000..b0e3d9c7b34
--- /dev/null
+++ b/app/controllers/concerns/params_backward_compatibility.rb
@@ -0,0 +1,7 @@
+module ParamsBackwardCompatibility
+ private
+
+ def set_non_archived_param
+ params[:non_archived] = params[:archived].blank?
+ end
+end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index be00d765f73..5a1efcab1a3 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -1,10 +1,11 @@
class Dashboard::ProjectsController < Dashboard::ApplicationController
- include FilterProjects
+ include ParamsBackwardCompatibility
+
+ before_action :set_non_archived_param
+ before_action :default_sorting
def index
- @projects = load_projects(current_user.authorized_projects)
- @projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.page(params[:page])
+ @projects = load_projects(params.merge(non_public: true)).page(params[:page])
respond_to do |format|
format.html { @last_push = current_user.recent_push }
@@ -21,10 +22,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
def starred
- @projects = load_projects(current_user.viewable_starred_projects)
- @projects = @projects.includes(:forked_from_project, :tags)
- @projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.page(params[:page])
+ @projects = load_projects(params.merge(starred: true)).
+ includes(:forked_from_project, :tags).page(params[:page])
@last_push = current_user.recent_push
@groups = []
@@ -41,14 +40,18 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
private
- def load_projects(base_scope)
- projects = base_scope.sorted_by_activity.includes(:route, namespace: :route)
+ def default_sorting
+ params[:sort] ||= 'latest_activity_desc'
+ @sort = params[:sort]
+ end
- filter_projects(projects)
+ def load_projects(finder_params)
+ ProjectsFinder.new(params: finder_params, current_user: current_user).
+ execute.includes(:route, namespace: :route)
end
def load_events
- @events = Event.in_projects(load_projects(current_user.authorized_projects))
+ @events = Event.in_projects(load_projects(params.merge(non_public: true)))
@events = event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 498690e8f11..4d7d45787fc 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -7,7 +7,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
@sort = params[:sort]
@todos = @todos.page(params[:page])
if @todos.out_of_range? && @todos.total_pages != 0
- redirect_to url_for(params.merge(page: @todos.total_pages))
+ redirect_to url_for(params.merge(page: @todos.total_pages, only_path: true))
end
end
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 6167f9bd335..8f1870759e4 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -1,14 +1,12 @@
class Explore::ProjectsController < Explore::ApplicationController
- include FilterProjects
+ include ParamsBackwardCompatibility
+
+ before_action :set_non_archived_param
def index
- @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?
- @projects = filter_projects(@projects)
- @projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.includes(:namespace).page(params[:page])
+ params[:sort] ||= 'latest_activity_desc'
+ @sort = params[:sort]
+ @projects = load_projects.page(params[:page])
respond_to do |format|
format.html
@@ -21,10 +19,9 @@ class Explore::ProjectsController < Explore::ApplicationController
end
def trending
- @projects = load_projects(Project.trending)
- @projects = filter_projects(@projects)
- @projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.page(params[:page])
+ params[:trending] = true
+ @sort = params[:sort]
+ @projects = load_projects.page(params[:page])
respond_to do |format|
format.html
@@ -37,10 +34,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end
def starred
- @projects = load_projects
- @projects = filter_projects(@projects)
- @projects = @projects.reorder('star_count DESC')
- @projects = @projects.page(params[:page])
+ @projects = load_projects.reorder('star_count DESC').page(params[:page])
respond_to do |format|
format.html
@@ -52,10 +46,10 @@ class Explore::ProjectsController < Explore::ApplicationController
end
end
- protected
+ private
- def load_projects(base_scope = nil)
- base_scope ||= ProjectsFinder.new.execute(current_user)
- base_scope.includes(:route, namespace: :route)
+ def load_projects
+ ProjectsFinder.new(current_user: current_user, params: params).
+ execute.includes(:route, namespace: :route)
end
end
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index 8b69c18d689..29ffaeb19c1 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -27,7 +27,7 @@ class Groups::ApplicationController < ApplicationController
end
def group_projects
- @projects ||= GroupProjectsFinder.new(group).execute(current_user)
+ @projects ||= GroupProjectsFinder.new(group: group, current_user: current_user).execute
end
def authorize_admin_group!
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 05f9ee1ee90..78c9f1f7004 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -1,7 +1,7 @@
class GroupsController < Groups::ApplicationController
- include FilterProjects
include IssuesAction
include MergeRequestsAction
+ include ParamsBackwardCompatibility
respond_to :html
@@ -105,15 +105,16 @@ class GroupsController < Groups::ApplicationController
protected
def setup_projects
+ set_non_archived_param
+ params[:sort] ||= 'latest_activity_desc'
+ @sort = params[:sort]
+
options = {}
options[:only_owned] = true if params[:shared] == '0'
options[:only_shared] = true if params[:shared] == '1'
- @projects = GroupProjectsFinder.new(group, options).execute(current_user)
+ @projects = GroupProjectsFinder.new(params: params, group: group, options: options, current_user: current_user).execute
@projects = @projects.includes(:namespace)
- @projects = @projects.sorted_by_activity
- @projects = filter_projects(@projects)
- @projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page]) if params[:name].blank?
end
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index ba46e2528e6..1eb3800e49d 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -9,7 +9,7 @@ class Projects::ForksController < Projects::ApplicationController
def index
base_query = project.forks.includes(:creator)
- @forks = base_query.merge(ProjectsFinder.new.execute(current_user))
+ @forks = base_query.merge(ProjectsFinder.new(current_user: current_user).execute)
@total_forks_count = base_query.size
@private_forks_count = @total_forks_count - @forks.size
@public_forks_count = @total_forks_count - @private_forks_count
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index d984e6d3918..a50e16fa4ff 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -11,10 +11,10 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
- :related_branches, :can_create_branch]
+ :related_branches, :can_create_branch, :rendered_title]
# Allow read any issue
- before_action :authorize_read_issue!, only: [:show]
+ before_action :authorize_read_issue!, only: [:show, :rendered_title]
# Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create]
@@ -31,7 +31,7 @@ class Projects::IssuesController < Projects::ApplicationController
@issuable_meta_data = issuable_meta_data(@issues, @collection_type)
if @issues.out_of_range? && @issues.total_pages != 0
- return redirect_to url_for(params.merge(page: @issues.total_pages))
+ return redirect_to url_for(params.merge(page: @issues.total_pages, only_path: true))
end
if params[:label_name].present?
@@ -200,6 +200,11 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
+ def rendered_title
+ Gitlab::PollingInterval.set_header(response, interval: 3_000)
+ render json: { title: view_context.markdown_field(@issue, :title) }
+ end
+
protected
def issue
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 37e3ac05916..a79d801991a 100755
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -43,7 +43,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
- return redirect_to url_for(params.merge(page: @merge_requests.total_pages))
+ return redirect_to url_for(params.merge(page: @merge_requests.total_pages, only_path: true))
end
if params[:label_name].present?
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 2683614d2e8..a452bbba422 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -140,6 +140,6 @@ class UsersController < ApplicationController
end
def projects_for_current_user
- ProjectsFinder.new.execute(current_user)
+ ProjectsFinder.new(current_user: current_user).execute
end
end
diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb
index 3b9a421b118..f043c38c6f9 100644
--- a/app/finders/group_projects_finder.rb
+++ b/app/finders/group_projects_finder.rb
@@ -1,42 +1,63 @@
-class GroupProjectsFinder < UnionFinder
- def initialize(group, options = {})
+# GroupProjectsFinder
+#
+# Used to filter Projects by set of params
+#
+# Arguments:
+# current_user - which user use
+# project_ids_relation: int[] - project ids to use
+# group
+# options:
+# only_owned: boolean
+# only_shared: boolean
+# params:
+# sort: string
+# visibility_level: int
+# tags: string[]
+# personal: boolean
+# search: string
+# non_archived: boolean
+#
+class GroupProjectsFinder < ProjectsFinder
+ attr_reader :group, :options
+
+ def initialize(group:, params: {}, options: {}, current_user: nil, project_ids_relation: nil)
+ super(params: params, current_user: current_user, project_ids_relation: project_ids_relation)
@group = group
@options = options
end
- def execute(current_user = nil)
- segments = group_projects(current_user)
- find_union(segments, Project)
- end
-
private
- def group_projects(current_user)
- only_owned = @options.fetch(:only_owned, false)
- only_shared = @options.fetch(:only_shared, false)
+ def init_collection
+ only_owned = options.fetch(:only_owned, false)
+ only_shared = options.fetch(:only_shared, false)
projects = []
if current_user
- if @group.users.include?(current_user)
- projects << @group.projects unless only_shared
- projects << @group.shared_projects unless only_owned
+ if group.users.include?(current_user)
+ projects << group.projects unless only_shared
+ projects << group.shared_projects unless only_owned
else
unless only_shared
- projects << @group.projects.visible_to_user(current_user)
- projects << @group.projects.public_to_user(current_user)
+ projects << group.projects.visible_to_user(current_user)
+ projects << group.projects.public_to_user(current_user)
end
unless only_owned
- projects << @group.shared_projects.visible_to_user(current_user)
- projects << @group.shared_projects.public_to_user(current_user)
+ projects << group.shared_projects.visible_to_user(current_user)
+ projects << group.shared_projects.public_to_user(current_user)
end
end
else
- projects << @group.projects.public_only unless only_shared
- projects << @group.shared_projects.public_only unless only_owned
+ projects << group.projects.public_only unless only_shared
+ projects << group.shared_projects.public_only unless only_owned
end
projects
end
+
+ def union(items)
+ find_union(items, Project)
+ end
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index f7ebb1807d7..4cc42b88a2a 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -116,9 +116,9 @@ class IssuableFinder
if current_user && params[:authorized_only].presence && !current_user_related?
current_user.authorized_projects
elsif group
- GroupProjectsFinder.new(group).execute(current_user)
+ GroupProjectsFinder.new(group: group, current_user: current_user).execute
else
- projects_finder.execute(current_user, item_project_ids(items))
+ ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids(items)).execute
end
@projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
@@ -405,8 +405,4 @@ class IssuableFinder
def current_user_related?
params[:scope] == 'created-by-me' || params[:scope] == 'authored' || params[:scope] == 'assigned-to-me'
end
-
- def projects_finder
- @projects_finder ||= ProjectsFinder.new
- end
end
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index e52083f86e4..042d792dada 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -83,7 +83,7 @@ class LabelsFinder < UnionFinder
def projects
return @projects if defined?(@projects)
- @projects = skip_authorization ? Project.all : ProjectsFinder.new.execute(current_user)
+ @projects = skip_authorization ? Project.all : ProjectsFinder.new(current_user: current_user).execute
@projects = @projects.in_namespace(params[:group_id]) if group?
@projects = @projects.where(id: params[:project_ids]) if projects?
@projects = @projects.reorder(nil)
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 18ec45f300d..f6d8226bf3f 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -1,19 +1,93 @@
+# ProjectsFinder
+#
+# Used to filter Projects by set of params
+#
+# Arguments:
+# current_user - which user use
+# project_ids_relation: int[] - project ids to use
+# params:
+# trending: boolean
+# non_public: boolean
+# starred: boolean
+# sort: string
+# visibility_level: int
+# tags: string[]
+# personal: boolean
+# search: string
+# non_archived: boolean
+#
class ProjectsFinder < UnionFinder
- def execute(current_user = nil, project_ids_relation = nil)
- segments = all_projects(current_user)
- segments.map! { |s| s.where(id: project_ids_relation) } if project_ids_relation
+ attr_accessor :params
+ attr_reader :current_user, :project_ids_relation
- find_union(segments, Project).with_route
+ def initialize(params: {}, current_user: nil, project_ids_relation: nil)
+ @params = params
+ @current_user = current_user
+ @project_ids_relation = project_ids_relation
+ end
+
+ def execute
+ items = init_collection
+ items = by_ids(items)
+ items = union(items)
+ items = by_personal(items)
+ items = by_visibilty_level(items)
+ items = by_tags(items)
+ items = by_search(items)
+ items = by_archived(items)
+ sort(items)
end
private
- def all_projects(current_user)
+ def init_collection
projects = []
- projects << current_user.authorized_projects if current_user
- projects << Project.unscoped.public_to_user(current_user)
+ if params[:trending].present?
+ projects << Project.trending
+ elsif params[:starred].present? && current_user
+ projects << current_user.viewable_starred_projects
+ else
+ projects << current_user.authorized_projects if current_user
+ projects << Project.unscoped.public_to_user(current_user) unless params[:non_public].present?
+ end
projects
end
+
+ def by_ids(items)
+ project_ids_relation ? items.map { |item| item.where(id: project_ids_relation) } : items
+ end
+
+ def union(items)
+ find_union(items, Project).with_route
+ end
+
+ def by_personal(items)
+ (params[:personal].present? && current_user) ? items.personal(current_user) : items
+ end
+
+ def by_visibilty_level(items)
+ params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items
+ end
+
+ def by_tags(items)
+ params[:tag].present? ? items.tagged_with(params[:tag]) : items
+ end
+
+ def by_search(items)
+ params[:search] ||= params[:name]
+ params[:search].present? ? items.search(params[:search]) : items
+ end
+
+ def sort(items)
+ params[:sort].present? ? items.sort(params[:sort]) : items
+ end
+
+ def by_archived(projects)
+ # Back-compatibility with the places where `params[:archived]` can be set explicitly to `false`
+ params[:non_archived] = !Gitlab::Utils.to_boolean(params[:archived]) if params.key?(:archived)
+
+ params[:non_archived] ? projects.non_archived : projects
+ end
end
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index b7f091f334d..dc13386184e 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -95,7 +95,7 @@ class TodosFinder
def projects(items)
item_project_ids = items.reorder(nil).select(:project_id)
- ProjectsFinder.new.execute(current_user, item_project_ids)
+ ProjectsFinder.new(current_user: current_user, project_ids_relation: item_project_ids).execute
end
def type?
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index bd0c2cd661e..6b9e4267281 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -407,7 +407,10 @@ module ProjectsHelper
def sanitize_repo_path(project, message)
return '' unless message.present?
- message.strip.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]")
+ exports_path = File.join(Settings.shared['path'], 'tmp/project_exports')
+ filtered_message = message.strip.gsub(exports_path, "[REPO EXPORT PATH]")
+
+ filtered_message.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]")
end
def project_feature_options
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 5c89cbea3fc..3a5d1b97c36 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -25,8 +25,8 @@ module SortingHelper
def projects_sort_options_hash
options = {
sort_value_name => sort_title_name,
- sort_value_recently_updated => sort_title_recently_updated,
- sort_value_oldest_updated => sort_title_oldest_updated,
+ sort_value_latest_activity => sort_title_latest_activity,
+ sort_value_oldest_activity => sort_title_oldest_activity,
sort_value_recently_created => sort_title_recently_created,
sort_value_oldest_created => sort_title_oldest_created
}
@@ -78,6 +78,14 @@ module SortingHelper
'Last updated'
end
+ def sort_title_oldest_activity
+ 'Oldest updated'
+ end
+
+ def sort_title_latest_activity
+ 'Last updated'
+ end
+
def sort_title_oldest_created
'Oldest created'
end
@@ -198,6 +206,14 @@ module SortingHelper
'updated_desc'
end
+ def sort_value_oldest_activity
+ 'latest_activity_asc'
+ end
+
+ def sort_value_latest_activity
+ 'latest_activity_desc'
+ end
+
def sort_value_oldest_created
'created_asc'
end
diff --git a/app/models/blob.rb b/app/models/blob.rb
index f82126f8e65..801d3442803 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -58,6 +58,10 @@ class Blob < SimpleDelegator
binary? && extname.downcase.delete('.') == 'sketch'
end
+ def stl?
+ extname.downcase.delete('.') == 'stl'
+ end
+
def size_within_svg_limits?
size <= MAXIMUM_SVG_SIZE
end
@@ -81,6 +85,8 @@ class Blob < SimpleDelegator
'notebook'
elsif sketch?
'sketch'
+ elsif stl?
+ 'stl'
elsif text?
'text'
else
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 472796df9df..f9704b0d754 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -40,6 +40,8 @@ class Issue < ActiveRecord::Base
scope :include_associations, -> { includes(:assignee, :labels, project: :namespace) }
+ after_save :expire_etag_cache
+
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
@@ -252,4 +254,13 @@ class Issue < ActiveRecord::Base
def publicly_visible?
project.public? && !confidential?
end
+
+ def expire_etag_cache
+ key = Gitlab::Routing.url_helpers.rendered_title_namespace_project_issue_path(
+ project.namespace,
+ project,
+ self
+ )
+ Gitlab::EtagCaching::Store.new.touch(key)
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 12fd0668ff8..1f95d00baf8 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -349,10 +349,15 @@ class Project < ActiveRecord::Base
end
def sort(method)
- if method == 'storage_size_desc'
+ case method.to_s
+ when 'storage_size_desc'
# storage_size is a joined column so we need to
# pass a string to avoid AR adding the table name
reorder('project_statistics.storage_size DESC, projects.id DESC')
+ when 'latest_activity_desc'
+ reorder(last_activity_at: :desc)
+ when 'latest_activity_asc'
+ reorder(last_activity_at: :asc)
else
order_by(method)
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 4cc21696eb6..cb58c115d54 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -12,7 +12,7 @@ class GroupPolicy < BasePolicy
can_read ||= globally_viewable
can_read ||= member
can_read ||= @user.admin?
- can_read ||= GroupProjectsFinder.new(@subject).execute(@user).any?
+ can_read ||= GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
can! :read_group if can_read
# Only group masters and group owners can create new projects
@@ -41,6 +41,6 @@ class GroupPolicy < BasePolicy
return true if @subject.internal? && !@user.external?
return true if @subject.users.include?(@user)
- GroupProjectsFinder.new(@subject).execute(@user).any?
+ GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
end
end
diff --git a/app/serializers/build_action_entity.rb b/app/serializers/build_action_entity.rb
index 184f5fd4b52..184b4b7a681 100644
--- a/app/serializers/build_action_entity.rb
+++ b/app/serializers/build_action_entity.rb
@@ -11,4 +11,6 @@ class BuildActionEntity < Grape::Entity
build.project,
build)
end
+
+ expose :playable?, as: :playable
end
diff --git a/app/serializers/build_entity.rb b/app/serializers/build_entity.rb
index fadd6c5c597..b804d6d0e8a 100644
--- a/app/serializers/build_entity.rb
+++ b/app/serializers/build_entity.rb
@@ -16,6 +16,7 @@ class BuildEntity < Grape::Entity
path_to(:play_namespace_project_build, build)
end
+ expose :playable?, as: :playable
expose :created_at
expose :updated_at
expose :detailed_status, as: :status, with: StatusEntity
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 5a53b973059..582d5c47b66 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -39,7 +39,7 @@ module MergeRequests
private
# Returns all origin and fork merge requests from `@project` satisfying passed arguments.
- def merge_requests_for(source_branch, mr_states: [:opened])
+ def merge_requests_for(source_branch, mr_states: [:opened, :reopened])
MergeRequest
.with_state(mr_states)
.where(source_branch: source_branch, source_project_id: @project.id)
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index fdce542bd9e..d45da5180e1 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -21,7 +21,9 @@ module MergeRequests
delegate :target_branch, :source_branch, :source_project, :target_project, :compare_commits, :wip_title, :description, :errors, to: :merge_request
def find_source_project
- source_project || project
+ return source_project if source_project.present? && can?(current_user, :read_project, source_project)
+
+ project
end
def find_target_project
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index c1549df5ac6..8409b592b72 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -8,7 +8,7 @@ module Search
def execute
group = Group.find_by(id: params[:group_id]) if params[:group_id].present?
- projects = ProjectsFinder.new.execute(current_user)
+ projects = ProjectsFinder.new(current_user: current_user).execute
if group
projects = projects.inside_path(group.full_path)
diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml
index 5aae410a63f..3ca45fbf751 100644
--- a/app/views/award_emoji/_awards_block.html.haml
+++ b/app/views/award_emoji/_awards_block.html.haml
@@ -13,5 +13,7 @@
%button.btn.award-control.has-tooltip.js-add-award{ type: 'button',
'aria-label': 'Add emoji',
data: { title: 'Add emoji', placement: "bottom" } }
- = icon('smile-o', class: "award-control-icon award-control-icon-normal")
+ %span{ class: "award-control-icon award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face')
+ %span{ class: "award-control-icon award-control-icon-positive" }= custom_icon('emoji_smiley')
+ %span{ class: "award-control-icon award-control-icon-super-positive" }= custom_icon('emoji_smile')
= icon('spinner spin', class: "award-control-icon award-control-icon-loading")
diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index 1bbd4602ecf..8843d4e7c84 100644
--- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
@@ -1,4 +1,4 @@
-- publicish_project_count = ProjectsFinder.new.execute(current_user).count
+- publicish_project_count = ProjectsFinder.new(current_user: current_user).execute.count
.blank-state.blank-state-welcome
%h2.blank-state-welcome-title
Welcome to GitLab
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index f6d8bb08a64..a611481a0a4 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -23,7 +23,7 @@
%title= page_title(site_name)
%meta{ name: "description", content: page_description }
- = favicon_link_tag favicon
+ = favicon_link_tag favicon, id: 'favicon'
= stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print"
diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml
index 44b40ca1ac5..a83faa839df 100644
--- a/app/views/notify/pipeline_failed_email.html.haml
+++ b/app/views/notify/pipeline_failed_email.html.haml
@@ -3,8 +3,8 @@
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
%tbody
%tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;" }
- %img{ alt: "x", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;padding-right:5px;line-height:1;" }
+ %img{ alt: "✖", height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red-inverted.gif'), style: "display:block;", width: "13" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;vertical-align:middle;color:#ffffff;text-align:center;" }
Your pipeline has failed.
%tr.spacer
@@ -16,7 +16,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;" }
- namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
- namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
%a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
@@ -26,7 +26,7 @@
= @project.name
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
@@ -37,7 +37,7 @@
= @pipeline.ref
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:400;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
@@ -52,13 +52,13 @@
= @merge_request.to_reference
.commit{ style: "color:#5c5c5c;font-weight:300;" }
= @pipeline.git_commit_message.truncate(50)
+ - commit = @pipeline.commit
%tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit Author
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
- - commit = @pipeline.commit
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
@@ -68,15 +68,48 @@
- else
%span
= commit.author_name
+ - if commit.different_committer?
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Committed by
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ - if commit.committer
+ %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
+ = commit.committer.name
+ - else
+ %span
+ = commit.committer_name
+
%tr.spacer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp;
-- failed = @pipeline.statuses.latest.failed
%tr.pre-section
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 0;" }
- Pipeline
- %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
- = "\##{@pipeline.id}"
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px 0 5px;text-align:center;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
+ Pipeline
+ %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
+ = "\##{@pipeline.id}"
+ triggered by
+ - if @pipeline.user
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" }
+ %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
+ %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
+ = @pipeline.user.name
+ - else
+ %td{ style: "font-family:'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace;font-size:14px;line-height:1.4;vertical-align:baseline;padding:0 5px;" }
+ API
+- failed = @pipeline.statuses.latest.failed
+%tr
+ %td{ colspan: 2, style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:300;line-height:1.4;padding:15px 5px;text-align:center;" }
had
= failed.size
failed
@@ -94,8 +127,8 @@
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;" }
- %img{ alt: "x", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#d22f57;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;line-height:10px" }
+ %img{ alt: "✖", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" }
= build.stage
%td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" }
diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb
index b973acb1906..294238eee51 100644
--- a/app/views/notify/pipeline_failed_email.text.erb
+++ b/app/views/notify/pipeline_failed_email.text.erb
@@ -14,9 +14,21 @@ Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> )
<% else -%>
Commit Author: <%= commit.author_name %>
<% end -%>
+<% if commit.different_committer? -%>
+<% if commit.committer -%>
+Committed by: <%= commit.committer.name %> ( <%= user_url(commit.committer) %> )
+<% else -%>
+Committed by: <%= commit.committer_name %>
+<% end -%>
+<% end -%>
+<% if @pipeline.user -%>
+Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> )
+<% else -%>
+Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
+<% end -%>
<% failed = @pipeline.statuses.latest.failed -%>
-Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
+had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
<% failed.each do |build| -%>
<%= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %>
diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml
index 19d4add06f5..9c2e2a599b2 100644
--- a/app/views/notify/pipeline_success_email.html.haml
+++ b/app/views/notify/pipeline_success_email.html.haml
@@ -16,7 +16,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;" } Project
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;" }
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;" }
- namespace_name = @project.group ? @project.group.name : @project.namespace.owner.name
- namespace_url = @project.group ? group_url(@project.group) : user_url(@project.namespace.owner)
%a.muted{ href: namespace_url, style: "color:#333333;text-decoration:none;" }
@@ -26,7 +26,7 @@
= @project.name
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Branch
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
@@ -37,7 +37,7 @@
= @pipeline.ref
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:400;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
@@ -52,13 +52,13 @@
= @merge_request.to_reference
.commit{ style: "color:#5c5c5c;font-weight:300;" }
= @pipeline.git_commit_message.truncate(50)
+ - commit = @pipeline.commit
%tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Author
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Commit Author
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
%table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
%tbody
%tr
- - commit = @pipeline.commit
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
@@ -68,17 +68,50 @@
- else
%span
= commit.author_name
+ - if commit.different_committer?
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Committed by
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:500;padding:14px 0;margin:0;color:#333333;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
+ %img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
+ - if commit.committer
+ %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
+ = commit.committer.name
+ - else
+ %span
+ = commit.committer_name
+
%tr.spacer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;height:18px;font-size:18px;line-height:18px;" }
&nbsp;
%tr.success-message
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px;text-align:center;" }
- - build_count = @pipeline.statuses.latest.size
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:400;line-height:1.4;padding:15px 5px 0 5px;text-align:center;" }
+ %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" }
+ %tbody
+ %tr
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
+ Pipeline
+ %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
+ = "\##{@pipeline.id}"
+ triggered by
+ - if @pipeline.user
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" }
+ %img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
+ %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
+ = @pipeline.user.name
+ - else
+ %td{ style: "font-family:'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace;font-size:14px;line-height:1.4;vertical-align:baseline;padding:0 5px;" }
+ API
+%tr
+ %td{ colspan: 2, style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:300;line-height:1.4;padding:15px 5px;text-align:center;" }
+ - job_count = @pipeline.statuses.latest.size
- stage_count = @pipeline.stages_count
- Pipeline
- %a{ href: pipeline_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
- = "\##{@pipeline.id}"
successfully completed
- #{build_count} #{'build'.pluralize(build_count)}
+ #{job_count} #{'job'.pluralize(job_count)}
in
#{stage_count} #{'stage'.pluralize(stage_count)}.
diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb
index 0970a3a4e09..ddced2279e1 100644
--- a/app/views/notify/pipeline_success_email.text.erb
+++ b/app/views/notify/pipeline_success_email.text.erb
@@ -14,7 +14,19 @@ Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> )
<% else -%>
Commit Author: <%= commit.author_name %>
<% end -%>
+<% if commit.different_committer? -%>
+<% if commit.committer -%>
+Committed by: <%= commit.committer.name %> ( <%= user_url(commit.committer) %> )
+<% else -%>
+Committed by: <%= commit.committer_name %>
+<% end -%>
+<% end -%>
<% build_count = @pipeline.statuses.latest.size -%>
<% stage_count = @pipeline.stages_count -%>
-Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
+<% if @pipeline.user -%>
+Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> )
+<% else -%>
+Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
+<% end -%>
+successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>.
diff --git a/app/views/projects/blob/_stl.html.haml b/app/views/projects/blob/_stl.html.haml
new file mode 100644
index 00000000000..a9332a0eeb6
--- /dev/null
+++ b/app/views/projects/blob/_stl.html.haml
@@ -0,0 +1,12 @@
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('stl_viewer')
+
+.file-content.is-stl-loading
+ .text-center#js-stl-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
+ = icon('spinner spin 2x', class: 'prepend-top-default append-bottom-default', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
+ .text-center.prepend-top-default.append-bottom-default.stl-controls
+ .btn-group
+ %button.btn.btn-default.btn-sm.js-material-changer{ data: { type: 'wireframe' } }
+ Wireframe
+ %button.btn.btn-default.btn-sm.active.js-material-changer{ data: { type: 'default' } }
+ Solid
diff --git a/app/views/projects/environments/_metrics_button.html.haml b/app/views/projects/environments/_metrics_button.html.haml
index e27281d6917..b4102fcf103 100644
--- a/app/views/projects/environments/_metrics_button.html.haml
+++ b/app/views/projects/environments/_metrics_button.html.haml
@@ -1,6 +1,6 @@
- environment = local_assigns.fetch(:environment)
-- return unless environment.has_metrics? && can?(current_user, :read_environment, environment)
+- return unless can?(current_user, :read_environment, environment)
= link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do
= icon('area-chart')
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index 92dc58cd38d..2e54af698aa 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -5,7 +5,7 @@
= page_specific_javascript_bundle_tag('monitoring')
= render "projects/pipelines/head"
-%div{ class: container_class }
+.prometheus-container{ class: container_class, 'data-has-metrics': "#{@environment.has_metrics?}" }
.top-area
.row
.col-sm-6
@@ -16,13 +16,68 @@
.col-sm-6
.nav-controls
= render 'projects/deployments/actions', deployment: @environment.last_deployment
- .row
- .col-sm-12
- %h4
- CPU utilization
- %svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
- .row
- .col-sm-12
- %h4
- Memory usage
- %svg.prometheus-graph{ 'graph-type' => 'memory_values' }
+ .prometheus-state
+ .js-getting-started.hidden
+ .row
+ .col-md-4.col-md-offset-4.state-svg
+ = render "shared/empty_states/monitoring/getting_started.svg"
+ .row
+ .col-md-6.col-md-offset-3
+ %h4.text-center.state-title
+ Get started with performance monitoring
+ .row
+ .col-md-6.col-md-offset-3
+ .description-text.text-center.state-description
+ Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments.
+ = link_to help_page_path('administration/monitoring/prometheus/index.md') do
+ Learn more about performance monitoring
+ .row.state-button-section
+ .col-md-4.col-md-offset-4.text-center.state-button
+ = link_to edit_namespace_project_service_path(@project.namespace, @project, 'prometheus'), class: 'btn btn-success' do
+ Configure Prometheus
+ .js-loading.hidden
+ .row
+ .col-md-4.col-md-offset-4.state-svg
+ = render "shared/empty_states/monitoring/loading.svg"
+ .row
+ .col-md-6.col-md-offset-3
+ %h4.text-center.state-title
+ Waiting for performance data
+ .row
+ .col-md-6.col-md-offset-3
+ .description-text.text-center.state-description
+ Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.
+ .row.state-button-section
+ .col-md-4.col-md-offset-4.text-center.state-button
+ = link_to help_page_path('administration/monitoring/prometheus/index.md'), class: 'btn btn-success' do
+ View documentation
+ .js-unable-to-connect.hidden
+ .row
+ .col-md-4.col-md-offset-4.state-svg
+ = render "shared/empty_states/monitoring/unable_to_connect.svg"
+ .row
+ .col-md-6.col-md-offset-3
+ %h4.text-center.state-title
+ Unable to connect to Prometheus server
+ .row
+ .col-md-6.col-md-offset-3
+ .description-text.text-center.state-description
+ Ensure connectivity is available from the GitLab server to the
+ = link_to edit_namespace_project_service_path(@project.namespace, @project, 'prometheus') do
+ Prometheus server
+ .row.state-button-section
+ .col-md-4.col-md-offset-4.text-center.state-button
+ = link_to help_page_path('administration/monitoring/prometheus/index.md'), class:'btn btn-success' do
+ View documentation
+
+ .prometheus-graphs
+ .row
+ .col-sm-12
+ %h4
+ CPU utilization
+ %svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
+ .row
+ .col-sm-12
+ %h4
+ Memory usage
+ %svg.prometheus-graph{ 'graph-type' => 'memory_values' }
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 6ac05bf3afe..885795ccb5c 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -49,11 +49,12 @@
= 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? ) }
- %h2.title
- = markdown_field(@issue, :title)
+ .issue-title-data.hidden{ "data" => { "initial-title" => markdown_field(@issue, :title),
+ "endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue),
+ } }
+ .issue-title-entrypoint
- if @issue.description.present?
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
.wiki
@@ -77,3 +78,5 @@
= render 'projects/issues/discussion'
= render 'shared/issuable/sidebar', issuable: @issue
+
+= page_specific_javascript_bundle_tag('issue_show')
diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml
index 0b0fb7854c2..c716b69b35b 100644
--- a/app/views/projects/merge_requests/widget/_show.html.haml
+++ b/app/views/projects/merge_requests/widget/_show.html.haml
@@ -12,6 +12,7 @@
merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
check_enable: #{@merge_request.unchecked? ? "true" : "false"},
ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
+ pipeline_status_url: "#{pipeline_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
ci_environments_status_url: "#{ci_environments_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
gitlab_icon: "#{asset_path 'gitlab_logo.png'}",
ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}",
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 6c0e6d48d6c..18afa811bad 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -59,7 +59,9 @@
- if note.emoji_awardable?
= link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do
= icon('spinner spin')
- = icon('smile-o', class: 'link-highlight')
+ %span{ class: "link-highlight award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face')
+ %span{ class: "link-highlight award-control-icon-positive" }= custom_icon('emoji_smiley')
+ %span{ class: "link-highlight award-control-icon-super-positive" }= custom_icon('emoji_smile')
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
diff --git a/app/views/shared/empty_states/monitoring/_getting_started.svg b/app/views/shared/empty_states/monitoring/_getting_started.svg
new file mode 100644
index 00000000000..db7a1c2e708
--- /dev/null
+++ b/app/views/shared/empty_states/monitoring/_getting_started.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="0" width="159.8" height="127.81" x=".196" y="5" rx="10"/><rect id="2" width="160" height="128" x=".666" y=".41" rx="10"/><rect id="4" width="160.19" height="128.19" x=".339" y=".59" rx="10"/><mask id="1" width="159.8" height="127.81" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="3" width="160" height="128" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask><mask id="5" width="160.19" height="128.19" x="0" y="0" fill="#fff"><use xlink:href="#4"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(12 3)"><rect width="160" height="128" x="122.08" y="146.08" fill="#f9f9f9" transform="matrix(.99619.08716-.08716.99619 19.08-16.813)" rx="10"/><g transform="matrix(.96593.25882-.25882.96593 227.1 57.47)"><rect width="159.8" height="127.81" x="1.64" y="10.06" fill="#f9f9f9" rx="8"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#1)" xlink:href="#0"/><g transform="translate(24.368 36.951)"><path fill="#d2caea" fill-rule="nonzero" d="m71.785 44.2c.761.296 1.625.099 2.184-.496l35.956-38.34c.756-.806.715-2.071-.091-2.827-.806-.756-2.071-.715-2.827.091l-35.03 37.36-41.888-16.285c-.749-.291-1.6-.106-2.16.471l-26.368 27.16c-.769.793-.751 2.059.042 2.828.793.769 2.059.751 2.828-.042l25.444-26.21 41.911 16.294"/><g fill="#fff"><circle cx="5.716" cy="5.104" r="5" stroke="#6b4fbb" stroke-width="4" transform="translate(65.917 34.945)"/><g stroke="#fb722e"><ellipse cx="4.632" cy="50.05" stroke-width="3.2" rx="4" ry="3.999"/><g stroke-width="4"><ellipse cx="29.632" cy="27.05" rx="4" ry="3.999"/><ellipse cx="107.63" cy="4.048" rx="4" ry="3.999"/></g></g></g></g></g><rect width="160.19" height="128.19" x="36.28" y="86.74" fill="#f9f9f9" transform="matrix(.99619-.08716.08716.99619-12.703 10.717)" rx="10"/><g transform="matrix(.99619.08716-.08716.99619 126.61 137.8)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#3)" xlink:href="#2"/><path fill="#6b4fbb" stroke="#6b4fbb" stroke-width="3.2" d="m84.67 28.41c18.225 0 33 15.07 33 33.651h-33v-33.651" stroke-linecap="round" stroke-linejoin="round"/><path fill="#d2caea" fill-rule="nonzero" d="m78.67 66.41h30c1.105 0 2 .895 2 2 0 18.778-15.222 34-34 34-18.778 0-34-15.222-34-34 0-18.778 15.222-34 34-34 1.105 0 2 .895 2 2v30m-32 2c0 16.569 13.431 30 30 30 15.896 0 28.905-12.364 29.934-28h-29.934c-1.105 0-2-.895-2-2v-29.934c-15.636 1.029-28 14.04-28 29.934"/></g><g transform="matrix(.99619-.08716.08716.99619 30 88.03)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#5)" xlink:href="#4"/><g transform="translate(42 34)"><path fill="#fef0ea" d="m0 13.391c0-.768.628-1.391 1.4-1.391h9.2c.773 0 1.4.626 1.4 1.391v49.609h-12v-49.609"/><path fill="#fb722e" d="m66 21.406c0-.777.628-1.406 1.4-1.406h9.2c.773 0 1.4.624 1.4 1.406v41.594h-12v-41.594"/><path fill="#6b4fbb" d="m22 1.404c0-.776.628-1.404 1.4-1.404h9.2c.773 0 1.4.624 1.4 1.404v61.6h-12v-61.6"/><path fill="#d2caea" d="m44 39.4c0-.772.628-1.398 1.4-1.398h9.2c.773 0 1.4.618 1.4 1.398v23.602h-12v-23.602"/></g></g><g fill="#fee8dc"><path d="m6.226 94.95l-2.84.631c-1.075.239-1.752-.445-1.515-1.515l.631-2.84-.631-2.84c-.239-1.075.445-1.752 1.515-1.515l2.84.631 2.84-.631c1.075-.239 1.752.445 1.515 1.515l-.631 2.84.631 2.84c.239 1.075-.445 1.752-1.515 1.515l-2.84-.631" transform="matrix(.70711.70711-.70711.70711 66.33 22.317)"/><path d="m312.78 53.43l-3.634.807c-1.296.288-2.115-.52-1.825-1.825l.807-3.634-.807-3.634c-.288-1.296.52-2.115 1.825-1.825l3.634.807 3.634-.807c1.296-.288 2.115.52 1.825 1.825l-.807 3.634.807 3.634c.288 1.296-.52 2.115-1.825 1.825l-3.634-.807" transform="matrix(.70711.70711-.70711.70711 126.1-206.88)"/></g><path fill="#e1dcf1" d="m124.78 12.43l-3.617.804c-1.306.29-2.129-.53-1.839-1.839l.804-3.617-.804-3.617c-.29-1.306.53-2.129 1.839-1.839l3.617.804 3.617-.804c1.306-.29 2.129.53 1.839 1.839l-.804 3.617.804 3.617c.29 1.306-.53 2.129-1.839 1.839l-3.617-.804" transform="matrix(.70711-.70711.70711.70711 31.05 90.51)"/><path fill="#d2caea" d="m374.78 244.43l-3.617.804c-1.306.29-2.129-.53-1.839-1.839l.804-3.617-.804-3.617c-.29-1.306.53-2.129 1.839-1.839l3.617.804 3.617-.804c1.306-.29 2.129.53 1.839 1.839l-.804 3.617.804 3.617c.29 1.306-.53 2.129-1.839 1.839l-3.617-.804" transform="matrix(.70711-.70711.70711.70711-59.779 335.24)"/></g></svg> \ No newline at end of file
diff --git a/app/views/shared/empty_states/monitoring/_loading.svg b/app/views/shared/empty_states/monitoring/_loading.svg
new file mode 100644
index 00000000000..6bbd7a6c5b9
--- /dev/null
+++ b/app/views/shared/empty_states/monitoring/_loading.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="C" width="161" height="100" x="92" y="181" rx="10"/><rect id="E" width="151" height="32" x="20" rx="10"/><rect id="G" width="191" height="62" y="10" rx="10"/><circle id="I" cx="23" cy="41" r="9"/><circle id="4" cx="36.5" cy="36.5" r="36.5"/><circle id="8" cx="262.5" cy="169.5" r="15.5"/><circle id="A" cx="79.5" cy="169.5" r="15.5"/><circle id="K" cx="45" cy="41" r="9"/><circle id="0" cx="30.5" cy="30.5" r="30.5"/><circle id="2" cx="18" cy="34" r="3"/><ellipse id="6" cx="43.5" cy="43.5" rx="43.5" ry="43.5"/><mask id="H" width="191" height="62" x="0" y="0" fill="#fff"><use xlink:href="#G"/></mask><mask id="J" width="18" height="18" x="0" y="0" fill="#fff"><use xlink:href="#I"/></mask><mask id="D" width="161" height="100" x="0" y="0" fill="#fff"><use xlink:href="#C"/></mask><mask id="F" width="151" height="32" x="0" y="0" fill="#fff"><use xlink:href="#E"/></mask><mask id="9" width="31" height="31" x="0" y="0" fill="#fff"><use xlink:href="#8"/></mask><mask id="1" width="61" height="61" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="B" width="31" height="31" x="0" y="0" fill="#fff"><use xlink:href="#A"/></mask><mask id="3" width="6" height="6" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask><mask id="7" width="87" height="87" x="0" y="0" fill="#fff"><use xlink:href="#6"/></mask><mask id="L" width="18" height="18" x="0" y="0" fill="#fff"><use xlink:href="#K"/></mask><mask id="5" width="73" height="73" x="0" y="0" fill="#fff"><use xlink:href="#4"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(28 2)"><g transform="translate(133 87)"><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#1)" xlink:href="#0"/><path stroke="#d2caea" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" d="m19 32l2-9 5 17 4-12 4 5 6-10 3 5"/><g fill="#fff" stroke="#fb722e"><use stroke-width="4" mask="url(#3)" xlink:href="#2"/><circle cx="44" cy="30" r="2" stroke-width="2"/></g></g><g transform="translate(188 29)"><circle cx="36.5" cy="41.5" r="36.5" fill="#f9f9f9"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#5)" xlink:href="#4"/><rect width="27" height="4" x="23" y="27" fill="#d2caea" rx="2"/><rect width="10.5" height="4" x="23" y="27" fill="#6b4fbb" rx="2"/><rect width="27" height="4" x="23" y="36" fill="#d2caea" rx="2"/><rect width="19" height="4" x="23" y="36" fill="#6b4fbb" rx="2"/><rect width="27" height="4" x="23" y="45" fill="#d2caea" rx="2"/><rect width="7" height="4" x="23" y="45" fill="#6b4fbb" rx="2"/></g><path fill="#eee" fill-rule="nonzero" d="m247 292v1c0 5.519-4.469 9.993-10.01 9.993h-125.99c-5.177 0-9.436-3.927-9.954-8.96 1.348.998 2.957 1.666 4.705 1.883 1.027 1.835 2.992 3.077 5.248 3.077h125.99c2.485 0 4.611-1.497 5.526-3.637 1.796-.675 3.347-1.852 4.48-3.359m1.947-8.962c-.518 5.03-4.774 8.958-9.95 8.958h-131.99c-4.929 0-9.03-3.563-9.851-8.25 1.382.767 2.964 1.216 4.649 1.248 1.037 1.794 2.978 3 5.202 3h131.99c2.255 0 4.219-1.241 5.245-3.076 1.748-.216 3.356-.883 4.705-1.882"/><g transform="translate(79)"><ellipse cx="43.5" cy="47.5" fill="#f9f9f9" rx="43.5" ry="43.5"/><g fill="#fff"><g stroke="#eee"><use stroke-width="8" mask="url(#7)" xlink:href="#6"/><path stroke-width="4" d="m18.595 49c2.515 11.44 12.71 20 24.905 20 14.08 0 25.5-11.417 25.5-25.5 0-12.195-8.56-22.391-20-24.905v15.959c3 1.848 5 5.164 5 8.946 0 5.799-4.701 10.5-10.5 10.5-3.782 0-7.098-2-8.946-5h-15.959" stroke-linejoin="round"/></g><path stroke="#d2caea" stroke-width="4" d="m18 44c-.003-.166-.005-.333-.005-.5 0-14.08 11.417-25.5 25.5-25.5.167 0 .334.002.5.005v15.01c-.166-.008-.332-.012-.5-.012-5.799 0-10.5 4.701-10.5 10.5 0 .168.004.334.012.5h-15.01" stroke-linejoin="round"/></g></g><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#9)" xlink:href="#8"/><use mask="url(#B)" xlink:href="#A"/><use mask="url(#D)" xlink:href="#C"/></g><g fill="#eee"><rect width="15" height="2" x="226" y="247" rx="1"/><rect width="15" height="2" x="226" y="242" rx="1"/><rect width="15" height="2" x="226" y="252" rx="1"/></g><rect width="10" height="52" x="118" y="196" fill="#d2caea" rx="2"/><rect width="10" height="47" x="154" y="196" fill="#6b4fbb" rx="2"/><rect width="10" height="37" x="190" y="196" fill="#d2caea" rx="2"/><g fill="#fee8dc"><rect width="10" height="52" x="132" y="185" rx="2"/><rect width="10" height="38" x="168" y="185" rx="2"/></g><rect width="10" height="58" x="204" y="185" fill="#fb722e" rx="2"/><g transform="translate(76 128)"><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#F)" xlink:href="#E"/><use mask="url(#H)" xlink:href="#G"/></g><g fill="#d2caea"><rect width="16" height="4" x="156" y="35" rx="2"/><rect width="16" height="4" x="156" y="43" rx="2"/></g><g fill="#fff" stroke-width="8"><use stroke="#fee8dc" mask="url(#J)" xlink:href="#I"/><use stroke="#fb722e" mask="url(#L)" xlink:href="#K"/></g></g><g fill="#fb722e"><path d="m6.226 220.95l-2.84.631c-1.075.239-1.752-.445-1.515-1.515l.631-2.84-.631-2.84c-.239-1.075.445-1.752 1.515-1.515l2.84.631 2.84-.631c1.075-.239 1.752.445 1.515 1.515l-.631 2.84.631 2.84c.239 1.075-.445 1.752-1.515 1.515l-2.84-.631" opacity=".2" transform="matrix(.70711.70711-.70711.70711 155.43 59.22)"/><path d="m256.23 9.95l-2.84.631c-1.075.239-1.752-.445-1.515-1.515l.631-2.84-.631-2.84c-.239-1.075.445-1.752 1.515-1.515l2.84.631 2.84-.631c1.075-.239 1.752.445 1.515 1.515l-.631 2.84.631 2.84c.239 1.075-.445 1.752-1.515 1.515l-2.84-.631" opacity=".2" transform="matrix(.70711.70711-.70711.70711 79.45-179.36)"/></g><path fill="#fee8dc" d="m312.78 150.43l-3.634.807c-1.296.288-2.115-.52-1.825-1.825l.807-3.634-.807-3.634c-.288-1.296.52-2.115 1.825-1.825l3.634.807 3.634-.807c1.296-.288 2.115.52 1.825 1.825l-.807 3.634.807 3.634c.288 1.296-.52 2.115-1.825 1.825l-3.634-.807" transform="matrix(.70711.70711-.70711.70711 194.69-178.47)"/><path fill="#6b4fbb" d="m43.778 80.43l-3.617.804c-1.306.29-2.129-.53-1.839-1.839l.804-3.617-.804-3.617c-.29-1.306.53-2.129 1.839-1.839l3.617.804 3.617-.804c1.306-.29 2.129.53 1.839 1.839l-.804 3.617.804 3.617c.29 1.306-.53 2.129-1.839 1.839l-3.617-.804" opacity=".2" transform="matrix(.70711-.70711.70711.70711-40.761 53.15)"/></g></svg> \ No newline at end of file
diff --git a/app/views/shared/empty_states/monitoring/_unable_to_connect.svg b/app/views/shared/empty_states/monitoring/_unable_to_connect.svg
new file mode 100644
index 00000000000..62537d87d5d
--- /dev/null
+++ b/app/views/shared/empty_states/monitoring/_unable_to_connect.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 406 305" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><use id="0" xlink:href="#E"/><use id="2" xlink:href="#E"/><use id="4" xlink:href="#E"/><path id="6" d="m74 93h26v47h-26z"/><path id="8" d="m74 93h26v47h-26z"/><rect id="A" width="65" height="14" x="55" y="135" rx="4"/><rect id="C" width="175" height="118" rx="10"/><rect id="E" width="159" rx="10" height="56"/><rect id="F" width="160" y="2" rx="10" height="56" fill="#f9f9f9"/><mask id="B" width="65" height="14" x="0" y="0" fill="#fff"><use xlink:href="#A"/></mask><mask id="9" width="26" height="47" x="0" y="0" fill="#fff"><use xlink:href="#8"/></mask><mask id="D" width="175" height="118" x="0" y="0" fill="#fff"><use xlink:href="#C"/></mask><mask id="7" width="26" height="47" x="0" y="0" fill="#fff"><use xlink:href="#6"/></mask><mask id="3" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#2"/></mask><mask id="1" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#0"/></mask><mask id="5" width="159" height="56" x="0" y="0" fill="#fff"><use xlink:href="#4"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(1 65)"><g transform="translate(244)"><use xlink:href="#F"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#1)" xlink:href="#0"/><g fill-rule="nonzero"><path fill="#fb722e" d="m134 31c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/><path fill="#fee8dc" d="m117 31c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6m-17-4c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/></g><g fill="#d2caea"><rect width="50" height="4" x="19" y="20" rx="2"/><rect width="50" height="4" x="19" y="34" rx="2"/></g><g transform="translate(0 59)"><use xlink:href="#F"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#3)" xlink:href="#2"/><g fill-rule="nonzero"><path fill="#fee8dc" d="m134 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/><path fill="#fb722e" d="m117 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/><path fill="#fee8dc" d="m100 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/></g><rect width="50" height="4" x="19" y="19" fill="#d2caea" rx="2" id="G"/><rect width="50" height="4" x="19" y="33" fill="#d2caea" rx="2" id="H"/></g><g transform="translate(0 118)"><use xlink:href="#F"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#5)" xlink:href="#4"/><g fill-rule="nonzero"><path fill="#fb722e" d="m134 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/><path fill="#fee8dc" d="m117 30c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6m-17-4c1.105 0 2-.895 2-2 0-1.105-.895-2-2-2-1.105 0-2 .895-2 2 0 1.105.895 2 2 2m0 4c-3.314 0-6-2.686-6-6 0-3.314 2.686-6 6-6 3.314 0 6 2.686 6 6 0 3.314-2.686 6-6 6"/></g><use xlink:href="#G"/><use xlink:href="#H"/></g></g><g transform="translate(163 55)"><g fill="#eee"><rect width="29" height="4" y="29" rx="2"/><rect width="28" height="4" x="55" y="29" rx="2"/></g><g transform="translate(16)"><circle cx="30" cy="30" r="24" fill="#fef0ea"/><g fill="#fb722e"><circle cx="30.5" cy="30.5" r="30.5" opacity=".1"/><circle cx="30.5" cy="30.5" r="19.5" opacity=".1"/></g><circle cx="30.5" cy="30.5" r="13.5" fill="#fff"/><path fill="#fb722e" d="m32.621 30.5l2.481-2.481c.586-.586.58-1.529-.006-2.115-.59-.59-1.533-.589-2.115-.006l-2.481 2.481-2.481-2.481c-.586-.586-1.529-.58-2.115.006-.59.59-.589 1.533-.006 2.115l2.481 2.481-2.481 2.481c-.586.586-.58 1.529.006 2.115.59.59 1.533.589 2.115.006l2.481-2.481 2.481 2.481c.586.586 1.529.58 2.115-.006.59-.59.589-1.533.006-2.115l-2.481-2.481"/></g></g><g transform="translate(0 13)"><rect width="65" height="14" x="55" y="137" fill="#f9f9f9" rx="4"/><use fill="#fff" stroke="#eee" stroke-width="8" mask="url(#7)" xlink:href="#6"/><rect width="175" height="118" y="3" fill="#f9f9f9" rx="10"/><g fill="#fff" stroke="#eee" stroke-width="8"><use mask="url(#9)" xlink:href="#8"/><use mask="url(#B)" xlink:href="#A"/><use mask="url(#D)" xlink:href="#C"/></g><g fill-rule="nonzero"><path fill="#eee" d="m163 105v-93h-152v93h152m-156-93.01c0-2.204 1.797-3.99 3.995-3.99h152.01c2.206 0 3.995 1.796 3.995 3.99v93.02c0 2.204-1.797 3.99-3.995 3.99h-152.01c-2.206 0-3.995-1.796-3.995-3.99v-93.02"/><path fill="#d2caea" d="m86 92c-11.598 0-21-9.402-21-21 0-11.598 9.402-21 21-21 11.598 0 21 9.402 21 21 0 11.598-9.402 21-21 21m0-4c9.389 0 17-7.611 17-17 0-9.389-7.611-17-17-17-9.389 0-17 7.611-17 17 0 9.389 7.611 17 17 17"/></g><path fill="#6b4fbb" d="m83 63c0-1.659 1.347-3 3-3 1.657 0 3 1.342 3 3v7.993c0 1.659-1.347 3-3 3-1.657 0-3-1.342-3-3v-7.993m3 18.997c-1.657 0-3-1.343-3-3 0-1.657 1.343-3 3-3 1.657 0 3 1.343 3 3 0 1.657-1.343 3-3 3"/><g fill="#eee"><rect width="134" height="4" x="20" y="30" rx="2"/><rect width="14" height="4" x="20" y="20" rx="2"/><circle cx="87" cy="21" r="5"/></g></g></g></svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_emoji_slightly_smiling_face.svg b/app/views/shared/icons/_emoji_slightly_smiling_face.svg
new file mode 100644
index 00000000000..56dbad91554
--- /dev/null
+++ b/app/views/shared/icons/_emoji_slightly_smiling_face.svg
@@ -0,0 +1 @@
+<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369.721.721 0 0 1 .568.047.715.715 0 0 1 .37.445c.195.625.556 1.131 1.084 1.518A2.93 2.93 0 0 0 9 12.75a2.93 2.93 0 0 0 1.775-.58 2.913 2.913 0 0 0 1.084-1.518.711.711 0 0 1 .375-.445.737.737 0 0 1 .575-.047c.195.063.34.186.433.37.094.183.11.372.047.568zM7.5 6c0 .414-.146.768-.44 1.06-.292.294-.646.44-1.06.44-.414 0-.768-.146-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06.292-.294.646-.44 1.06-.44.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06-.292.294-.646.44-1.06.44-.414 0-.768-.146-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06.292-.294.646-.44 1.06-.44.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></svg>
diff --git a/app/views/shared/icons/_emoji_smile.svg b/app/views/shared/icons/_emoji_smile.svg
new file mode 100644
index 00000000000..ce645fee46f
--- /dev/null
+++ b/app/views/shared/icons/_emoji_smile.svg
@@ -0,0 +1 @@
+<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568zM14 6.37c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm-6.5 0c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm9 2.63a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></svg>
diff --git a/app/views/shared/icons/_emoji_smiley.svg b/app/views/shared/icons/_emoji_smiley.svg
new file mode 100644
index 00000000000..ddfae50e566
--- /dev/null
+++ b/app/views/shared/icons/_emoji_smiley.svg
@@ -0,0 +1 @@
+<svg width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568h.001zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6c.92.397 1.91.6 2.912.598a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39c.397-.92.6-1.91.598-2.912zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="nonzero"/></svg>
diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml
index 2d25b8aad62..8939aeb6c3a 100644
--- a/app/views/shared/projects/_dropdown.html.haml
+++ b/app/views/shared/projects/_dropdown.html.haml
@@ -1,4 +1,4 @@
-- @sort ||= sort_value_recently_updated
+- @sort ||= sort_value_latest_activity
.dropdown
- toggle_text = projects_sort_options_hash[@sort]
= dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' })
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 68a6fd76e70..b33ba2ed7c1 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -2,6 +2,8 @@ class RepositoryImportWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
+ sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_EXPIRATION
+
attr_accessor :project, :current_user
def perform(project_id)
@@ -12,7 +14,7 @@ class RepositoryImportWorker
import_url: @project.import_url,
path: @project.path_with_namespace)
- project.update_column(:import_error, nil)
+ project.update_columns(import_jid: self.jid, import_error: nil)
result = Projects::ImportService.new(project, current_user).execute
diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb
new file mode 100644
index 00000000000..bfc5e667bb6
--- /dev/null
+++ b/app/workers/stuck_import_jobs_worker.rb
@@ -0,0 +1,37 @@
+class StuckImportJobsWorker
+ include Sidekiq::Worker
+ include CronjobQueue
+
+ IMPORT_EXPIRATION = 15.hours.to_i
+
+ def perform
+ stuck_projects.find_in_batches(batch_size: 500) do |group|
+ jids = group.map(&:import_jid)
+
+ # Find the jobs that aren't currently running or that exceeded the threshold.
+ completed_jids = Gitlab::SidekiqStatus.completed_jids(jids)
+
+ if completed_jids.any?
+ completed_ids = group.select { |project| completed_jids.include?(project.import_jid) }.map(&:id)
+
+ fail_batch!(completed_jids, completed_ids)
+ end
+ end
+ end
+
+ private
+
+ def stuck_projects
+ Project.select('id, import_jid').with_import_status(:started).where.not(import_jid: nil)
+ end
+
+ def fail_batch!(completed_jids, completed_ids)
+ Project.where(id: completed_ids).update_all(import_status: 'failed', import_error: error_message)
+
+ Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_jids.join(', ')}")
+ end
+
+ def error_message
+ "Import timed out. Import took longer than #{IMPORT_EXPIRATION} seconds"
+ end
+end
diff --git a/changelogs/unreleased/12818-ci-status-as-favicon.yml b/changelogs/unreleased/12818-ci-status-as-favicon.yml
new file mode 100644
index 00000000000..70194178d90
--- /dev/null
+++ b/changelogs/unreleased/12818-ci-status-as-favicon.yml
@@ -0,0 +1,4 @@
+---
+title: Show CI status as Favicon on Pipelines, Job and MR pages
+merge_request: 10144
+author:
diff --git a/changelogs/unreleased/28810-projectfinder-should-handle-more-options.yml b/changelogs/unreleased/28810-projectfinder-should-handle-more-options.yml
new file mode 100644
index 00000000000..e4be16d4b37
--- /dev/null
+++ b/changelogs/unreleased/28810-projectfinder-should-handle-more-options.yml
@@ -0,0 +1,4 @@
+---
+title: ProjectsFinder should handle more options
+merge_request: 9682
+author: Jacopo Beschi @jacopo-beschi
diff --git a/changelogs/unreleased/29364-private-projects-mr-fix.yml b/changelogs/unreleased/29364-private-projects-mr-fix.yml
new file mode 100644
index 00000000000..ab93d6f337b
--- /dev/null
+++ b/changelogs/unreleased/29364-private-projects-mr-fix.yml
@@ -0,0 +1,4 @@
+---
+title: Don’t show source project name when user does not have access
+merge_request:
+author:
diff --git a/changelogs/unreleased/30021-api-deploy_keys-can_push-is-not-honoured.yml b/changelogs/unreleased/30021-api-deploy_keys-can_push-is-not-honoured.yml
new file mode 100644
index 00000000000..7584995a11f
--- /dev/null
+++ b/changelogs/unreleased/30021-api-deploy_keys-can_push-is-not-honoured.yml
@@ -0,0 +1,4 @@
+---
+title: Enable creation of deploy keys with write access via the API
+merge_request:
+author:
diff --git a/changelogs/unreleased/30125-markdown-security.yml b/changelogs/unreleased/30125-markdown-security.yml
new file mode 100644
index 00000000000..b766caf7d08
--- /dev/null
+++ b/changelogs/unreleased/30125-markdown-security.yml
@@ -0,0 +1,4 @@
+---
+title: Remove the class attribute from the whitelist for HTML generated from Markdown.
+merge_request:
+author:
diff --git a/changelogs/unreleased/30195-document-search-param-on-api.yml b/changelogs/unreleased/30195-document-search-param-on-api.yml
new file mode 100644
index 00000000000..f19f6ab699e
--- /dev/null
+++ b/changelogs/unreleased/30195-document-search-param-on-api.yml
@@ -0,0 +1,4 @@
+---
+title: Add search optional param and docs for V4
+merge_request:
+author:
diff --git a/changelogs/unreleased/30291-reopen-mr.yml b/changelogs/unreleased/30291-reopen-mr.yml
new file mode 100644
index 00000000000..4ae3e90eeba
--- /dev/null
+++ b/changelogs/unreleased/30291-reopen-mr.yml
@@ -0,0 +1,4 @@
+---
+title: Include reopened MRs when searching for opened ones
+merge_request: 10407
+author:
diff --git a/changelogs/unreleased/30493-env-deploy-tooltip.yml b/changelogs/unreleased/30493-env-deploy-tooltip.yml
new file mode 100644
index 00000000000..8fadaaa7bd2
--- /dev/null
+++ b/changelogs/unreleased/30493-env-deploy-tooltip.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes HTML structure that was preventing the tooltip to disappear when hovering
+ out of the button.
+merge_request:
+author:
diff --git a/changelogs/unreleased/add-error-empty-states.yml b/changelogs/unreleased/add-error-empty-states.yml
new file mode 100644
index 00000000000..ec6c7b6dce9
--- /dev/null
+++ b/changelogs/unreleased/add-error-empty-states.yml
@@ -0,0 +1,4 @@
+---
+title: Introduced error/empty states for the environments performance metrics
+merge_request: 10271
+author:
diff --git a/changelogs/unreleased/add_remove_concurrent_index_to_database_helper.yml b/changelogs/unreleased/add_remove_concurrent_index_to_database_helper.yml
new file mode 100644
index 00000000000..c7b06e45607
--- /dev/null
+++ b/changelogs/unreleased/add_remove_concurrent_index_to_database_helper.yml
@@ -0,0 +1,4 @@
+---
+title: Add remove_concurrent_index to database helper
+merge_request: 10441
+author: blackst0ne
diff --git a/changelogs/unreleased/award-emoji-button-smiley-animation.yml b/changelogs/unreleased/award-emoji-button-smiley-animation.yml
new file mode 100644
index 00000000000..31903aeb040
--- /dev/null
+++ b/changelogs/unreleased/award-emoji-button-smiley-animation.yml
@@ -0,0 +1,4 @@
+---
+title: Added award emoji animation and improved active state
+merge_request:
+author:
diff --git a/changelogs/unreleased/feature-gh-rake-task.yml b/changelogs/unreleased/feature-gh-rake-task.yml
new file mode 100644
index 00000000000..5b1d380690c
--- /dev/null
+++ b/changelogs/unreleased/feature-gh-rake-task.yml
@@ -0,0 +1,4 @@
+---
+title: Add rake task to import GitHub projects from the command line
+merge_request:
+author:
diff --git a/changelogs/unreleased/file-import-export-path-disclosure.yml b/changelogs/unreleased/file-import-export-path-disclosure.yml
new file mode 100644
index 00000000000..1a297d07187
--- /dev/null
+++ b/changelogs/unreleased/file-import-export-path-disclosure.yml
@@ -0,0 +1,5 @@
+---
+title: Fix path disclosure in project import/export
+merge_request:
+author:
+
diff --git a/changelogs/unreleased/fix-gh-import-status-check.yml b/changelogs/unreleased/fix-gh-import-status-check.yml
new file mode 100644
index 00000000000..d04bc2954a0
--- /dev/null
+++ b/changelogs/unreleased/fix-gh-import-status-check.yml
@@ -0,0 +1,4 @@
+---
+title: Periodically mark projects that are stuck in importing as failed
+merge_request:
+author:
diff --git a/changelogs/unreleased/open-redirect-continue-params.yml b/changelogs/unreleased/open-redirect-continue-params.yml
new file mode 100644
index 00000000000..def3bc7d929
--- /dev/null
+++ b/changelogs/unreleased/open-redirect-continue-params.yml
@@ -0,0 +1,4 @@
+---
+title: Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
+merge_request:
+author:
diff --git a/changelogs/unreleased/open-redirect-host-field.yml b/changelogs/unreleased/open-redirect-host-field.yml
new file mode 100644
index 00000000000..bed4b47cf04
--- /dev/null
+++ b/changelogs/unreleased/open-redirect-host-field.yml
@@ -0,0 +1,4 @@
+---
+title: Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
+merge_request:
+author:
diff --git a/changelogs/unreleased/tc-fix-pipeline-recipient.yml b/changelogs/unreleased/tc-fix-pipeline-recipient.yml
new file mode 100644
index 00000000000..0337533fdb2
--- /dev/null
+++ b/changelogs/unreleased/tc-fix-pipeline-recipient.yml
@@ -0,0 +1,4 @@
+---
+title: Clearly show who triggered the pipeline in email
+merge_request: 10283
+author:
diff --git a/changelogs/unreleased/tc-fix-unplayable-build-action-404.yml b/changelogs/unreleased/tc-fix-unplayable-build-action-404.yml
new file mode 100644
index 00000000000..e5e22c1daf7
--- /dev/null
+++ b/changelogs/unreleased/tc-fix-unplayable-build-action-404.yml
@@ -0,0 +1,4 @@
+---
+title: Disable pipeline and environment actions that are not playable
+merge_request: 10052
+author:
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index e8fef0000c1..f7cae84088e 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -349,6 +349,9 @@ Settings.cron_jobs['trending_projects_worker']['job_class'] = 'TrendingProjectsW
Settings.cron_jobs['remove_unreferenced_lfs_objects_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['cron'] ||= '20 0 * * *'
Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['job_class'] = 'RemoveUnreferencedLfsObjectsWorker'
+Settings.cron_jobs['stuck_import_jobs_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['stuck_import_jobs_worker']['cron'] ||= '15 * * * *'
+Settings.cron_jobs['stuck_import_jobs_worker']['job_class'] = 'StuckImportJobsWorker'
#
# GitLab Shell
diff --git a/config/initializers/8_gitaly.rb b/config/initializers/8_gitaly.rb
index c7f27c78535..42ec7240b0f 100644
--- a/config/initializers/8_gitaly.rb
+++ b/config/initializers/8_gitaly.rb
@@ -2,17 +2,5 @@ require 'uri'
# Make sure we initialize our Gitaly channels before Sidekiq starts multi-threaded execution.
if Gitlab.config.gitaly.enabled || Rails.env.test?
- Gitlab.config.repositories.storages.each do |name, params|
- address = params['gitaly_address']
-
- unless address.present?
- raise "storage #{name.inspect} is missing a gitaly_address"
- end
-
- unless URI(address).scheme.in?(%w(tcp unix))
- raise "Unsupported Gitaly address: #{address.inspect}"
- end
-
- Gitlab::GitalyClient.configure_channel(name, address)
- end
+ Gitlab::GitalyClient.configure_channels
end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 7244f851869..62e2e6145fd 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -250,6 +250,7 @@ constraints(ProjectUrlConstrainer.new) do
get :referenced_merge_requests
get :related_branches
get :can_create_branch
+ get :rendered_title
end
collection do
post :bulk_update
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 9b597f7a04e..dc431e4d566 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -42,10 +42,12 @@ var config = {
profile: './profile/profile_bundle.js',
protected_branches: './protected_branches/protected_branches_bundle.js',
snippet: './snippet/snippet_bundle.js',
+ stl_viewer: './blob/stl_viewer.js',
terminal: './terminal/terminal_bundle.js',
u2f: ['vendor/u2f'],
users: './users/users_bundle.js',
vue_pipelines: './vue_pipelines_index/index.js',
+ issue_show: './issue_show/index.js',
},
output: {
diff --git a/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb b/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb
index 7a8ed99c68f..178e4bf5ed3 100644
--- a/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb
+++ b/db/migrate/20160615142710_add_index_on_requested_at_to_members.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class AddIndexOnRequestedAtToMembers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb b/db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb
index 4bb4204cebd..081df23f394 100644
--- a/db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb
+++ b/db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class RemoveKeysFingerprintIndexIfExists < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb b/db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb
index e35af38aac3..76bb6a09639 100644
--- a/db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb
+++ b/db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class AddUniqueIndexToKeysFingerprint < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160620115026_add_index_on_runners_locked.rb b/db/migrate/20160620115026_add_index_on_runners_locked.rb
index 6ca486c63d1..48f4495b0a4 100644
--- a/db/migrate/20160620115026_add_index_on_runners_locked.rb
+++ b/db/migrate/20160620115026_add_index_on_runners_locked.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable RemoveIndex
class AddIndexOnRunnersLocked < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb b/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb
index a05a4c679e3..fec17ffb7f6 100644
--- a/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb
+++ b/db/migrate/20160715134306_add_index_for_pipeline_user_id.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class AddIndexForPipelineUserId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb b/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb
index 75a3eb15124..12e11bc3fbe 100644
--- a/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb
+++ b/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable RemoveIndex
class MergeRequestDiffRemoveUniq < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20160725104452_merge_request_diff_add_index.rb b/db/migrate/20160725104452_merge_request_diff_add_index.rb
index 6d04242dd25..60d81e0bdc0 100644
--- a/db/migrate/20160725104452_merge_request_diff_add_index.rb
+++ b/db/migrate/20160725104452_merge_request_diff_add_index.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class MergeRequestDiffAddIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20160802010328_remove_builds_enable_index_on_projects.rb b/db/migrate/20160802010328_remove_builds_enable_index_on_projects.rb
index 5fd51cb65f1..6d7733762c8 100644
--- a/db/migrate/20160802010328_remove_builds_enable_index_on_projects.rb
+++ b/db/migrate/20160802010328_remove_builds_enable_index_on_projects.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class RemoveBuildsEnableIndexOnProjects < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160803161903_add_unique_index_to_lists_label_id.rb b/db/migrate/20160803161903_add_unique_index_to_lists_label_id.rb
index baf2e70b127..9c1511963f7 100644
--- a/db/migrate/20160803161903_add_unique_index_to_lists_label_id.rb
+++ b/db/migrate/20160803161903_add_unique_index_to_lists_label_id.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class AddUniqueIndexToListsLabelId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb b/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb
index 3f074723b4a..30d98a0124e 100644
--- a/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb
+++ b/db/migrate/20160805041956_add_deleted_at_to_namespaces.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class AddDeletedAtToNamespaces < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160808085602_add_index_for_build_token.rb b/db/migrate/20160808085602_add_index_for_build_token.rb
index 6c5d7268e72..0446b2f2e15 100644
--- a/db/migrate/20160808085602_add_index_for_build_token.rb
+++ b/db/migrate/20160808085602_add_index_for_build_token.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class AddIndexForBuildToken < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160810142633_remove_redundant_indexes.rb b/db/migrate/20160810142633_remove_redundant_indexes.rb
index 8641c6ffa8f..d7ab022d7bc 100644
--- a/db/migrate/20160810142633_remove_redundant_indexes.rb
+++ b/db/migrate/20160810142633_remove_redundant_indexes.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable RemoveIndex
class RemoveRedundantIndexes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160819221631_add_index_to_note_discussion_id.rb b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb
index 8f693e97a58..843643c4e95 100644
--- a/db/migrate/20160819221631_add_index_to_note_discussion_id.rb
+++ b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable RemoveIndex
class AddIndexToNoteDiscussionId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160819232256_add_incoming_email_token_to_users.rb b/db/migrate/20160819232256_add_incoming_email_token_to_users.rb
index bcad3416d04..a004a3802a2 100644
--- a/db/migrate/20160819232256_add_incoming_email_token_to_users.rb
+++ b/db/migrate/20160819232256_add_incoming_email_token_to_users.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable RemoveIndex
class AddIncomingEmailTokenToUsers < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160919145149_add_group_id_to_labels.rb b/db/migrate/20160919145149_add_group_id_to_labels.rb
index e20e693f3aa..917c2b0c521 100644
--- a/db/migrate/20160919145149_add_group_id_to_labels.rb
+++ b/db/migrate/20160919145149_add_group_id_to_labels.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class AddGroupIdToLabels < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20160920160832_add_index_to_labels_title.rb b/db/migrate/20160920160832_add_index_to_labels_title.rb
index 19f7b1076a7..e38c655baee 100644
--- a/db/migrate/20160920160832_add_index_to_labels_title.rb
+++ b/db/migrate/20160920160832_add_index_to_labels_title.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class AddIndexToLabelsTitle < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20161017125927_add_unique_index_to_labels.rb b/db/migrate/20161017125927_add_unique_index_to_labels.rb
index f2b56ebfb7b..b8f6a803a0a 100644
--- a/db/migrate/20161017125927_add_unique_index_to_labels.rb
+++ b/db/migrate/20161017125927_add_unique_index_to_labels.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class AddUniqueIndexToLabels < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
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 35ad22b6c01..b77daf12f68 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
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable RemoveIndex
class AddPipelineIdToMergeRequestMetrics < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20161031181638_add_unique_index_to_subscriptions.rb b/db/migrate/20161031181638_add_unique_index_to_subscriptions.rb
index 4b1b29e1265..f263377fbc6 100644
--- a/db/migrate/20161031181638_add_unique_index_to_subscriptions.rb
+++ b/db/migrate/20161031181638_add_unique_index_to_subscriptions.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class AddUniqueIndexToSubscriptions < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20161106185620_add_project_import_data_project_index.rb b/db/migrate/20161106185620_add_project_import_data_project_index.rb
index 94b8ddd46f5..b3746dc4f6c 100644
--- a/db/migrate/20161106185620_add_project_import_data_project_index.rb
+++ b/db/migrate/20161106185620_add_project_import_data_project_index.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class AddProjectImportDataProjectIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20161124111395_add_index_to_parent_id.rb b/db/migrate/20161124111395_add_index_to_parent_id.rb
index 73f9d92bb22..065643e058d 100644
--- a/db/migrate/20161124111395_add_index_to_parent_id.rb
+++ b/db/migrate/20161124111395_add_index_to_parent_id.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable RemoveIndex
class AddIndexToParentId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20161128142110_remove_unnecessary_indexes.rb b/db/migrate/20161128142110_remove_unnecessary_indexes.rb
index 8100287ef48..699a9368eb3 100644
--- a/db/migrate/20161128142110_remove_unnecessary_indexes.rb
+++ b/db/migrate/20161128142110_remove_unnecessary_indexes.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable RemoveIndex
class RemoveUnnecessaryIndexes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20161202152035_add_index_to_routes.rb b/db/migrate/20161202152035_add_index_to_routes.rb
index 6d6c8906204..552b5fab68c 100644
--- a/db/migrate/20161202152035_add_index_to_routes.rb
+++ b/db/migrate/20161202152035_add_index_to_routes.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable RemoveIndex
class AddIndexToRoutes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb b/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb
index 2977917f2d1..7d39c2ae626 100644
--- a/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb
+++ b/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable RemoveIndex
class RemoveUniqPathIndexFromNamespace < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20161206153751_add_path_index_to_namespace.rb b/db/migrate/20161206153751_add_path_index_to_namespace.rb
index b0bac7d121e..623037e35cd 100644
--- a/db/migrate/20161206153751_add_path_index_to_namespace.rb
+++ b/db/migrate/20161206153751_add_path_index_to_namespace.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable RemoveIndex
class AddPathIndexToNamespace < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb b/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb
index cc9d4974baa..9296ae36aa5 100644
--- a/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb
+++ b/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable RemoveIndex
class RemoveUniqNameIndexFromNamespace < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20161206153754_add_name_index_to_namespace.rb b/db/migrate/20161206153754_add_name_index_to_namespace.rb
index b3f3cb68a99..2bbd039ff27 100644
--- a/db/migrate/20161206153754_add_name_index_to_namespace.rb
+++ b/db/migrate/20161206153754_add_name_index_to_namespace.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable RemoveIndex
class AddNameIndexToNamespace < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20161207231621_create_environment_name_unique_index.rb b/db/migrate/20161207231621_create_environment_name_unique_index.rb
index 5ff0f5bae4d..15093350f12 100644
--- a/db/migrate/20161207231621_create_environment_name_unique_index.rb
+++ b/db/migrate/20161207231621_create_environment_name_unique_index.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class CreateEnvironmentNameUniqueIndex < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
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 ede0316e860..42a90091b87 100644
--- a/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
+++ b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable RemoveIndex
class AddUniqueIndexForEnvironmentSlug < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
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 53f4c6bbb18..76db5179795 100644
--- a/db/migrate/20161212142807_add_lower_path_index_to_routes.rb
+++ b/db/migrate/20161212142807_add_lower_path_index_to_routes.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable RemoveIndex
class AddLowerPathIndexToRoutes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170121123724_add_index_to_ci_builds_for_status_runner_id_and_type.rb b/db/migrate/20170121123724_add_index_to_ci_builds_for_status_runner_id_and_type.rb
index 4ea953f2b78..c006098fafd 100644
--- a/db/migrate/20170121123724_add_index_to_ci_builds_for_status_runner_id_and_type.rb
+++ b/db/migrate/20170121123724_add_index_to_ci_builds_for_status_runner_id_and_type.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class AddIndexToCiBuildsForStatusRunnerIdAndType < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170121130655_add_index_to_ci_runners_for_is_shared.rb b/db/migrate/20170121130655_add_index_to_ci_runners_for_is_shared.rb
index 620befcf4d7..00aa0b311b1 100644
--- a/db/migrate/20170121130655_add_index_to_ci_runners_for_is_shared.rb
+++ b/db/migrate/20170121130655_add_index_to_ci_runners_for_is_shared.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class AddIndexToCiRunnersForIsShared < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170130204620_add_index_to_project_authorizations.rb b/db/migrate/20170130204620_add_index_to_project_authorizations.rb
index 629b49436e3..f256251516a 100644
--- a/db/migrate/20170130204620_add_index_to_project_authorizations.rb
+++ b/db/migrate/20170130204620_add_index_to_project_authorizations.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class AddIndexToProjectAuthorizations < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
@@ -6,7 +7,9 @@ class AddIndexToProjectAuthorizations < ActiveRecord::Migration
disable_ddl_transaction!
def up
- add_concurrent_index(:project_authorizations, :project_id)
+ unless index_exists?(:project_authorizations, :project_id)
+ add_concurrent_index(:project_authorizations, :project_id)
+ end
end
def down
diff --git a/db/migrate/20170131221752_add_relative_position_to_issues.rb b/db/migrate/20170131221752_add_relative_position_to_issues.rb
index 1baad0893e3..fd18d8b6a60 100644
--- a/db/migrate/20170131221752_add_relative_position_to_issues.rb
+++ b/db/migrate/20170131221752_add_relative_position_to_issues.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable RemoveIndex
class AddRelativePositionToIssues < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb
index 31ef458c44f..b1b0a601007 100644
--- a/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb
+++ b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class AddIndexToLabelsForTypeAndProject < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb b/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb
index 70fb0ef12f9..2c20f6a48ab 100644
--- a/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb
+++ b/db/migrate/20170210062829_add_index_to_labels_for_title_and_project.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class AddIndexToLabelsForTitleAndProject < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb
index 07d4f8af27f..c31057f2617 100644
--- a/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb
+++ b/db/migrate/20170210075922_add_index_to_ci_trigger_requests_for_commit_id.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class AddIndexToCiTriggerRequestsForCommitId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170210103609_add_index_to_user_agent_detail.rb b/db/migrate/20170210103609_add_index_to_user_agent_detail.rb
index 2d8329b7862..ba4976a5ce8 100644
--- a/db/migrate/20170210103609_add_index_to_user_agent_detail.rb
+++ b/db/migrate/20170210103609_add_index_to_user_agent_detail.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable RemoveIndex
class AddIndexToUserAgentDetail < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
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 8a96a784c97..884c4e569d6 100644
--- a/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb
+++ b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class AddIndexForLatestSuccessfulPipeline < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
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 a2839f52d89..56ad566ca67 100644
--- a/db/migrate/20170216141440_drop_index_for_builds_project_status.rb
+++ b/db/migrate/20170216141440_drop_index_for_builds_project_status.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class DropIndexForBuildsProjectStatus < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
diff --git a/db/migrate/20170222143500_remove_old_project_id_columns.rb b/db/migrate/20170222143500_remove_old_project_id_columns.rb
index eac93e8e407..268144a2552 100644
--- a/db/migrate/20170222143500_remove_old_project_id_columns.rb
+++ b/db/migrate/20170222143500_remove_old_project_id_columns.rb
@@ -1,3 +1,4 @@
+# rubocop:disable RemoveIndex
class RemoveOldProjectIdColumns < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
diff --git a/db/migrate/20170313213916_add_index_to_user_ghost.rb b/db/migrate/20170313213916_add_index_to_user_ghost.rb
index c429039c275..fe5847ed225 100644
--- a/db/migrate/20170313213916_add_index_to_user_ghost.rb
+++ b/db/migrate/20170313213916_add_index_to_user_ghost.rb
@@ -1,6 +1,7 @@
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
+# rubocop:disable RemoveIndex
class AddIndexToUserGhost < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
diff --git a/db/migrate/20170405080720_add_import_jid_to_projects.rb b/db/migrate/20170405080720_add_import_jid_to_projects.rb
new file mode 100644
index 00000000000..55b87b9d56d
--- /dev/null
+++ b/db/migrate/20170405080720_add_import_jid_to_projects.rb
@@ -0,0 +1,9 @@
+class AddImportJidToProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :projects, :import_jid, :string
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ccf18d07179..582f68cbee7 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: 20170402231018) do
+ActiveRecord::Schema.define(version: 20170405080720) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -920,6 +920,7 @@ ActiveRecord::Schema.define(version: 20170402231018) do
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
+ t.string "import_jid"
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
diff --git a/doc/administration/raketasks/github_import.md b/doc/administration/raketasks/github_import.md
new file mode 100644
index 00000000000..affb4d17861
--- /dev/null
+++ b/doc/administration/raketasks/github_import.md
@@ -0,0 +1,36 @@
+# GitHub import
+
+>**Note:**
+>
+> - [Introduced][ce-10308] in GitLab 9.1.
+> - You need a personal access token in order to retrieve and import GitHub
+> projects. You can get it from: https://github.com/settings/tokens
+> - You also need to pass an username as the second argument to the rake task
+> which will become the owner of the project.
+
+To import a project from the list of your GitHub projects available:
+
+```bash
+# Omnibus installations
+sudo gitlab-rake import:github[access_token,root,foo/bar]
+
+# Installations from source
+bundle exec rake import:github[access_token,root,foo/bar] RAILS_ENV=production
+```
+
+In this case, `access_token` is your GitHub personal access token, `root`
+is your GitLab username, and `foo/bar` is the new GitLab namespace/project that
+will get created from your GitHub project. Subgroups are also possible: `foo/foo/bar`.
+
+
+To import a specific GitHub project (named `foo/github_repo` here):
+
+```bash
+# Omnibus installations
+sudo gitlab-rake import:github[access_token,root,foo/bar,foo/github_repo]
+
+# Installations from source
+bundle exec rake import:github[access_token,root,foo/bar,foo/github_repo] RAILS_ENV=production
+```
+
+[ce-10308]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10308
diff --git a/doc/api/issues.md b/doc/api/issues.md
index a19c965a8c3..5702cdcf3c1 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -26,16 +26,20 @@ GET /issues?labels=foo,bar&state=opened
GET /issues?milestone=1.0.0
GET /issues?milestone=1.0.0&state=opened
GET /issues?iids[]=42&iids[]=43
+GET /issues?search=issue+title+or+description
```
-| 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. `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` |
-| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
+|-------------+----------------+----------+-----------------------------------------------------------------------------------------------------------------------------|
+| 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. `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` |
+| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Search issues against their `title` and `description` |
+|-------------+----------------+----------+-----------------------------------------------------------------------------------------------------------------------------|
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/issues
@@ -104,17 +108,21 @@ GET /groups/:id/issues?labels=foo,bar&state=opened
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
+GET /groups/:id/issues?search=issue+title+or+description
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `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. `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` |
-| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
+|-------------+----------------+----------+-----------------------------------------------------------------------------------------------------------------------------|
+| Attribute | Type | Required | Description |
+|-------------+----------------+----------+-----------------------------------------------------------------------------------------------------------------------------|
+| `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. `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` |
+| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Search group issues against their `title` and `description` |
+|-------------+----------------+----------+-----------------------------------------------------------------------------------------------------------------------------|
```bash
@@ -184,17 +192,21 @@ GET /projects/:id/issues?labels=foo,bar&state=opened
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
+GET /projects/:id/issues?search=issue+title+or+description
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `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. `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` |
+|-------------+----------------+----------+-----------------------------------------------------------------------------------------------------------------------------|
+| Attribute | Type | Required | Description |
+|-------------+----------------+----------+-----------------------------------------------------------------------------------------------------------------------------|
+| `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. `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` |
+| `search` | string | no | Search project issues against their `title` and `description` |
+|-------------+----------------+----------+-----------------------------------------------------------------------------------------------------------------------------|
```bash
@@ -258,10 +270,12 @@ Get a single project issue.
GET /projects/:id/issues/:issue_iid
```
+|-------------+---------+----------+--------------------------------------|
| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
+|-------------+---------+----------+--------------------------------------|
| `id` | integer | yes | The ID of a project |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
+|-------------+---------+----------+--------------------------------------|
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/41
@@ -323,19 +337,23 @@ Creates a new project issue.
POST /projects/:id/issues
```
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
-| `title` | string | yes | The title of an issue |
-| `description` | string | no | The description of an issue |
-| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. |
-| `assignee_id` | integer | no | The ID of a user to assign issue |
-| `milestone_id` | integer | no | The ID of a milestone to assign issue |
-| `labels` | string | no | Comma-separated label names for an issue |
-| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
-| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
-| `merge_request_to_resolve_discussions_of` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. When passing a description or title, these values will take precedence over the default values. |
-| `discussion_to_resolve` | string | no | The ID of a discussion to resolve. This will fill in the issue with a default description and mark the discussion as resolved. Use in combination with `merge_request_to_resolve_discussions_of`. |
+|-------------------------------------------+---------+----------+------------------------------------------------------------------------------------------------------------------------------------------------------|
+| Attribute | Type | Required | Description |
+|-------------------------------------------+---------+----------+------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `id` | integer | yes | The ID of a project |
+| `title` | string | yes | The title of an issue |
+| `description` | string | no | The description of an issue |
+| `confidential` | boolean | no | Set an issue to be confidential. Default is `false`. |
+| `assignee_id` | integer | no | The ID of a user to assign issue |
+| `milestone_id` | integer | no | The ID of a milestone to assign issue |
+| `labels` | string | no | Comma-separated label names for an issue |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
+| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
+| `merge_request_to_resolve_discussions_of` | integer | no | The IID of a merge request in which to resolve all issues. This will fill the issue with a default description and mark all discussions as resolved. |
+| - | - | - | When passing a description or title, these values will take precedence over the default values. |
+| `discussion_to_resolve` | string | no | The ID of a discussion to resolve. This will fill in the issue with a default description and mark the discussion |
+| - | - | - | as resolved. Use in combination with `merge_request_to_resolve_discussions_of`. |
+|-------------------------------------------+---------+----------+------------------------------------------------------------------------------------------------------------------------------------------------------|
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues?title=Issues%20with%20auth&labels=bug
@@ -383,8 +401,9 @@ closed.
PUT /projects/:id/issues/:issue_iid
```
+|----------------+---------+----------+------------------------------------------------------------------------------------------------------------|
| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
+|----------------+---------+----------+------------------------------------------------------------------------------------------------------------|
| `id` | integer | yes | The ID of a project |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
| `title` | string | no | The title of an issue |
@@ -396,6 +415,7 @@ PUT /projects/:id/issues/:issue_iid
| `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it |
| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
+|----------------+---------+----------+------------------------------------------------------------------------------------------------------------|
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/85?state_event=close
@@ -442,10 +462,12 @@ Only for admins and project owners. Soft deletes the issue in question.
DELETE /projects/:id/issues/:issue_iid
```
+|-------------+---------+----------+--------------------------------------|
| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
+|-------------+---------+----------+--------------------------------------|
| `id` | integer | yes | The ID of a project |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
+|-------------+---------+----------+--------------------------------------|
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/85
@@ -464,11 +486,13 @@ project, it will then be assigned to the issue that is being moved.
POST /projects/:id/issues/:issue_iid/move
```
+|-----------------+---------+----------+--------------------------------------|
| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
+|-----------------+---------+----------+--------------------------------------|
| `id` | integer | yes | The ID of a project |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
| `to_project_id` | integer | yes | The ID of the new project |
+|-----------------+---------+----------+--------------------------------------|
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/85/move
@@ -520,10 +544,12 @@ is returned.
POST /projects/:id/issues/:issue_iid/subscribe
```
+|-------------+---------+----------+--------------------------------------|
| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
+|-------------+---------+----------+--------------------------------------|
| `id` | integer | yes | The ID of a project |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
+|-------------+---------+----------+--------------------------------------|
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/subscribe
@@ -575,10 +601,12 @@ status code `304` is returned.
POST /projects/:id/issues/:issue_iid/unsubscribe
```
+|-------------+---------+----------+--------------------------------------|
| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
+|-------------+---------+----------+--------------------------------------|
| `id` | integer | yes | The ID of a project |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
+|-------------+---------+----------+--------------------------------------|
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/unsubscribe
@@ -594,10 +622,12 @@ returned.
POST /projects/:id/issues/:issue_iid/todo
```
+|-------------+---------+----------+--------------------------------------|
| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
+|-------------+---------+----------+--------------------------------------|
| `id` | integer | yes | The ID of a project |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
+|-------------+---------+----------+--------------------------------------|
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/todo
@@ -685,11 +715,13 @@ Sets an estimated time of work for this issue.
POST /projects/:id/issues/:issue_iid/time_estimate
```
+|-------------+---------+----------+------------------------------------------|
| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
+|-------------+---------+----------+------------------------------------------|
| `id` | integer | yes | The ID of a project |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
| `duration` | string | yes | The duration in human format. e.g: 3h30m |
+|-------------+---------+----------+------------------------------------------|
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/time_estimate?duration=3h30m
@@ -714,10 +746,12 @@ Resets the estimated time for this issue to 0 seconds.
POST /projects/:id/issues/:issue_iid/reset_time_estimate
```
+|-------------+---------+----------+--------------------------------------|
| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
+|-------------+---------+----------+--------------------------------------|
| `id` | integer | yes | The ID of a project |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
+|-------------+---------+----------+--------------------------------------|
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/reset_time_estimate
@@ -742,11 +776,13 @@ Adds spent time for this issue
POST /projects/:id/issues/:issue_iid/add_spent_time
```
+|-------------+---------+----------+------------------------------------------|
| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
+|-------------+---------+----------+------------------------------------------|
| `id` | integer | yes | The ID of a project |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
| `duration` | string | yes | The duration in human format. e.g: 3h30m |
+|-------------+---------+----------+------------------------------------------|
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/add_spent_time?duration=1h
@@ -771,10 +807,12 @@ Resets the total spent time for this issue to 0 seconds.
POST /projects/:id/issues/:issue_iid/reset_spent_time
```
+|-------------+---------+----------+--------------------------------------|
| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
+|-------------+---------+----------+--------------------------------------|
| `id` | integer | yes | The ID of a project |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
+|-------------+---------+----------+--------------------------------------|
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/reset_spent_time
@@ -797,10 +835,12 @@ Example response:
GET /projects/:id/issues/:issue_iid/time_stats
```
+|-------------+---------+----------+--------------------------------------|
| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
+|-------------+---------+----------+--------------------------------------|
| `id` | integer | yes | The ID of a project |
| `issue_iid` | integer | yes | The internal ID of a project's issue |
+|-------------+---------+----------+--------------------------------------|
```bash
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/time_stats
diff --git a/doc/ci/README.md b/doc/ci/README.md
index d8fba5d7a77..b3780a08828 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -1,34 +1,147 @@
-# GitLab CI Documentation
+# GitLab Continuous Integration (GitLab CI)
-## CI User documentation
+![CI/CD pipeline graph](img/cicd_pipeline_infograph.png)
+
+The benefits of Continuous Integration are huge when automation plays an
+integral part of your workflow. GitLab comes with built-in Continuous
+Integration, Continuous Deployment, and Continuous Delivery support to build,
+test, and deploy your application.
+
+Here's some info we've gathered to get you started.
+
+## Getting started
+
+The first steps towards your GitLab CI journey.
- [Getting started with GitLab CI](quick_start/README.md)
-- [CI examples for various languages](examples/README.md)
-- [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md)
- [Pipelines and jobs](pipelines.md)
-- [Environments and deployments](environments.md)
-- [Learn how `.gitlab-ci.yml` works](yaml/README.md)
- [Configure a Runner, the application that runs your jobs](runners/README.md)
-- [Use Docker images with GitLab Runner](docker/using_docker_images.md)
-- [Use CI to build Docker images](docker/using_docker_build.md)
+- **Articles:**
+ - [Getting started with GitLab and GitLab CI - Intro to CI](https://about.gitlab.com/2015/12/14/getting-started-with-gitlab-and-gitlab-ci/)
+ - [Continuous Integration, Delivery, and Deployment with GitLab - Intro to CI/CD](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/)
+ - [GitLab CI: Run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/)
+ - [Setting up GitLab Runner For Continuous Integration](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/)
+ - [GitLab CI: Deployment & environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
+- **Videos:**
+ - [Demo (March, 2017): how to get started using CI/CD with GitLab](https://about.gitlab.com/2017/03/13/ci-cd-demo/)
+ - [Webcast (April, 2016): getting started with CI in GitLab](https://about.gitlab.com/2016/04/20/webcast-recording-and-slides-introduction-to-ci-in-gitlab/)
+- **Third-party videos:**
+ - [Intégration continue avec GitLab (September, 2016)](https://www.youtube.com/watch?v=URcMBXjIr24&t=13s)
+ - [GitLab CI for Minecraft Plugins (July, 2016)](https://www.youtube.com/watch?v=Z4pcI9F8yf8)
+
+## Reference guides
+
+Once you get familiar with the getting started guides, you'll find yourself
+digging into specific reference guides.
+
+- [`.gitlab-ci.yml` reference](yaml/README.md) - Learn all about the ins and
+ outs of `.gitlab-ci.yml` definitions
- [CI Variables](variables/README.md) - Learn how to use variables defined in
your `.gitlab-ci.yml` or secured ones defined in your project's settings
-- [Use SSH keys in your build environment](ssh_keys/README.md)
-- [Trigger jobs through the API](triggers/README.md)
+- **The permissions model** - Learn about the access levels a user can have for
+ performing certain CI actions
+ - [User permissions](../user/permissions.md#gitlab-ci)
+ - [Jobs permissions](../user/permissions.md#jobs-permissions)
+
+## GitLab CI + Docker
+
+Leverage the power of Docker to run your CI pipelines.
+
+- [Use Docker images with GitLab Runner](docker/using_docker_images.md)
+- [Use CI to build Docker images](docker/using_docker_build.md)
+- [CI services (linked Docker containers)](services/README.md)
+- **Articles:**
+ - [Setting up GitLab Runner For Continuous Integration](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/)
+
+## Advanced use
+
+Once you get familiar with the basics of GitLab CI, it's time to dive in and
+learn how to leverage its potential even more.
+
+- [Environments and deployments](environments.md) - Separate your jobs into
+ environments and use them for different purposes like testing, building and
+ deploying
- [Job artifacts](../user/project/pipelines/job_artifacts.md)
-- [User permissions](../user/permissions.md#gitlab-ci)
-- [Jobs permissions](../user/permissions.md#jobs-permissions)
-- [API](../api/ci/README.md)
-- [CI services (linked docker containers)](services/README.md)
-- [CI/CD pipelines settings](../user/project/pipelines/settings.md)
-- [Review Apps](review_apps/index.md)
-- [Git submodules](git_submodules.md) Using Git submodules in your CI jobs
+- [Git submodules](git_submodules.md) - How to run your CI jobs when Git
+ submodules are involved
- [Auto deploy](autodeploy/index.md)
+- [Use SSH keys in your build environment](ssh_keys/README.md)
+- [Trigger jobs through the GitLab API](triggers/README.md)
+
+## Review Apps
+
+- [Review Apps](review_apps/index.md)
+- **Articles:**
+ - [Introducing Review Apps](https://about.gitlab.com/2016/11/22/introducing-review-apps/)
+ - [Example project that shows how to use Review Apps](https://gitlab.com/gitlab-examples/review-apps-nginx/)
+
+## GitLab CI for GitLab Pages
+
+See the topic on [GitLab Pages](../user/project/pages/index.md).
+
+## Special configuration
+
+You can change the default behavior of GitLab CI in your whole GitLab instance
+as well as in each project.
+
+- **Project specific**
+ - [CI/CD pipelines settings](../user/project/pipelines/settings.md)
+ - [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md)
+- **Affecting the whole GitLab instance**
+ - [Continuous Integration admin settings](../user/admin_area/settings/continuous_integration.md)
+
+## Examples
+
+>**Note:**
+A collection of `.gitlab-ci.yml` files is maintained at the
+[GitLab CI Yml project][gitlab-ci-templates].
+If your favorite programming language or framework is missing we would love
+your help by sending a merge request with a `.gitlab-ci.yml`.
+
+Here is an collection of tutorials and guides on setting up your CI pipeline.
+
+- [GitLab CI examples](examples/README.md) for the following languages and frameworks:
+ - [PHP](examples/php.md)
+ - [Ruby](examples/test-and-deploy-ruby-application-to-heroku.md)
+ - [Python](examples/test-and-deploy-python-application-to-heroku.md)
+ - [Clojure](examples/test-clojure-application.md)
+ - [Scala](examples/test-scala-application.md)
+ - [Phoenix](examples/test-phoenix-application.md)
+ - [Run PHP Composer & NPM scripts then deploy them to a staging server](examples/deployment/composer-npm-deploy.md)
+- **Blog posts**
+ - [Automated Debian packaging](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/)
+ - [Spring boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/)
+ - [Setting up CI for iOS projects](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/)
+ - [Using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
+ - [Setting up GitLab CI for Android projects](https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/)
+ - [Building a new GitLab Docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
+ - [CI/CD with GitLab in action](https://about.gitlab.com/2017/03/13/ci-cd-demo/)
+ - [Building an Elixir Release into a Docker image using GitLab CI](https://about.gitlab.com/2016/08/11/building-an-elixir-release-into-docker-image-using-gitlab-ci-part-1/)
+- **Miscellaneous**
+ - [Using `dpl` as deployment tool](examples/deployment/README.md)
+ - [Repositories with examples for various languages](https://gitlab.com/groups/gitlab-examples)
+ - [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
+ - [Example project that shows how to use Review Apps](https://gitlab.com/gitlab-examples/review-apps-nginx/)
+
+## Integrations
+
+- **Articles:**
+ - [Continuous Delivery with GitLab and Convox](https://about.gitlab.com/2016/06/09/continuous-delivery-with-gitlab-and-convox/)
+ - [Getting Started with GitLab and Shippable Continuous Integration](https://about.gitlab.com/2016/05/05/getting-started-gitlab-and-shippable/)
+ - [GitLab Partners with DigitalOcean to make Continuous Integration faster, safer, and more affordable](https://about.gitlab.com/2016/04/19/gitlab-partners-with-digitalocean-to-make-continuous-integration-faster-safer-and-more-affordable/)
+
+## Why GitLab CI?
+
+- **Articles:**
+ - [Why We Chose GitLab CI for our CI/CD Solution](https://about.gitlab.com/2016/10/17/gitlab-ci-oohlala/)
+ - [Building our web-app on GitLab CI: 5 reasons why Captain Train migrated from Jenkins to GitLab CI](https://about.gitlab.com/2016/07/22/building-our-web-app-on-gitlab-ci/)
## Breaking changes
-- [CI variables renaming](variables/README.md#9-0-renaming) Read about the
+- [CI variables renaming for GitLab 9.0](variables/README.md#9-0-renaming) Read about the
deprecated CI variables and what you should use for GitLab 9.0+.
- [New CI job permissions model](../user/project/new_ci_build_permissions_model.md)
Read about what changed in GitLab 8.12 and how that affects your jobs.
There's a new way to access your Git submodules and LFS objects in jobs.
+
+[gitlab-ci-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index 5377bf9ee80..33c27b39a8a 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -1,4 +1,4 @@
-# CI Examples
+# GitLab CI Examples
A collection of `.gitlab-ci.yml` files is maintained at the [GitLab CI Yml project][gitlab-ci-templates].
If your favorite programming language or framework are missing we would love your help by sending a merge request
@@ -6,22 +6,73 @@ with a `.gitlab-ci.yml`.
Apart from those, here is an collection of tutorials and guides on setting up your CI pipeline:
+## Languages, frameworks, OSs
+
+### PHP
+
- [Testing a PHP application](php.md)
+- [Run PHP Composer & NPM scripts then deploy them to a staging server](deployment/composer-npm-deploy.md)
+
+### Ruby
+
- [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
+
+### Python
+
- [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
-- [Test a Clojure application](test-clojure-application.md)
+
+### Java
+
+- **Articles:**
+ - [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/)
+
+### Scala
+
- [Test a Scala application](test-scala-application.md)
+
+### Clojure
+
+- [Test a Clojure application](test-clojure-application.md)
+
+### Elixir
+
- [Test a Phoenix application](test-phoenix-application.md)
-- [Using `dpl` as deployment tool](deployment/README.md)
-- [Example project that shows how to use Review Apps](https://gitlab.com/gitlab-examples/review-apps-nginx/)
-- [Run PHP Composer & NPM scripts then deploy them to a staging server](deployment/composer-npm-deploy.md)
-- Help your favorite programming language and GitLab by sending a merge request
- with a guide for that language.
+- **Articles:**
+ - [Building an Elixir Release into a Docker image using GitLab CI](https://about.gitlab.com/2016/08/11/building-an-elixir-release-into-docker-image-using-gitlab-ci-part-1/)
+
+### iOS
+
+- **Articles:**
+ - [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
+
+### Android
-## Outside the documentation
+- **Articles:**
+ - [Setting up GitLab CI for Android projects](https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/)
-- [Blog post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
+### Other
+
+- [Using `dpl` as deployment tool](deployment/README.md)
- [Repositories with examples for various languages](https://gitlab.com/groups/gitlab-examples)
- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
+- **Articles:**
+ - [Continuous Deployment with GitLab: how to build and deploy a Debian Package with GitLab CI](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/)
+
+## GitLab CI for GitLab Pages
+
+- [Example projects](https://gitlab.com/pages)
+- **Articles:**
+ - [Creating and Tweaking `.gitlab-ci.yml` for GitLab Pages](../../user/project/pages/getting_started_part_four.md)
+ - [SSGs Part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/):
+ examples for Ruby-, NodeJS-, Python-, and GoLang-based SSGs
+ - [Building a new GitLab docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
+ - [Publish code coverage reports with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/)
+
+See the topic [GitLab Pages](../../user/project/pages/index.md) for a complete overview.
+
+## More
+
+Contributions are very much welcomed! You can help your favorite programming
+language and GitLab by sending a merge request with a guide for that language.
[gitlab-ci-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml
diff --git a/doc/ci/img/cicd_pipeline_infograph.png b/doc/ci/img/cicd_pipeline_infograph.png
new file mode 100644
index 00000000000..9ddd4aa828b
--- /dev/null
+++ b/doc/ci/img/cicd_pipeline_infograph.png
Binary files differ
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index fd8335d251e..587922d0136 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -58,10 +58,22 @@ migration was tested.
## Removing indices
-If you need to remove index, please add a condition like in following example:
+When removing an index make sure to use the method `remove_concurrent_index` instead
+of the regular `remove_index` method. The `remove_concurrent_index` method
+automatically drops concurrent indexes when using PostgreSQL, removing the
+need for downtime. To use this method you must disable transactions by calling
+the method `disable_ddl_transaction!` in the body of your migration class like
+so:
```ruby
-remove_index :namespaces, column: :name if index_exists?(:namespaces, :name)
+class MyMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ def up
+ remove_concurrent_index :table_name, :column_name if index_exists?(:table_name, :column_name)
+ end
+end
```
## Adding indices
diff --git a/generator_templates/active_record/migration/create_table_migration.rb b/generator_templates/active_record/migration/create_table_migration.rb
index aad8626a720..59a9d37df0f 100644
--- a/generator_templates/active_record/migration/create_table_migration.rb
+++ b/generator_templates/active_record/migration/create_table_migration.rb
@@ -12,12 +12,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration
# migration requires downtime.
# DOWNTIME_REASON = ''
- # When using the methods "add_concurrent_index" or "add_column_with_default"
- # you must disable the use of transactions as these methods can not run in an
- # existing transaction. When using "add_concurrent_index" make sure that this
- # method is the _only_ method called in the migration, any other changes
- # should go in a separate migration. This ensures that upon failure _only_ the
- # index creation fails and can be retried or reverted easily.
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
diff --git a/generator_templates/active_record/migration/migration.rb b/generator_templates/active_record/migration/migration.rb
index 825bc8bdf61..08752b3af50 100644
--- a/generator_templates/active_record/migration/migration.rb
+++ b/generator_templates/active_record/migration/migration.rb
@@ -12,12 +12,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration
# migration requires downtime.
# DOWNTIME_REASON = ''
- # When using the methods "add_concurrent_index" or "add_column_with_default"
- # you must disable the use of transactions as these methods can not run in an
- # existing transaction. When using "add_concurrent_index" make sure that this
- # method is the _only_ method called in the migration, any other changes
- # should go in a separate migration. This ensures that upon failure _only_ the
- # index creation fails and can be retried or reverted easily.
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
diff --git a/generator_templates/rails/post_deployment_migration/migration.rb b/generator_templates/rails/post_deployment_migration/migration.rb
index 1a7b8d5bf35..f2dff84b618 100644
--- a/generator_templates/rails/post_deployment_migration/migration.rb
+++ b/generator_templates/rails/post_deployment_migration/migration.rb
@@ -6,12 +6,14 @@ class <%= migration_class_name %> < ActiveRecord::Migration
DOWNTIME = false
- # When using the methods "add_concurrent_index" or "add_column_with_default"
- # you must disable the use of transactions as these methods can not run in an
- # existing transaction. When using "add_concurrent_index" make sure that this
- # method is the _only_ method called in the migration, any other changes
- # should go in a separate migration. This ensures that upon failure _only_ the
- # index creation fails and can be retried or reverted easily.
+ # When using the methods "add_concurrent_index", "remove_concurrent_index" or
+ # "add_column_with_default" you must disable the use of transactions
+ # as these methods can not run in an existing transaction.
+ # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure
+ # that either of them is the _only_ method called in the migration,
+ # any other changes should go in a separate migration.
+ # This ensures that upon failure _only_ the index creation or removing fails
+ # and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index b888ede6fe8..8a54f7f3f05 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -47,6 +47,7 @@ module API
params do
requires :key, type: String, desc: 'The new deploy key'
requires :title, type: String, desc: 'The name of the deploy key'
+ optional :can_push, type: Boolean, desc: "Can deploy key push to the project's repository"
end
post ":id/deploy_keys" do
params[:key].strip!
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 8f3799417e3..605769eddde 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -142,7 +142,7 @@ module API
end
get ":id/projects" do
group = find_group!(params[:id])
- projects = GroupProjectsFinder.new(group).execute(current_user)
+ projects = GroupProjectsFinder.new(group: group, current_user: current_user).execute
projects = filter_projects(projects)
entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project
present paginate(projects), with: entity, current_user: current_user
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 4dce5dd130a..09053e615cb 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -26,6 +26,7 @@ module API
desc: 'Return issues sorted in `asc` or `desc` order.'
optional :milestone, type: String, desc: 'Return issues for a specific milestone'
optional :iids, type: Array[Integer], desc: 'The IID array of issues'
+ optional :search, type: String, desc: 'Search issues for text present in the title or description'
use :pagination
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 0fbe1669d45..766fbea53e6 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -84,7 +84,7 @@ module API
end
get do
entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
- present_projects ProjectsFinder.new.execute(current_user), with: entity, statistics: params[:statistics]
+ present_projects ProjectsFinder.new(current_user: current_user).execute, with: entity, statistics: params[:statistics]
end
desc 'Create new project' do
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 530ca0b5235..992a751b37d 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -341,7 +341,7 @@ module API
not_found!('User') unless user
events = user.events.
- merge(ProjectsFinder.new.execute(current_user)).
+ merge(ProjectsFinder.new(current_user: current_user).execute).
references(:project).
with_associations.
recent
diff --git a/lib/api/v3/groups.rb b/lib/api/v3/groups.rb
index c5b37622d79..9b27411ae21 100644
--- a/lib/api/v3/groups.rb
+++ b/lib/api/v3/groups.rb
@@ -151,7 +151,7 @@ module API
end
get ":id/projects" do
group = find_group!(params[:id])
- projects = GroupProjectsFinder.new(group).execute(current_user)
+ projects = GroupProjectsFinder.new(group: group, current_user: current_user).execute
projects = filter_projects(projects)
entity = params[:simple] ? ::API::Entities::BasicProjectDetails : Entities::Project
present paginate(projects), with: entity, current_user: current_user
diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb
index b753dbab381..ba9748ada59 100644
--- a/lib/api/v3/projects.rb
+++ b/lib/api/v3/projects.rb
@@ -107,7 +107,7 @@ module API
end
get '/visible' do
entity = current_user ? ::API::V3::Entities::ProjectWithAccess : ::API::Entities::BasicProjectDetails
- present_projects ProjectsFinder.new.execute(current_user), with: entity
+ present_projects ProjectsFinder.new(current_user: current_user).execute, with: entity
end
desc 'Get a projects list for authenticated user' do
diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb
index 5e18cecc431..f4cda3b2eba 100644
--- a/lib/api/v3/users.rb
+++ b/lib/api/v3/users.rb
@@ -138,7 +138,7 @@ module API
not_found!('User') unless user
events = user.events.
- merge(ProjectsFinder.new.execute(current_user)).
+ merge(ProjectsFinder.new(current_user: current_user).execute).
references(:project).
with_associations.
recent
diff --git a/lib/banzai/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb
index ff580ec68f8..ee73fa91589 100644
--- a/lib/banzai/filter/markdown_filter.rb
+++ b/lib/banzai/filter/markdown_filter.rb
@@ -14,7 +14,7 @@ module Banzai
def self.renderer
@renderer ||= begin
- renderer = Redcarpet::Render::HTML.new
+ renderer = Banzai::Renderer::HTML.new
Redcarpet::Markdown.new(renderer, redcarpet_options)
end
end
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index d5f9e252f62..522217deae4 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -24,10 +24,6 @@ module Banzai
# Only push these customizations once
return if customized?(whitelist[:transformers])
- # Allow code highlighting
- whitelist[:attributes]['pre'] = %w(class v-pre)
- whitelist[:attributes]['span'] = %w(class)
-
# Allow table alignment
whitelist[:attributes]['th'] = %w(style)
whitelist[:attributes]['td'] = %w(style)
@@ -52,9 +48,6 @@ module Banzai
# Remove `rel` attribute from `a` elements
whitelist[:transformers].push(self.class.remove_rel)
- # Remove `class` attribute from non-highlight spans
- whitelist[:transformers].push(self.class.clean_spans)
-
whitelist
end
@@ -84,21 +77,6 @@ module Banzai
end
end
end
-
- def clean_spans
- lambda do |env|
- node = env[:node]
-
- return unless node.name == 'span'
- return unless node.has_attribute?('class')
-
- unless node.ancestors.any? { |n| n.name.casecmp('pre').zero? }
- node.remove_attribute('class')
- end
-
- { node_whitelist: [node] }
- end
- end
end
end
end
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index 9f09ca90697..7da565043d1 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -14,7 +14,7 @@ module Banzai
end
def highlight_node(node)
- language = node.attr('class')
+ language = node.attr('lang')
code = node.text
css_classes = "code highlight"
lexer = lexer_for(language)
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index fd4a6a107c2..bd4d1aa9ff8 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -9,9 +9,9 @@ module Banzai
# The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb.
def self.filters
@filters ||= FilterArray[
- Filter::SyntaxHighlightFilter,
Filter::PlantumlFilter,
Filter::SanitizationFilter,
+ Filter::SyntaxHighlightFilter,
Filter::MathFilter,
Filter::UploadLinkFilter,
diff --git a/lib/banzai/renderer/html.rb b/lib/banzai/renderer/html.rb
new file mode 100644
index 00000000000..252caa35947
--- /dev/null
+++ b/lib/banzai/renderer/html.rb
@@ -0,0 +1,13 @@
+module Banzai
+ module Renderer
+ class HTML < Redcarpet::Render::HTML
+ def block_code(code, lang)
+ lang_attr = lang ? %Q{ lang="#{lang}"} : ''
+
+ "\n<pre>" \
+ "<code#{lang_attr}>#{html_escape(code)}</code>" \
+ "</pre>"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index fc445ab9483..525aa920328 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -26,6 +26,30 @@ module Gitlab
add_index(table_name, column_name, options)
end
+ # Removes an existed index, concurrently when supported
+ #
+ # On PostgreSQL this method removes an index concurrently.
+ #
+ # Example:
+ #
+ # remove_concurrent_index :users, :some_column
+ #
+ # See Rails' `remove_index` for more info on the available arguments.
+ def remove_concurrent_index(table_name, column_name, options = {})
+ if transaction_open?
+ raise 'remove_concurrent_index can not be run inside a transaction, ' \
+ 'you can disable transactions by calling disable_ddl_transaction! ' \
+ 'in the body of your migration class'
+ end
+
+ if Database.postgresql?
+ options = options.merge({ algorithm: :concurrently })
+ disable_statement_timeout
+ end
+
+ remove_index(table_name, options.merge({ column: column_name }))
+ end
+
# Adds a foreign key with only minimal locking on the tables involved.
#
# This method only requires minimal locking when using PostgreSQL. When
diff --git a/lib/gitlab/etag_caching/middleware.rb b/lib/gitlab/etag_caching/middleware.rb
index ab8dfc67880..cd4e318033d 100644
--- a/lib/gitlab/etag_caching/middleware.rb
+++ b/lib/gitlab/etag_caching/middleware.rb
@@ -3,7 +3,8 @@ module Gitlab
class Middleware
RESERVED_WORDS = NamespaceValidator::WILDCARD_ROUTES.map { |word| "/#{word}/" }.join('|')
ROUTE_REGEXP = Regexp.union(
- %r(^(?!.*(#{RESERVED_WORDS})).*/noteable/issue/\d+/notes\z)
+ %r(^(?!.*(#{RESERVED_WORDS})).*/noteable/issue/\d+/notes\z),
+ %r(^(?!.*(#{RESERVED_WORDS})).*/issues/\d+/rendered_title\z)
)
def initialize(app)
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index fe15fb12adb..bcdf1b1faa8 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -4,11 +4,23 @@ module Gitlab
module GitalyClient
SERVER_VERSION_FILE = 'GITALY_SERVER_VERSION'.freeze
- def self.configure_channel(storage, address)
- @addresses ||= {}
- @addresses[storage] = address
- @channels ||= {}
- @channels[storage] = new_channel(address)
+ # This function is not thread-safe because it updates Hashes in instance variables.
+ def self.configure_channels
+ @addresses = {}
+ @channels = {}
+ Gitlab.config.repositories.storages.each do |name, params|
+ address = params['gitaly_address']
+ unless address.present?
+ raise "storage #{name.inspect} is missing a gitaly_address"
+ end
+
+ unless URI(address).scheme.in?(%w(tcp unix))
+ raise "Unsupported Gitaly address: #{address.inspect}"
+ end
+
+ @addresses[name] = address
+ @channels[name] = new_channel(address)
+ end
end
def self.new_channel(address)
@@ -21,10 +33,26 @@ module Gitlab
end
def self.get_channel(storage)
+ if !Rails.env.production? && @channels.nil?
+ # In development mode the Rails auto-loader may reset the instance
+ # variables of this class. What we do here is not thread-safe. In normal
+ # circumstances (including production) these instance variables have
+ # been initialized from config/initializers.
+ configure_channels
+ end
+
@channels[storage]
end
def self.get_address(storage)
+ if !Rails.env.production? && @addresses.nil?
+ # In development mode the Rails auto-loader may reset the instance
+ # variables of this class. What we do here is not thread-safe. In normal
+ # circumstances (including development) these instance variables have
+ # been initialized from config/initializers.
+ configure_channels
+ end
+
@addresses[storage]
end
diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb
index 11e5f1b645c..ca8d3271541 100644
--- a/lib/gitlab/sidekiq_status.rb
+++ b/lib/gitlab/sidekiq_status.rb
@@ -72,6 +72,8 @@ module Gitlab
# job_ids - The Sidekiq job IDs to check.
#
# Returns an array of true or false indicating job completion.
+ # true = job is still running
+ # false = job completed
def self.job_status(job_ids)
keys = job_ids.map { |jid| key_for(jid) }
@@ -82,6 +84,17 @@ module Gitlab
end
end
+ # Returns the JIDs that are completed
+ #
+ # job_ids - The Sidekiq job IDs to check.
+ #
+ # Returns an array of completed JIDs
+ def self.completed_jids(job_ids)
+ Sidekiq.redis do |redis|
+ job_ids.reject { |jid| redis.exists(key_for(jid)) }
+ end
+ end
+
def self.key_for(jid)
STATUS_KEY % jid
end
diff --git a/lib/gitlab/sidekiq_status/client_middleware.rb b/lib/gitlab/sidekiq_status/client_middleware.rb
index d47609f490d..00983b3284a 100644
--- a/lib/gitlab/sidekiq_status/client_middleware.rb
+++ b/lib/gitlab/sidekiq_status/client_middleware.rb
@@ -2,7 +2,9 @@ module Gitlab
module SidekiqStatus
class ClientMiddleware
def call(_, job, _, _)
- Gitlab::SidekiqStatus.set(job['jid'])
+ status_expiration = job['status_expiration'] || Gitlab::SidekiqStatus::DEFAULT_EXPIRATION
+
+ Gitlab::SidekiqStatus.set(job['jid'], status_expiration)
yield
end
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 08011301d3c..a8a7bf9bc12 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -45,12 +45,7 @@ module Gitlab
raise "Unsupported action: #{action}"
end
- if feature_enabled
- params[:GitalyAddress] = address
- # TODO deprecate GitalySocketPath once GITLAB_WORKHORSE_VERSION points
- # to a version that supports GitalyAddress.
- params[:GitalySocketPath] = URI(address).path
- end
+ params[:GitalyAddress] = address if feature_enabled
end
params
diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake
new file mode 100644
index 00000000000..350afeb5c0b
--- /dev/null
+++ b/lib/tasks/import.rake
@@ -0,0 +1,204 @@
+require 'benchmark'
+require 'rainbow/ext/string'
+require_relative '../gitlab/shell_adapter'
+require_relative '../gitlab/github_import/importer'
+
+class NewImporter < ::Gitlab::GithubImport::Importer
+ def execute
+ # Same as ::Gitlab::GithubImport::Importer#execute, but showing some progress.
+ puts 'Importing repository...'.color(:aqua)
+ import_repository unless project.repository_exists?
+
+ puts 'Importing labels...'.color(:aqua)
+ import_labels
+
+ puts 'Importing milestones...'.color(:aqua)
+ import_milestones
+
+ puts 'Importing pull requests...'.color(:aqua)
+ import_pull_requests
+
+ puts 'Importing issues...'.color(:aqua)
+ import_issues
+
+ puts 'Importing issue comments...'.color(:aqua)
+ import_comments(:issues)
+
+ puts 'Importing pull request comments...'.color(:aqua)
+ import_comments(:pull_requests)
+
+ puts 'Importing wiki...'.color(:aqua)
+ import_wiki
+
+ # Gitea doesn't have a Release API yet
+ # See https://github.com/go-gitea/gitea/issues/330
+ unless project.gitea_import?
+ import_releases
+ end
+
+ handle_errors
+
+ project.repository.after_import
+ project.import_finish
+
+ true
+ end
+
+ def import_repository
+ begin
+ raise '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
+ project.repository.before_import if project.repository_exists?
+
+ raise "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}"
+ end
+ end
+end
+
+class GithubImport
+ def self.run!(*args)
+ new(*args).run!
+ end
+
+ def initialize(token, gitlab_username, project_path, extras)
+ @token = token
+ @project_path = project_path
+ @current_user = User.find_by_username(gitlab_username)
+ @github_repo = extras.empty? ? nil : extras.first
+ end
+
+ def run!
+ @repo = GithubRepos.new(@token, @current_user, @github_repo).choose_one!
+
+ raise 'No repo found!' unless @repo
+
+ show_warning!
+
+ @project = Project.find_by_full_path(@project_path) || new_project
+
+ import!
+ end
+
+ private
+
+ def show_warning!
+ puts "This will import GH #{@repo.full_name.bright} into GL #{@project_path.bright} as #{@current_user.name}"
+ puts "Permission checks are ignored. Press any key to continue.".color(:red)
+
+ STDIN.getch
+
+ puts 'Starting the import...'.color(:green)
+ end
+
+ def import!
+ import_url = @project.import_url.gsub(/\:\/\/(.*@)?/, "://#{@token}@")
+ @project.update(import_url: import_url)
+
+ @project.import_start
+
+ timings = Benchmark.measure do
+ NewImporter.new(@project).execute
+ end
+
+ puts "Import finished. Timings: #{timings}".color(:green)
+ end
+
+ def new_project
+ Project.transaction do
+ namespace_path, _sep, name = @project_path.rpartition('/')
+ namespace = find_or_create_namespace(namespace_path)
+
+ Project.create!(
+ import_url: "https://#{@token}@github.com/#{@repo.full_name}.git",
+ name: name,
+ path: name,
+ description: @repo.description,
+ namespace: namespace,
+ visibility_level: visibility_level,
+ import_type: 'github',
+ import_source: @repo.full_name,
+ creator: @current_user
+ )
+ end
+ end
+
+ def find_or_create_namespace(names)
+ return @current_user.namespace if names == @current_user.namespace_path
+ return @current_user.namespace unless @current_user.can_create_group?
+
+ names = params[:target_namespace].presence || names
+ full_path_namespace = Namespace.find_by_full_path(names)
+
+ return full_path_namespace if full_path_namespace
+
+ names.split('/').inject(nil) do |parent, name|
+ begin
+ namespace = Group.create!(name: name,
+ path: name,
+ owner: @current_user,
+ parent: parent)
+ namespace.add_owner(@current_user)
+
+ namespace
+ rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
+ Namespace.where(parent: parent).find_by_path_or_name(name)
+ end
+ end
+ end
+
+ def full_path_namespace(names)
+ @full_path_namespace ||= Namespace.find_by_full_path(names)
+ end
+
+ def visibility_level
+ @repo.private ? Gitlab::VisibilityLevel::PRIVATE : current_application_settings.default_project_visibility
+ end
+end
+
+class GithubRepos
+ def initialize(token, current_user, github_repo)
+ @token = token
+ @current_user = current_user
+ @github_repo = github_repo
+ end
+
+ def choose_one!
+ return found_github_repo if @github_repo
+
+ repos.each do |repo|
+ print "ID: #{repo[:id].to_s.bright} ".color(:green)
+ puts "- Name: #{repo[:full_name]}".color(:green)
+ end
+
+ print 'ID? '.bright
+
+ repos.find { |repo| repo[:id] == repo_id }
+ end
+
+ def found_github_repo
+ repos.find { |repo| repo[:full_name] == @github_repo }
+ end
+
+ def repo_id
+ @repo_id ||= STDIN.gets.chomp.to_i
+ end
+
+ def repos
+ @repos ||= client.repos
+ end
+
+ def client
+ @client ||= Gitlab::GithubImport::Client.new(@token, {})
+ end
+end
+
+namespace :import do
+ desc 'Import a GitHub project - Example: import:github[ToKeN,root,root/blah,my/github_repo] (optional my/github_repo)'
+ task :github, [:token, :gitlab_username, :project_path] => :environment do |_t, args|
+ abort 'Project path must be: namespace(s)/project_name'.color(:red) unless args.project_path.include?('/')
+
+ GithubImport.run!(args.token, args.gitlab_username, args.project_path, args.extras)
+ end
+end
diff --git a/package.json b/package.json
index 3f64d65d57a..312e38f7407 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,9 @@
"raw-loader": "^0.5.1",
"select2": "3.5.2-browserify",
"stats-webpack-plugin": "^0.4.3",
+ "three": "^0.84.0",
+ "three-orbit-controls": "^82.1.0",
+ "three-stl-loader": "^1.0.4",
"timeago.js": "^2.0.5",
"underscore": "^1.8.3",
"visibilityjs": "^1.2.4",
diff --git a/rubocop/cop/migration/add_concurrent_index.rb b/rubocop/cop/migration/add_concurrent_index.rb
index 332fb7dcbd7..69852f4d580 100644
--- a/rubocop/cop/migration/add_concurrent_index.rb
+++ b/rubocop/cop/migration/add_concurrent_index.rb
@@ -9,7 +9,7 @@ module RuboCop
include MigrationHelpers
MSG = '`add_concurrent_index` is not reversible so you must manually define ' \
- 'the `up` and `down` methods in your migration class, using `remove_index` in `down`'.freeze
+ 'the `up` and `down` methods in your migration class, using `remove_concurrent_index` in `down`'.freeze
def on_send(node)
return unless in_migration?(node)
diff --git a/rubocop/cop/migration/remove_concurrent_index.rb b/rubocop/cop/migration/remove_concurrent_index.rb
new file mode 100644
index 00000000000..268c49865cb
--- /dev/null
+++ b/rubocop/cop/migration/remove_concurrent_index.rb
@@ -0,0 +1,29 @@
+require_relative '../../migration_helpers'
+
+module RuboCop
+ module Cop
+ module Migration
+ # Cop that checks if `remove_concurrent_index` is used with `up`/`down` methods
+ # and not `change`.
+ class RemoveConcurrentIndex < RuboCop::Cop::Cop
+ include MigrationHelpers
+
+ MSG = '`remove_concurrent_index` is not reversible so you must manually define ' \
+ 'the `up` and `down` methods in your migration class, using `add_concurrent_index` in `down`'.freeze
+
+ def on_send(node)
+ return unless in_migration?(node)
+ return unless node.children[1] == :remove_concurrent_index
+
+ node.each_ancestor(:def) do |def_node|
+ add_offense(def_node, :name) if method_name(def_node) == :change
+ end
+ end
+
+ def method_name(node)
+ node.children[0]
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/cop/migration/remove_index.rb b/rubocop/cop/migration/remove_index.rb
new file mode 100644
index 00000000000..613b35dd00d
--- /dev/null
+++ b/rubocop/cop/migration/remove_index.rb
@@ -0,0 +1,26 @@
+require_relative '../../migration_helpers'
+
+module RuboCop
+ module Cop
+ module Migration
+ # Cop that checks if indexes are removed in a concurrent manner.
+ class RemoveIndex < RuboCop::Cop::Cop
+ include MigrationHelpers
+
+ MSG = '`remove_index` requires downtime, use `remove_concurrent_index` instead'.freeze
+
+ def on_def(node)
+ return unless in_migration?(node)
+
+ node.each_descendant(:send) do |send_node|
+ add_offense(send_node, :selector) if method_name(send_node) == :remove_index
+ end
+ end
+
+ def method_name(node)
+ node.children[1]
+ end
+ end
+ end
+ end
+end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index a50a522cf9d..d580aa6857a 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -5,3 +5,5 @@ require_relative 'cop/migration/add_column_with_default'
require_relative 'cop/migration/add_concurrent_foreign_key'
require_relative 'cop/migration/add_concurrent_index'
require_relative 'cop/migration/add_index'
+require_relative 'cop/migration/remove_concurrent_index'
+require_relative 'cop/migration/remove_index'
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index 71a4a2c43c7..6075259ea99 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -35,6 +35,13 @@ describe Dashboard::TodosController do
expect(assigns(:todos).current_page).to eq(last_page)
expect(response).to have_http_status(200)
end
+
+ it 'does not redirect to external sites when provided a host field' do
+ external_host = "www.example.com"
+ get :index, page: (last_page + 1).to_param, host: external_host
+
+ expect(response).to redirect_to(dashboard_todos_path(page: last_page))
+ end
end
end
diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb
index 7c75815f3c4..6724b474179 100644
--- a/spec/controllers/projects/imports_controller_spec.rb
+++ b/spec/controllers/projects/imports_controller_spec.rb
@@ -96,12 +96,19 @@ describe Projects::ImportsController do
}
end
- it 'redirects to params[:to]' do
+ it 'redirects to internal params[:to]' do
get :show, namespace_id: project.namespace.to_param, project_id: project, continue: params
expect(flash[:notice]).to eq params[:notice]
expect(response).to redirect_to params[:to]
end
+
+ it 'does not redirect to external params[:to]' do
+ params[:to] = "//google.com"
+
+ get :show, namespace_id: project.namespace.to_param, project_id: project, continue: params
+ expect(response).not_to redirect_to params[:to]
+ end
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 734966d50b2..d5f1d46bf7f 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -83,6 +83,17 @@ describe Projects::IssuesController do
expect(assigns(:issues).current_page).to eq(last_page)
expect(response).to have_http_status(200)
end
+
+ it 'does not redirect to external sites when provided a host field' do
+ external_host = "www.example.com"
+ get :index,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ page: (last_page + 1).to_param,
+ host: external_host
+
+ expect(response).to redirect_to(namespace_project_issues_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
+ end
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 72f41f7209a..99d5583e683 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -176,6 +176,18 @@ describe Projects::MergeRequestsController do
expect(assigns(:merge_requests).current_page).to eq(last_page)
expect(response).to have_http_status(200)
end
+
+ it 'does not redirect to external sites when provided a host field' do
+ external_host = "www.example.com"
+ get :index,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ state: 'opened',
+ page: (last_page + 1).to_param,
+ host: external_host
+
+ expect(response).to redirect_to(namespace_project_merge_requests_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
+ end
end
context 'when filtering by opened state' do
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 1fe5ed25f71..b62def83ee4 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -192,5 +192,10 @@ FactoryGirl.define do
trait :no_options do
options { {} }
end
+
+ trait :non_playable do
+ status 'created'
+ self.when 'manual'
+ end
end
end
diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb
index 0852dda6b29..3fbf24b5c7d 100644
--- a/spec/factories/environments.rb
+++ b/spec/factories/environments.rb
@@ -32,5 +32,10 @@ FactoryGirl.define do
environment.update_attribute(:deployments, [deployment])
end
end
+
+ trait :non_playable do
+ status 'created'
+ self.when 'manual'
+ end
end
end
diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb
index dd93b439b2b..4e140102492 100644
--- a/spec/factories/keys.rb
+++ b/spec/factories/keys.rb
@@ -23,5 +23,9 @@ FactoryGirl.define do
factory :another_deploy_key, class: 'DeployKey' do
end
end
+
+ factory :write_access_key, class: 'DeployKey' do
+ can_push true
+ end
end
end
diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb
index 84d73d693bc..876f33dd03e 100644
--- a/spec/features/gitlab_flavored_markdown_spec.rb
+++ b/spec/features/gitlab_flavored_markdown_spec.rb
@@ -48,7 +48,9 @@ describe "GitLab Flavored Markdown", feature: true do
end
end
- describe "for issues" do
+ describe "for issues", feature: true, js: true do
+ include WaitForVueResource
+
before do
@other_issue = create(:issue,
author: @user,
@@ -79,6 +81,14 @@ describe "GitLab Flavored Markdown", feature: true do
expect(page).to have_link(fred.to_reference)
end
+
+ it "renders updated subject once edited somewhere else in issues#show" do
+ visit namespace_project_issue_path(project.namespace, project, @issue)
+ @issue.update(title: "fix #{@other_issue.to_reference} and update")
+
+ wait_for_vue_resource
+ expect(page).to have_text("fix #{@other_issue.to_reference} and update")
+ end
end
describe "for merge requests" do
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index 16e453bc328..8e67ab028d7 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -2,6 +2,7 @@ require 'rails_helper'
describe 'Awards Emoji', feature: true do
include WaitForAjax
+ include WaitForVueResource
let!(:project) { create(:project, :public) }
let!(:user) { create(:user) }
@@ -22,10 +23,11 @@ describe 'Awards Emoji', feature: true 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)
+ wait_for_vue_resource
end
# Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529
- it 'does not shows a 500 page' do
+ it 'does not shows a 500 page', js: true do
expect(page).to have_text(issue.title)
end
end
@@ -35,6 +37,7 @@ describe 'Awards Emoji', feature: true do
before do
visit namespace_project_issue_path(project.namespace, project, issue)
+ wait_for_vue_resource
end
it 'increments the thumbsdown emoji', js: true do
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index f89b4db9e62..6c09903a2f6 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -37,8 +37,8 @@ feature 'issue move to another project' do
edit_issue(issue)
end
- scenario 'moving issue to another project' do
- first('#move_to_project_id', visible: false).set(new_project.id)
+ scenario 'moving issue to another project', js: true do
+ find('#move_to_project_id', visible: false).set(new_project.id)
click_button('Save changes')
expect(current_url).to include project_path(new_project)
diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb
index 4bc9b49f889..6001476d0ca 100644
--- a/spec/features/issues/spam_issues_spec.rb
+++ b/spec/features/issues/spam_issues_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-describe 'New issue', feature: true do
+describe 'New issue', feature: true, js: true do
include StubENV
let(:project) { create(:project, :public) }
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 7afceb88cf9..e3213d24f6a 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -695,4 +695,21 @@ describe 'Issues', feature: true do
end
end
end
+
+ describe 'title issue#show', js: true do
+ include WaitForVueResource
+
+ it 'updates the title', js: true do
+ issue = create(:issue, author: @user, assignee: @user, project: project, title: 'new title')
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ expect(page).to have_text("new title")
+
+ issue.update(title: "updated title")
+
+ wait_for_vue_resource
+ expect(page).to have_text("updated title")
+ end
+ end
end
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index f36781167fb..d4fe67c224f 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -70,6 +70,18 @@ feature 'Create New Merge Request', feature: true, js: true do
visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_project_id: private_project.id })
expect(page).not_to have_content private_project.path_with_namespace
+ expect(page).to have_content project.path_with_namespace
+ end
+ end
+
+ context 'when source project cannot be viewed by the current user' do
+ it 'does not leak the private project name & namespace' do
+ private_project = create(:project, :private)
+
+ visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { source_project_id: private_project.id })
+
+ expect(page).not_to have_content private_project.path_with_namespace
+ expect(page).to have_content project.path_with_namespace
end
end
diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb
index ef97b061ca7..3c7c9bdcd08 100644
--- a/spec/finders/group_projects_finder_spec.rb
+++ b/spec/finders/group_projects_finder_spec.rb
@@ -3,8 +3,9 @@ require 'spec_helper'
describe GroupProjectsFinder do
let(:group) { create(:group) }
let(:current_user) { create(:user) }
+ let(:options) { {} }
- let(:finder) { described_class.new(source_user) }
+ let(:finder) { described_class.new(group: group, current_user: current_user, options: options) }
let!(:public_project) { create(:empty_project, :public, group: group, path: '1') }
let!(:private_project) { create(:empty_project, :private, group: group, path: '2') }
@@ -18,22 +19,27 @@ describe GroupProjectsFinder do
shared_project_3.project_group_links.create(group_access: Gitlab::Access::MASTER, group: group)
end
+ subject { finder.execute }
+
describe 'with a group member current user' do
- before { group.add_user(current_user, Gitlab::Access::MASTER) }
+ before do
+ group.add_master(current_user)
+ end
context "only shared" do
- subject { described_class.new(group, only_shared: true).execute(current_user) }
- it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1]) }
+ let(:options) { { only_shared: true } }
+
+ it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1]) }
end
context "only owned" do
- subject { described_class.new(group, only_owned: true).execute(current_user) }
- it { is_expected.to eq([private_project, public_project]) }
+ let(:options) { { only_owned: true } }
+
+ it { is_expected.to match_array([private_project, public_project]) }
end
context "all" do
- subject { described_class.new(group).execute(current_user) }
- it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) }
+ it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) }
end
end
@@ -44,47 +50,57 @@ describe GroupProjectsFinder do
end
context "only shared" do
+ let(:options) { { only_shared: true } }
+
context "without external user" do
- subject { described_class.new(group, only_shared: true).execute(current_user) }
- it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1]) }
+ it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1]) }
end
context "with external user" do
- before { current_user.update_attributes(external: true) }
- subject { described_class.new(group, only_shared: true).execute(current_user) }
- it { is_expected.to eq([shared_project_2, shared_project_1]) }
+ before do
+ current_user.update_attributes(external: true)
+ end
+
+ it { is_expected.to match_array([shared_project_2, shared_project_1]) }
end
end
context "only owned" do
+ let(:options) { { only_owned: true } }
+
context "without external user" do
- before { private_project.team << [current_user, Gitlab::Access::MASTER] }
- subject { described_class.new(group, only_owned: true).execute(current_user) }
- it { is_expected.to eq([private_project, public_project]) }
+ before do
+ private_project.team << [current_user, Gitlab::Access::MASTER]
+ end
+
+ it { is_expected.to match_array([private_project, public_project]) }
end
context "with external user" do
- before { current_user.update_attributes(external: true) }
- subject { described_class.new(group, only_owned: true).execute(current_user) }
- it { is_expected.to eq([public_project]) }
- end
+ before do
+ current_user.update_attributes(external: true)
+ end
- context "all" do
- subject { described_class.new(group).execute(current_user) }
- it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1, public_project]) }
+ it { is_expected.to eq([public_project]) }
end
end
+
+ context "all" do
+ it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, public_project]) }
+ end
end
describe "no user" do
context "only shared" do
- subject { described_class.new(group, only_shared: true).execute(current_user) }
- it { is_expected.to eq([shared_project_3, shared_project_1]) }
+ let(:options) { { only_shared: true } }
+
+ it { is_expected.to match_array([shared_project_3, shared_project_1]) }
end
context "only owned" do
- subject { described_class.new(group, only_owned: true).execute(current_user) }
- it { is_expected.to eq([public_project]) }
+ let(:options) { { only_owned: true } }
+
+ it { is_expected.to eq([public_project]) }
end
end
end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index e44e7434c80..148adcffe3b 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -21,38 +21,144 @@ describe ProjectsFinder do
create(:empty_project, :private, name: 'D', path: 'D')
end
- let(:finder) { described_class.new }
+ let(:params) { {} }
+ let(:current_user) { user }
+ let(:project_ids_relation) { nil }
+ let(:finder) { described_class.new(params: params, current_user: current_user, project_ids_relation: project_ids_relation) }
+
+ subject { finder.execute }
describe 'without a user' do
- subject { finder.execute }
+ let(:current_user) { nil }
it { is_expected.to eq([public_project]) }
end
describe 'with a user' do
- subject { finder.execute(user) }
-
describe 'without private projects' do
- it { is_expected.to eq([public_project, internal_project]) }
+ it { is_expected.to match_array([public_project, internal_project]) }
end
describe 'with private projects' do
before do
- private_project.add_user(user, Gitlab::Access::MASTER)
+ private_project.add_master(user)
end
- it do
- is_expected.to eq([public_project, internal_project, private_project])
- end
+ it { is_expected.to match_array([public_project, internal_project, private_project]) }
end
end
describe 'with project_ids_relation' do
let(:project_ids_relation) { Project.where(id: internal_project.id) }
- subject { finder.execute(user, project_ids_relation) }
-
it { is_expected.to eq([internal_project]) }
end
+
+ describe 'filter by visibility_level' do
+ before do
+ private_project.add_master(user)
+ end
+
+ context 'private' do
+ let(:params) { { visibility_level: Gitlab::VisibilityLevel::PRIVATE } }
+
+ it { is_expected.to eq([private_project]) }
+ end
+
+ context 'internal' do
+ let(:params) { { visibility_level: Gitlab::VisibilityLevel::INTERNAL } }
+
+ it { is_expected.to eq([internal_project]) }
+ end
+
+ context 'public' do
+ let(:params) { { visibility_level: Gitlab::VisibilityLevel::PUBLIC } }
+
+ it { is_expected.to eq([public_project]) }
+ end
+ end
+
+ describe 'filter by tags' do
+ before do
+ public_project.tag_list.add('foo')
+ public_project.save!
+ end
+
+ let(:params) { { tag: 'foo' } }
+
+ it { is_expected.to eq([public_project]) }
+ end
+
+ describe 'filter by personal' do
+ let!(:personal_project) { create(:empty_project, namespace: user.namespace) }
+ let(:params) { { personal: true } }
+
+ it { is_expected.to eq([personal_project]) }
+ end
+
+ describe 'filter by search' do
+ let(:params) { { search: 'C' } }
+
+ it { is_expected.to eq([public_project]) }
+ end
+
+ describe 'filter by name for backward compatibility' do
+ let(:params) { { name: 'C' } }
+
+ it { is_expected.to eq([public_project]) }
+ end
+
+ describe 'filter by archived' do
+ let!(:archived_project) { create(:empty_project, :public, :archived, name: 'E', path: 'E') }
+
+ context 'non_archived=true' do
+ let(:params) { { non_archived: true } }
+
+ it { is_expected.to match_array([public_project, internal_project]) }
+ end
+
+ context 'non_archived=false' do
+ let(:params) { { non_archived: false } }
+
+ it { is_expected.to match_array([public_project, internal_project, archived_project]) }
+ end
+
+ describe 'filter by archived for backward compatibility' do
+ let(:params) { { archived: false } }
+
+ it { is_expected.to match_array([public_project, internal_project]) }
+ end
+ end
+
+ describe 'filter by trending' do
+ let!(:trending_project) { create(:trending_project, project: public_project) }
+ let(:params) { { trending: true } }
+
+ it { is_expected.to eq([public_project]) }
+ end
+
+ describe 'filter by non_public' do
+ let(:params) { { non_public: true } }
+ before do
+ private_project.add_developer(current_user)
+ end
+
+ it { is_expected.to eq([private_project]) }
+ end
+
+ describe 'filter by viewable_starred_projects' do
+ let(:params) { { starred: true } }
+ before do
+ current_user.toggle_star(public_project)
+ end
+
+ it { is_expected.to eq([public_project]) }
+ end
+
+ describe 'sorting' do
+ let(:params) { { sort: 'name_asc' } }
+
+ it { is_expected.to eq([internal_project, public_project]) }
+ end
end
end
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index 70443d27f33..a7c3c281083 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -2,8 +2,10 @@ require 'spec_helper'
describe EventsHelper do
describe '#event_note' do
+ let(:user) { build(:user) }
+
before do
- allow(helper).to receive(:current_user).and_return(double)
+ allow(helper).to receive(:current_user).and_return(user)
end
it 'displays one line of plain text without alteration' do
@@ -60,11 +62,26 @@ describe EventsHelper do
expect(helper.event_note(input)).to eq(expected)
end
- it 'preserves style attribute within a tag' do
- input = '<span class="" style="background-color: #44ad8e; color: #FFFFFF;"></span>'
- expected = '<p><span style="background-color: #44ad8e; color: #FFFFFF;"></span></p>'
+ context 'labels formatting' do
+ let(:input) { 'this should be ~label_1' }
- expect(helper.event_note(input)).to eq(expected)
+ def format_event_note(project)
+ create(:label, title: 'label_1', project: project)
+
+ helper.event_note(input, { project: project })
+ end
+
+ it 'preserves style attribute for a label that can be accessed by current_user' do
+ project = create(:empty_project, :public)
+
+ expect(format_event_note(project)).to match(/span class=.*style=.*/)
+ end
+
+ it 'does not style a label that can not be accessed by current_user' do
+ project = create(:empty_project, :private)
+
+ expect(format_event_note(project)).to eq("<p>#{input}</p>")
+ end
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index fc6ad6419ac..44312ada438 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -167,6 +167,7 @@ describe ProjectsHelper do
before do
allow(project).to receive(:repository_storage_path).and_return('/base/repo/path')
+ allow(Settings.shared).to receive(:[]).with('path').and_return('/base/repo/export/path')
end
it 'removes the repo path' do
@@ -175,6 +176,13 @@ describe ProjectsHelper do
expect(sanitize_repo_path(project, import_error)).to eq('Could not clone [REPOS PATH]/namespace/test.git')
end
+
+ it 'removes the temporary repo path used for uploads/exports' do
+ repo = '/base/repo/export/path/tmp/project_exports/uploads/test.tar.gz'
+ import_error = "Unable to decompress #{repo}\n"
+
+ expect(sanitize_repo_path(project, import_error)).to eq('Unable to decompress [REPO EXPORT PATH]/uploads/test.tar.gz')
+ end
end
describe '#last_push_event' do
diff --git a/spec/javascripts/blob/3d_viewer/mesh_object_spec.js b/spec/javascripts/blob/3d_viewer/mesh_object_spec.js
new file mode 100644
index 00000000000..d1ebae33dab
--- /dev/null
+++ b/spec/javascripts/blob/3d_viewer/mesh_object_spec.js
@@ -0,0 +1,42 @@
+import {
+ BoxGeometry,
+} from 'three/build/three.module';
+import MeshObject from '~/blob/3d_viewer/mesh_object';
+
+describe('Mesh object', () => {
+ it('defaults to non-wireframe material', () => {
+ const object = new MeshObject(
+ new BoxGeometry(10, 10, 10),
+ );
+
+ expect(object.material.wireframe).toBeFalsy();
+ });
+
+ it('changes to wirefame material', () => {
+ const object = new MeshObject(
+ new BoxGeometry(10, 10, 10),
+ );
+
+ object.changeMaterial('wireframe');
+
+ expect(object.material.wireframe).toBeTruthy();
+ });
+
+ it('scales object down', () => {
+ const object = new MeshObject(
+ new BoxGeometry(10, 10, 10),
+ );
+ const radius = object.geometry.boundingSphere.radius;
+
+ expect(radius).not.toBeGreaterThan(4);
+ });
+
+ it('does not scale object down', () => {
+ const object = new MeshObject(
+ new BoxGeometry(1, 1, 1),
+ );
+ const radius = object.geometry.boundingSphere.radius;
+
+ expect(radius).toBeLessThan(1);
+ });
+});
diff --git a/spec/javascripts/blob/pdf/index_spec.js b/spec/javascripts/blob/pdf/index_spec.js
index 19a4e55a9db..d3a4d04345b 100644
--- a/spec/javascripts/blob/pdf/index_spec.js
+++ b/spec/javascripts/blob/pdf/index_spec.js
@@ -3,6 +3,18 @@ import testPDF from './test.pdf';
describe('PDF renderer', () => {
let viewer;
+ let app;
+
+ const checkLoaded = (done) => {
+ if (app.loading) {
+ setTimeout(() => {
+ checkLoaded(done);
+ }, 100);
+ } else {
+ done();
+ }
+ };
+
preloadFixtures('static/pdf_viewer.html.raw');
beforeEach(() => {
@@ -21,11 +33,9 @@ describe('PDF renderer', () => {
describe('successful response', () => {
beforeEach((done) => {
- renderPDF();
+ app = renderPDF();
- setTimeout(() => {
- done();
- }, 500);
+ checkLoaded(done);
});
it('does not show loading icon', () => {
@@ -50,11 +60,9 @@ describe('PDF renderer', () => {
describe('error getting file', () => {
beforeEach((done) => {
viewer.dataset.endpoint = 'invalid/endpoint';
- renderPDF();
+ app = renderPDF();
- setTimeout(() => {
- done();
- }, 500);
+ checkLoaded(done);
});
it('does not show loading icon', () => {
diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js
index a3b6b0235cb..cb48f14fd9a 100644
--- a/spec/javascripts/build_spec.js
+++ b/spec/javascripts/build_spec.js
@@ -77,6 +77,7 @@ describe('Build', () => {
);
expect(dataType).toBe('json');
expect(success).toEqual(jasmine.any(Function));
+ spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
success.call(context, { html: '<span>Example</span>', status: 'running' });
@@ -85,6 +86,7 @@ describe('Build', () => {
it('removes the spinner', () => {
const [{ success, context }] = $.ajax.calls.argsFor(0);
+ spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
success.call(context, { trace_html: '<span>Example</span>', status: 'success' });
expect($('.js-build-refresh').length).toBe(0);
diff --git a/spec/javascripts/environments/environment_actions_spec.js b/spec/javascripts/environments/environment_actions_spec.js
index 13840b42bd6..6348d97b0a5 100644
--- a/spec/javascripts/environments/environment_actions_spec.js
+++ b/spec/javascripts/environments/environment_actions_spec.js
@@ -19,6 +19,11 @@ describe('Actions Component', () => {
name: 'foo',
play_path: '#',
},
+ {
+ name: 'foo bar',
+ play_path: 'url',
+ playable: false,
+ },
];
spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
@@ -49,4 +54,14 @@ describe('Actions Component', () => {
expect(spy).toHaveBeenCalledWith(actionsMock[0].play_path);
});
+
+ it('should render a disabled action when it\'s not playable', () => {
+ expect(
+ component.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'),
+ ).toEqual('disabled');
+
+ expect(
+ component.$el.querySelector('.dropdown-menu li:last-child button').classList.contains('disabled'),
+ ).toEqual(true);
+ });
});
diff --git a/spec/javascripts/fixtures/environments/metrics.html.haml b/spec/javascripts/fixtures/environments/metrics.html.haml
index 483063fb889..e2dd9519898 100644
--- a/spec/javascripts/fixtures/environments/metrics.html.haml
+++ b/spec/javascripts/fixtures/environments/metrics.html.haml
@@ -1,12 +1,62 @@
-%div
+.prometheus-container{ 'data-has-metrics': "false", 'data-doc-link': '/help/administration/monitoring/prometheus/index.md', 'data-prometheus-integration': '/root/hello-prometheus/services/prometheus/edit' }
.top-area
.row
.col-sm-6
%h3.page-title
Metrics for environment
- .row
- .col-sm-12
- %svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
- .row
- .col-sm-12
- %svg.prometheus-graph{ 'graph-type' => 'memory_values' } \ No newline at end of file
+ .prometheus-state
+ .js-getting-started.hidden
+ .row
+ .col-md-4.col-md-offset-4.state-svg
+ %svg
+ .row
+ .col-md-6.col-md-offset-3
+ %h4.text-center.state-title
+ Get started with performance monitoring
+ .row
+ .col-md-6.col-md-offset-3
+ .description-text.text-center.state-description
+ Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments. Learn more about performance monitoring
+ .row.state-button-section
+ .col-md-4.col-md-offset-4.text-center.state-button
+ %a.btn.btn-success
+ Configure Prometheus
+ .js-loading.hidden
+ .row
+ .col-md-4.col-md-offset-4.state-svg
+ %svg
+ .row
+ .col-md-6.col-md-offset-3
+ %h4.text-center.state-title
+ Waiting for performance data
+ .row
+ .col-md-6.col-md-offset-3
+ .description-text.text-center.state-description
+ Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.
+ .row.state-button-section
+ .col-md-4.col-md-offset-4.text-center.state-button
+ %a.btn.btn-success
+ View documentation
+ .js-unable-to-connect.hidden
+ .row
+ .col-md-4.col-md-offset-4.state-svg
+ %svg
+ .row
+ .col-md-6.col-md-offset-3
+ %h4.text-center.state-title
+ Unable to connect to Prometheus server
+ .row
+ .col-md-6.col-md-offset-3
+ .description-text.text-center.state-description
+ Ensure connectivity is available from the GitLab server to the Prometheus server
+ .row.state-button-section
+ .col-md-4.col-md-offset-4.text-center.state-button
+ %a.btn.btn-success
+ View documentation
+ .prometheus-graphs
+ .row
+ .col-sm-12
+ %svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
+ .row
+ .col-sm-12
+ %svg.prometheus-graph{ 'graph-type' => 'memory_values' }
diff --git a/spec/javascripts/issue_show/issue_title_spec.js b/spec/javascripts/issue_show/issue_title_spec.js
new file mode 100644
index 00000000000..806d728a874
--- /dev/null
+++ b/spec/javascripts/issue_show/issue_title_spec.js
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import issueTitle from '~/issue_show/issue_title';
+
+describe('Issue Title', () => {
+ let IssueTitleComponent;
+
+ beforeEach(() => {
+ IssueTitleComponent = Vue.extend(issueTitle);
+ });
+
+ it('should render a title', () => {
+ const component = new IssueTitleComponent({
+ propsData: {
+ initialTitle: 'wow',
+ endpoint: '/gitlab-org/gitlab-shell/issues/9/rendered_title',
+ },
+ }).$mount();
+
+ expect(component.$el.classList).toContain('title');
+ expect(component.$el.innerHTML).toContain('wow');
+ });
+});
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index 5a93d479c1f..03f3c206f44 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -310,5 +310,56 @@ require('~/lib/utils/common_utils');
});
}, 10000);
});
+
+ describe('gl.utils.setFavicon', () => {
+ it('should set page favicon to provided favicon', () => {
+ const faviconName = 'custom_favicon';
+ const fakeLink = {
+ setAttribute() {},
+ };
+
+ spyOn(window.document, 'getElementById').and.callFake(() => fakeLink);
+ spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => {
+ expect(attr).toEqual('href');
+ expect(val.indexOf('/assets/custom_favicon.ico') > -1).toBe(true);
+ });
+ gl.utils.setFavicon(faviconName);
+ });
+ });
+
+ describe('gl.utils.resetFavicon', () => {
+ it('should reset page favicon to tanuki', () => {
+ const fakeLink = {
+ setAttribute() {},
+ };
+
+ spyOn(window.document, 'getElementById').and.callFake(() => fakeLink);
+ spyOn(fakeLink, 'setAttribute').and.callFake((attr, val) => {
+ expect(attr).toEqual('href');
+ expect(val).toMatch(/favicon/);
+ });
+ gl.utils.resetFavicon();
+ });
+ });
+
+ describe('gl.utils.setCiStatusFavicon', () => {
+ it('should set page favicon to CI status favicon based on provided status', () => {
+ const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1/status.json`;
+ const FAVICON_PATH = 'ci_favicons/';
+ const FAVICON = 'icon_status_success';
+ const spySetFavicon = spyOn(gl.utils, 'setFavicon').and.stub();
+ const spyResetFavicon = spyOn(gl.utils, 'resetFavicon').and.stub();
+ spyOn($, 'ajax').and.callFake(function (options) {
+ options.success({ icon: FAVICON });
+ expect(spySetFavicon).toHaveBeenCalledWith(FAVICON_PATH + FAVICON);
+ options.success();
+ expect(spyResetFavicon).toHaveBeenCalled();
+ options.error();
+ expect(spyResetFavicon).toHaveBeenCalled();
+ });
+
+ gl.utils.setCiStatusFavicon(BUILD_URL);
+ });
+ });
});
})();
diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js
index d5193b41c33..88dae8c3e06 100644
--- a/spec/javascripts/merge_request_widget_spec.js
+++ b/spec/javascripts/merge_request_widget_spec.js
@@ -142,18 +142,21 @@ require('~/lib/utils/datetime_utility');
it('should call showCIStatus even if a notification should not be displayed', function() {
var spy;
spy = spyOn(this["class"], 'showCIStatus').and.stub();
+ spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false);
return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status);
});
it('should call showCIStatus when a notification should be displayed', function() {
var spy;
spy = spyOn(this["class"], 'showCIStatus').and.stub();
+ spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(true);
return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status);
});
it('should call showCICoverage when the coverage rate is set', function() {
var spy;
spy = spyOn(this["class"], 'showCICoverage').and.stub();
+ spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false);
return expect(spy).toHaveBeenCalledWith(this.ciStatusData.coverage);
});
@@ -161,12 +164,14 @@ require('~/lib/utils/datetime_utility');
var spy;
this.ciStatusData.coverage = null;
spy = spyOn(this["class"], 'showCICoverage').and.stub();
+ spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false);
return expect(spy).not.toHaveBeenCalled();
});
it('should not display a notification on the first check after the widget has been created', function() {
var spy;
spy = spyOn(window, 'notify');
+ spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"] = new window.gl.MergeRequestWidget(this.opts);
this["class"].getCIStatus(true);
return expect(spy).not.toHaveBeenCalled();
@@ -174,6 +179,7 @@ require('~/lib/utils/datetime_utility');
it('should update the pipeline URL when the pipeline changes', function() {
var spy;
spy = spyOn(this["class"], 'updatePipelineUrls').and.stub();
+ spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false);
this.ciStatusData.pipeline += 1;
this["class"].getCIStatus(false);
@@ -182,6 +188,7 @@ require('~/lib/utils/datetime_utility');
it('should update the commit URL when the sha changes', function() {
var spy;
spy = spyOn(this["class"], 'updateCommitUrls').and.stub();
+ spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false);
this.ciStatusData.sha = "9b50b99a";
this["class"].getCIStatus(false);
diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js
index c2bcd9c0f7c..4b904fc2960 100644
--- a/spec/javascripts/monitoring/prometheus_graph_spec.js
+++ b/spec/javascripts/monitoring/prometheus_graph_spec.js
@@ -1,5 +1,4 @@
import 'jquery';
-import '~/lib/utils/common_utils';
import PrometheusGraph from '~/monitoring/prometheus_graph';
import { prometheusMockData } from './prometheus_mock_data';
@@ -12,6 +11,7 @@ describe('PrometheusGraph', () => {
beforeEach(() => {
loadFixtures(fixtureName);
+ $('.prometheus-container').data('has-metrics', 'true');
this.prometheusGraph = new PrometheusGraph();
const self = this;
const fakeInit = (metricsResponse) => {
@@ -75,3 +75,24 @@ describe('PrometheusGraph', () => {
});
});
});
+
+describe('PrometheusGraphs UX states', () => {
+ const fixtureName = 'static/environments/metrics.html.raw';
+ preloadFixtures(fixtureName);
+
+ beforeEach(() => {
+ loadFixtures(fixtureName);
+ this.prometheusGraph = new PrometheusGraph();
+ });
+
+ it('shows a specified state', () => {
+ this.prometheusGraph.state = '.js-getting-started';
+ this.prometheusGraph.updateState();
+ const $state = $('.js-getting-started');
+ expect($state).toBeDefined();
+ expect($('.state-title', $state)).toBeDefined();
+ expect($('.state-svg', $state)).toBeDefined();
+ expect($('.state-description', $state)).toBeDefined();
+ expect($('.state-button', $state)).toBeDefined();
+ });
+});
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index b30c5da8822..07dc51a7815 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -64,6 +64,7 @@ if (process.env.BABEL_ENV === 'coverage') {
'./snippet/snippet_bundle.js',
'./terminal/terminal_bundle.js',
'./users/users_bundle.js',
+ './issue_show/index.js',
];
describe('Uncovered files', function () {
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js
index dba998c7688..0910df61915 100644
--- a/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js
+++ b/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js
@@ -15,6 +15,11 @@ describe('Pipelines Actions dropdown', () => {
name: 'stop_review',
path: '/root/review-app/builds/1893/play',
},
+ {
+ name: 'foo',
+ path: '#',
+ playable: false,
+ },
];
spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
@@ -59,4 +64,14 @@ describe('Pipelines Actions dropdown', () => {
expect(component.$el.querySelector('.fa-spinner')).toEqual(null);
});
+
+ it('should render a disabled action when it\'s not playable', () => {
+ expect(
+ component.$el.querySelector('.dropdown-menu li:last-child button').getAttribute('disabled'),
+ ).toEqual('disabled');
+
+ expect(
+ component.$el.querySelector('.dropdown-menu li:last-child button').classList.contains('disabled'),
+ ).toEqual(true);
+ });
});
diff --git a/spec/lib/banzai/filter/markdown_filter_spec.rb b/spec/lib/banzai/filter/markdown_filter_spec.rb
new file mode 100644
index 00000000000..897288b8ad5
--- /dev/null
+++ b/spec/lib/banzai/filter/markdown_filter_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Banzai::Filter::MarkdownFilter, lib: true do
+ include FilterSpecHelper
+
+ context 'code block' do
+ it 'adds language to lang attribute when specified' do
+ result = filter("```html\nsome code\n```")
+
+ expect(result).to start_with("\n<pre><code lang=\"html\">")
+ end
+
+ it 'does not add language to lang attribute when not specified' do
+ result = filter("```\nsome code\n```")
+
+ expect(result).to start_with("\n<pre><code>")
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb
index b4cd5f63a15..fdbc65b5e00 100644
--- a/spec/lib/banzai/filter/sanitization_filter_spec.rb
+++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb
@@ -49,11 +49,12 @@ describe Banzai::Filter::SanitizationFilter, lib: true do
instance = described_class.new('Foo')
3.times { instance.whitelist }
- expect(instance.whitelist[:transformers].size).to eq 5
+ expect(instance.whitelist[:transformers].size).to eq 4
end
- it 'allows syntax highlighting' do
- exp = act = %q{<pre class="code highlight white c"><code><span class="k">def</span></code></pre>}
+ it 'sanitizes `class` attribute from all elements' do
+ act = %q{<pre class="code highlight white c"><code>&lt;span class="k"&gt;def&lt;/span&gt;</code></pre>}
+ exp = %q{<pre><code>&lt;span class="k"&gt;def&lt;/span&gt;</code></pre>}
expect(filter(act).to_html).to eq exp
end
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
index 63fb1bb25c4..f61fc8ceb9e 100644
--- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -12,14 +12,14 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
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>')
+ result = filter('<pre><code lang="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 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>')
+ result = filter('<pre><code lang="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><span id="LC1" class="line" lang="plaintext">This is a test</span></code></pre>')
end
end
@@ -30,7 +30,7 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
end
it "highlights as plaintext" do
- result = filter('<pre><code class="ruby">This is a test</code></pre>')
+ result = filter('<pre><code lang="ruby">This is a test</code></pre>')
expect(result.to_html).to eq('<pre class="code highlight" lang="" v-pre="true"><code>This is a test</code></pre>')
end
end
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index e007044868c..4ac79454647 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -58,6 +58,48 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
end
end
+ describe '#remove_concurrent_index' do
+ context 'outside a transaction' do
+ before do
+ allow(model).to receive(:transaction_open?).and_return(false)
+ end
+
+ context 'using PostgreSQL' do
+ before do
+ allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+ allow(model).to receive(:disable_statement_timeout)
+ end
+
+ it 'removes the index concurrently' do
+ expect(model).to receive(:remove_index).
+ with(:users, { algorithm: :concurrently, column: :foo })
+
+ model.remove_concurrent_index(:users, :foo)
+ end
+ end
+
+ context 'using MySQL' do
+ it 'removes an index' do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
+ expect(model).to receive(:remove_index).
+ with(:users, { column: :foo })
+
+ model.remove_concurrent_index(:users, :foo)
+ end
+ end
+ end
+
+ context 'inside a transaction' do
+ it 'raises RuntimeError' do
+ expect(model).to receive(:transaction_open?).and_return(true)
+
+ expect { model.remove_concurrent_index(:users, :foo) }.
+ to raise_error(RuntimeError)
+ end
+ end
+ end
+
describe '#add_concurrent_foreign_key' do
context 'inside a transaction' do
it 'raises an error' do
diff --git a/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb b/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb
index 287bf62d9bd..6307f8c16a3 100644
--- a/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb
+++ b/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::SidekiqStatus::ClientMiddleware do
describe '#call' do
it 'tracks the job in Redis' do
- expect(Gitlab::SidekiqStatus).to receive(:set).with('123')
+ expect(Gitlab::SidekiqStatus).to receive(:set).with('123', Gitlab::SidekiqStatus::DEFAULT_EXPIRATION)
described_class.new.
call('Foo', { 'jid' => '123' }, double(:queue), double(:pool)) { nil }
diff --git a/spec/lib/gitlab/sidekiq_status_spec.rb b/spec/lib/gitlab/sidekiq_status_spec.rb
index 56f06b61afb..496e50fbae4 100644
--- a/spec/lib/gitlab/sidekiq_status_spec.rb
+++ b/spec/lib/gitlab/sidekiq_status_spec.rb
@@ -73,4 +73,17 @@ describe Gitlab::SidekiqStatus do
expect(key).to include('123')
end
end
+
+ describe 'completed', :redis do
+ it 'returns the completed job' do
+ expect(described_class.completed_jids(%w(123))).to eq(['123'])
+ end
+
+ it 'returns only the jobs completed' do
+ described_class.set('123')
+ described_class.set('456')
+
+ expect(described_class.completed_jids(%w(123 456 789))).to eq(['789'])
+ end
+ end
end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 9fbcb1fee69..b703e9808a8 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -188,10 +188,8 @@ describe Gitlab::Workhorse, lib: true do
context 'when Gitaly is enabled' do
let(:gitaly_params) do
- address = Gitlab::GitalyClient.get_address('default')
{
- GitalySocketPath: URI(address).path,
- GitalyAddress: address,
+ GitalyAddress: Gitlab::GitalyClient.get_address('default'),
}
end
diff --git a/spec/mailers/previews/notify_preview.rb b/spec/mailers/previews/notify_preview.rb
new file mode 100644
index 00000000000..0e1ccb5b847
--- /dev/null
+++ b/spec/mailers/previews/notify_preview.rb
@@ -0,0 +1,11 @@
+class NotifyPreview < ActionMailer::Preview
+ def pipeline_success_email
+ pipeline = Ci::Pipeline.last
+ Notify.pipeline_success_email(pipeline, pipeline.user.try(:email))
+ end
+
+ def pipeline_failed_email
+ pipeline = Ci::Pipeline.last
+ Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email))
+ end
+end
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index 09b1fda3796..0f29766db41 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -111,6 +111,20 @@ describe Blob do
end
end
+ describe '#stl?' do
+ it 'is falsey with image extension' do
+ git_blob = Gitlab::Git::Blob.new(name: 'file.png')
+
+ expect(described_class.decorate(git_blob)).not_to be_stl
+ end
+
+ it 'is truthy with STL extension' do
+ git_blob = Gitlab::Git::Blob.new(name: 'file.stl')
+
+ expect(described_class.decorate(git_blob)).to be_stl
+ end
+ end
+
describe '#to_partial_path' do
let(:project) { double(lfs_enabled?: true) }
@@ -122,7 +136,8 @@ describe Blob do
lfs_pointer?: false,
svg?: false,
text?: false,
- binary?: false
+ binary?: false,
+ stl?: false
)
described_class.decorate(double).tap do |blob|
@@ -175,6 +190,11 @@ describe Blob do
blob = stubbed_blob(text?: true, sketch?: true, binary?: true)
expect(blob.to_partial_path(project)).to eq 'sketch'
end
+
+ it 'handles STLs' do
+ blob = stubbed_blob(text?: true, stl?: true)
+ expect(blob.to_partial_path(project)).to eq 'stl'
+ end
end
describe '#size_within_svg_limits?' do
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index 4f4b18cf0e0..e1beac28dab 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -108,6 +108,15 @@ describe API::DeployKeys, api: true do
expect(response).to have_http_status(201)
end
+
+ it 'accepts can_push parameter' do
+ key_attrs = attributes_for :write_access_key
+
+ post api("/projects/#{project.id}/deploy_keys", admin), key_attrs
+
+ expect(response).to have_http_status(201)
+ expect(json_response['can_push']).to eq(true)
+ end
end
describe 'DELETE /projects/:id/deploy_keys/:key_id' do
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 4729adba11c..2b27ce6390a 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -12,6 +12,8 @@ describe API::Issues, api: true do
let(:assignee) { create(:assignee) }
let(:admin) { create(:user, :admin) }
let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) }
+ let(:issue_title) { 'foo' }
+ let(:issue_description) { 'closed' }
let!(:closed_issue) do
create :closed_issue,
author: user,
@@ -38,7 +40,9 @@ describe API::Issues, api: true do
project: project,
milestone: milestone,
created_at: generate(:past_time),
- updated_at: 1.hour.ago
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description
end
let!(:label) do
create(:label, title: 'label', color: '#FFAABB', project: project)
@@ -61,6 +65,7 @@ describe API::Issues, api: true do
context "when unauthenticated" do
it "returns authentication error" do
get api("/issues")
+
expect(response).to have_http_status(401)
end
end
@@ -69,9 +74,7 @@ describe API::Issues, api: true do
it "returns an array of issues" do
get api("/issues", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
+ expect_paginated_array_response(size: 2)
expect(json_response.first['title']).to eq(issue.title)
expect(json_response.last).to have_key('web_url')
end
@@ -79,41 +82,43 @@ describe API::Issues, api: true do
it 'returns an array of closed issues' do
get api('/issues?state=closed', user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
+ expect_paginated_array_response(size: 1)
expect(json_response.first['id']).to eq(closed_issue.id)
end
it 'returns an array of opened issues' do
get api('/issues?state=opened', user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
+ expect_paginated_array_response(size: 1)
expect(json_response.first['id']).to eq(issue.id)
end
it 'returns an array of all issues' do
get api('/issues?state=all', user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(2)
+ expect_paginated_array_response(size: 2)
expect(json_response.first['id']).to eq(issue.id)
expect(json_response.second['id']).to eq(closed_issue.id)
end
+ it 'returns issues matching given search string for title' do
+ get api("/issues?search=#{issue.title}", user)
+
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['id']).to eq(issue.id)
+ end
+
+ it 'returns issues matching given search string for description' do
+ get api("/issues?search=#{issue.description}", user)
+
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['id']).to eq(issue.id)
+ end
+
it 'returns an array of labeled issues' do
get api("/issues?labels=#{label.title}", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
+ expect_paginated_array_response(size: 1)
expect(json_response.first['labels']).to eq([label.title])
end
@@ -126,29 +131,20 @@ describe API::Issues, api: true do
get api("/issues", user), labels: "#{label.title},#{label_b.title},#{label_c.title}"
- 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_paginated_array_response(size: 1)
expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title])
end
it 'returns an empty array if no issue matches labels' do
get api('/issues?labels=foo,bar', user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
+ expect_paginated_array_response(size: 0)
end
it 'returns an array of labeled issues matching given state' do
get api("/issues?labels=#{label.title}&state=opened", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
+ expect_paginated_array_response(size: 1)
expect(json_response.first['labels']).to eq([label.title])
expect(json_response.first['state']).to eq('opened')
end
@@ -156,47 +152,32 @@ describe API::Issues, api: true do
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_paginated_array_response(size: 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)
- 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(0)
+ expect_paginated_array_response(size: 0)
end
it 'returns an empty array if no issue matches milestone' do
get api("/issues?milestone=#{empty_milestone.title}", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
+ expect_paginated_array_response(size: 0)
end
it 'returns an empty array if milestone does not exist' do
get api("/issues?milestone=foo", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
+ expect_paginated_array_response(size: 0)
end
it 'returns an array of issues in given milestone' do
get api("/issues?milestone=#{milestone.title}", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(2)
+ expect_paginated_array_response(size: 2)
expect(json_response.first['id']).to eq(issue.id)
expect(json_response.second['id']).to eq(closed_issue.id)
end
@@ -205,49 +186,36 @@ describe API::Issues, api: true do
get api("/issues?milestone=#{milestone.title}"\
'&state=closed', user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
+ expect_paginated_array_response(size: 1)
expect(json_response.first['id']).to eq(closed_issue.id)
end
it 'returns an array of issues with no milestone' do
get api("/issues?milestone=#{no_milestone_title}", author)
- 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_paginated_array_response(size: 1)
expect(json_response.first['id']).to eq(confidential_issue.id)
end
it 'returns an array of issues found by iids' do
get api('/issues', user), iids: [closed_issue.iid]
- 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_paginated_array_response(size: 1)
expect(json_response.first['id']).to eq(closed_issue.id)
end
it 'returns an empty array if iid does not exist' do
get api("/issues", user), iids: [99999]
- 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(0)
+ expect_paginated_array_response(size: 0)
end
it 'sorts by created_at descending by default' do
get api('/issues', user)
response_dates = json_response.map { |issue| issue['created_at'] }
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
+
+ expect_paginated_array_response(size: 2)
expect(response_dates).to eq(response_dates.sort.reverse)
end
@@ -255,9 +223,8 @@ describe API::Issues, api: true do
get api('/issues?sort=asc', user)
response_dates = json_response.map { |issue| issue['created_at'] }
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
+
+ expect_paginated_array_response(size: 2)
expect(response_dates).to eq(response_dates.sort)
end
@@ -265,9 +232,8 @@ describe API::Issues, api: true do
get api('/issues?order_by=updated_at', user)
response_dates = json_response.map { |issue| issue['updated_at'] }
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
+
+ expect_paginated_array_response(size: 2)
expect(response_dates).to eq(response_dates.sort.reverse)
end
@@ -275,9 +241,8 @@ describe API::Issues, api: true do
get api('/issues?order_by=updated_at&sort=asc', user)
response_dates = json_response.map { |issue| issue['updated_at'] }
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
+
+ expect_paginated_array_response(size: 2)
expect(response_dates).to eq(response_dates.sort)
end
@@ -316,7 +281,9 @@ describe API::Issues, api: true do
assignee: user,
project: group_project,
milestone: group_milestone,
- updated_at: 1.hour.ago
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description
end
let!(:group_label) do
create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project)
@@ -336,74 +303,65 @@ describe API::Issues, api: true do
it 'returns all group issues (including opened and closed)' do
get api(base_url, admin)
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect_paginated_array_response(size: 3)
end
it 'returns group issues without confidential issues for non project members' do
get api("#{base_url}?state=opened", non_member)
- 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_paginated_array_response(size: 1)
expect(json_response.first['title']).to eq(group_issue.title)
end
it 'returns group confidential issues for author' do
get api("#{base_url}?state=opened", author)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(2)
+ expect_paginated_array_response(size: 2)
end
it 'returns group confidential issues for assignee' do
get api("#{base_url}?state=opened", assignee)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(2)
+ expect_paginated_array_response(size: 2)
end
it 'returns group issues with confidential issues for project members' do
get api("#{base_url}?state=opened", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(2)
+ expect_paginated_array_response(size: 2)
end
it 'returns group confidential issues for admin' do
get api("#{base_url}?state=opened", admin)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(2)
+ expect_paginated_array_response(size: 2)
end
it 'returns an array of labeled group issues' do
get api("#{base_url}?labels=#{group_label.title}", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
+ expect_paginated_array_response(size: 1)
expect(json_response.first['labels']).to eq([group_label.title])
end
it 'returns an array of labeled group issues where all labels match' do
get api("#{base_url}?labels=#{group_label.title},foo,bar", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
+ expect_paginated_array_response(size: 0)
+ end
+
+ it 'returns issues matching given search string for title' do
+ get api("#{base_url}?search=#{group_issue.title}", user)
+
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['id']).to eq(group_issue.id)
+ end
+
+ it 'returns issues matching given search string for description' do
+ get api("#{base_url}?search=#{group_issue.description}", user)
+
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['id']).to eq(group_issue.id)
end
it 'returns an array of labeled issues when all labels matches' do
@@ -415,65 +373,45 @@ describe API::Issues, api: true do
get api("#{base_url}", user), labels: "#{group_label.title},#{label_b.title},#{label_c.title}"
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
+ expect_paginated_array_response(size: 1)
expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title])
end
it 'returns an array of issues found by iids' do
get api(base_url, user), iids: [group_issue.iid]
- 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_paginated_array_response(size: 1)
expect(json_response.first['id']).to eq(group_issue.id)
end
it 'returns an empty array if iid does not exist' do
get api(base_url, user), iids: [99999]
- 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(0)
+ expect_paginated_array_response(size: 0)
end
it 'returns an empty array if no group issue matches labels' do
get api("#{base_url}?labels=foo,bar", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
+ expect_paginated_array_response(size: 0)
end
it 'returns an empty array if no issue matches milestone' do
get api("#{base_url}?milestone=#{group_empty_milestone.title}", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
+ expect_paginated_array_response(size: 0)
end
it 'returns an empty array if milestone does not exist' do
get api("#{base_url}?milestone=foo", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
+ expect_paginated_array_response(size: 0)
end
it 'returns an array of issues in given milestone' do
get api("#{base_url}?state=opened&milestone=#{group_milestone.title}", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
+ expect_paginated_array_response(size: 1)
expect(json_response.first['id']).to eq(group_issue.id)
end
@@ -481,10 +419,7 @@ describe API::Issues, api: true do
get api("#{base_url}?milestone=#{group_milestone.title}"\
'&state=closed', user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
+ expect_paginated_array_response(size: 1)
expect(json_response.first['id']).to eq(group_closed_issue.id)
end
@@ -492,9 +427,8 @@ describe API::Issues, api: true do
get api("#{base_url}?milestone=#{no_milestone_title}", user)
expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
+
+ expect_paginated_array_response(size: 1)
expect(json_response.first['id']).to eq(group_confidential_issue.id)
end
@@ -502,9 +436,8 @@ describe API::Issues, api: true do
get api(base_url, user)
response_dates = json_response.map { |issue| issue['created_at'] }
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
+
+ expect_paginated_array_response(size: 3)
expect(response_dates).to eq(response_dates.sort.reverse)
end
@@ -512,9 +445,8 @@ describe API::Issues, api: true do
get api("#{base_url}?sort=asc", user)
response_dates = json_response.map { |issue| issue['created_at'] }
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
+
+ expect_paginated_array_response(size: 3)
expect(response_dates).to eq(response_dates.sort)
end
@@ -522,9 +454,8 @@ describe API::Issues, api: true do
get api("#{base_url}?order_by=updated_at", user)
response_dates = json_response.map { |issue| issue['updated_at'] }
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
+
+ expect_paginated_array_response(size: 3)
expect(response_dates).to eq(response_dates.sort.reverse)
end
@@ -532,9 +463,8 @@ describe API::Issues, api: true do
get api("#{base_url}?order_by=updated_at&sort=asc", user)
response_dates = json_response.map { |issue| issue['updated_at'] }
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
+
+ expect_paginated_array_response(size: 3)
expect(response_dates).to eq(response_dates.sort)
end
end
@@ -563,79 +493,55 @@ describe API::Issues, api: true do
get api("/projects/#{restricted_project.id}/issues", non_member)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response).to eq([])
+ expect_paginated_array_response(size: 0)
end
it 'returns project issues without confidential issues for non project members' do
get api("#{base_url}/issues", non_member)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(2)
+ expect_paginated_array_response(size: 2)
expect(json_response.first['title']).to eq(issue.title)
end
it 'returns project issues without confidential issues for project members with guest role' do
get api("#{base_url}/issues", guest)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(2)
+ expect_paginated_array_response(size: 2)
expect(json_response.first['title']).to eq(issue.title)
end
it 'returns project confidential issues for author' do
get api("#{base_url}/issues", author)
- 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(3)
+ expect_paginated_array_response(size: 3)
expect(json_response.first['title']).to eq(issue.title)
end
it 'returns project confidential issues for assignee' do
get api("#{base_url}/issues", assignee)
- 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(3)
+ expect_paginated_array_response(size: 3)
expect(json_response.first['title']).to eq(issue.title)
end
it 'returns project issues with confidential issues for project members' do
get api("#{base_url}/issues", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(3)
+ expect_paginated_array_response(size: 3)
expect(json_response.first['title']).to eq(issue.title)
end
it 'returns project confidential issues for admin' do
get api("#{base_url}/issues", admin)
- 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(3)
+ expect_paginated_array_response(size: 3)
expect(json_response.first['title']).to eq(issue.title)
end
it 'returns an array of labeled project issues' do
get api("#{base_url}/issues?labels=#{label.title}", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
+ expect_paginated_array_response(size: 1)
expect(json_response.first['labels']).to eq([label.title])
end
@@ -648,74 +554,65 @@ describe API::Issues, api: true do
get api("#{base_url}/issues", user), labels: "#{label.title},#{label_b.title},#{label_c.title}"
- 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_paginated_array_response(size: 1)
expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title])
end
+ it 'returns issues matching given search string for title' do
+ get api("#{base_url}/issues?search=#{issue.title}", user)
+
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['id']).to eq(issue.id)
+ end
+
+ it 'returns issues matching given search string for description' do
+ get api("#{base_url}/issues?search=#{issue.description}", user)
+
+ expect_paginated_array_response(size: 1)
+ expect(json_response.first['id']).to eq(issue.id)
+ end
+
it 'returns an array of issues found by iids' do
get api("#{base_url}/issues", user), iids: [issue.iid]
- 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_paginated_array_response(size: 1)
expect(json_response.first['id']).to eq(issue.id)
end
it 'returns an empty array if iid does not exist' do
get api("#{base_url}/issues", user), iids: [99999]
- 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(0)
+ expect_paginated_array_response(size: 0)
end
it 'returns an empty array if not all labels matches' do
get api("#{base_url}/issues?labels=#{label.title},foo", user)
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
+ expect_paginated_array_response(size: 0)
end
it 'returns an empty array if no project issue matches labels' do
get api("#{base_url}/issues?labels=foo,bar", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
+ expect_paginated_array_response(size: 0)
end
it 'returns an empty array if no issue matches milestone' do
get api("#{base_url}/issues?milestone=#{empty_milestone.title}", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
+ expect_paginated_array_response(size: 0)
end
it 'returns an empty array if milestone does not exist' do
get api("#{base_url}/issues?milestone=foo", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
+ expect_paginated_array_response(size: 0)
end
it 'returns an array of issues in given milestone' do
get api("#{base_url}/issues?milestone=#{milestone.title}", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(2)
+ expect_paginated_array_response(size: 2)
expect(json_response.first['id']).to eq(issue.id)
expect(json_response.second['id']).to eq(closed_issue.id)
end
@@ -723,20 +620,14 @@ describe API::Issues, api: true do
it 'returns an array of issues matching state in milestone' do
get api("#{base_url}/issues?milestone=#{milestone.title}&state=closed", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
+ expect_paginated_array_response(size: 1)
expect(json_response.first['id']).to eq(closed_issue.id)
end
it 'returns an array of issues with no milestone' do
get api("#{base_url}/issues?milestone=#{no_milestone_title}", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
+ expect_paginated_array_response(size: 1)
expect(json_response.first['id']).to eq(confidential_issue.id)
end
@@ -744,9 +635,8 @@ describe API::Issues, api: true do
get api("#{base_url}/issues", user)
response_dates = json_response.map { |issue| issue['created_at'] }
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
+
+ expect_paginated_array_response(size: 3)
expect(response_dates).to eq(response_dates.sort.reverse)
end
@@ -754,9 +644,8 @@ describe API::Issues, api: true do
get api("#{base_url}/issues?sort=asc", user)
response_dates = json_response.map { |issue| issue['created_at'] }
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
+
+ expect_paginated_array_response(size: 3)
expect(response_dates).to eq(response_dates.sort)
end
@@ -764,9 +653,8 @@ describe API::Issues, api: true do
get api("#{base_url}/issues?order_by=updated_at", user)
response_dates = json_response.map { |issue| issue['updated_at'] }
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
+
+ expect_paginated_array_response(size: 3)
expect(response_dates).to eq(response_dates.sort.reverse)
end
@@ -774,9 +662,8 @@ describe API::Issues, api: true do
get api("#{base_url}/issues?order_by=updated_at&sort=asc", user)
response_dates = json_response.map { |issue| issue['updated_at'] }
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
+
+ expect_paginated_array_response(size: 3)
expect(response_dates).to eq(response_dates.sort)
end
end
@@ -1457,4 +1344,11 @@ describe API::Issues, api: true do
include_examples 'time tracking endpoints', 'issue'
end
+
+ def expect_paginated_array_response(size: nil)
+ 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(size) if size
+ end
end
diff --git a/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb b/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb
new file mode 100644
index 00000000000..a714bf4e5d5
--- /dev/null
+++ b/spec/rubocop/cop/migration/remove_concurrent_index_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/migration/remove_concurrent_index'
+
+describe RuboCop::Cop::Migration::RemoveConcurrentIndex do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'in migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ end
+
+ it 'registers an offense when remove_concurrent_index is used inside a change method' do
+ inspect_source(cop, 'def change; remove_concurrent_index :table, :column; end')
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ end
+ end
+
+ it 'registers no offense when remove_concurrent_index is used inside an up method' do
+ inspect_source(cop, 'def up; remove_concurrent_index :table, :column; end')
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+
+ context 'outside of migration' do
+ it 'registers no offense' do
+ inspect_source(cop, 'def change; remove_concurrent_index :table, :column; end')
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/remove_index_spec.rb b/spec/rubocop/cop/migration/remove_index_spec.rb
new file mode 100644
index 00000000000..31923cb7429
--- /dev/null
+++ b/spec/rubocop/cop/migration/remove_index_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../../rubocop/cop/migration/remove_index'
+
+describe RuboCop::Cop::Migration::RemoveIndex do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'in migration' do
+ before do
+ allow(cop).to receive(:in_migration?).and_return(true)
+ end
+
+ it 'registers an offense when remove_index is used' do
+ inspect_source(cop, 'def change; remove_index :table, :column; end')
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ end
+ end
+ end
+
+ context 'outside of migration' do
+ it 'registers no offense' do
+ inspect_source(cop, 'def change; remove_index :table, :column; end')
+
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+end
diff --git a/spec/serializers/build_action_entity_spec.rb b/spec/serializers/build_action_entity_spec.rb
index 0f7be8b2c39..54ac17447b1 100644
--- a/spec/serializers/build_action_entity_spec.rb
+++ b/spec/serializers/build_action_entity_spec.rb
@@ -17,5 +17,9 @@ describe BuildActionEntity do
it 'contains path to the action play' do
expect(subject[:path]).to include "builds/#{build.id}/play"
end
+
+ it 'contains whether it is playable' do
+ expect(subject[:playable]).to eq build.playable?
+ end
end
end
diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/build_entity_spec.rb
index 7dcdf54fd93..f76a5cf72d1 100644
--- a/spec/serializers/build_entity_spec.rb
+++ b/spec/serializers/build_entity_spec.rb
@@ -24,6 +24,10 @@ describe BuildEntity do
expect(subject).not_to include(/variables/)
end
+ it 'contains whether it is playable' do
+ expect(subject[:playable]).to eq build.playable?
+ end
+
it 'contains timestamps' do
expect(subject).to include(:created_at, :updated_at)
end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index c8bd4d4601a..be9f9ea2dec 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -4,6 +4,8 @@ describe MergeRequests::BuildService, services: true do
include RepoHelpers
let(:project) { create(:project, :repository) }
+ let(:source_project) { nil }
+ let(:target_project) { nil }
let(:user) { create(:user) }
let(:issue_confidential) { false }
let(:issue) { create(:issue, project: project, title: 'A bug', confidential: issue_confidential) }
@@ -20,7 +22,9 @@ describe MergeRequests::BuildService, services: true do
MergeRequests::BuildService.new(project, user,
description: description,
source_branch: source_branch,
- target_branch: target_branch)
+ target_branch: target_branch,
+ source_project: source_project,
+ target_project: target_project)
end
before do
@@ -256,5 +260,41 @@ describe MergeRequests::BuildService, services: true do
)
end
end
+
+ context 'target_project is set and accessible by current_user' do
+ let(:target_project) { create(:project, :public, :repository)}
+ let(:commits) { Commit.decorate([commit_1], project) }
+
+ it 'sets target project correctly' do
+ expect(merge_request.target_project).to eq(target_project)
+ end
+ end
+
+ context 'target_project is set but not accessible by current_user' do
+ let(:target_project) { create(:project, :private, :repository)}
+ let(:commits) { Commit.decorate([commit_1], project) }
+
+ it 'sets target project correctly' do
+ expect(merge_request.target_project).to eq(project)
+ end
+ end
+
+ context 'source_project is set and accessible by current_user' do
+ let(:source_project) { create(:project, :public, :repository)}
+ let(:commits) { Commit.decorate([commit_1], project) }
+
+ it 'sets target project correctly' do
+ expect(merge_request.source_project).to eq(source_project)
+ end
+ end
+
+ context 'source_project is set but not accessible by current_user' do
+ let(:source_project) { create(:project, :private, :repository)}
+ let(:commits) { Commit.decorate([commit_1], project) }
+
+ it 'sets target project correctly' do
+ expect(merge_request.source_project).to eq(project)
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index c22d145ca5d..03215a4624a 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -49,6 +49,7 @@ describe MergeRequests::RefreshService, services: true do
context 'push to origin repo source branch' do
let(:refresh_service) { service.new(@project, @user) }
+
before do
allow(refresh_service).to receive(:execute_hooks)
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
@@ -70,6 +71,32 @@ describe MergeRequests::RefreshService, services: true do
end
end
+ context 'push to origin repo source branch when an MR was reopened' do
+ let(:refresh_service) { service.new(@project, @user) }
+
+ before do
+ @merge_request.update(state: :reopened)
+
+ allow(refresh_service).to receive(:execute_hooks)
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
+ reload_mrs
+ end
+
+ it 'executes hooks with update action' do
+ expect(refresh_service).to have_received(:execute_hooks).
+ with(@merge_request, 'update', @oldrev)
+
+ expect(@merge_request.notes).not_to be_empty
+ expect(@merge_request).to be_open
+ expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey
+ expect(@merge_request.diff_head_sha).to eq(@newrev)
+ expect(@fork_merge_request).to be_open
+ expect(@fork_merge_request.notes).to be_empty
+ expect(@build_failed_todo).to be_done
+ expect(@fork_build_failed_todo).to be_done
+ end
+ end
+
context 'push to origin repo target branch' do
before do
service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature')
diff --git a/spec/views/notify/pipeline_failed_email.html.haml_spec.rb b/spec/views/notify/pipeline_failed_email.html.haml_spec.rb
new file mode 100644
index 00000000000..f627f9165fb
--- /dev/null
+++ b/spec/views/notify/pipeline_failed_email.html.haml_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe 'notify/pipeline_failed_email.html.haml' do
+ include Devise::Test::ControllerHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ user: user,
+ ref: project.default_branch,
+ sha: project.commit.sha,
+ status: :success)
+ end
+
+ before do
+ assign(:project, project)
+ assign(:pipeline, pipeline)
+ assign(:merge_request, merge_request)
+ end
+
+ context 'pipeline with user' do
+ it 'renders the email correctly' do
+ render
+
+ expect(rendered).to have_content "Your pipeline has failed"
+ expect(rendered).to have_content pipeline.project.name
+ expect(rendered).to have_content pipeline.git_commit_message.truncate(50)
+ expect(rendered).to have_content pipeline.commit.author_name
+ expect(rendered).to have_content "##{pipeline.id}"
+ expect(rendered).to have_content pipeline.user.name
+ end
+ end
+
+ context 'pipeline without user' do
+ before do
+ pipeline.update_attribute(:user, nil)
+ end
+
+ it 'renders the email correctly' do
+ render
+
+ expect(rendered).to have_content "Your pipeline has failed"
+ expect(rendered).to have_content pipeline.project.name
+ expect(rendered).to have_content pipeline.git_commit_message.truncate(50)
+ expect(rendered).to have_content pipeline.commit.author_name
+ expect(rendered).to have_content "##{pipeline.id}"
+ expect(rendered).to have_content "by API"
+ end
+ end
+end
diff --git a/spec/views/notify/pipeline_success_email.html.haml_spec.rb b/spec/views/notify/pipeline_success_email.html.haml_spec.rb
new file mode 100644
index 00000000000..ecd096ee579
--- /dev/null
+++ b/spec/views/notify/pipeline_success_email.html.haml_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe 'notify/pipeline_success_email.html.haml' do
+ include Devise::Test::ControllerHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ user: user,
+ ref: project.default_branch,
+ sha: project.commit.sha,
+ status: :success)
+ end
+
+ before do
+ assign(:project, project)
+ assign(:pipeline, pipeline)
+ assign(:merge_request, merge_request)
+ end
+
+ context 'pipeline with user' do
+ it 'renders the email correctly' do
+ render
+
+ expect(rendered).to have_content "Your pipeline has passed"
+ expect(rendered).to have_content pipeline.project.name
+ expect(rendered).to have_content pipeline.git_commit_message.truncate(50)
+ expect(rendered).to have_content pipeline.commit.author_name
+ expect(rendered).to have_content "##{pipeline.id}"
+ expect(rendered).to have_content pipeline.user.name
+ end
+ end
+
+ context 'pipeline without user' do
+ before do
+ pipeline.update_attribute(:user, nil)
+ end
+
+ it 'renders the email correctly' do
+ render
+
+ expect(rendered).to have_content "Your pipeline has passed"
+ expect(rendered).to have_content pipeline.project.name
+ expect(rendered).to have_content pipeline.git_commit_message.truncate(50)
+ expect(rendered).to have_content pipeline.commit.author_name
+ expect(rendered).to have_content "##{pipeline.id}"
+ expect(rendered).to have_content "by API"
+ end
+ end
+end
diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
index fbb22439f33..5a2c0671dac 100644
--- a/spec/workers/repository_import_worker_spec.rb
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -23,10 +23,12 @@ describe RepositoryImportWorker do
error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
expect_any_instance_of(Projects::ImportService).to receive(:execute).
and_return({ status: :error, message: error })
+ allow(subject).to receive(:jid).and_return('123')
subject.perform(project.id)
expect(project.reload.import_error).to include("https://*****:*****@test.com/root/repoC.git/")
+ expect(project.reload.import_jid).not_to be_nil
end
end
end
diff --git a/spec/workers/stuck_import_jobs_worker_spec.rb b/spec/workers/stuck_import_jobs_worker_spec.rb
new file mode 100644
index 00000000000..466277a5e5e
--- /dev/null
+++ b/spec/workers/stuck_import_jobs_worker_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe StuckImportJobsWorker do
+ let(:worker) { described_class.new }
+ let(:exclusive_lease_uuid) { SecureRandom.uuid }
+
+ before do
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(exclusive_lease_uuid)
+ end
+
+ describe 'long running import' do
+ let(:project) { create(:empty_project, import_jid: '123', import_status: 'started') }
+
+ before do
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(['123'])
+ end
+
+ it 'marks the project as failed' do
+ expect { worker.perform }.to change { project.reload.import_status }.to('failed')
+ end
+ end
+
+ describe 'running import' do
+ let(:project) { create(:empty_project, import_jid: '123', import_status: 'started') }
+
+ before do
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([])
+ end
+
+ it 'does not mark the project as failed' do
+ worker.perform
+
+ expect(project.reload.import_status).to eq('started')
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index 65eef75af1a..9f2b8fe3d6e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4305,6 +4305,18 @@ text-table@~0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
+three-orbit-controls@^82.1.0:
+ version "82.1.0"
+ resolved "https://registry.yarnpkg.com/three-orbit-controls/-/three-orbit-controls-82.1.0.tgz#11a7f33d0a20ecec98f098b37780f6537374fab4"
+
+three-stl-loader@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/three-stl-loader/-/three-stl-loader-1.0.4.tgz#6b3319a31e3b910aab1883d19b00c81a663c3e03"
+
+three@^0.84.0:
+ version "0.84.0"
+ resolved "https://registry.yarnpkg.com/three/-/three-0.84.0.tgz#95be85a55a0fa002aa625ed559130957dcffd918"
+
throttleit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"