summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.babelrc21
-rw-r--r--.gitlab-ci.yml6
-rw-r--r--.gitlab/issue_templates/Feature Proposal.md10
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock6
-rw-r--r--app/assets/javascripts/boards/components/board.js5
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.js119
-rw-r--r--app/assets/javascripts/boards/components/modal/empty_state.js4
-rw-r--r--app/assets/javascripts/boards/components/modal/tabs.js2
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js9
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_service.js44
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js146
-rw-r--r--app/assets/javascripts/diff_notes/components/diff_note_avatars.js1
-rw-r--r--app/assets/javascripts/dispatcher.js3
-rw-r--r--app/assets/javascripts/due_date_select.js2
-rw-r--r--app/assets/javascripts/environments/components/environment.js14
-rw-r--r--app/assets/javascripts/environments/components/environment_item.js8
-rw-r--r--app/assets/javascripts/environments/components/environments_table.js4
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.js15
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js3
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js1
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown.js3
-rw-r--r--app/assets/javascripts/issue.js8
-rw-r--r--app/assets/javascripts/labels_select.js2
-rw-r--r--app/assets/javascripts/merge_request_widget.js2
-rw-r--r--app/assets/javascripts/milestone_select.js4
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js6
-rw-r--r--app/assets/javascripts/monitoring/prometheus_graph.js34
-rw-r--r--app/assets/javascripts/users_select.js4
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/async_button.js92
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js56
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js71
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js32
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/stage.js116
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/status.js60
-rw-r--r--app/assets/javascripts/vue_pipelines_index/components/time_ago.js71
-rw-r--r--app/assets/javascripts/vue_pipelines_index/event_hub.js3
-rw-r--r--app/assets/javascripts/vue_pipelines_index/index.js23
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_actions.js123
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_url.js63
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipelines.js190
-rw-r--r--app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js44
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stage.js119
-rw-r--r--app/assets/javascripts/vue_pipelines_index/status.js64
-rw-r--r--app/assets/javascripts/vue_pipelines_index/store.js31
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js (renamed from app/assets/javascripts/commit/pipelines/pipelines_store.js)37
-rw-r--r--app/assets/javascripts/vue_pipelines_index/time_ago.js78
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.js307
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table.js80
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table_row.js393
-rw-r--r--app/assets/javascripts/vue_shared/components/table_pagination.js254
-rw-r--r--app/assets/javascripts/vue_shared/vue_resource_interceptor.js10
-rw-r--r--app/assets/stylesheets/framework/common.scss6
-rw-r--r--app/assets/stylesheets/framework/emojis.scss1
-rw-r--r--app/assets/stylesheets/framework/filters.scss23
-rw-r--r--app/assets/stylesheets/framework/nav.scss1
-rw-r--r--app/assets/stylesheets/framework/typography.scss5
-rw-r--r--app/assets/stylesheets/pages/note_form.scss12
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss24
-rw-r--r--app/controllers/concerns/service_params.rb1
-rw-r--r--app/controllers/dashboard/todos_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb2
-rw-r--r--app/controllers/projects/wikis_controller.rb3
-rw-r--r--app/controllers/projects_controller.rb1
-rw-r--r--app/finders/todos_finder.rb12
-rw-r--r--app/helpers/nav_helper.rb1
-rw-r--r--app/helpers/todos_helper.rb10
-rw-r--r--app/mailers/emails/builds.rb30
-rw-r--r--app/mailers/notify.rb1
-rw-r--r--app/models/ci/build.rb21
-rw-r--r--app/models/ci/pipeline.rb4
-rw-r--r--app/models/ci/runner.rb4
-rw-r--r--app/models/ci/runner_project.rb6
-rw-r--r--app/models/ci/trigger.rb2
-rw-r--r--app/models/ci/variable.rb4
-rw-r--r--app/models/commit.rb9
-rw-r--r--app/models/commit_status.rb8
-rw-r--r--app/models/concerns/issuable.rb1
-rw-r--r--app/models/concerns/routable.rb10
-rw-r--r--app/models/issue.rb8
-rw-r--r--app/models/label.rb4
-rw-r--r--app/models/merge_request.rb5
-rw-r--r--app/models/project.rb13
-rw-r--r--app/models/project_services/builds_email_service.rb102
-rw-r--r--app/models/project_services/chat_message/build_message.rb102
-rw-r--r--app/models/project_services/chat_notification_service.rb18
-rw-r--r--app/models/project_services/hipchat_service.rb45
-rw-r--r--app/models/project_services/mattermost_service.rb1
-rw-r--r--app/models/project_services/slack_service.rb1
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/user.rb2
-rw-r--r--app/models/wiki_page.rb8
-rw-r--r--app/services/ci/register_job_service.rb6
-rw-r--r--app/services/merge_requests/build_service.rb2
-rw-r--r--app/services/merge_requests/get_urls_service.rb2
-rw-r--r--app/services/notification_recipient_service.rb293
-rw-r--r--app/services/notification_service.rb323
-rw-r--r--app/views/notify/build_fail_email.html.haml24
-rw-r--r--app/views/notify/build_fail_email.text.erb11
-rw-r--r--app/views/notify/build_success_email.html.haml24
-rw-r--r--app/views/notify/build_success_email.text.erb11
-rw-r--r--app/views/projects/_merge_request_merge_settings.html.haml4
-rw-r--r--app/views/projects/boards/components/_blank_state.html.haml15
-rw-r--r--app/views/projects/boards/components/_board.html.haml2
-rw-r--r--app/views/projects/empty.html.haml1
-rw-r--r--app/views/projects/environments/metrics.html.haml3
-rw-r--r--app/views/projects/issues/index.html.haml17
-rw-r--r--app/views/projects/issues/show.html.haml57
-rw-r--r--app/views/shared/_sort_dropdown.html.haml1
-rw-r--r--app/views/shared/empty_states/_issues.html.haml5
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml10
-rw-r--r--app/workers/build_email_worker.rb20
-rw-r--r--changelogs/unreleased/21451-allow-disable-mr-link.yml4
-rw-r--r--changelogs/unreleased/23993-drop-ci_projects.yml6
-rw-r--r--changelogs/unreleased/24683-sidebar-spinners.yml4
-rw-r--r--changelogs/unreleased/26236-monospace-gfm.yml4
-rw-r--r--changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml4
-rw-r--r--changelogs/unreleased/28499-fix-large-text-tooltip-in-diff-file-name.yml4
-rw-r--r--changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml4
-rw-r--r--changelogs/unreleased/28890-allow-creating-mr-without-target-branch-in-url.yml5
-rw-r--r--changelogs/unreleased/29405-fix-project-wiki-update.yml4
-rw-r--r--changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml4
-rw-r--r--changelogs/unreleased/29604-v3-fix-branch-creation.yml4
-rw-r--r--changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml4
-rw-r--r--changelogs/unreleased/add-labels-to-issue-hook.yml4
-rw-r--r--changelogs/unreleased/feature-use-gitaly-for-commit-show.yml4
-rw-r--r--changelogs/unreleased/fix-prometheus-including-d3-main-bundle.yml4
-rw-r--r--changelogs/unreleased/fl-remove-ujs-pipelines.yml4
-rw-r--r--changelogs/unreleased/issue_27212.yml4
-rw-r--r--changelogs/unreleased/make-karma-fast-again.yml4
-rw-r--r--changelogs/unreleased/migrate-pipeline-events-and-email-service.yml4
-rw-r--r--changelogs/unreleased/remove-unused-ci-tables.yml4
-rw-r--r--changelogs/unreleased/rename-ci_commits-to-ci_pipeline.yml4
-rw-r--r--changelogs/unreleased/rename_all_issues.yml4
-rw-r--r--changelogs/unreleased/routes-lower-case.yml4
-rw-r--r--changelogs/unreleased/simplify-docs-trigger.yml4
-rw-r--r--config/initializers/sidekiq.rb2
-rw-r--r--config/karma.config.js32
-rw-r--r--config/webpack.config.js11
-rw-r--r--db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb2
-rw-r--r--db/migrate/20170222143317_drop_ci_projects.rb34
-rw-r--r--db/migrate/20170222143500_remove_old_project_id_columns.rb28
-rw-r--r--db/migrate/20170222143603_rename_gl_project_id_to_project_id.rb14
-rw-r--r--db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb18
-rw-r--r--db/migrate/20170301195939_rename_ci_commits_to_ci_pipelines.rb10
-rw-r--r--db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb83
-rw-r--r--db/migrate/20170315194013_add_closed_at_to_issues.rb7
-rw-r--r--db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb87
-rw-r--r--db/schema.rb120
-rw-r--r--doc/administration/monitoring/prometheus/index.md16
-rw-r--r--doc/api/branches.md5
-rw-r--r--doc/api/services.md40
-rw-r--r--doc/ci/quick_start/README.md9
-rw-r--r--doc/development/frontend.md21
-rw-r--r--doc/project_services/builds_emails.md1
-rw-r--r--doc/ssh/README.md6
-rw-r--r--doc/update/8.12-to-8.13.md2
-rw-r--r--doc/user/award_emojis.md51
-rw-r--r--doc/user/img/award_emoji_comment_awarded.png (renamed from doc/workflow/img/award_emoji_comment_awarded.png)bin19159 -> 19159 bytes
-rw-r--r--doc/user/img/award_emoji_comment_picker.png (renamed from doc/workflow/img/award_emoji_comment_picker.png)bin72883 -> 72883 bytes
-rw-r--r--doc/user/img/award_emoji_select.pngbin0 -> 17827 bytes
-rw-r--r--doc/user/img/award_emoji_votes_sort_options.pngbin0 -> 99941 bytes
-rw-r--r--doc/user/project/integrations/builds_emails.md15
-rw-r--r--doc/user/project/integrations/mattermost.md2
-rw-r--r--doc/user/project/integrations/project_services.md2
-rw-r--r--doc/user/project/integrations/slack.md2
-rw-r--r--doc/user/project/integrations/webhooks.md14
-rw-r--r--doc/user/project/settings/import_export.md2
-rw-r--r--doc/workflow/award_emoji.md66
-rw-r--r--doc/workflow/img/award_emoji_select.pngbin23779 -> 0 bytes
-rw-r--r--doc/workflow/img/award_emoji_votes_least_popular.pngbin50191 -> 0 bytes
-rw-r--r--doc/workflow/img/award_emoji_votes_most_popular.pngbin48342 -> 0 bytes
-rw-r--r--doc/workflow/img/award_emoji_votes_sort_options.pngbin57145 -> 0 bytes
-rw-r--r--lib/api/branches.rb2
-rw-r--r--lib/api/entities.rb4
-rw-r--r--lib/api/helpers/runner.rb8
-rw-r--r--lib/api/runner.rb20
-rw-r--r--lib/api/services.rb25
-rw-r--r--lib/api/triggers.rb2
-rw-r--r--lib/api/v3/branches.rb21
-rw-r--r--lib/api/v3/triggers.rb2
-rw-r--r--lib/gitlab/database.rb6
-rw-r--r--lib/gitlab/git/diff.rb21
-rw-r--r--lib/gitlab/git/diff_collection.rb4
-rw-r--r--lib/gitlab/git_ref_validator.rb3
-rw-r--r--lib/gitlab/gitaly_client.rb14
-rw-r--r--lib/gitlab/gitaly_client/commit.rb25
-rw-r--r--lib/gitlab/import_export/import_export.yml4
-rw-r--r--lib/gitlab/import_export/relation_factory.rb8
-rw-r--r--lib/support/nginx/gitlab7
-rw-r--r--lib/support/nginx/gitlab-ssl7
-rw-r--r--package.json11
-rw-r--r--qa/qa/page/main/entry.rb10
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb11
-rw-r--r--spec/controllers/projects/variables_controller_spec.rb2
-rw-r--r--spec/factories/ci/runner_projects.rb2
-rw-r--r--spec/factories/merge_requests.rb1
-rw-r--r--spec/features/admin/admin_settings_spec.rb3
-rw-r--r--spec/features/groups/group_name_toggle_spec.rb (renamed from spec/features/groups/group_name_toggle.rb)2
-rw-r--r--spec/features/issues_spec.rb20
-rw-r--r--spec/features/merge_requests/created_from_fork_spec.rb3
-rw-r--r--spec/features/merge_requests/reset_filters_spec.rb20
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb1
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin681799 -> 679892 bytes
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb32
-rw-r--r--spec/features/projects/services/slack_service_spec.rb4
-rw-r--r--spec/features/projects/settings/merge_requests_settings_spec.rb23
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb33
-rw-r--r--spec/helpers/milestones_helper_spec.rb18
-rw-r--r--spec/helpers/todos_helper_spec.rb34
-rw-r--r--spec/javascripts/boards/board_blank_state_spec.js93
-rw-r--r--spec/javascripts/commit/pipelines/mock_data.js5
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js17
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_store_spec.js33
-rw-r--r--spec/javascripts/issue_spec.js15
-rw-r--r--spec/javascripts/test_bundle.js39
-rw-r--r--spec/javascripts/vue_pipelines_index/async_button_spec.js93
-rw-r--r--spec/javascripts/vue_pipelines_index/pipeline_url_spec.js100
-rw-r--r--spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js62
-rw-r--r--spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js40
-rw-r--r--spec/javascripts/vue_pipelines_index/pipelines_store_spec.js72
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js27
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_row_spec.js14
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_spec.js31
-rw-r--r--spec/javascripts/vue_shared/components/table_pagination_spec.js52
-rw-r--r--spec/lib/git_ref_validator_spec.rb5
-rw-r--r--spec/lib/gitlab/database_spec.rb14
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb37
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_spec.rb58
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/import_export/project.json51
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb19
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml5
-rw-r--r--spec/mailers/emails/builds_spec.rb64
-rw-r--r--spec/mailers/notify_spec.rb63
-rw-r--r--spec/migrations/migrate_build_events_to_pipeline_events_spec.rb74
-rw-r--r--spec/models/ci/build_spec.rb4
-rw-r--r--spec/models/ci/variable_spec.rb2
-rw-r--r--spec/models/commit_spec.rb28
-rw-r--r--spec/models/concerns/issuable_spec.rb10
-rw-r--r--spec/models/issue_spec.rb24
-rw-r--r--spec/models/project_services/builds_email_service_spec.rb111
-rw-r--r--spec/models/project_services/chat_message/build_message_spec.rb77
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb42
-rw-r--r--spec/models/project_spec.rb3
-rw-r--r--spec/models/user_spec.rb4
-rw-r--r--spec/requests/api/branches_spec.rb181
-rw-r--r--spec/requests/api/internal_spec.rb11
-rw-r--r--spec/requests/api/runner_spec.rb78
-rw-r--r--spec/requests/api/triggers_spec.rb30
-rw-r--r--spec/requests/api/v3/branches_spec.rb52
-rw-r--r--spec/requests/api/v3/triggers_spec.rb28
-rw-r--r--spec/services/ci/retry_build_service_spec.rb2
-rw-r--r--spec/services/merge_requests/build_service_spec.rb5
-rw-r--r--spec/services/merge_requests/get_urls_service_spec.rb10
-rw-r--r--spec/services/notification_service_spec.rb71
-rw-r--r--spec/support/matchers/email_matchers.rb5
-rw-r--r--spec/workers/build_email_worker_spec.rb36
-rw-r--r--yarn.lock70
259 files changed, 4183 insertions, 3149 deletions
diff --git a/.babelrc b/.babelrc
new file mode 100644
index 00000000000..ee4c391da30
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,21 @@
+{
+ "presets": [
+ ["latest", { "es2015": { "modules": false } }],
+ "stage-2"
+ ],
+ "env": {
+ "coverage": {
+ "plugins": [
+ ["istanbul", {
+ "exclude": [
+ "app/assets/javascripts/droplab/**/*",
+ "spec/javascripts/**/*"
+ ]
+ }],
+ ["transform-define", {
+ "process.env.BABEL_ENV": "coverage"
+ }]
+ ]
+ }
+ }
+}
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 080d8cd6c7f..34c10b3b77f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -277,6 +277,8 @@ rake karma:
stage: test
<<: *use-db
<<: *dedicated-runner
+ variables:
+ BABEL_ENV: "coverage"
script:
- bundle exec rake karma
artifacts:
@@ -389,9 +391,11 @@ trigger_docs:
cache: {}
artifacts: {}
script:
- - "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=ce https://gitlab.com/api/v3/projects/1794617/trigger/builds"
+ - "HTTP_STATUS=$(curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=${CI_PROJECT_NAME} --silent --output curl.log --write-out '%{http_code}' https://gitlab.com/api/v3/projects/1794617/trigger/builds)"
+ - if [ "${HTTP_STATUS}" -ne "201" ]; then echo "Error ${HTTP_STATUS}"; cat curl.log; echo; exit 1; fi
only:
- master@gitlab-org/gitlab-ce
+ - master@gitlab-org/gitlab-ee
# Notify slack in the end
notify:slack:
diff --git a/.gitlab/issue_templates/Feature Proposal.md b/.gitlab/issue_templates/Feature Proposal.md
index ea895ee6275..2636010e2fb 100644
--- a/.gitlab/issue_templates/Feature Proposal.md
+++ b/.gitlab/issue_templates/Feature Proposal.md
@@ -5,3 +5,13 @@
### Proposal
### Links / references
+
+### Documentation blurb
+
+(Write the start of the documentation of this feature here, include:
+
+1. Why should someone use it; what's the underlying problem.
+2. What is the solution.
+3. How does someone use this
+
+During implementation, this can then be copied and used as a starter for the documentation.)
diff --git a/Gemfile b/Gemfile
index 2f813324a35..6af27ce0f3e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -352,4 +352,4 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client
-gem 'gitaly', '~> 0.2.1'
+gem 'gitaly', '~> 0.3.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index c60c045a4c2..043ca4f8800 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -250,7 +250,7 @@ GEM
json
get_process_mem (0.2.0)
gherkin-ruby (0.3.2)
- gitaly (0.2.1)
+ gitaly (0.3.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@@ -304,7 +304,7 @@ GEM
multi_json (~> 1.10)
retriable (~> 1.4)
signet (~> 0.6)
- google-protobuf (3.2.0)
+ google-protobuf (3.2.0.2)
googleauth (0.5.1)
faraday (~> 0.9)
jwt (~> 1.4)
@@ -896,7 +896,7 @@ DEPENDENCIES
fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.0)
- gitaly (~> 0.2.1)
+ gitaly (~> 0.3.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1)
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index 30d3be453be..67c0c419713 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -2,7 +2,8 @@
/* global Vue */
/* global Sortable */
-require('./board_blank_state');
+import boardBlankState from './board_blank_state';
+
require('./board_delete');
require('./board_list');
@@ -17,7 +18,7 @@ require('./board_list');
components: {
'board-list': gl.issueBoards.BoardList,
'board-delete': gl.issueBoards.BoardDelete,
- 'board-blank-state': gl.issueBoards.BoardBlankState
+ boardBlankState,
},
props: {
list: Object,
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js b/app/assets/javascripts/boards/components/board_blank_state.js
index d76314c1892..52893d4642b 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.js
+++ b/app/assets/javascripts/boards/components/board_blank_state.js
@@ -1,53 +1,84 @@
-/* eslint-disable space-before-function-paren, comma-dangle */
-/* global Vue */
/* global ListLabel */
+/* global Cookies */
+const Store = gl.issueBoards.BoardsStore;
-(() => {
- const Store = gl.issueBoards.BoardsStore;
+export default {
+ template: `
+ <div class="board-blank-state">
+ <p>
+ Add the following default lists to your Issue Board with one click:
+ </p>
+ <ul class="board-blank-state-list">
+ <li v-for="label in predefinedLabels">
+ <span
+ class="label-color"
+ :style="{ backgroundColor: label.color }">
+ </span>
+ {{ label.title }}
+ </li>
+ </ul>
+ <p>
+ Starting out with the default set of lists will get you right on the way to making the most of your board.
+ </p>
+ <button
+ class="btn btn-create btn-inverted btn-block"
+ type="button"
+ @click.stop="addDefaultLists">
+ Add default lists
+ </button>
+ <button
+ class="btn btn-default btn-block"
+ type="button"
+ @click.stop="clearBlankState">
+ Nevermind, I'll use my own
+ </button>
+ </div>
+ `,
+ data() {
+ return {
+ predefinedLabels: [
+ new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
+ new ListLabel({ title: 'Doing', color: '#5CB85C' }),
+ ],
+ };
+ },
+ methods: {
+ addDefaultLists() {
+ this.clearBlankState();
- window.gl = window.gl || {};
- window.gl.issueBoards = window.gl.issueBoards || {};
-
- gl.issueBoards.BoardBlankState = Vue.extend({
- data () {
- return {
- predefinedLabels: [
- new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
- new ListLabel({ title: 'Doing', color: '#5CB85C' })
- ]
- };
- },
- methods: {
- addDefaultLists () {
- this.clearBlankState();
-
- this.predefinedLabels.forEach((label, i) => {
- Store.addList({
+ this.predefinedLabels.forEach((label, i) => {
+ Store.addList({
+ title: label.title,
+ position: i,
+ list_type: 'label',
+ label: {
title: label.title,
- position: i,
- list_type: 'label',
- label: {
- title: label.title,
- color: label.color
- }
- });
+ color: label.color,
+ },
});
+ });
- Store.state.lists = _.sortBy(Store.state.lists, 'position');
+ Store.state.lists = _.sortBy(Store.state.lists, 'position');
- // Save the labels
- gl.boardService.generateDefaultLists()
- .then((resp) => {
- resp.json().forEach((listObj) => {
- const list = Store.findList('title', listObj.title);
+ // Save the labels
+ gl.boardService.generateDefaultLists()
+ .then((resp) => {
+ resp.json().forEach((listObj) => {
+ const list = Store.findList('title', listObj.title);
- list.id = listObj.id;
- list.label.id = listObj.label.id;
- list.getIssues();
- });
+ list.id = listObj.id;
+ list.label.id = listObj.label.id;
+ list.getIssues();
});
- },
- clearBlankState: Store.removeBlankState.bind(Store)
- }
- });
-})();
+ })
+ .catch(() => {
+ Store.removeList(undefined, 'label');
+ Cookies.remove('issue_board_welcome_hidden', {
+ path: '',
+ });
+ Store.addBlankState();
+ });
+ },
+ clearBlankState: Store.removeBlankState.bind(Store),
+ },
+};
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js
index 9538f5b69e9..e6973c3fd59 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.js
+++ b/app/assets/javascripts/boards/components/modal/empty_state.js
@@ -30,7 +30,7 @@
if (this.activeTab === 'selected') {
obj.title = 'You haven\'t selected any issues yet';
obj.content = `
- Go back to <strong>All issues</strong> and select some issues
+ Go back to <strong>Open issues</strong> and select some issues
to add to your board.
`;
}
@@ -59,7 +59,7 @@
class="btn btn-default"
@click="changeTab('all')"
v-if="activeTab === 'selected'">
- All issues
+ Open issues
</button>
</div>
</div>
diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js
index e8cb43f3503..1cd6ca0ee88 100644
--- a/app/assets/javascripts/boards/components/modal/tabs.js
+++ b/app/assets/javascripts/boards/components/modal/tabs.js
@@ -23,7 +23,7 @@
href="#"
role="button"
@click.prevent="changeTab('all')">
- All issues
+ Open issues
<span class="badge">
{{ issuesCount }}
</span>
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index b5a988df897..a9f2d462c31 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -1,8 +1,9 @@
-/* eslint-disable no-new, no-param-reassign */
-/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
+/* eslint-disable no-param-reassign */
+import CommitPipelinesTable from './pipelines_table';
window.Vue = require('vue');
-require('./pipelines_table');
+window.Vue.use(require('vue-resource'));
+
/**
* Commits View > Pipelines Tab > Pipelines Table.
* Merge Request View > Pipelines Tab > Pipelines Table.
@@ -21,7 +22,7 @@ $(() => {
}
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
- gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView();
+ gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable();
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js b/app/assets/javascripts/commit/pipelines/pipelines_service.js
deleted file mode 100644
index 8ae98f9bf97..00000000000
--- a/app/assets/javascripts/commit/pipelines/pipelines_service.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/* globals Vue */
-/* eslint-disable no-unused-vars, no-param-reassign */
-
-/**
- * Pipelines service.
- *
- * Used to fetch the data used to render the pipelines table.
- * Uses Vue.Resource
- */
-class PipelinesService {
-
- /**
- * FIXME: The url provided to request the pipelines in the new merge request
- * page already has `.json`.
- * This should be fixed when the endpoint is improved.
- *
- * @param {String} root
- */
- constructor(root) {
- let endpoint;
-
- if (root.indexOf('.json') === -1) {
- endpoint = `${root}.json`;
- } else {
- endpoint = root;
- }
- this.pipelines = Vue.resource(endpoint);
- }
-
- /**
- * Given the root param provided when the class is initialized, will
- * make a GET request.
- *
- * @return {Promise}
- */
- all() {
- return this.pipelines.get();
- }
-}
-
-window.gl = window.gl || {};
-gl.commits = gl.commits || {};
-gl.commits.pipelines = gl.commits.pipelines || {};
-gl.commits.pipelines.PipelinesService = PipelinesService;
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js
index 631ed34851c..832c4b1bd2a 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js
@@ -1,13 +1,12 @@
-/* eslint-disable no-new, no-param-reassign */
-/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
-
-window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../../lib/utils/common_utils');
-require('../../vue_shared/vue_resource_interceptor');
-require('../../vue_shared/components/pipelines_table');
-require('./pipelines_service');
-const PipelineStore = require('./pipelines_store');
+/* eslint-disable no-new*/
+/* global Flash */
+import Vue from 'vue';
+import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
+import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
+import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
+import eventHub from '../../vue_pipelines_index/event_hub';
+import '../../lib/utils/common_utils';
+import '../../vue_shared/vue_resource_interceptor';
/**
*
@@ -20,48 +19,59 @@ const PipelineStore = require('./pipelines_store');
* as soon as we have Webpack and can load them directly into JS files.
*/
-(() => {
- window.gl = window.gl || {};
- gl.commits = gl.commits || {};
- gl.commits.pipelines = gl.commits.pipelines || {};
+export default Vue.component('pipelines-table', {
+ components: {
+ 'pipelines-table-component': PipelinesTableComponent,
+ },
- gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', {
+ /**
+ * Accesses the DOM to provide the needed data.
+ * Returns the necessary props to render `pipelines-table-component` component.
+ *
+ * @return {Object}
+ */
+ data() {
+ const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
+ const store = new PipelineStore();
- components: {
- 'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
- },
+ return {
+ endpoint: pipelinesTableData.endpoint,
+ store,
+ state: store.state,
+ isLoading: false,
+ };
+ },
- /**
- * Accesses the DOM to provide the needed data.
- * Returns the necessary props to render `pipelines-table-component` component.
- *
- * @return {Object}
- */
- data() {
- const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
- const store = new PipelineStore();
+ /**
+ * When the component is about to be mounted, tell the service to fetch the data
+ *
+ * A request to fetch the pipelines will be made.
+ * In case of a successfull response we will store the data in the provided
+ * store, in case of a failed response we need to warn the user.
+ *
+ */
+ beforeMount() {
+ this.service = new PipelinesService(this.endpoint);
- return {
- endpoint: pipelinesTableData.endpoint,
- store,
- state: store.state,
- isLoading: false,
- };
- },
+ this.fetchPipelines();
+
+ eventHub.$on('refreshPipelines', this.fetchPipelines);
+ },
+
+ beforeUpdate() {
+ if (this.state.pipelines.length && this.$children) {
+ this.store.startTimeAgoLoops.call(this, Vue);
+ }
+ },
- /**
- * When the component is about to be mounted, tell the service to fetch the data
- *
- * A request to fetch the pipelines will be made.
- * In case of a successfull response we will store the data in the provided
- * store, in case of a failed response we need to warn the user.
- *
- */
- beforeMount() {
- const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint);
+ beforeDestroyed() {
+ eventHub.$off('refreshPipelines');
+ },
+ methods: {
+ fetchPipelines() {
this.isLoading = true;
- return pipelinesService.all()
+ return this.service.getPipelines()
.then(response => response.json())
.then((json) => {
// depending of the endpoint the response can either bring a `pipelines` key or not.
@@ -71,34 +81,30 @@ const PipelineStore = require('./pipelines_store');
})
.catch(() => {
this.isLoading = false;
- new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert');
+ new Flash('An error occurred while fetching the pipelines, please reload the page again.');
});
},
+ },
- beforeUpdate() {
- if (this.state.pipelines.length && this.$children) {
- PipelineStore.startTimeAgoLoops.call(this, Vue);
- }
- },
-
- template: `
- <div class="pipelines">
- <div class="realtime-loading" v-if="isLoading">
- <i class="fa fa-spinner fa-spin"></i>
- </div>
+ template: `
+ <div class="pipelines">
+ <div class="realtime-loading" v-if="isLoading">
+ <i class="fa fa-spinner fa-spin"></i>
+ </div>
- <div class="blank-state blank-state-no-icon"
- v-if="!isLoading && state.pipelines.length === 0">
- <h2 class="blank-state-title js-blank-state-title">
- No pipelines to show
- </h2>
- </div>
+ <div class="blank-state blank-state-no-icon"
+ v-if="!isLoading && state.pipelines.length === 0">
+ <h2 class="blank-state-title js-blank-state-title">
+ No pipelines to show
+ </h2>
+ </div>
- <div class="table-holder pipelines"
- v-if="!isLoading && state.pipelines.length > 0">
- <pipelines-table-component :pipelines="state.pipelines"/>
- </div>
+ <div class="table-holder pipelines"
+ v-if="!isLoading && state.pipelines.length > 0">
+ <pipelines-table-component
+ :pipelines="state.pipelines"
+ :service="service" />
</div>
- `,
- });
-})();
+ </div>
+ `,
+});
diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
index 788daa96b3d..dd7081aefb7 100644
--- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -25,6 +25,7 @@ import collapseIcon from '../icons/collapse_icon.svg';
role="button"
data-container="body"
data-placement="top"
+ data-html="true"
:data-line-type="lineType"
:title="note.authorName + ': ' + note.noteTruncated"
:src="note.authorAvatar"
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index db1a2848d8d..3557f6f617e 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -1,4 +1,3 @@
-import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make this a bundle
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
/* global UsernameValidator */
/* global ActiveTabMemoizer */
@@ -329,8 +328,6 @@ const UserCallout = require('./user_callout');
case 'ci:lints:show':
new gl.CILintEditor();
break;
- case 'projects:environments:metrics':
- new PrometheusGraph();
case 'users:show':
new UserCallout();
break;
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index fdbb4644971..db10b383913 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -132,7 +132,7 @@ class DueDateSelect {
const selectedDateValue = this.datePayload[this.abilityName].due_date;
const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
- this.$loading.fadeIn();
+ this.$loading.removeClass('hidden').fadeIn();
if (isDropdown) {
this.$dropdown.trigger('loading.gl.dropdown');
diff --git a/app/assets/javascripts/environments/components/environment.js b/app/assets/javascripts/environments/components/environment.js
index 0923ce6b550..51aab8460f6 100644
--- a/app/assets/javascripts/environments/components/environment.js
+++ b/app/assets/javascripts/environments/components/environment.js
@@ -1,21 +1,18 @@
-/* eslint-disable no-param-reassign, no-new */
+/* eslint-disable no-new */
/* global Flash */
+import Vue from 'vue';
import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from './environments_table';
import EnvironmentsStore from '../stores/environments_store';
+import TablePaginationComponent from '../../vue_shared/components/table_pagination';
+import '../../lib/utils/common_utils';
import eventHub from '../event_hub';
-const Vue = window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../../vue_shared/components/table_pagination');
-require('../../lib/utils/common_utils');
-require('../../vue_shared/vue_resource_interceptor');
-
export default Vue.component('environment-component', {
components: {
'environment-table': EnvironmentTable,
- 'table-pagination': gl.VueGlPagination,
+ 'table-pagination': TablePaginationComponent,
},
data() {
@@ -59,7 +56,6 @@ export default Vue.component('environment-component', {
canCreateEnvironmentParsed() {
return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment);
},
-
},
/**
diff --git a/app/assets/javascripts/environments/components/environment_item.js b/app/assets/javascripts/environments/components/environment_item.js
index 93919d41c60..66ed10e19d1 100644
--- a/app/assets/javascripts/environments/components/environment_item.js
+++ b/app/assets/javascripts/environments/components/environment_item.js
@@ -1,24 +1,22 @@
import Timeago from 'timeago.js';
+import '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions';
import ExternalUrlComponent from './environment_external_url';
import StopComponent from './environment_stop';
import RollbackComponent from './environment_rollback';
import TerminalButtonComponent from './environment_terminal_button';
-import '../../lib/utils/text_utility';
-import '../../vue_shared/components/commit';
+import CommitComponent from '../../vue_shared/components/commit';
/**
* Envrionment Item Component
*
* Renders a table row for each environment.
*/
-
const timeagoInstance = new Timeago();
export default {
-
components: {
- 'commit-component': gl.CommitComponent,
+ 'commit-component': CommitComponent,
'actions-component': ActionsComponent,
'external-url-component': ExternalUrlComponent,
'stop-component': StopComponent,
diff --git a/app/assets/javascripts/environments/components/environments_table.js b/app/assets/javascripts/environments/components/environments_table.js
index 5f07b612b91..338dff40bc9 100644
--- a/app/assets/javascripts/environments/components/environments_table.js
+++ b/app/assets/javascripts/environments/components/environments_table.js
@@ -1,11 +1,11 @@
/**
* Render environments table.
*/
-import EnvironmentItem from './environment_item';
+import EnvironmentTableRowComponent from './environment_item';
export default {
components: {
- 'environment-item': EnvironmentItem,
+ 'environment-item': EnvironmentTableRowComponent,
},
props: {
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js b/app/assets/javascripts/environments/folder/environments_folder_view.js
index 7abcf6dbbea..8abbcf0c227 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.js
@@ -1,20 +1,17 @@
-/* eslint-disable no-param-reassign, no-new */
+/* eslint-disable no-new */
/* global Flash */
+import Vue from 'vue';
import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from '../components/environments_table';
import EnvironmentsStore from '../stores/environments_store';
-
-const Vue = window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../../vue_shared/components/table_pagination');
-require('../../lib/utils/common_utils');
-require('../../vue_shared/vue_resource_interceptor');
+import TablePaginationComponent from '../../vue_shared/components/table_pagination';
+import '../../lib/utils/common_utils';
+import '../../vue_shared/vue_resource_interceptor';
export default Vue.component('environment-folder-view', {
-
components: {
'environment-table': EnvironmentTable,
- 'table-pagination': gl.VueGlPagination,
+ 'table-pagination': TablePaginationComponent,
},
data() {
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
index 76296c83d11..07040bf0d73 100644
--- a/app/assets/javascripts/environments/services/environments_service.js
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -1,5 +1,8 @@
/* eslint-disable class-methods-use-this */
import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
export default class EnvironmentsService {
constructor(endpoint) {
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index d3fe3872c56..3c3084f3b78 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -1,5 +1,4 @@
import '~/lib/utils/common_utils';
-
/**
* Environments Store.
*
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
index 134bdc6ad80..e7bf530d343 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
@@ -38,6 +38,7 @@
gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
}
+ this.resetFilters();
this.dismissDropdown();
this.dispatchInputEvent();
}
@@ -107,7 +108,7 @@
const hook = this.getCurrentHook();
if (hook) {
- const data = hook.list.data;
+ const data = hook.list.data || [];
const results = data.map((o) => {
const updated = o;
updated.droplab_hidden = false;
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index ef4029a8623..47e675f537e 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -2,6 +2,7 @@
/* global Flash */
require('./flash');
+require('~/lib/utils/text_utility');
require('vendor/jquery.waitforimages');
require('./task_list');
@@ -50,20 +51,21 @@ class Issue {
success: function(data, textStatus, jqXHR) {
if ('id' in data) {
$(document).trigger('issuable:change');
- const currentTotal = Number($('.issue_counter').text());
+ let total = Number($('.issue_counter').text().replace(/[^\d]/, ''));
if (isClose) {
$('a.btn-close').addClass('hidden');
$('a.btn-reopen').removeClass('hidden');
$('div.status-box-closed').removeClass('hidden');
$('div.status-box-open').addClass('hidden');
- $('.issue_counter').text(currentTotal - 1);
+ total -= 1;
} else {
$('a.btn-reopen').addClass('hidden');
$('a.btn-close').removeClass('hidden');
$('div.status-box-closed').addClass('hidden');
$('div.status-box-open').removeClass('hidden');
- $('.issue_counter').text(currentTotal + 1);
+ total += 1;
}
+ $('.issue_counter').text(gl.text.addDelimiter(total));
} else {
new Flash(issueFailMessage, 'alert');
}
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index c648a0f076c..443fb3e0ca9 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -76,7 +76,7 @@
if (!selected.length) {
data[abilityName].label_ids = [''];
}
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js
index 66cc270ab4d..94a4f24f1d7 100644
--- a/app/assets/javascripts/merge_request_widget.js
+++ b/app/assets/javascripts/merge_request_widget.js
@@ -176,7 +176,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
_this.opts.ci_sha = data.sha;
_this.updateCommitUrls(data.sha);
}
- if (showNotification) {
+ if (showNotification && data.status) {
status = _this.ciLabelForStatus(data.status);
if (status === "preparing") {
title = _this.opts.ci_title.preparing;
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 02ff6f5682c..40e977df693 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -159,7 +159,7 @@
}
$dropdown.trigger('loading.gl.dropdown');
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () {
@@ -171,7 +171,7 @@
data = {};
data[abilityName] = {};
data[abilityName].milestone_id = selected != null ? selected : null;
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
new file mode 100644
index 00000000000..b3ce9310417
--- /dev/null
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -0,0 +1,6 @@
+import PrometheusGraph from './prometheus_graph';
+
+document.addEventListener('DOMContentLoaded', function onLoad() {
+ document.removeEventListener('DOMContentLoaded', onLoad, false);
+ return new PrometheusGraph();
+}, false);
diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js
index 71eb746edac..fcffc11a2df 100644
--- a/app/assets/javascripts/monitoring/prometheus_graph.js
+++ b/app/assets/javascripts/monitoring/prometheus_graph.js
@@ -2,10 +2,9 @@
/* global Flash */
import d3 from 'd3';
-import _ from 'underscore';
import statusCodes from '~/lib/utils/http_status';
-import '~/lib/utils/common_utils';
-import '~/flash';
+import '../lib/utils/common_utils';
+import '../flash';
const prometheusGraphsContainer = '.prometheus-graph';
const metricsEndpoint = 'metrics.json';
@@ -31,22 +30,21 @@ class PrometheusGraph {
}
createGraph() {
- const self = this;
- _.each(this.data, (value, key) => {
- if (value.length > 0 && (key === 'cpu_values' || key === 'memory_values')) {
- self.plotValues(value, key);
+ Object.keys(this.data).forEach((key) => {
+ const value = this.data[key];
+ if (value.length > 0) {
+ this.plotValues(value, key);
}
});
}
init() {
- const self = this;
this.getData().then((metricsResponse) => {
- if (metricsResponse === {}) {
+ if (Object.keys(metricsResponse).length === 0) {
new Flash('Empty metrics', 'alert');
} else {
- self.transformData(metricsResponse);
- self.createGraph();
+ this.transformData(metricsResponse);
+ this.createGraph();
}
});
}
@@ -321,12 +319,14 @@ class PrometheusGraph {
transformData(metricsResponse) {
const metricTypes = {};
- _.each(metricsResponse.metrics, (value, key) => {
- const metricValues = value[0].values;
- metricTypes[key] = _.map(metricValues, metric => ({
- time: new Date(metric[0] * 1000),
- value: metric[1],
- }));
+ Object.keys(metricsResponse.metrics).forEach((key) => {
+ if (key === 'cpu_values' || key === 'memory_values') {
+ const metricValues = (metricsResponse.metrics[key])[0];
+ metricTypes[key] = metricValues.values.map(metric => ({
+ time: new Date(metric[0] * 1000),
+ value: metric[1],
+ }));
+ }
});
this.data = metricTypes;
}
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index c7a57b47834..eb897e9dfe9 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -53,7 +53,7 @@
$loading = $block.find('.block-loading').fadeOut();
var updateIssueBoardsIssue = function () {
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () {
$loading.fadeOut();
@@ -90,7 +90,7 @@
data = {};
data[abilityName] = {};
data[abilityName].assignee_id = selected != null ? selected : null;
- $loading.fadeIn();
+ $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
diff --git a/app/assets/javascripts/vue_pipelines_index/components/async_button.js b/app/assets/javascripts/vue_pipelines_index/components/async_button.js
new file mode 100644
index 00000000000..aaebf29d8ae
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/async_button.js
@@ -0,0 +1,92 @@
+/* eslint-disable no-new, no-alert */
+/* global Flash */
+import '~/flash';
+import eventHub from '../event_hub';
+
+export default {
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+
+ title: {
+ type: String,
+ required: true,
+ },
+
+ icon: {
+ type: String,
+ required: true,
+ },
+
+ cssClass: {
+ type: String,
+ required: true,
+ },
+
+ confirmActionMessage: {
+ type: String,
+ required: false,
+ },
+ },
+
+ data() {
+ return {
+ isLoading: false,
+ };
+ },
+
+ computed: {
+ iconClass() {
+ return `fa fa-${this.icon}`;
+ },
+
+ buttonClass() {
+ return `btn has-tooltip ${this.cssClass}`;
+ },
+ },
+
+ methods: {
+ onClick() {
+ if (this.confirmActionMessage && confirm(this.confirmActionMessage)) {
+ this.makeRequest();
+ } else if (!this.confirmActionMessage) {
+ this.makeRequest();
+ }
+ },
+
+ makeRequest() {
+ this.isLoading = true;
+
+ this.service.postAction(this.endpoint)
+ .then(() => {
+ this.isLoading = false;
+ eventHub.$emit('refreshPipelines');
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occured while making the request.');
+ });
+ },
+ },
+
+ template: `
+ <button
+ type="button"
+ @click="onClick"
+ :class="buttonClass"
+ :title="title"
+ :aria-label="title"
+ data-placement="top"
+ :disabled="isLoading">
+ <i :class="iconClass" aria-hidden="true"/>
+ <i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading" />
+ </button>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js b/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js
new file mode 100644
index 00000000000..4e183d5c8ec
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js
@@ -0,0 +1,56 @@
+export default {
+ props: [
+ 'pipeline',
+ ],
+ computed: {
+ user() {
+ return !!this.pipeline.user;
+ },
+ },
+ template: `
+ <td>
+ <a
+ :href="pipeline.path"
+ class="js-pipeline-url-link">
+ <span class="pipeline-id">#{{pipeline.id}}</span>
+ </a>
+ <span>by</span>
+ <a
+ class="js-pipeline-url-user"
+ v-if="user"
+ :href="pipeline.user.web_url">
+ <img
+ v-if="user"
+ class="avatar has-tooltip s20 "
+ :title="pipeline.user.name"
+ data-container="body"
+ :src="pipeline.user.avatar_url"
+ >
+ </a>
+ <span
+ v-if="!user"
+ class="js-pipeline-url-api api monospace">
+ API
+ </span>
+ <span
+ v-if="pipeline.flags.latest"
+ class="js-pipeline-url-lastest label label-success has-tooltip"
+ title="Latest pipeline for this branch"
+ data-original-title="Latest pipeline for this branch">
+ latest
+ </span>
+ <span
+ v-if="pipeline.flags.yaml_errors"
+ class="js-pipeline-url-yaml label label-danger has-tooltip"
+ :title="pipeline.yaml_errors"
+ :data-original-title="pipeline.yaml_errors">
+ yaml invalid
+ </span>
+ <span
+ v-if="pipeline.flags.stuck"
+ class="js-pipeline-url-stuck label label-warning">
+ stuck
+ </span>
+ </td>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js
new file mode 100644
index 00000000000..4bb2b048884
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js
@@ -0,0 +1,71 @@
+/* eslint-disable no-new */
+/* global Flash */
+import '~/flash';
+import playIconSvg from 'icons/_icon_play.svg';
+import eventHub from '../event_hub';
+
+export default {
+ props: {
+ actions: {
+ type: Array,
+ required: true,
+ },
+
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ playIconSvg,
+ isLoading: false,
+ };
+ },
+
+ methods: {
+ onClickAction(endpoint) {
+ this.isLoading = true;
+
+ this.service.postAction(endpoint)
+ .then(() => {
+ this.isLoading = false;
+ eventHub.$emit('refreshPipelines');
+ })
+ .catch(() => {
+ this.isLoading = false;
+ new Flash('An error occured while making the request.');
+ });
+ },
+ },
+
+ template: `
+ <div class="btn-group" v-if="actions">
+ <button
+ type="button"
+ class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
+ title="Manual job"
+ data-toggle="dropdown"
+ data-placement="top"
+ aria-label="Manual job"
+ :disabled="isLoading">
+ ${playIconSvg}
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+ </button>
+
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for="action in actions">
+ <button
+ type="button"
+ class="js-pipeline-action-link no-btn"
+ @click="onClickAction(action.path)">
+ ${playIconSvg}
+ <span>{{action.name}}</span>
+ </button>
+ </li>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js
new file mode 100644
index 00000000000..3555040d60f
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js
@@ -0,0 +1,32 @@
+export default {
+ props: {
+ artifacts: {
+ type: Array,
+ required: true,
+ },
+ },
+
+ template: `
+ <div class="btn-group" role="group">
+ <button
+ class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
+ title="Artifacts"
+ data-placement="top"
+ data-toggle="dropdown"
+ aria-label="Artifacts">
+ <i class="fa fa-download" aria-hidden="true"></i>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
+ <ul class="dropdown-menu dropdown-menu-align-right">
+ <li v-for="artifact in artifacts">
+ <a
+ rel="nofollow"
+ :href="artifact.path">
+ <i class="fa fa-download" aria-hidden="true"></i>
+ <span>Download {{artifact.name}} artifacts</span>
+ </a>
+ </li>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/stage.js b/app/assets/javascripts/vue_pipelines_index/components/stage.js
new file mode 100644
index 00000000000..a2c29002707
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/stage.js
@@ -0,0 +1,116 @@
+/* global Flash */
+import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
+import createdSvg from 'icons/_icon_status_created_borderless.svg';
+import failedSvg from 'icons/_icon_status_failed_borderless.svg';
+import manualSvg from 'icons/_icon_status_manual_borderless.svg';
+import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
+import runningSvg from 'icons/_icon_status_running_borderless.svg';
+import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
+import successSvg from 'icons/_icon_status_success_borderless.svg';
+import warningSvg from 'icons/_icon_status_warning_borderless.svg';
+
+export default {
+ data() {
+ const svgsDictionary = {
+ icon_status_canceled: canceledSvg,
+ icon_status_created: createdSvg,
+ icon_status_failed: failedSvg,
+ icon_status_manual: manualSvg,
+ icon_status_pending: pendingSvg,
+ icon_status_running: runningSvg,
+ icon_status_skipped: skippedSvg,
+ icon_status_success: successSvg,
+ icon_status_warning: warningSvg,
+ };
+
+ return {
+ builds: '',
+ spinner: '<span class="fa fa-spinner fa-spin"></span>',
+ svg: svgsDictionary[this.stage.status.icon],
+ };
+ },
+
+ props: {
+ stage: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ updated() {
+ if (this.builds) {
+ this.stopDropdownClickPropagation();
+ }
+ },
+
+ methods: {
+ fetchBuilds(e) {
+ const ariaExpanded = e.currentTarget.attributes['aria-expanded'];
+
+ if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null;
+
+ return this.$http.get(this.stage.dropdown_path)
+ .then((response) => {
+ this.builds = JSON.parse(response.body).html;
+ }, () => {
+ const flash = new Flash('Something went wrong on our end.');
+ return flash;
+ });
+ },
+
+ /**
+ * When the user right clicks or cmd/ctrl + click in the job name
+ * the dropdown should not be closed and the link should open in another tab,
+ * so we stop propagation of the click event inside the dropdown.
+ *
+ * Since this component is rendered multiple times per page we need to guarantee we only
+ * target the click event of this component.
+ */
+ stopDropdownClickPropagation() {
+ $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => {
+ e.stopPropagation();
+ });
+ },
+ },
+ computed: {
+ buildsOrSpinner() {
+ return this.builds ? this.builds : this.spinner;
+ },
+ dropdownClass() {
+ if (this.builds) return 'js-builds-dropdown-container';
+ return 'js-builds-dropdown-loading builds-dropdown-loading';
+ },
+ buildStatus() {
+ return `Build: ${this.stage.status.label}`;
+ },
+ tooltip() {
+ return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
+ },
+ triggerButtonClass() {
+ return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
+ },
+ },
+ template: `
+ <div>
+ <button
+ @click="fetchBuilds($event)"
+ :class="triggerButtonClass"
+ :title="stage.title"
+ data-placement="top"
+ data-toggle="dropdown"
+ type="button"
+ :aria-label="stage.title">
+ <span v-html="svg" aria-hidden="true"></span>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
+ <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
+ <div class="arrow-up" aria-hidden="true"></div>
+ <div
+ :class="dropdownClass"
+ class="js-builds-dropdown-list scrollable-menu"
+ v-html="buildsOrSpinner">
+ </div>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/status.js b/app/assets/javascripts/vue_pipelines_index/components/status.js
new file mode 100644
index 00000000000..21a281af438
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/status.js
@@ -0,0 +1,60 @@
+import canceledSvg from 'icons/_icon_status_canceled.svg';
+import createdSvg from 'icons/_icon_status_created.svg';
+import failedSvg from 'icons/_icon_status_failed.svg';
+import manualSvg from 'icons/_icon_status_manual.svg';
+import pendingSvg from 'icons/_icon_status_pending.svg';
+import runningSvg from 'icons/_icon_status_running.svg';
+import skippedSvg from 'icons/_icon_status_skipped.svg';
+import successSvg from 'icons/_icon_status_success.svg';
+import warningSvg from 'icons/_icon_status_warning.svg';
+
+export default {
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ const svgsDictionary = {
+ icon_status_canceled: canceledSvg,
+ icon_status_created: createdSvg,
+ icon_status_failed: failedSvg,
+ icon_status_manual: manualSvg,
+ icon_status_pending: pendingSvg,
+ icon_status_running: runningSvg,
+ icon_status_skipped: skippedSvg,
+ icon_status_success: successSvg,
+ icon_status_warning: warningSvg,
+ };
+
+ return {
+ svg: svgsDictionary[this.pipeline.details.status.icon],
+ };
+ },
+
+ computed: {
+ cssClasses() {
+ return `ci-status ci-${this.pipeline.details.status.group}`;
+ },
+
+ detailsPath() {
+ const { status } = this.pipeline.details;
+ return status.has_details ? status.details_path : false;
+ },
+
+ content() {
+ return `${this.svg} ${this.pipeline.details.status.text}`;
+ },
+ },
+ template: `
+ <td class="commit-link">
+ <a
+ :class="cssClasses"
+ :href="detailsPath"
+ v-html="content">
+ </a>
+ </td>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/time_ago.js b/app/assets/javascripts/vue_pipelines_index/components/time_ago.js
new file mode 100644
index 00000000000..498d0715f54
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/time_ago.js
@@ -0,0 +1,71 @@
+import iconTimerSvg from 'icons/_icon_timer.svg';
+import '../../lib/utils/datetime_utility';
+
+export default {
+ data() {
+ return {
+ currentTime: new Date(),
+ iconTimerSvg,
+ };
+ },
+ props: ['pipeline'],
+ computed: {
+ timeAgo() {
+ return gl.utils.getTimeago();
+ },
+ localTimeFinished() {
+ return gl.utils.formatDate(this.pipeline.details.finished_at);
+ },
+ timeStopped() {
+ const changeTime = this.currentTime;
+ const options = {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ };
+ options.timeZoneName = 'short';
+ const finished = this.pipeline.details.finished_at;
+ if (!finished && changeTime) return false;
+ return ({ words: this.timeAgo.format(finished) });
+ },
+ duration() {
+ const { duration } = this.pipeline.details;
+ const date = new Date(duration * 1000);
+
+ let hh = date.getUTCHours();
+ let mm = date.getUTCMinutes();
+ let ss = date.getSeconds();
+
+ if (hh < 10) hh = `0${hh}`;
+ if (mm < 10) mm = `0${mm}`;
+ if (ss < 10) ss = `0${ss}`;
+
+ if (duration !== null) return `${hh}:${mm}:${ss}`;
+ return false;
+ },
+ },
+ methods: {
+ changeTime() {
+ this.currentTime = new Date();
+ },
+ },
+ template: `
+ <td class="pipelines-time-ago">
+ <p class="duration" v-if='duration'>
+ <span v-html="iconTimerSvg"></span>
+ {{duration}}
+ </p>
+ <p class="finished-at" v-if='timeStopped'>
+ <i class="fa fa-calendar"></i>
+ <time
+ data-toggle="tooltip"
+ data-placement="top"
+ data-container="body"
+ :data-original-title='localTimeFinished'>
+ {{timeStopped.words}}
+ </time>
+ </p>
+ </td>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/event_hub.js b/app/assets/javascripts/vue_pipelines_index/event_hub.js
new file mode 100644
index 00000000000..0948c2e5352
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/vue_pipelines_index/index.js b/app/assets/javascripts/vue_pipelines_index/index.js
index a90bd1518e9..b4e2d3a1143 100644
--- a/app/assets/javascripts/vue_pipelines_index/index.js
+++ b/app/assets/javascripts/vue_pipelines_index/index.js
@@ -1,29 +1,28 @@
-/* eslint-disable no-param-reassign */
-/* global Vue, VueResource, gl */
-window.Vue = require('vue');
+import PipelinesStore from './stores/pipelines_store';
+import PipelinesComponent from './pipelines';
+import '../vue_shared/vue_resource_interceptor';
+
+const Vue = window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
-require('../lib/utils/common_utils');
-require('../vue_shared/vue_resource_interceptor');
-require('./pipelines');
$(() => new Vue({
el: document.querySelector('.vue-pipelines-index'),
data() {
const project = document.querySelector('.pipelines');
+ const store = new PipelinesStore();
return {
- scope: project.dataset.url,
- store: new gl.PipelineStore(),
+ store,
+ endpoint: project.dataset.url,
};
},
components: {
- 'vue-pipelines': gl.VuePipelines,
+ 'vue-pipelines': PipelinesComponent,
},
template: `
<vue-pipelines
- :scope="scope"
- :store="store">
- </vue-pipelines>
+ :endpoint="endpoint"
+ :store="store" />
`,
}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js
deleted file mode 100644
index 583d6915a85..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js
+++ /dev/null
@@ -1,123 +0,0 @@
-/* global Vue, Flash, gl */
-/* eslint-disable no-param-reassign, no-alert */
-const playIconSvg = require('icons/_icon_play.svg');
-
-((gl) => {
- gl.VuePipelineActions = Vue.extend({
- props: ['pipeline'],
- computed: {
- actions() {
- return this.pipeline.details.manual_actions.length > 0;
- },
- artifacts() {
- return this.pipeline.details.artifacts.length > 0;
- },
- },
- methods: {
- download(name) {
- return `Download ${name} artifacts`;
- },
-
- /**
- * Shows a dialog when the user clicks in the cancel button.
- * We need to prevent the default behavior and stop propagation because the
- * link relies on UJS.
- *
- * @param {Event} event
- */
- confirmAction(event) {
- if (!confirm('Are you sure you want to cancel this pipeline?')) {
- event.preventDefault();
- event.stopPropagation();
- }
- },
- },
-
- data() {
- return { playIconSvg };
- },
-
- template: `
- <td class="pipeline-actions">
- <div class="pull-right">
- <div class="btn-group">
- <div class="btn-group" v-if="actions">
- <button
- class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
- data-toggle="dropdown"
- title="Manual job"
- data-placement="top"
- data-container="body"
- aria-label="Manual job">
- <span v-html="playIconSvg" aria-hidden="true"></span>
- <i class="fa fa-caret-down" aria-hidden="true"></i>
- </button>
- <ul class="dropdown-menu dropdown-menu-align-right">
- <li v-for='action in pipeline.details.manual_actions'>
- <a
- rel="nofollow"
- data-method="post"
- :href="action.path" >
- <span v-html="playIconSvg" aria-hidden="true"></span>
- <span>{{action.name}}</span>
- </a>
- </li>
- </ul>
- </div>
-
- <div class="btn-group" v-if="artifacts">
- <button
- class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
- title="Artifacts"
- data-placement="top"
- data-container="body"
- data-toggle="dropdown"
- aria-label="Artifacts">
- <i class="fa fa-download" aria-hidden="true"></i>
- <i class="fa fa-caret-down" aria-hidden="true"></i>
- </button>
- <ul class="dropdown-menu dropdown-menu-align-right">
- <li v-for='artifact in pipeline.details.artifacts'>
- <a
- rel="nofollow"
- :href="artifact.path">
- <i class="fa fa-download" aria-hidden="true"></i>
- <span>{{download(artifact.name)}}</span>
- </a>
- </li>
- </ul>
- </div>
- <div class="btn-group" v-if="pipeline.flags.retryable">
- <a
- class="btn btn-default btn-retry has-tooltip"
- title="Retry"
- rel="nofollow"
- data-method="post"
- data-placement="top"
- data-container="body"
- data-toggle="dropdown"
- :href='pipeline.retry_path'
- aria-label="Retry">
- <i class="fa fa-repeat" aria-hidden="true"></i>
- </a>
- </div>
- <div class="btn-group" v-if="pipeline.flags.cancelable">
- <a
- class="btn btn-remove has-tooltip"
- title="Cancel"
- rel="nofollow"
- data-method="post"
- data-placement="top"
- data-container="body"
- data-toggle="dropdown"
- :href='pipeline.cancel_path'
- aria-label="Cancel">
- <i class="fa fa-remove" aria-hidden="true"></i>
- </a>
- </div>
- </div>
- </div>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js
deleted file mode 100644
index ae5649f0519..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
- gl.VuePipelineUrl = Vue.extend({
- props: [
- 'pipeline',
- ],
- computed: {
- user() {
- return !!this.pipeline.user;
- },
- },
- template: `
- <td>
- <a :href='pipeline.path'>
- <span class="pipeline-id">#{{pipeline.id}}</span>
- </a>
- <span>by</span>
- <a
- v-if='user'
- :href='pipeline.user.web_url'
- >
- <img
- v-if='user'
- class="avatar has-tooltip s20 "
- :title='pipeline.user.name'
- data-container="body"
- :src='pipeline.user.avatar_url'
- >
- </a>
- <span
- v-if='!user'
- class="api monospace"
- >
- API
- </span>
- <span
- v-if='pipeline.flags.latest'
- class="label label-success has-tooltip"
- title="Latest pipeline for this branch"
- data-original-title="Latest pipeline for this branch"
- >
- latest
- </span>
- <span
- v-if='pipeline.flags.yaml_errors'
- class="label label-danger has-tooltip"
- :title='pipeline.yaml_errors'
- :data-original-title='pipeline.yaml_errors'
- >
- yaml invalid
- </span>
- <span
- v-if='pipeline.flags.stuck'
- class="label label-warning"
- >
- stuck
- </span>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js b/app/assets/javascripts/vue_pipelines_index/pipelines.js
index 601ef41e917..f389e5e4950 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipelines.js
+++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js
@@ -1,87 +1,121 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
+/* global Flash */
+/* eslint-disable no-new */
+import '~/flash';
+import Vue from 'vue';
+import PipelinesService from './services/pipelines_service';
+import eventHub from './event_hub';
+import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
+import TablePaginationComponent from '../vue_shared/components/table_pagination';
-window.Vue = require('vue');
-require('../vue_shared/components/table_pagination');
-require('./store');
-require('../vue_shared/components/pipelines_table');
-const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_store');
-
-((gl) => {
- gl.VuePipelines = Vue.extend({
-
- components: {
- 'gl-pagination': gl.VueGlPagination,
- 'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
+export default {
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
},
- data() {
- return {
- pipelines: [],
- timeLoopInterval: '',
- intervalId: '',
- apiScope: 'all',
- pageInfo: {},
- pagenum: 1,
- count: {},
- pageRequest: false,
- };
- },
- props: ['scope', 'store'],
- created() {
- const pagenum = gl.utils.getParameterByName('page');
- const scope = gl.utils.getParameterByName('scope');
- if (pagenum) this.pagenum = pagenum;
- if (scope) this.apiScope = scope;
-
- this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope);
+ store: {
+ type: Object,
+ required: true,
},
+ },
+
+ components: {
+ 'gl-pagination': TablePaginationComponent,
+ 'pipelines-table-component': PipelinesTableComponent,
+ },
+
+ data() {
+ return {
+ state: this.store.state,
+ apiScope: 'all',
+ pagenum: 1,
+ pageRequest: false,
+ };
+ },
+
+ created() {
+ this.service = new PipelinesService(this.endpoint);
+
+ this.fetchPipelines();
+
+ eventHub.$on('refreshPipelines', this.fetchPipelines);
+ },
+
+ beforeUpdate() {
+ if (this.state.pipelines.length && this.$children) {
+ this.store.startTimeAgoLoops.call(this, Vue);
+ }
+ },
- beforeUpdate() {
- if (this.pipelines.length && this.$children) {
- CommitPipelinesStoreWithTimeAgo.startTimeAgoLoops.call(this, Vue);
- }
+ beforeDestroyed() {
+ eventHub.$off('refreshPipelines');
+ },
+
+ methods: {
+ /**
+ * Will change the page number and update the URL.
+ *
+ * @param {Number} pageNumber desired page to go to.
+ */
+ change(pageNumber) {
+ const param = gl.utils.setParamInURL('page', pageNumber);
+
+ gl.utils.visitUrl(param);
+ return param;
},
- methods: {
- /**
- * Will change the page number and update the URL.
- *
- * @param {Number} pageNumber desired page to go to.
- */
- change(pageNumber) {
- const param = gl.utils.setParamInURL('page', pageNumber);
-
- gl.utils.visitUrl(param);
- return param;
- },
+ fetchPipelines() {
+ const pageNumber = gl.utils.getParameterByName('page') || this.pagenum;
+ const scope = gl.utils.getParameterByName('scope') || this.apiScope;
+
+ this.pageRequest = true;
+ return this.service.getPipelines(scope, pageNumber)
+ .then(resp => ({
+ headers: resp.headers,
+ body: resp.json(),
+ }))
+ .then((response) => {
+ this.store.storeCount(response.body.count);
+ this.store.storePipelines(response.body.pipelines);
+ this.store.storePagination(response.headers);
+ })
+ .then(() => {
+ this.pageRequest = false;
+ })
+ .catch(() => {
+ this.pageRequest = false;
+ new Flash('An error occurred while fetching the pipelines, please reload the page again.');
+ });
},
- template: `
- <div>
- <div class="pipelines realtime-loading" v-if='pageRequest'>
- <i class="fa fa-spinner fa-spin"></i>
- </div>
-
- <div class="blank-state blank-state-no-icon"
- v-if="!pageRequest && pipelines.length === 0">
- <h2 class="blank-state-title js-blank-state-title">
- No pipelines to show
- </h2>
- </div>
-
- <div class="table-holder" v-if='!pageRequest && pipelines.length'>
- <pipelines-table-component :pipelines='pipelines'/>
- </div>
-
- <gl-pagination
- v-if='!pageRequest && pipelines.length && pageInfo.total > pageInfo.perPage'
- :pagenum='pagenum'
- :change='change'
- :count='count.all'
- :pageInfo='pageInfo'
- >
- </gl-pagination>
+ },
+ template: `
+ <div>
+ <div class="pipelines realtime-loading" v-if="pageRequest">
+ <i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+ </div>
+
+ <div class="blank-state blank-state-no-icon"
+ v-if="!pageRequest && state.pipelines.length === 0">
+ <h2 class="blank-state-title js-blank-state-title">
+ No pipelines to show
+ </h2>
+ </div>
+
+ <div class="table-holder" v-if="!pageRequest && state.pipelines.length">
+ <pipelines-table-component
+ :pipelines="state.pipelines"
+ :service="service"/>
</div>
- `,
- });
-})(window.gl || (window.gl = {}));
+
+ <gl-pagination
+ v-if="!pageRequest && state.pipelines.length && state.pageInfo.total > state.pageInfo.perPage"
+ :pagenum="pagenum"
+ :change="change"
+ :count="state.count.all"
+ :pageInfo="state.pageInfo"
+ >
+ </gl-pagination>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js b/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js
new file mode 100644
index 00000000000..708f5068dd3
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js
@@ -0,0 +1,44 @@
+/* eslint-disable class-methods-use-this */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+export default class PipelinesService {
+
+ /**
+ * Commits and merge request endpoints need to be requested with `.json`.
+ *
+ * The url provided to request the pipelines in the new merge request
+ * page already has `.json`.
+ *
+ * @param {String} root
+ */
+ constructor(root) {
+ let endpoint;
+
+ if (root.indexOf('.json') === -1) {
+ endpoint = `${root}.json`;
+ } else {
+ endpoint = root;
+ }
+
+ this.pipelines = Vue.resource(endpoint);
+ }
+
+ getPipelines(scope, page) {
+ return this.pipelines.get({ scope, page });
+ }
+
+ /**
+ * Post request for all pipelines actions.
+ * Endpoint content type needs to be:
+ * `Content-Type:application/x-www-form-urlencoded`
+ *
+ * @param {String} endpoint
+ * @return {Promise}
+ */
+ postAction(endpoint) {
+ return Vue.http.post(endpoint, {}, { emulateJSON: true });
+ }
+}
diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js b/app/assets/javascripts/vue_pipelines_index/stage.js
deleted file mode 100644
index ae4f0b4a53b..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/stage.js
+++ /dev/null
@@ -1,119 +0,0 @@
-/* global Vue, Flash, gl */
-/* eslint-disable no-param-reassign */
-import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
-import createdSvg from 'icons/_icon_status_created_borderless.svg';
-import failedSvg from 'icons/_icon_status_failed_borderless.svg';
-import manualSvg from 'icons/_icon_status_manual_borderless.svg';
-import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
-import runningSvg from 'icons/_icon_status_running_borderless.svg';
-import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
-import successSvg from 'icons/_icon_status_success_borderless.svg';
-import warningSvg from 'icons/_icon_status_warning_borderless.svg';
-
-((gl) => {
- gl.VueStage = Vue.extend({
- data() {
- const svgsDictionary = {
- icon_status_canceled: canceledSvg,
- icon_status_created: createdSvg,
- icon_status_failed: failedSvg,
- icon_status_manual: manualSvg,
- icon_status_pending: pendingSvg,
- icon_status_running: runningSvg,
- icon_status_skipped: skippedSvg,
- icon_status_success: successSvg,
- icon_status_warning: warningSvg,
- };
-
- return {
- builds: '',
- spinner: '<span class="fa fa-spinner fa-spin"></span>',
- svg: svgsDictionary[this.stage.status.icon],
- };
- },
-
- props: {
- stage: {
- type: Object,
- required: true,
- },
- },
-
- updated() {
- if (this.builds) {
- this.stopDropdownClickPropagation();
- }
- },
-
- methods: {
- fetchBuilds(e) {
- const areaExpanded = e.currentTarget.attributes['aria-expanded'];
-
- if (areaExpanded && (areaExpanded.textContent === 'true')) return null;
-
- return this.$http.get(this.stage.dropdown_path)
- .then((response) => {
- this.builds = JSON.parse(response.body).html;
- }, () => {
- const flash = new Flash('Something went wrong on our end.');
- return flash;
- });
- },
-
- /**
- * When the user right clicks or cmd/ctrl + click in the job name
- * the dropdown should not be closed and the link should open in another tab,
- * so we stop propagation of the click event inside the dropdown.
- *
- * Since this component is rendered multiple times per page we need to guarantee we only
- * target the click event of this component.
- */
- stopDropdownClickPropagation() {
- $(this.$el).on('click', '.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item', (e) => {
- e.stopPropagation();
- });
- },
- },
- computed: {
- buildsOrSpinner() {
- return this.builds ? this.builds : this.spinner;
- },
- dropdownClass() {
- if (this.builds) return 'js-builds-dropdown-container';
- return 'js-builds-dropdown-loading builds-dropdown-loading';
- },
- buildStatus() {
- return `Build: ${this.stage.status.label}`;
- },
- tooltip() {
- return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
- },
- triggerButtonClass() {
- return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
- },
- },
- template: `
- <div>
- <button
- @click="fetchBuilds($event)"
- :class="triggerButtonClass"
- :title="stage.title"
- data-placement="top"
- data-toggle="dropdown"
- type="button"
- :aria-label="stage.title">
- <span v-html="svg" aria-hidden="true"></span>
- <i class="fa fa-caret-down" aria-hidden="true"></i>
- </button>
- <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
- <div class="arrow-up" aria-hidden="true"></div>
- <div
- :class="dropdownClass"
- class="js-builds-dropdown-list scrollable-menu"
- v-html="buildsOrSpinner">
- </div>
- </ul>
- </div>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/status.js b/app/assets/javascripts/vue_pipelines_index/status.js
deleted file mode 100644
index 8d9f83ac113..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/status.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-import canceledSvg from 'icons/_icon_status_canceled.svg';
-import createdSvg from 'icons/_icon_status_created.svg';
-import failedSvg from 'icons/_icon_status_failed.svg';
-import manualSvg from 'icons/_icon_status_manual.svg';
-import pendingSvg from 'icons/_icon_status_pending.svg';
-import runningSvg from 'icons/_icon_status_running.svg';
-import skippedSvg from 'icons/_icon_status_skipped.svg';
-import successSvg from 'icons/_icon_status_success.svg';
-import warningSvg from 'icons/_icon_status_warning.svg';
-
-((gl) => {
- gl.VueStatusScope = Vue.extend({
- props: [
- 'pipeline',
- ],
-
- data() {
- const svgsDictionary = {
- icon_status_canceled: canceledSvg,
- icon_status_created: createdSvg,
- icon_status_failed: failedSvg,
- icon_status_manual: manualSvg,
- icon_status_pending: pendingSvg,
- icon_status_running: runningSvg,
- icon_status_skipped: skippedSvg,
- icon_status_success: successSvg,
- icon_status_warning: warningSvg,
- };
-
- return {
- svg: svgsDictionary[this.pipeline.details.status.icon],
- };
- },
-
- computed: {
- cssClasses() {
- const cssObject = { 'ci-status': true };
- cssObject[`ci-${this.pipeline.details.status.group}`] = true;
- return cssObject;
- },
-
- detailsPath() {
- const { status } = this.pipeline.details;
- return status.has_details ? status.details_path : false;
- },
-
- content() {
- return `${this.svg} ${this.pipeline.details.status.text}`;
- },
- },
- template: `
- <td class="commit-link">
- <a
- :class="cssClasses"
- :href="detailsPath"
- v-html="content">
- </a>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/store.js b/app/assets/javascripts/vue_pipelines_index/store.js
deleted file mode 100644
index 909007267b9..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/store.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/* global gl, Flash */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
- const pageValues = (headers) => {
- const normalized = gl.utils.normalizeHeaders(headers);
- const paginationInfo = gl.utils.parseIntPagination(normalized);
- return paginationInfo;
- };
-
- gl.PipelineStore = class {
- fetchDataLoop(Vue, pageNum, url, apiScope) {
- this.pageRequest = true;
-
- return this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
- .then((response) => {
- const pageInfo = pageValues(response.headers);
- this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
-
- const res = JSON.parse(response.body);
- this.count = Object.assign({}, this.count, res.count);
- this.pipelines = Object.assign([], this.pipelines, res.pipelines);
-
- this.pageRequest = false;
- }, () => {
- this.pageRequest = false;
- return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
- });
- }
- };
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
index f1b80e45444..7ac10086a55 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_store.js
+++ b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
@@ -1,31 +1,46 @@
/* eslint-disable no-underscore-dangle*/
-/**
- * Pipelines' Store for commits view.
- *
- * Used to store the Pipelines rendered in the commit view in the pipelines table.
- */
-require('../../vue_realtime_listener');
-
-class PipelinesStore {
+import '../../vue_realtime_listener';
+
+export default class PipelinesStore {
constructor() {
this.state = {};
+
this.state.pipelines = [];
+ this.state.count = {};
+ this.state.pageInfo = {};
}
storePipelines(pipelines = []) {
this.state.pipelines = pipelines;
+ }
- return pipelines;
+ storeCount(count = {}) {
+ this.state.count = count;
+ }
+
+ storePagination(pagination = {}) {
+ let paginationInfo;
+
+ if (Object.keys(pagination).length) {
+ const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
+ paginationInfo = gl.utils.parseIntPagination(normalizedHeaders);
+ } else {
+ paginationInfo = pagination;
+ }
+
+ this.state.pageInfo = paginationInfo;
}
/**
+ * FIXME: Move this inside the component.
+ *
* Once the data is received we will start the time ago loops.
*
* Everytime a request is made like retry or cancel a pipeline, every 10 seconds we
* update the time to show how long as passed.
*
*/
- static startTimeAgoLoops() {
+ startTimeAgoLoops() {
const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => {
this.$children[0].$children.reduce((acc, component) => {
@@ -44,5 +59,3 @@ class PipelinesStore {
gl.VueRealtimeListener(removeIntervals, startIntervals);
}
}
-
-module.exports = PipelinesStore;
diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js b/app/assets/javascripts/vue_pipelines_index/time_ago.js
deleted file mode 100644
index a383570857d..00000000000
--- a/app/assets/javascripts/vue_pipelines_index/time_ago.js
+++ /dev/null
@@ -1,78 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-window.Vue = require('vue');
-require('../lib/utils/datetime_utility');
-
-const iconTimerSvg = require('../../../views/shared/icons/_icon_timer.svg');
-
-((gl) => {
- gl.VueTimeAgo = Vue.extend({
- data() {
- return {
- currentTime: new Date(),
- iconTimerSvg,
- };
- },
- props: ['pipeline'],
- computed: {
- timeAgo() {
- return gl.utils.getTimeago();
- },
- localTimeFinished() {
- return gl.utils.formatDate(this.pipeline.details.finished_at);
- },
- timeStopped() {
- const changeTime = this.currentTime;
- const options = {
- weekday: 'long',
- year: 'numeric',
- month: 'short',
- day: 'numeric',
- };
- options.timeZoneName = 'short';
- const finished = this.pipeline.details.finished_at;
- if (!finished && changeTime) return false;
- return ({ words: this.timeAgo.format(finished) });
- },
- duration() {
- const { duration } = this.pipeline.details;
- const date = new Date(duration * 1000);
-
- let hh = date.getUTCHours();
- let mm = date.getUTCMinutes();
- let ss = date.getSeconds();
-
- if (hh < 10) hh = `0${hh}`;
- if (mm < 10) mm = `0${mm}`;
- if (ss < 10) ss = `0${ss}`;
-
- if (duration !== null) return `${hh}:${mm}:${ss}`;
- return false;
- },
- },
- methods: {
- changeTime() {
- this.currentTime = new Date();
- },
- },
- template: `
- <td class="pipelines-time-ago">
- <p class="duration" v-if='duration'>
- <span v-html="iconTimerSvg"></span>
- {{duration}}
- </p>
- <p class="finished-at" v-if='timeStopped'>
- <i class="fa fa-calendar"></i>
- <time
- data-toggle="tooltip"
- data-placement="top"
- data-container="body"
- :data-original-title='localTimeFinished'>
- {{timeStopped.words}}
- </time>
- </p>
- </td>
- `,
- });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_shared/components/commit.js b/app/assets/javascripts/vue_shared/components/commit.js
index 4381487b79e..fb68abd95a2 100644
--- a/app/assets/javascripts/vue_shared/components/commit.js
+++ b/app/assets/javascripts/vue_shared/components/commit.js
@@ -1,164 +1,157 @@
-/* global Vue */
-window.Vue = require('vue');
-const commitIconSvg = require('icons/_icon_commit.svg');
-
-(() => {
- window.gl = window.gl || {};
-
- window.gl.CommitComponent = Vue.component('commit-component', {
-
- props: {
- /**
- * Indicates the existance of a tag.
- * Used to render the correct icon, if true will render `fa-tag` icon,
- * if false will render `fa-code-fork` icon.
- */
- tag: {
- type: Boolean,
- required: false,
- default: false,
- },
-
- /**
- * If provided is used to render the branch name and url.
- * Should contain the following properties:
- * name
- * ref_url
- */
- commitRef: {
- type: Object,
- required: false,
- default: () => ({}),
- },
-
- /**
- * Used to link to the commit sha.
- */
- commitUrl: {
- type: String,
- required: false,
- default: '',
- },
-
- /**
- * Used to show the commit short sha that links to the commit url.
- */
- shortSha: {
- type: String,
- required: false,
- default: '',
- },
-
- /**
- * If provided shows the commit tile.
- */
- title: {
- type: String,
- required: false,
- default: '',
- },
-
- /**
- * If provided renders information about the author of the commit.
- * When provided should include:
- * `avatar_url` to render the avatar icon
- * `web_url` to link to user profile
- * `username` to render alt and title tags
- */
- author: {
- type: Object,
- required: false,
- default: () => ({}),
- },
+import commitIconSvg from 'icons/_icon_commit.svg';
+
+export default {
+ props: {
+ /**
+ * Indicates the existance of a tag.
+ * Used to render the correct icon, if true will render `fa-tag` icon,
+ * if false will render `fa-code-fork` icon.
+ */
+ tag: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- computed: {
- /**
- * Used to verify if all the properties needed to render the commit
- * ref section were provided.
- *
- * TODO: Improve this! Use lodash _.has when we have it.
- *
- * @returns {Boolean}
- */
- hasCommitRef() {
- return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
- },
-
- /**
- * Used to verify if all the properties needed to render the commit
- * author section were provided.
- *
- * TODO: Improve this! Use lodash _.has when we have it.
- *
- * @returns {Boolean}
- */
- hasAuthor() {
- return this.author &&
- this.author.avatar_url &&
- this.author.web_url &&
- this.author.username;
- },
-
- /**
- * If information about the author is provided will return a string
- * to be rendered as the alt attribute of the img tag.
- *
- * @returns {String}
- */
- userImageAltDescription() {
- return this.author &&
- this.author.username ? `${this.author.username}'s avatar` : null;
- },
+ /**
+ * If provided is used to render the branch name and url.
+ * Should contain the following properties:
+ * name
+ * ref_url
+ */
+ commitRef: {
+ type: Object,
+ required: false,
+ default: () => ({}),
},
- data() {
- return { commitIconSvg };
+ /**
+ * Used to link to the commit sha.
+ */
+ commitUrl: {
+ type: String,
+ required: false,
+ default: '',
},
- template: `
- <div class="branch-commit">
-
- <div v-if="hasCommitRef" class="icon-container">
- <i v-if="tag" class="fa fa-tag"></i>
- <i v-if="!tag" class="fa fa-code-fork"></i>
- </div>
-
- <a v-if="hasCommitRef"
- class="monospace branch-name"
- :href="commitRef.ref_url">
- {{commitRef.name}}
- </a>
-
- <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
-
- <a class="commit-id monospace"
- :href="commitUrl">
- {{shortSha}}
- </a>
-
- <p class="commit-title">
- <span v-if="title">
- <a v-if="hasAuthor"
- class="avatar-image-container"
- :href="author.web_url">
- <img
- class="avatar has-tooltip s20"
- :src="author.avatar_url"
- :alt="userImageAltDescription"
- :title="author.username" />
- </a>
-
- <a class="commit-row-message"
- :href="commitUrl">
- {{title}}
- </a>
- </span>
- <span v-else>
- Cant find HEAD commit for this branch
- </span>
- </p>
+ /**
+ * Used to show the commit short sha that links to the commit url.
+ */
+ shortSha: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * If provided shows the commit tile.
+ */
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
+
+ /**
+ * If provided renders information about the author of the commit.
+ * When provided should include:
+ * `avatar_url` to render the avatar icon
+ * `web_url` to link to user profile
+ * `username` to render alt and title tags
+ */
+ author: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+
+ computed: {
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * ref section were provided.
+ *
+ * TODO: Improve this! Use lodash _.has when we have it.
+ *
+ * @returns {Boolean}
+ */
+ hasCommitRef() {
+ return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
+ },
+
+ /**
+ * Used to verify if all the properties needed to render the commit
+ * author section were provided.
+ *
+ * TODO: Improve this! Use lodash _.has when we have it.
+ *
+ * @returns {Boolean}
+ */
+ hasAuthor() {
+ return this.author &&
+ this.author.avatar_url &&
+ this.author.web_url &&
+ this.author.username;
+ },
+
+ /**
+ * If information about the author is provided will return a string
+ * to be rendered as the alt attribute of the img tag.
+ *
+ * @returns {String}
+ */
+ userImageAltDescription() {
+ return this.author &&
+ this.author.username ? `${this.author.username}'s avatar` : null;
+ },
+ },
+
+ data() {
+ return { commitIconSvg };
+ },
+
+ template: `
+ <div class="branch-commit">
+
+ <div v-if="hasCommitRef" class="icon-container">
+ <i v-if="tag" class="fa fa-tag"></i>
+ <i v-if="!tag" class="fa fa-code-fork"></i>
</div>
- `,
- });
-})();
+
+ <a v-if="hasCommitRef"
+ class="monospace branch-name"
+ :href="commitRef.ref_url">
+ {{commitRef.name}}
+ </a>
+
+ <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
+
+ <a class="commit-id monospace"
+ :href="commitUrl">
+ {{shortSha}}
+ </a>
+
+ <p class="commit-title">
+ <span v-if="title">
+ <a v-if="hasAuthor"
+ class="avatar-image-container"
+ :href="author.web_url">
+ <img
+ class="avatar has-tooltip s20"
+ :src="author.avatar_url"
+ :alt="userImageAltDescription"
+ :title="author.username" />
+ </a>
+
+ <a class="commit-row-message"
+ :href="commitUrl">
+ {{title}}
+ </a>
+ </span>
+ <span v-else>
+ Cant find HEAD commit for this branch
+ </span>
+ </p>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js b/app/assets/javascripts/vue_shared/components/pipelines_table.js
index 0d8f85db965..afd8d7acf6b 100644
--- a/app/assets/javascripts/vue_shared/components/pipelines_table.js
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js
@@ -1,52 +1,48 @@
-/* eslint-disable no-param-reassign */
-/* global Vue */
+import PipelinesTableRowComponent from './pipelines_table_row';
-require('./pipelines_table_row');
/**
* Pipelines Table Component.
*
* Given an array of objects, renders a table.
*/
-
-(() => {
- window.gl = window.gl || {};
- gl.pipelines = gl.pipelines || {};
-
- gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', {
-
- props: {
- pipelines: {
- type: Array,
- required: true,
- default: () => ([]),
- },
-
+export default {
+ props: {
+ pipelines: {
+ type: Array,
+ required: true,
+ default: () => ([]),
},
- components: {
- 'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent,
+ service: {
+ type: Object,
+ required: true,
},
+ },
+
+ components: {
+ 'pipelines-table-row-component': PipelinesTableRowComponent,
+ },
- template: `
- <table class="table ci-table">
- <thead>
- <tr>
- <th class="js-pipeline-status pipeline-status">Status</th>
- <th class="js-pipeline-info pipeline-info">Pipeline</th>
- <th class="js-pipeline-commit pipeline-commit">Commit</th>
- <th class="js-pipeline-stages pipeline-stages">Stages</th>
- <th class="js-pipeline-date pipeline-date"></th>
- <th class="js-pipeline-actions pipeline-actions"></th>
- </tr>
- </thead>
- <tbody>
- <template v-for="model in pipelines"
- v-bind:model="model">
- <tr is="pipelines-table-row-component"
- :pipeline="model"></tr>
- </template>
- </tbody>
- </table>
- `,
- });
-})();
+ template: `
+ <table class="table ci-table">
+ <thead>
+ <tr>
+ <th class="js-pipeline-status pipeline-status">Status</th>
+ <th class="js-pipeline-info pipeline-info">Pipeline</th>
+ <th class="js-pipeline-commit pipeline-commit">Commit</th>
+ <th class="js-pipeline-stages pipeline-stages">Stages</th>
+ <th class="js-pipeline-date pipeline-date"></th>
+ <th class="js-pipeline-actions pipeline-actions"></th>
+ </tr>
+ </thead>
+ <tbody>
+ <template v-for="model in pipelines"
+ v-bind:model="model">
+ <tr is="pipelines-table-row-component"
+ :pipeline="model"
+ :service="service"></tr>
+ </template>
+ </tbody>
+ </table>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
index e5e88186a85..f5b3cb9214e 100644
--- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
@@ -1,199 +1,228 @@
/* eslint-disable no-param-reassign */
-/* global Vue */
-
-require('../../vue_pipelines_index/status');
-require('../../vue_pipelines_index/pipeline_url');
-require('../../vue_pipelines_index/stage');
-require('../../vue_pipelines_index/pipeline_actions');
-require('../../vue_pipelines_index/time_ago');
-require('./commit');
+
+import AsyncButtonComponent from '../../vue_pipelines_index/components/async_button';
+import PipelinesActionsComponent from '../../vue_pipelines_index/components/pipelines_actions';
+import PipelinesArtifactsComponent from '../../vue_pipelines_index/components/pipelines_artifacts';
+import PipelinesStatusComponent from '../../vue_pipelines_index/components/status';
+import PipelinesStageComponent from '../../vue_pipelines_index/components/stage';
+import PipelinesUrlComponent from '../../vue_pipelines_index/components/pipeline_url';
+import PipelinesTimeagoComponent from '../../vue_pipelines_index/components/time_ago';
+import CommitComponent from './commit';
+
/**
* Pipeline table row.
*
* Given the received object renders a table row in the pipelines' table.
*/
-(() => {
- window.gl = window.gl || {};
- gl.pipelines = gl.pipelines || {};
-
- gl.pipelines.PipelinesTableRowComponent = Vue.component('pipelines-table-row-component', {
-
- props: {
- pipeline: {
- type: Object,
- required: true,
- default: () => ({}),
- },
+export default {
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ service: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ components: {
+ 'async-button-component': AsyncButtonComponent,
+ 'pipelines-actions-component': PipelinesActionsComponent,
+ 'pipelines-artifacts-component': PipelinesArtifactsComponent,
+ 'commit-component': CommitComponent,
+ 'dropdown-stage': PipelinesStageComponent,
+ 'pipeline-url': PipelinesUrlComponent,
+ 'status-scope': PipelinesStatusComponent,
+ 'time-ago': PipelinesTimeagoComponent,
+ },
+
+ computed: {
+ /**
+ * If provided, returns the commit tag.
+ * Needed to render the commit component column.
+ *
+ * This field needs a lot of verification, because of different possible cases:
+ *
+ * 1. person who is an author of a commit might be a GitLab user
+ * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
+ * 3. If GitLab user does not have avatar he/she might have a Gravatar
+ * 4. If committer is not a GitLab User he/she can have a Gravatar
+ * 5. We do not have consistent API object in this case
+ * 6. We should improve API and the code
+ *
+ * @returns {Object|Undefined}
+ */
+ commitAuthor() {
+ let commitAuthorInformation;
+
+ // 1. person who is an author of a commit might be a GitLab user
+ if (this.pipeline &&
+ this.pipeline.commit &&
+ this.pipeline.commit.author) {
+ // 2. if person who is an author of a commit is a GitLab user
+ // he/she can have a GitLab avatar
+ if (this.pipeline.commit.author.avatar_url) {
+ commitAuthorInformation = this.pipeline.commit.author;
+
+ // 3. If GitLab user does not have avatar he/she might have a Gravatar
+ } else if (this.pipeline.commit.author_gravatar_url) {
+ commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
+ avatar_url: this.pipeline.commit.author_gravatar_url,
+ });
+ }
+ }
+
+ // 4. If committer is not a GitLab User he/she can have a Gravatar
+ if (this.pipeline &&
+ this.pipeline.commit) {
+ commitAuthorInformation = {
+ avatar_url: this.pipeline.commit.author_gravatar_url,
+ web_url: `mailto:${this.pipeline.commit.author_email}`,
+ username: this.pipeline.commit.author_name,
+ };
+ }
+
+ return commitAuthorInformation;
},
- components: {
- 'commit-component': gl.CommitComponent,
- 'pipeline-actions': gl.VuePipelineActions,
- 'dropdown-stage': gl.VueStage,
- 'pipeline-url': gl.VuePipelineUrl,
- 'status-scope': gl.VueStatusScope,
- 'time-ago': gl.VueTimeAgo,
+ /**
+ * If provided, returns the commit tag.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTag() {
+ if (this.pipeline.ref &&
+ this.pipeline.ref.tag) {
+ return this.pipeline.ref.tag;
+ }
+ return undefined;
},
- computed: {
- /**
- * If provided, returns the commit tag.
- * Needed to render the commit component column.
- *
- * This field needs a lot of verification, because of different possible cases:
- *
- * 1. person who is an author of a commit might be a GitLab user
- * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
- * 3. If GitLab user does not have avatar he/she might have a Gravatar
- * 4. If committer is not a GitLab User he/she can have a Gravatar
- * 5. We do not have consistent API object in this case
- * 6. We should improve API and the code
- *
- * @returns {Object|Undefined}
- */
- commitAuthor() {
- let commitAuthorInformation;
-
- // 1. person who is an author of a commit might be a GitLab user
- if (this.pipeline &&
- this.pipeline.commit &&
- this.pipeline.commit.author) {
- // 2. if person who is an author of a commit is a GitLab user
- // he/she can have a GitLab avatar
- if (this.pipeline.commit.author.avatar_url) {
- commitAuthorInformation = this.pipeline.commit.author;
-
- // 3. If GitLab user does not have avatar he/she might have a Gravatar
- } else if (this.pipeline.commit.author_gravatar_url) {
- commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
- avatar_url: this.pipeline.commit.author_gravatar_url,
- });
+ /**
+ * If provided, returns the commit ref.
+ * Needed to render the commit component column.
+ *
+ * Matches `path` prop sent in the API to `ref_url` prop needed
+ * in the commit component.
+ *
+ * @returns {Object|Undefined}
+ */
+ commitRef() {
+ if (this.pipeline.ref) {
+ return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
+ if (prop === 'path') {
+ accumulator.ref_url = this.pipeline.ref[prop];
+ } else {
+ accumulator[prop] = this.pipeline.ref[prop];
}
- }
+ return accumulator;
+ }, {});
+ }
- // 4. If committer is not a GitLab User he/she can have a Gravatar
- if (this.pipeline &&
- this.pipeline.commit) {
- commitAuthorInformation = {
- avatar_url: this.pipeline.commit.author_gravatar_url,
- web_url: `mailto:${this.pipeline.commit.author_email}`,
- username: this.pipeline.commit.author_name,
- };
- }
+ return undefined;
+ },
- return commitAuthorInformation;
- },
-
- /**
- * If provided, returns the commit tag.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitTag() {
- if (this.pipeline.ref &&
- this.pipeline.ref.tag) {
- return this.pipeline.ref.tag;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit ref.
- * Needed to render the commit component column.
- *
- * Matches `path` prop sent in the API to `ref_url` prop needed
- * in the commit component.
- *
- * @returns {Object|Undefined}
- */
- commitRef() {
- if (this.pipeline.ref) {
- return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
- if (prop === 'path') {
- accumulator.ref_url = this.pipeline.ref[prop];
- } else {
- accumulator[prop] = this.pipeline.ref[prop];
- }
- return accumulator;
- }, {});
- }
+ /**
+ * If provided, returns the commit url.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitUrl() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.commit_path) {
+ return this.pipeline.commit.commit_path;
+ }
+ return undefined;
+ },
- return undefined;
- },
-
- /**
- * If provided, returns the commit url.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitUrl() {
- if (this.pipeline.commit &&
- this.pipeline.commit.commit_path) {
- return this.pipeline.commit.commit_path;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit short sha.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitShortSha() {
- if (this.pipeline.commit &&
- this.pipeline.commit.short_id) {
- return this.pipeline.commit.short_id;
- }
- return undefined;
- },
-
- /**
- * If provided, returns the commit title.
- * Needed to render the commit component column.
- *
- * @returns {String|Undefined}
- */
- commitTitle() {
- if (this.pipeline.commit &&
- this.pipeline.commit.title) {
- return this.pipeline.commit.title;
- }
- return undefined;
- },
+ /**
+ * If provided, returns the commit short sha.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitShortSha() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.short_id) {
+ return this.pipeline.commit.short_id;
+ }
+ return undefined;
},
- template: `
- <tr class="commit">
- <status-scope :pipeline="pipeline"/>
-
- <pipeline-url :pipeline="pipeline"></pipeline-url>
-
- <td>
- <commit-component
- :tag="commitTag"
- :commit-ref="commitRef"
- :commit-url="commitUrl"
- :short-sha="commitShortSha"
- :title="commitTitle"
- :author="commitAuthor"/>
- </td>
-
- <td class="stage-cell">
- <div class="stage-container dropdown js-mini-pipeline-graph"
- v-if="pipeline.details.stages.length > 0"
- v-for="stage in pipeline.details.stages">
- <dropdown-stage :stage="stage"/>
- </div>
- </td>
-
- <time-ago :pipeline="pipeline"/>
-
- <pipeline-actions :pipeline="pipeline" />
- </tr>
- `,
- });
-})();
+ /**
+ * If provided, returns the commit title.
+ * Needed to render the commit component column.
+ *
+ * @returns {String|Undefined}
+ */
+ commitTitle() {
+ if (this.pipeline.commit &&
+ this.pipeline.commit.title) {
+ return this.pipeline.commit.title;
+ }
+ return undefined;
+ },
+ },
+
+ template: `
+ <tr class="commit">
+ <status-scope :pipeline="pipeline"/>
+
+ <pipeline-url :pipeline="pipeline"></pipeline-url>
+
+ <td>
+ <commit-component
+ :tag="commitTag"
+ :commit-ref="commitRef"
+ :commit-url="commitUrl"
+ :short-sha="commitShortSha"
+ :title="commitTitle"
+ :author="commitAuthor"/>
+ </td>
+
+ <td class="stage-cell">
+ <div class="stage-container dropdown js-mini-pipeline-graph"
+ v-if="pipeline.details.stages.length > 0"
+ v-for="stage in pipeline.details.stages">
+ <dropdown-stage :stage="stage"/>
+ </div>
+ </td>
+
+ <time-ago :pipeline="pipeline"/>
+
+ <td class="pipeline-actions">
+ <div class="pull-right btn-group">
+ <pipelines-actions-component
+ v-if="pipeline.details.manual_actions.length"
+ :actions="pipeline.details.manual_actions"
+ :service="service" />
+
+ <pipelines-artifacts-component
+ v-if="pipeline.details.artifacts.length"
+ :artifacts="pipeline.details.artifacts" />
+
+ <async-button-component
+ v-if="pipeline.flags.retryable"
+ :service="service"
+ :endpoint="pipeline.retry_path"
+ css-class="js-pipelines-retry-button btn-default btn-retry"
+ title="Retry"
+ icon="repeat" />
+
+ <async-button-component
+ v-if="pipeline.flags.cancelable"
+ :service="service"
+ :endpoint="pipeline.cancel_path"
+ css-class="js-pipelines-cancel-button btn-remove"
+ title="Cancel"
+ icon="remove"
+ confirm-action-message="Are you sure you want to cancel this pipeline?" />
+ </div>
+ </td>
+ </tr>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js b/app/assets/javascripts/vue_shared/components/table_pagination.js
index 8943b850a72..b9cd28f6249 100644
--- a/app/assets/javascripts/vue_shared/components/table_pagination.js
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.js
@@ -1,147 +1,135 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign, no-plusplus */
-
-window.Vue = require('vue');
-
-((gl) => {
- const PAGINATION_UI_BUTTON_LIMIT = 4;
- const UI_LIMIT = 6;
- const SPREAD = '...';
- const PREV = 'Prev';
- const NEXT = 'Next';
- const FIRST = '<< First';
- const LAST = 'Last >>';
-
- gl.VueGlPagination = Vue.extend({
- props: {
-
- // TODO: Consider refactoring in light of turbolinks removal.
-
- /**
- This function will take the information given by the pagination component
-
- Here is an example `change` method:
-
- change(pagenum) {
- gl.utils.visitUrl(`?page=${pagenum}`);
- },
- */
-
- change: {
- type: Function,
- required: true,
+const PAGINATION_UI_BUTTON_LIMIT = 4;
+const UI_LIMIT = 6;
+const SPREAD = '...';
+const PREV = 'Prev';
+const NEXT = 'Next';
+const FIRST = '<< First';
+const LAST = 'Last >>';
+
+export default {
+ props: {
+ /**
+ This function will take the information given by the pagination component
+
+ Here is an example `change` method:
+
+ change(pagenum) {
+ gl.utils.visitUrl(`?page=${pagenum}`);
},
+ */
+ change: {
+ type: Function,
+ required: true,
+ },
- /**
- pageInfo will come from the headers of the API call
- in the `.then` clause of the VueResource API call
- there should be a function that contructs the pageInfo for this component
-
- This is an example:
-
- const pageInfo = headers => ({
- perPage: +headers['X-Per-Page'],
- page: +headers['X-Page'],
- total: +headers['X-Total'],
- totalPages: +headers['X-Total-Pages'],
- nextPage: +headers['X-Next-Page'],
- previousPage: +headers['X-Prev-Page'],
- });
- */
-
- pageInfo: {
- type: Object,
- required: true,
- },
+ /**
+ pageInfo will come from the headers of the API call
+ in the `.then` clause of the VueResource API call
+ there should be a function that contructs the pageInfo for this component
+
+ This is an example:
+
+ const pageInfo = headers => ({
+ perPage: +headers['X-Per-Page'],
+ page: +headers['X-Page'],
+ total: +headers['X-Total'],
+ totalPages: +headers['X-Total-Pages'],
+ nextPage: +headers['X-Next-Page'],
+ previousPage: +headers['X-Prev-Page'],
+ });
+ */
+ pageInfo: {
+ type: Object,
+ required: true,
},
- methods: {
- changePage(e) {
- const text = e.target.innerText;
- const { totalPages, nextPage, previousPage } = this.pageInfo;
-
- switch (text) {
- case SPREAD:
- break;
- case LAST:
- this.change(totalPages);
- break;
- case NEXT:
- this.change(nextPage);
- break;
- case PREV:
- this.change(previousPage);
- break;
- case FIRST:
- this.change(1);
- break;
- default:
- this.change(+text);
- break;
- }
- },
+ },
+ methods: {
+ changePage(e) {
+ const text = e.target.innerText;
+ const { totalPages, nextPage, previousPage } = this.pageInfo;
+
+ switch (text) {
+ case SPREAD:
+ break;
+ case LAST:
+ this.change(totalPages);
+ break;
+ case NEXT:
+ this.change(nextPage);
+ break;
+ case PREV:
+ this.change(previousPage);
+ break;
+ case FIRST:
+ this.change(1);
+ break;
+ default:
+ this.change(+text);
+ break;
+ }
},
- computed: {
- prev() {
- return this.pageInfo.previousPage;
- },
- next() {
- return this.pageInfo.nextPage;
- },
- getItems() {
- const total = this.pageInfo.totalPages;
- const page = this.pageInfo.page;
- const items = [];
+ },
+ computed: {
+ prev() {
+ return this.pageInfo.previousPage;
+ },
+ next() {
+ return this.pageInfo.nextPage;
+ },
+ getItems() {
+ const total = this.pageInfo.totalPages;
+ const page = this.pageInfo.page;
+ const items = [];
- if (page > 1) items.push({ title: FIRST });
+ if (page > 1) items.push({ title: FIRST });
- if (page > 1) {
- items.push({ title: PREV, prev: true });
- } else {
- items.push({ title: PREV, disabled: true, prev: true });
- }
+ if (page > 1) {
+ items.push({ title: PREV, prev: true });
+ } else {
+ items.push({ title: PREV, disabled: true, prev: true });
+ }
- if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
+ if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
- const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
- const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
+ const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
+ const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
- for (let i = start; i <= end; i++) {
- const isActive = i === page;
- items.push({ title: i, active: isActive, page: true });
- }
+ for (let i = start; i <= end; i += 1) {
+ const isActive = i === page;
+ items.push({ title: i, active: isActive, page: true });
+ }
- if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
- items.push({ title: SPREAD, separator: true, page: true });
- }
+ if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
+ items.push({ title: SPREAD, separator: true, page: true });
+ }
- if (page === total) {
- items.push({ title: NEXT, disabled: true, next: true });
- } else if (total - page >= 1) {
- items.push({ title: NEXT, next: true });
- }
+ if (page === total) {
+ items.push({ title: NEXT, disabled: true, next: true });
+ } else if (total - page >= 1) {
+ items.push({ title: NEXT, next: true });
+ }
- if (total - page >= 1) items.push({ title: LAST, last: true });
+ if (total - page >= 1) items.push({ title: LAST, last: true });
- return items;
- },
+ return items;
},
- template: `
- <div class="gl-pagination">
- <ul class="pagination clearfix">
- <li v-for='item in getItems'
- :class='{
- page: item.page,
- prev: item.prev,
- next: item.next,
- separator: item.separator,
- active: item.active,
- disabled: item.disabled
- }'
- >
- <a @click="changePage($event)">{{item.title}}</a>
- </li>
- </ul>
- </div>
- `,
- });
-})(window.gl || (window.gl = {}));
+ },
+ template: `
+ <div class="gl-pagination">
+ <ul class="pagination clearfix">
+ <li v-for='item in getItems'
+ :class='{
+ page: item.page,
+ prev: item.prev,
+ next: item.next,
+ separator: item.separator,
+ active: item.active,
+ disabled: item.disabled
+ }'
+ >
+ <a @click="changePage($event)">{{item.title}}</a>
+ </li>
+ </ul>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
index 4157fefddc9..f1c1e553b16 100644
--- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
+++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
@@ -1,11 +1,13 @@
-/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars,
-no-param-reassign, no-plusplus */
-/* global Vue */
+/* eslint-disable no-param-reassign, no-plusplus */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
Vue.http.interceptors.push((request, next) => {
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
- next((response) => {
+ next(() => {
Vue.activeResources--;
});
});
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index a4b38723bbd..2c33b235980 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -429,3 +429,9 @@ table {
@include str-truncated(100%);
}
}
+
+.tooltip {
+ .tooltip-inner {
+ word-wrap: break-word;
+ }
+}
diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index 0a8bc95590e..d86ae57cd9a 100644
--- a/app/assets/stylesheets/framework/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -2,5 +2,6 @@ gl-emoji {
display: inline-block;
display: inline-flex;
vertical-align: middle;
+ font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1.5em;
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 2ebeaf9a40d..7cb72fe516f 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -76,12 +76,14 @@
}
.input-token {
- flex: 1;
- -webkit-flex: 1;
+ max-width: 200px;
}
- .filtered-search-token + .input-token:not(:last-child) {
- max-width: 200px;
+ .input-token:only-child,
+ .input-token:last-child {
+ flex: 1;
+ -webkit-flex: 1;
+ max-width: initial;
}
}
@@ -158,8 +160,8 @@
background-color: $white-light;
@media (max-width: $screen-xs-min) {
- -webkit-flex: 1 1 100%;
- flex: 1 1 100%;
+ -webkit-flex: 1 1 auto;
+ flex: 1 1 auto;
margin-bottom: 10px;
.dropdown-menu {
@@ -174,8 +176,7 @@
.form-control {
position: relative;
min-width: 200px;
- padding-left: 0;
- padding-right: 25px;
+ padding: 5px 25px 6px 0;
border-color: transparent;
&:focus ~ .fa-filter {
@@ -221,6 +222,10 @@
.filter-dropdown-container {
display: -webkit-flex;
display: flex;
+
+ .dropdown-toggle {
+ line-height: 22px;
+ }
}
.dropdown-menu .filter-dropdown-item {
@@ -246,7 +251,9 @@
background-color: $white-light;
border-top: 0;
}
+}
+@media (max-width: $screen-xs) {
.filter-dropdown-container {
.dropdown-toggle,
.dropdown {
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index ea45aaa0253..205d23b1329 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -138,7 +138,6 @@
.nav-links {
display: inline-block;
- width: 50%;
margin-bottom: 0;
border-bottom: none;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index db5e2c51fe7..c241816788b 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -306,6 +306,11 @@ a > code {
* Textareas intended for GFM
*
*/
+textarea.js-gfm-input {
+ font-family: $monospace_font;
+ font-size: 13px;
+}
+
.strikethrough {
text-decoration: line-through;
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index c2156a5ac69..927bf9805ce 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -148,6 +148,18 @@
.error-alert > .alert {
margin-top: 5px;
margin-bottom: 5px;
+
+ &.alert-dismissable {
+ .close {
+ color: $white-light;
+ opacity: 0.85;
+ font-weight: normal;
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+ }
}
.discussion-body,
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 20eabc83142..33b38ca6923 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -72,11 +72,6 @@
color: $gl-text-color-secondary;
font-size: 14px;
}
-
- svg,
- .fa {
- margin-right: 0;
- }
}
.btn-group {
@@ -921,3 +916,22 @@
}
}
}
+
+/**
+ * Play button with icon in dropdowns
+ */
+.ci-table .no-btn {
+ border: none;
+ background: none;
+ outline: none;
+ width: 100%;
+ text-align: left;
+
+ .icon-play {
+ position: relative;
+ top: 2px;
+ margin-right: 5px;
+ height: 13px;
+ width: 12px;
+ }
+}
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index 2992568ae66..a8c0937569c 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -37,7 +37,6 @@ module ServiceParams
:namespace,
:new_issue_url,
:notify,
- :notify_only_broken_builds,
:notify_only_broken_pipelines,
:password,
:priority,
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 498690e8f11..096de8032ae 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -51,7 +51,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
private
def find_todos
- @todos ||= TodosFinder.new(current_user, params).execute
+ @todos ||= TodosFinder.new(current_user, params.merge(include_associations: true)).execute
end
def todos_counts
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index f2fee62ebd6..491cd5dc351 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -6,6 +6,8 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuableCollections
include SpammableActions
+ prepend_before_action :authenticate_user!, only: [:new]
+
before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 8b6c83d4fed..f210f7e61d2 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -45,8 +45,9 @@ class Projects::WikisController < Projects::ApplicationController
return render('empty') unless can?(current_user, :create_wiki, @project)
@page = @project_wiki.find_page(params[:id])
+ @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page)
- if @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page)
+ if @page.valid?
redirect_to(
namespace_project_wiki_path(@project.namespace, @project, @page),
notice: 'Wiki was successfully updated.'
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 395a8bffe92..47f7e0b1b28 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -316,6 +316,7 @@ class ProjectsController < Projects::ApplicationController
:namespace_id,
:only_allow_merge_if_all_discussions_are_resolved,
:only_allow_merge_if_pipeline_succeeds,
+ :printing_merge_request_link_enabled,
:path,
:public_builds,
:request_access_enabled,
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index b7f091f334d..13d33a1c31b 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -24,6 +24,7 @@ class TodosFinder
def execute
items = current_user.todos
+ items = include_associations(items)
items = by_action_id(items)
items = by_action(items)
items = by_author(items)
@@ -38,6 +39,17 @@ class TodosFinder
private
+ def include_associations(items)
+ return items unless params[:include_associations]
+
+ items.includes(
+ [
+ target: { project: [:route, namespace: :route] },
+ author: { namespace: :route },
+ ]
+ )
+ end
+
def action_id?
action_id.present? && Todo::ACTION_NAMES.has_key?(action_id.to_i)
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index c1523b4dabf..a8f167cbff2 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -16,6 +16,7 @@ module NavHelper
"page-gutter build-sidebar right-sidebar-expanded"
elsif current_path?('wikis#show') ||
current_path?('wikis#edit') ||
+ current_path?('wikis#update') ||
current_path?('wikis#history') ||
current_path?('wikis#git_access')
"page-gutter wiki-sidebar right-sidebar-expanded"
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 4f5adf623f2..847a8fdfca6 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -39,9 +39,13 @@ module TodosHelper
namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project,
todo.target, anchor: anchor)
else
- path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target]
-
- path.unshift(:pipelines) if todo.build_failed?
+ if todo.build_failed?
+ # associated namespace and route would be loaded from the db again if todo.project was used
+ project = todo.target.project
+ path = [:pipelines, project.namespace.becomes(Namespace), project, todo.target]
+ else
+ path = [todo.target]
+ end
polymorphic_path(path, anchor: anchor)
end
diff --git a/app/mailers/emails/builds.rb b/app/mailers/emails/builds.rb
deleted file mode 100644
index 3853af6201a..00000000000
--- a/app/mailers/emails/builds.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module Emails
- module Builds
- def build_fail_email(build_id, to)
- @build = Ci::Build.find(build_id)
- @project = @build.project
-
- add_project_headers
- add_build_headers('failed')
-
- mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha))
- end
-
- def build_success_email(build_id, to)
- @build = Ci::Build.find(build_id)
- @project = @build.project
-
- add_project_headers
- add_build_headers('success')
- mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha))
- end
-
- private
-
- def add_build_headers(status)
- headers['X-GitLab-Build-Id'] = @build.id
- headers['X-GitLab-Build-Ref'] = @build.ref
- headers['X-GitLab-Build-Status'] = status.to_s
- end
- end
-end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 5b9226a6b81..14df6f8f0a3 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -6,7 +6,6 @@ class Notify < BaseMailer
include Emails::Notes
include Emails::Projects
include Emails::Profile
- include Emails::Builds
include Emails::Pipelines
include Emails::Members
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 3722047251d..ad0be70c32a 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -15,7 +15,7 @@ module Ci
def persisted_environment
@persisted_environment ||= Environment.find_by(
name: expanded_environment_name,
- project_id: gl_project_id
+ project: project
)
end
@@ -223,7 +223,8 @@ module Ci
def merge_request
merge_requests = MergeRequest.includes(:merge_request_diff)
- .where(source_branch: ref, source_project_id: pipeline.gl_project_id)
+ .where(source_branch: ref,
+ source_project: pipeline.project)
.reorder(iid: :asc)
merge_requests.find do |merge_request|
@@ -231,10 +232,6 @@ module Ci
end
end
- def project_id
- gl_project_id
- end
-
def repo_url
auth = "gitlab-ci-token:#{ensure_token!}@"
project.http_url_to_repo.sub(/^https?:\/\//) do |prefix|
@@ -542,6 +539,16 @@ module Ci
Gitlab::Ci::Build::Credentials::Factory.new(self).create!
end
+ def dependencies
+ depended_jobs = depends_on_builds
+
+ return depended_jobs unless options[:dependencies].present?
+
+ depended_jobs.select do |job|
+ options[:dependencies].include?(job.name)
+ end
+ end
+
private
def update_artifacts_size
@@ -561,7 +568,7 @@ module Ci
end
def unscoped_project
- @unscoped_project ||= Project.unscoped.find_by(id: gl_project_id)
+ @unscoped_project ||= Project.unscoped.find_by(id: project_id)
end
CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index d1009f88549..f12be98c80c 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -5,9 +5,7 @@ module Ci
include Importable
include AfterCommitQueue
- self.table_name = 'ci_commits'
-
- belongs_to :project, foreign_key: :gl_project_id
+ belongs_to :project
belongs_to :user
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index edd21f984c8..487ba61bc9c 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -9,7 +9,7 @@ module Ci
has_many :builds
has_many :runner_projects, dependent: :destroy
- has_many :projects, through: :runner_projects, foreign_key: :gl_project_id
+ has_many :projects, through: :runner_projects
has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
@@ -24,7 +24,7 @@ module Ci
scope :owned_or_shared, ->(project_id) do
joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
- .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
+ .where("ci_runner_projects.project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
end
scope :assignable_for, ->(project) do
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index 234376a7e4c..5f01a0daae9 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -1,10 +1,10 @@
module Ci
class RunnerProject < ActiveRecord::Base
extend Ci::Model
-
+
belongs_to :runner
- belongs_to :project, foreign_key: :gl_project_id
+ belongs_to :project
- validates :runner_id, uniqueness: { scope: :gl_project_id }
+ validates :runner_id, uniqueness: { scope: :project_id }
end
end
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 90473d41c04..cba1d81a861 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -4,7 +4,7 @@ module Ci
acts_as_paranoid
- belongs_to :project, foreign_key: :gl_project_id
+ belongs_to :project
belongs_to :owner, class_name: "User"
has_many :trigger_requests, dependent: :destroy
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 2c8698d8b5d..6c6586110c5 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -2,11 +2,11 @@ module Ci
class Variable < ActiveRecord::Base
extend Ci::Model
- belongs_to :project, foreign_key: :gl_project_id
+ belongs_to :project
validates :key,
presence: true,
- uniqueness: { scope: :gl_project_id },
+ uniqueness: { scope: :project_id },
length: { maximum: 255 },
format: { with: /\A[a-zA-Z0-9_]+\z/,
message: "can contain only letters, digits and '_'." }
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 6ea5b1ae51f..ce92cc369ad 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -321,7 +321,14 @@ class Commit
end
def raw_diffs(*args)
- raw.diffs(*args)
+ use_gitaly = Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
+ deltas_only = args.last.is_a?(Hash) && args.last[:deltas_only]
+
+ if use_gitaly && !deltas_only
+ Gitlab::GitalyClient::Commit.diff_from_parent(self, *args)
+ else
+ raw.diffs(*args)
+ end
end
def diffs(diff_options = nil)
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 7e23e14794f..8c71267da65 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -5,7 +5,7 @@ class CommitStatus < ActiveRecord::Base
self.table_name = 'ci_builds'
- belongs_to :project, foreign_key: :gl_project_id
+ belongs_to :project
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
belongs_to :user
@@ -133,6 +133,12 @@ class CommitStatus < ActiveRecord::Base
false
end
+ # Added in 9.0 to keep backward compatibility for projects exported in 8.17
+ # and prior.
+ def gl_project_id
+ 'dummy'
+ end
+
def detailed_status(current_user)
Gitlab::Ci::Status::Factory
.new(self, current_user)
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 3b2c6a178e7..91f4eb13ecc 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -261,6 +261,7 @@ module Issuable
user: user.hook_attrs,
project: project.hook_attrs,
object_attributes: hook_attrs,
+ labels: labels.map(&:hook_attrs),
# DEPRECATED
repository: project.hook_attrs.slice(:name, :url, :description, :homepage)
}
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 9f6d215ceb3..529fb5ce988 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -51,11 +51,13 @@ module Routable
paths.each do |path|
path = connection.quote(path)
- where = "(routes.path = #{path})"
- if cast_lower
- where = "(#{where} OR (LOWER(routes.path) = LOWER(#{path})))"
- end
+ where =
+ if cast_lower
+ "(LOWER(routes.path) = LOWER(#{path}))"
+ else
+ "(routes.path = #{path})"
+ end
wheres << where
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 1427fdc31a4..602eed86d9e 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -55,6 +55,14 @@ class Issue < ActiveRecord::Base
state :opened
state :reopened
state :closed
+
+ before_transition any => :closed do |issue|
+ issue.closed_at = Time.zone.now
+ end
+
+ before_transition closed: any do |issue|
+ issue.closed_at = nil
+ end
end
def hook_attrs
diff --git a/app/models/label.rb b/app/models/label.rb
index f68a8c9cff2..568fa6d44f5 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -169,6 +169,10 @@ class Label < ActiveRecord::Base
end
end
+ def hook_attrs
+ attributes
+ end
+
private
def issues_count(user, params = {})
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 4759829a15c..cef8ad76b07 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -7,6 +7,7 @@ class MergeRequest < ActiveRecord::Base
belongs_to :target_project, class_name: "Project"
belongs_to :source_project, class_name: "Project"
+ belongs_to :project, foreign_key: :target_project_id
belongs_to :merge_user, class_name: "User"
has_many :merge_request_diffs, dependent: :destroy
@@ -540,10 +541,6 @@ class MergeRequest < ActiveRecord::Base
target_project != source_project
end
- def project
- target_project
- end
-
# If the merge request closes any issues, save this information in the
# `MergeRequestsClosingIssues` model. This is a performance optimization.
# Calculating this information for a number of merge requests requires
diff --git a/app/models/project.rb b/app/models/project.rb
index 2ffaaac93f3..17cf8226bcc 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -89,7 +89,6 @@ class Project < ActiveRecord::Base
has_one :campfire_service, dependent: :destroy
has_one :drone_ci_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy
- has_one :builds_email_service, dependent: :destroy
has_one :pipelines_email_service, dependent: :destroy
has_one :irker_service, dependent: :destroy
has_one :pivotaltracker_service, dependent: :destroy
@@ -159,13 +158,13 @@ class Project < ActiveRecord::Base
has_one :project_feature, dependent: :destroy
has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete
- has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id
- has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
- has_many :builds, class_name: 'Ci::Build', foreign_key: :gl_project_id # the builds are created from the commit_statuses
- has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject', foreign_key: :gl_project_id
+ has_many :commit_statuses, dependent: :destroy
+ has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline'
+ has_many :builds, class_name: 'Ci::Build' # the builds are created from the commit_statuses
+ has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
- has_many :variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id
- has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id
+ has_many :variables, dependent: :destroy, class_name: 'Ci::Variable'
+ has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger'
has_many :environments, dependent: :destroy
has_many :deployments, dependent: :destroy
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index ebd21e37189..0c526b53d72 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -1,107 +1,11 @@
+# This class is to be removed with 9.1
+# We should also by then remove BuildsEmailService from database
class BuildsEmailService < Service
- prop_accessor :recipients
- boolean_accessor :add_pusher
- boolean_accessor :notify_only_broken_builds
- validates :recipients, presence: true, if: ->(s) { s.activated? && !s.add_pusher? }
-
- def initialize_properties
- if properties.nil?
- self.properties = {}
- self.notify_only_broken_builds = true
- end
- end
-
- def title
- 'Builds emails'
- end
-
- def description
- 'Email the builds status to a list of recipients.'
- end
-
def self.to_param
'builds_email'
end
def self.supported_events
- %w(build)
- end
-
- def execute(push_data)
- return unless supported_events.include?(push_data[:object_kind])
- return unless should_build_be_notified?(push_data)
-
- recipients = all_recipients(push_data)
-
- if recipients.any?
- BuildEmailWorker.perform_async(
- push_data[:build_id],
- recipients,
- push_data
- )
- end
- end
-
- def can_test?
- project.builds.any?
- end
-
- def disabled_title
- "Please setup a build on your repository."
- end
-
- def test_data(project = nil, user = nil)
- Gitlab::DataBuilder::Build.build(project.builds.last)
- end
-
- def fields
- [
- { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by comma' },
- { type: 'checkbox', name: 'add_pusher', label: 'Add pusher to recipients list' },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
- ]
- end
-
- def test(data)
- begin
- # bypass build status verification when testing
- data[:build_status] = "failed"
- data[:build_allow_failure] = false
-
- result = execute(data)
- rescue StandardError => error
- return { success: false, result: error }
- end
-
- { success: true, result: result }
- end
-
- def should_build_be_notified?(data)
- case data[:build_status]
- when 'success'
- !notify_only_broken_builds?
- when 'failed'
- !allow_failure?(data)
- else
- false
- end
- end
-
- def allow_failure?(data)
- data[:build_allow_failure] == true
- end
-
- def all_recipients(data)
- all_recipients = []
-
- unless recipients.blank?
- all_recipients += recipients.split(',').compact.reject(&:blank?)
- end
-
- if add_pusher? && data[:user][:email]
- all_recipients << data[:user][:email]
- end
-
- all_recipients
+ %w[]
end
end
diff --git a/app/models/project_services/chat_message/build_message.rb b/app/models/project_services/chat_message/build_message.rb
deleted file mode 100644
index c776e0a20c4..00000000000
--- a/app/models/project_services/chat_message/build_message.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-module ChatMessage
- class BuildMessage < BaseMessage
- attr_reader :sha
- attr_reader :ref_type
- attr_reader :ref
- attr_reader :status
- attr_reader :project_name
- attr_reader :project_url
- attr_reader :user_name
- attr_reader :user_url
- attr_reader :duration
- attr_reader :stage
- attr_reader :build_id
- attr_reader :build_name
-
- def initialize(params)
- @sha = params[:sha]
- @ref_type = params[:tag] ? 'tag' : 'branch'
- @ref = params[:ref]
- @project_name = params[:project_name]
- @project_url = params[:project_url]
- @status = params[:commit][:status]
- @user_name = params[:commit][:author_name]
- @user_url = params[:commit][:author_url]
- @duration = params[:commit][:duration]
- @stage = params[:build_stage]
- @build_name = params[:build_name]
- @build_id = params[:build_id]
- end
-
- def pretext
- ''
- end
-
- def fallback
- format(message)
- end
-
- def attachments
- [{ text: format(message), color: attachment_color }]
- end
-
- private
-
- def message
- "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_link} #{humanized_status} on build #{build_link} of stage #{stage} in #{duration} #{'second'.pluralize(duration)}"
- end
-
- def build_url
- "#{project_url}/builds/#{build_id}"
- end
-
- def build_link
- link(build_name, build_url)
- end
-
- def user_link
- link(user_name, user_url)
- end
-
- def format(string)
- Slack::Notifier::LinkFormatter.format(string)
- end
-
- def humanized_status
- case status
- when 'success'
- 'passed'
- else
- status
- end
- end
-
- def attachment_color
- if status == 'success'
- 'good'
- else
- 'danger'
- end
- end
-
- def branch_url
- "#{project_url}/commits/#{ref}"
- end
-
- def branch_link
- link(ref, branch_url)
- end
-
- def project_link
- link(project_name, project_url)
- end
-
- def commit_url
- "#{project_url}/commit/#{sha}/builds"
- end
-
- def commit_link
- link(Commit.truncate_sha(sha), commit_url)
- end
- end
-end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index 8468934425f..200be99f36b 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -6,7 +6,7 @@ class ChatNotificationService < Service
default_value_for :category, 'chat'
prop_accessor :webhook, :username, :channel
- boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
+ boolean_accessor :notify_only_broken_pipelines
validates :webhook, presence: true, url: true, if: :activated?
@@ -16,7 +16,6 @@ class ChatNotificationService < Service
if properties.nil?
self.properties = {}
- self.notify_only_broken_builds = true
self.notify_only_broken_pipelines = true
end
end
@@ -27,7 +26,7 @@ class ChatNotificationService < Service
def self.supported_events
%w[push issue confidential_issue merge_request note tag_push
- build pipeline wiki_page]
+ pipeline wiki_page]
end
def execute(data)
@@ -89,8 +88,6 @@ class ChatNotificationService < Service
ChatMessage::MergeMessage.new(data) unless is_update?(data)
when "note"
ChatMessage::NoteMessage.new(data)
- when "build"
- ChatMessage::BuildMessage.new(data) if should_build_be_notified?(data)
when "pipeline"
ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page"
@@ -125,17 +122,6 @@ class ChatNotificationService < Service
data[:object_attributes][:action] == 'update'
end
- def should_build_be_notified?(data)
- case data[:commit][:status]
- when 'success'
- !notify_only_broken_builds?
- when 'failed'
- true
- else
- false
- end
- end
-
def should_pipeline_be_notified?(data)
case data[:object_attributes][:status]
when 'success'
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index c4142c38b2f..8b181221bb0 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -9,13 +9,13 @@ class HipchatService < Service
].freeze
prop_accessor :token, :room, :server, :color, :api_version
- boolean_accessor :notify_only_broken_builds, :notify
+ boolean_accessor :notify_only_broken_pipelines, :notify
validates :token, presence: true, if: :activated?
def initialize_properties
if properties.nil?
self.properties = {}
- self.notify_only_broken_builds = true
+ self.notify_only_broken_pipelines = true
end
end
@@ -41,12 +41,12 @@ class HipchatService < Service
placeholder: 'Leave blank for default (v2)' },
{ type: 'text', name: 'server',
placeholder: 'Leave blank for default. https://hipchat.example.com' },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
def self.supported_events
- %w(push issue confidential_issue merge_request note tag_push build)
+ %w(push issue confidential_issue merge_request note tag_push pipeline)
end
def execute(data)
@@ -90,8 +90,8 @@ class HipchatService < Service
create_merge_request_message(data) unless is_update?(data)
when "note"
create_note_message(data)
- when "build"
- create_build_message(data) if should_build_be_notified?(data)
+ when "pipeline"
+ create_pipeline_message(data) if should_pipeline_be_notified?(data)
end
end
@@ -240,28 +240,29 @@ class HipchatService < Service
message
end
- def create_build_message(data)
- ref_type = data[:tag] ? 'tag' : 'branch'
- ref = data[:ref]
- sha = data[:sha]
- user_name = data[:commit][:author_name]
- status = data[:commit][:status]
- duration = data[:commit][:duration]
+ def create_pipeline_message(data)
+ pipeline_attributes = data[:object_attributes]
+ pipeline_id = pipeline_attributes[:id]
+ ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+ ref = pipeline_attributes[:ref]
+ user_name = (data[:user] && data[:user][:name]) || 'API'
+ status = pipeline_attributes[:status]
+ duration = pipeline_attributes[:duration]
branch_link = "<a href=\"#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"
- commit_link = "<a href=\"#{project_url}/commit/#{CGI.escape(sha)}/builds\">#{Commit.truncate_sha(sha)}</a>"
+ pipeline_url = "<a href=\"#{project_url}/pipelines/#{pipeline_id}\">##{pipeline_id}</a>"
- "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
+ "#{project_link}: Pipeline #{pipeline_url} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
end
def message_color(data)
- build_status_color(data) || color || 'yellow'
+ pipeline_status_color(data) || color || 'yellow'
end
- def build_status_color(data)
- return unless data && data[:object_kind] == 'build'
+ def pipeline_status_color(data)
+ return unless data && data[:object_kind] == 'pipeline'
- case data[:commit][:status]
+ case data[:object_attributes][:status]
when 'success'
'green'
else
@@ -294,10 +295,10 @@ class HipchatService < Service
end
end
- def should_build_be_notified?(data)
- case data[:commit][:status]
+ def should_pipeline_be_notified?(data)
+ case data[:object_attributes][:status]
when 'success'
- !notify_only_broken_builds?
+ !notify_only_broken_pipelines?
when 'failed'
true
else
diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb
index c13538e9fea..1156d050622 100644
--- a/app/models/project_services/mattermost_service.rb
+++ b/app/models/project_services/mattermost_service.rb
@@ -30,7 +30,6 @@ class MattermostService < ChatNotificationService
[
{ type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/…' },
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index da7496573ef..b657db6f9ee 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -29,7 +29,6 @@ class SlackService < ChatNotificationService
[
{ type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/…' },
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
- { type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
diff --git a/app/models/service.rb b/app/models/service.rb
index 2f75a2e4e7f..e73f7e5d1a3 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -215,7 +215,6 @@ class Service < ActiveRecord::Base
assembla
bamboo
buildkite
- builds_email
bugzilla
campfire
custom_issue_tracker
diff --git a/app/models/user.rb b/app/models/user.rb
index 39c1281179b..8c7ad5d5174 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -877,7 +877,7 @@ class User < ActiveRecord::Base
def ci_authorized_runners
@ci_authorized_runners ||= begin
runner_ids = Ci::RunnerProject.
- where("ci_runner_projects.gl_project_id IN (#{ci_projects_union.to_sql})").
+ where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})").
select(:runner_id)
Ci::Runner.specific.where(id: runner_ids)
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 465c4d903ac..c771c22f46a 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -155,7 +155,7 @@ class WikiPage
end
# Returns boolean True or False if this instance
- # has been fully saved to disk or not.
+ # has been fully created on disk or not.
def persisted?
@persisted == true
end
@@ -226,6 +226,8 @@ class WikiPage
end
def save(method, *args)
+ saved = false
+
project_wiki = wiki
if valid? && project_wiki.send(method, *args)
@@ -243,10 +245,10 @@ class WikiPage
set_attributes
@persisted = true
+ saved = true
else
errors.add(:base, project_wiki.error_message) if project_wiki.error_message
- @persisted = false
end
- @persisted
+ saved
end
end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 0ab9042bf24..d6a4280ce4c 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -55,13 +55,13 @@ module Ci
new_builds.
# don't run projects which have not enabled shared runners and builds
joins(:project).where(projects: { shared_runners_enabled: true }).
- joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id').
+ joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id').
where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
# Implement fair scheduling
# this returns builds that are ordered by number of running builds
# we prefer projects that don't use shared runners at all
- joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id").
+ joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id").
order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
end
@@ -71,7 +71,7 @@ module Ci
def running_builds_for_shared_runners
Ci::Build.running.where(runner: Ci::Runner.shared).
- group(:gl_project_id).select(:gl_project_id, 'count(*) AS running_builds')
+ group(:project_id).select(:project_id, 'count(*) AS running_builds')
end
def new_builds
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 9d4739e37bb..fdce542bd9e 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -6,7 +6,7 @@ module MergeRequests
merge_request.source_project = find_source_project
merge_request.target_project = find_target_project
merge_request.target_branch = find_target_branch
- merge_request.can_be_created = branches_valid? && source_branch_specified? && target_branch_specified?
+ merge_request.can_be_created = branches_valid?
compare_branches if branches_present?
assign_title_and_description if merge_request.can_be_created
diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb
index ae386b53f42..f00a33969a8 100644
--- a/app/services/merge_requests/get_urls_service.rb
+++ b/app/services/merge_requests/get_urls_service.rb
@@ -7,6 +7,8 @@ module MergeRequests
end
def execute(changes)
+ return [] unless project.printing_merge_request_link_enabled
+
branches = get_branches(changes)
merge_requests_map = opened_merge_requests_from_source_branches(branches)
branches.map do |branch|
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
new file mode 100644
index 00000000000..44ae23fad18
--- /dev/null
+++ b/app/services/notification_recipient_service.rb
@@ -0,0 +1,293 @@
+#
+# Used by NotificationService to determine who should receive notification
+#
+class NotificationRecipientService
+ attr_reader :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ def build_recipients(target, current_user, action: nil, previous_assignee: nil, skip_current_user: true)
+ custom_action = build_custom_key(action, target)
+
+ recipients = target.participants(current_user)
+
+ unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
+ recipients = add_project_watchers(recipients)
+ end
+
+ recipients = add_custom_notifications(recipients, custom_action)
+ recipients = reject_mention_users(recipients)
+
+ # Re-assign is considered as a mention of the new assignee so we add the
+ # new assignee to the list of recipients after we rejected users with
+ # the "on mention" notification level
+ if [:reassign_merge_request, :reassign_issue].include?(custom_action)
+ recipients << previous_assignee if previous_assignee
+ recipients << target.assignee
+ end
+
+ recipients = reject_muted_users(recipients)
+ recipients = add_subscribed_users(recipients, target)
+
+ if [:new_issue, :new_merge_request].include?(custom_action)
+ recipients = add_labels_subscribers(recipients, target)
+ end
+
+ recipients = reject_unsubscribed_users(recipients, target)
+ recipients = reject_users_without_access(recipients, target)
+
+ recipients.delete(current_user) if skip_current_user
+
+ recipients.uniq
+ end
+
+ def build_relabeled_recipients(target, current_user, labels:)
+ recipients = add_labels_subscribers([], target, labels: labels)
+ recipients = reject_unsubscribed_users(recipients, target)
+ recipients = reject_users_without_access(recipients, target)
+ recipients.delete(current_user)
+ recipients.uniq
+ end
+
+ def build_new_note_recipients(note)
+ target = note.noteable
+
+ ability, subject = if note.for_personal_snippet?
+ [:read_personal_snippet, note.noteable]
+ else
+ [:read_project, note.project]
+ end
+
+ mentioned_users = note.mentioned_users.select { |user| user.can?(ability, subject) }
+
+ # Add all users participating in the thread (author, assignee, comment authors)
+ recipients =
+ if target.respond_to?(:participants)
+ target.participants(note.author)
+ else
+ mentioned_users
+ end
+
+ unless note.for_personal_snippet?
+ # Merge project watchers
+ recipients = add_project_watchers(recipients)
+
+ # Merge project with custom notification
+ recipients = add_custom_notifications(recipients, :new_note)
+ end
+
+ # Reject users with Mention notification level, except those mentioned in _this_ note.
+ recipients = reject_mention_users(recipients - mentioned_users)
+ recipients = recipients + mentioned_users
+
+ recipients = reject_muted_users(recipients)
+
+ recipients = add_subscribed_users(recipients, note.noteable)
+ recipients = reject_unsubscribed_users(recipients, note.noteable)
+ recipients = reject_users_without_access(recipients, note.noteable)
+
+ recipients.delete(note.author)
+ recipients.uniq
+ end
+
+ # Remove users with disabled notifications from array
+ # Also remove duplications and nil recipients
+ def reject_muted_users(users)
+ reject_users(users, :disabled)
+ end
+
+ protected
+
+ # Get project/group users with CUSTOM notification level
+ def add_custom_notifications(recipients, action)
+ user_ids = []
+
+ # Users with a notification setting on group or project
+ user_ids += user_ids_notifiable_on(project, :custom, action)
+ user_ids += user_ids_notifiable_on(project.group, :custom, action)
+
+ # Users with global level custom
+ user_ids_with_project_level_global = user_ids_notifiable_on(project, :global)
+ user_ids_with_group_level_global = user_ids_notifiable_on(project.group, :global)
+
+ global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global)
+ user_ids += user_ids_with_global_level_custom(global_users_ids, action)
+
+ recipients.concat(User.find(user_ids))
+ end
+
+ def add_project_watchers(recipients)
+ recipients.concat(project_watchers).compact
+ end
+
+ # Get project users with WATCH notification level
+ def project_watchers
+ project_members_ids = user_ids_notifiable_on(project)
+
+ user_ids_with_project_global = user_ids_notifiable_on(project, :global)
+ user_ids_with_group_global = user_ids_notifiable_on(project.group, :global)
+
+ user_ids = user_ids_with_global_level_watch((user_ids_with_project_global + user_ids_with_group_global).uniq)
+
+ user_ids_with_project_setting = select_project_members_ids(project, user_ids_with_project_global, user_ids)
+ user_ids_with_group_setting = select_group_members_ids(project.group, project_members_ids, user_ids_with_group_global, user_ids)
+
+ User.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq).to_a
+ end
+
+ # Remove users with notification level 'Mentioned'
+ def reject_mention_users(users)
+ reject_users(users, :mention)
+ end
+
+ def add_subscribed_users(recipients, target)
+ return recipients unless target.respond_to? :subscribers
+
+ recipients + target.subscribers(project)
+ end
+
+ def user_ids_notifiable_on(resource, notification_level = nil, action = nil)
+ return [] unless resource
+
+ if notification_level
+ settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level])
+ settings = settings.select { |setting| setting.events[action] } if action.present?
+ settings.map(&:user_id)
+ else
+ resource.notification_settings.pluck(:user_id)
+ end
+ end
+
+ # Build a list of user_ids based on project notification settings
+ def select_project_members_ids(project, global_setting, user_ids_global_level_watch)
+ user_ids = user_ids_notifiable_on(project, :watch)
+
+ # If project setting is global, add to watch list if global setting is watch
+ global_setting.each do |user_id|
+ if user_ids_global_level_watch.include?(user_id)
+ user_ids << user_id
+ end
+ end
+
+ user_ids
+ end
+
+ # Build a list of user_ids based on group notification settings
+ def select_group_members_ids(group, project_members, global_setting, user_ids_global_level_watch)
+ uids = user_ids_notifiable_on(group, :watch)
+
+ # Group setting is watch, add to user_ids list if user is not project member
+ user_ids = []
+ uids.each do |user_id|
+ if project_members.exclude?(user_id)
+ user_ids << user_id
+ end
+ end
+
+ # Group setting is global, add to user_ids list if global setting is watch
+ global_setting.each do |user_id|
+ if project_members.exclude?(user_id) && user_ids_global_level_watch.include?(user_id)
+ user_ids << user_id
+ end
+ end
+
+ user_ids
+ end
+
+ def user_ids_with_global_level_watch(ids)
+ settings_with_global_level_of(:watch, ids).pluck(:user_id)
+ end
+
+ def user_ids_with_global_level_custom(ids, action)
+ settings = settings_with_global_level_of(:custom, ids)
+ settings = settings.select { |setting| setting.events[action] }
+ settings.map(&:user_id)
+ end
+
+ def settings_with_global_level_of(level, ids)
+ NotificationSetting.where(
+ user_id: ids,
+ source_type: nil,
+ level: NotificationSetting.levels[level]
+ )
+ end
+
+ # Reject users which has certain notification level
+ #
+ # Example:
+ # reject_users(users, :watch, project)
+ #
+ def reject_users(users, level)
+ level = level.to_s
+
+ unless NotificationSetting.levels.keys.include?(level)
+ raise 'Invalid notification level'
+ end
+
+ users = users.to_a.compact.uniq
+ users = users.select { |u| u.can?(:receive_notifications) }
+
+ users.reject do |user|
+ global_notification_setting = user.global_notification_setting
+
+ next global_notification_setting.level == level unless project
+
+ setting = user.notification_settings_for(project)
+
+ if project.group && (setting.nil? || setting.global?)
+ setting = user.notification_settings_for(project.group)
+ end
+
+ # reject users who globally set mention notification and has no setting per project/group
+ next global_notification_setting.level == level unless setting
+
+ # reject users who set mention notification in project
+ next true if setting.level == level
+
+ # reject users who have mention level in project and disabled in global settings
+ setting.global? && global_notification_setting.level == level
+ end
+ end
+
+ def reject_unsubscribed_users(recipients, target)
+ return recipients unless target.respond_to? :subscriptions
+
+ recipients.reject do |user|
+ subscription = target.subscriptions.find_by_user_id(user.id)
+ subscription && !subscription.subscribed
+ end
+ end
+
+ def reject_users_without_access(recipients, target)
+ ability = case target
+ when Issuable
+ :"read_#{target.to_ability_name}"
+ when Ci::Pipeline
+ :read_build # We have build trace in pipeline emails
+ end
+
+ return recipients unless ability
+
+ recipients.select do |user|
+ user.can?(ability, target)
+ end
+ end
+
+ def add_labels_subscribers(recipients, target, labels: nil)
+ return recipients unless target.respond_to? :labels
+
+ (labels || target.labels).each do |label|
+ recipients += label.subscribers(project)
+ end
+
+ recipients
+ end
+
+ # Build event key to search on custom notification level
+ # Check NotificationSetting::EMAIL_EVENTS
+ def build_custom_key(action, object)
+ "#{action}_#{object.class.model_name.name.underscore}".to_sym
+ end
+end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index fdaba9b95fb..f9aa2346759 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -150,7 +150,10 @@ class NotificationService
end
def resolve_all_discussions(merge_request, current_user)
- recipients = build_recipients(merge_request, merge_request.target_project, current_user, action: "resolve_all_discussions")
+ recipients = NotificationRecipientService.new(merge_request.target_project).build_recipients(
+ merge_request,
+ current_user,
+ action: "resolve_all_discussions")
recipients.each do |recipient|
mailer.resolved_all_discussions_email(recipient.id, merge_request.id, current_user.id).deliver_later
@@ -164,64 +167,15 @@ class NotificationService
end
# Notify users on new note in system
- #
- # TODO: split on methods and refactor
- #
def new_note(note)
return true unless note.noteable_type.present?
# ignore gitlab service messages
return true if note.cross_reference? && note.system?
- target = note.noteable
-
- recipients = []
-
- mentioned_users = note.mentioned_users
-
- ability, subject = if note.for_personal_snippet?
- [:read_personal_snippet, note.noteable]
- else
- [:read_project, note.project]
- end
-
- mentioned_users.select! do |user|
- user.can?(ability, subject)
- end
-
- # Add all users participating in the thread (author, assignee, comment authors)
- participants =
- if target.respond_to?(:participants)
- target.participants(note.author)
- else
- mentioned_users
- end
-
- recipients = recipients.concat(participants)
-
- unless note.for_personal_snippet?
- # Merge project watchers
- recipients = add_project_watchers(recipients, note.project)
-
- # Merge project with custom notification
- recipients = add_custom_notifications(recipients, note.project, :new_note)
- end
-
- # Reject users with Mention notification level, except those mentioned in _this_ note.
- recipients = reject_mention_users(recipients - mentioned_users, note.project)
- recipients = recipients + mentioned_users
-
- recipients = reject_muted_users(recipients, note.project)
-
- recipients = add_subscribed_users(recipients, note.project, note.noteable)
- recipients = reject_unsubscribed_users(recipients, note.noteable)
- recipients = reject_users_without_access(recipients, note.noteable)
-
- recipients.delete(note.author)
- recipients = recipients.uniq
-
notify_method = "note_#{note.to_ability_name}_email".to_sym
+ recipients = NotificationRecipientService.new(note.project).build_new_note_recipients(note)
recipients.each do |recipient|
mailer.send(notify_method, recipient.id, note.id).deliver_later
end
@@ -290,7 +244,7 @@ class NotificationService
def project_was_moved(project, old_path_with_namespace)
recipients = project.team.members
- recipients = reject_muted_users(recipients, project)
+ recipients = NotificationRecipientService.new(project).reject_muted_users(recipients)
recipients.each do |recipient|
mailer.project_was_moved_email(
@@ -302,7 +256,7 @@ class NotificationService
end
def issue_moved(issue, new_issue, current_user)
- recipients = build_recipients(issue, issue.project, current_user)
+ recipients = NotificationRecipientService.new(issue.project).build_recipients(issue, current_user)
recipients.map do |recipient|
email = mailer.issue_moved_email(recipient, issue, new_issue, current_user)
@@ -324,9 +278,8 @@ class NotificationService
return unless mailer.respond_to?(email_template)
- recipients ||= build_recipients(
+ recipients ||= NotificationRecipientService.new(pipeline.project).build_recipients(
pipeline,
- pipeline.project,
nil, # The acting user, who won't be added to recipients
action: pipeline.status).map(&:notification_email)
@@ -337,199 +290,8 @@ class NotificationService
protected
- # Get project/group users with CUSTOM notification level
- def add_custom_notifications(recipients, project, action)
- user_ids = []
-
- # Users with a notification setting on group or project
- user_ids += notification_settings_for(project, :custom, action)
- user_ids += notification_settings_for(project.group, :custom, action)
-
- # Users with global level custom
- users_with_project_level_global = notification_settings_for(project, :global)
- users_with_group_level_global = notification_settings_for(project.group, :global)
-
- global_users_ids = users_with_project_level_global.concat(users_with_group_level_global)
- user_ids += users_with_global_level_custom(global_users_ids, action)
-
- recipients.concat(User.find(user_ids))
- end
-
- # Get project users with WATCH notification level
- def project_watchers(project)
- project_members = notification_settings_for(project)
-
- users_with_project_level_global = notification_settings_for(project, :global)
- users_with_group_level_global = notification_settings_for(project.group, :global)
-
- users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq)
-
- users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users)
- users_with_group_setting = select_group_member_setting(project.group, project_members, users_with_group_level_global, users)
-
- User.where(id: users_with_project_setting.concat(users_with_group_setting).uniq).to_a
- end
-
- def notification_settings_for(resource, notification_level = nil, action = nil)
- return [] unless resource
-
- if notification_level
- settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level])
- settings = settings.select { |setting| setting.events[action] } if action.present?
- settings.map(&:user_id)
- else
- resource.notification_settings.pluck(:user_id)
- end
- end
-
- def users_with_global_level_watch(ids)
- settings_with_global_level_of(:watch, ids).pluck(:user_id)
- end
-
- def users_with_global_level_custom(ids, action)
- settings = settings_with_global_level_of(:custom, ids)
- settings = settings.select { |setting| setting.events[action] }
- settings.map(&:user_id)
- end
-
- def settings_with_global_level_of(level, ids)
- NotificationSetting.where(
- user_id: ids,
- source_type: nil,
- level: NotificationSetting.levels[level]
- )
- end
-
- # Build a list of users based on project notification settings
- def select_project_member_setting(project, global_setting, users_global_level_watch)
- users = notification_settings_for(project, :watch)
-
- # If project setting is global, add to watch list if global setting is watch
- global_setting.each do |user_id|
- if users_global_level_watch.include?(user_id)
- users << user_id
- end
- end
-
- users
- end
-
- # Build a list of users based on group notification settings
- def select_group_member_setting(group, project_members, global_setting, users_global_level_watch)
- uids = notification_settings_for(group, :watch)
-
- # Group setting is watch, add to users list if user is not project member
- users = []
- uids.each do |user_id|
- if project_members.exclude?(user_id)
- users << user_id
- end
- end
-
- # Group setting is global, add to users list if global setting is watch
- global_setting.each do |user_id|
- if project_members.exclude?(user_id) && users_global_level_watch.include?(user_id)
- users << user_id
- end
- end
-
- users
- end
-
- def add_project_watchers(recipients, project)
- recipients.concat(project_watchers(project)).compact
- end
-
- # Remove users with disabled notifications from array
- # Also remove duplications and nil recipients
- def reject_muted_users(users, project = nil)
- reject_users(users, :disabled, project)
- end
-
- # Remove users with notification level 'Mentioned'
- def reject_mention_users(users, project = nil)
- reject_users(users, :mention, project)
- end
-
- # Reject users which has certain notification level
- #
- # Example:
- # reject_users(users, :watch, project)
- #
- def reject_users(users, level, project = nil)
- level = level.to_s
-
- unless NotificationSetting.levels.keys.include?(level)
- raise 'Invalid notification level'
- end
-
- users = users.to_a.compact.uniq
- users = users.select { |u| u.can?(:receive_notifications) }
-
- users.reject do |user|
- global_notification_setting = user.global_notification_setting
-
- next global_notification_setting.level == level unless project
-
- setting = user.notification_settings_for(project)
-
- if project.group && (setting.nil? || setting.global?)
- setting = user.notification_settings_for(project.group)
- end
-
- # reject users who globally set mention notification and has no setting per project/group
- next global_notification_setting.level == level unless setting
-
- # reject users who set mention notification in project
- next true if setting.level == level
-
- # reject users who have mention level in project and disabled in global settings
- setting.global? && global_notification_setting.level == level
- end
- end
-
- def reject_unsubscribed_users(recipients, target)
- return recipients unless target.respond_to? :subscriptions
-
- recipients.reject do |user|
- subscription = target.subscriptions.find_by_user_id(user.id)
- subscription && !subscription.subscribed
- end
- end
-
- def reject_users_without_access(recipients, target)
- ability = case target
- when Issuable
- :"read_#{target.to_ability_name}"
- when Ci::Pipeline
- :read_build # We have build trace in pipeline emails
- end
-
- return recipients unless ability
-
- recipients.select do |user|
- user.can?(ability, target)
- end
- end
-
- def add_subscribed_users(recipients, project, target)
- return recipients unless target.respond_to? :subscribers
-
- recipients + target.subscribers(project)
- end
-
- def add_labels_subscribers(recipients, project, target, labels: nil)
- return recipients unless target.respond_to? :labels
-
- (labels || target.labels).each do |label|
- recipients += label.subscribers(project)
- end
-
- recipients
- end
-
def new_resource_email(target, project, method)
- recipients = build_recipients(target, project, target.author, action: "new")
+ recipients = NotificationRecipientService.new(project).build_recipients(target, target.author, action: "new")
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id).deliver_later
@@ -537,7 +299,7 @@ class NotificationService
end
def new_mentions_in_resource_email(target, project, new_mentioned_users, current_user, method)
- recipients = build_recipients(target, project, current_user, action: "new")
+ recipients = NotificationRecipientService.new(project).build_recipients(target, current_user, action: "new")
recipients = recipients & new_mentioned_users
recipients.each do |recipient|
@@ -548,9 +310,8 @@ class NotificationService
def close_resource_email(target, project, current_user, method, skip_current_user: true)
action = method == :merged_merge_request_email ? "merge" : "close"
- recipients = build_recipients(
+ recipients = NotificationRecipientService.new(project).build_recipients(
target,
- project,
current_user,
action: action,
skip_current_user: skip_current_user
@@ -565,7 +326,12 @@ class NotificationService
previous_assignee_id = previous_record(target, 'assignee_id')
previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- recipients = build_recipients(target, project, current_user, action: "reassign", previous_assignee: previous_assignee)
+ recipients = NotificationRecipientService.new(project).build_recipients(
+ target,
+ current_user,
+ action: "reassign",
+ previous_assignee: previous_assignee
+ )
recipients.each do |recipient|
mailer.send(
@@ -579,7 +345,7 @@ class NotificationService
end
def relabeled_resource_email(target, project, labels, current_user, method)
- recipients = build_relabeled_recipients(target, project, current_user, labels: labels)
+ recipients = NotificationRecipientService.new(project).build_relabeled_recipients(target, current_user, labels: labels)
label_names = labels.map(&:name)
recipients.each do |recipient|
@@ -588,58 +354,13 @@ class NotificationService
end
def reopen_resource_email(target, project, current_user, method, status)
- recipients = build_recipients(target, project, current_user, action: "reopen")
+ recipients = NotificationRecipientService.new(project).build_recipients(target, current_user, action: "reopen")
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, status, current_user.id).deliver_later
end
end
- def build_recipients(target, project, current_user, action: nil, previous_assignee: nil, skip_current_user: true)
- custom_action = build_custom_key(action, target)
-
- recipients = target.participants(current_user)
-
- unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
- recipients = add_project_watchers(recipients, project)
- end
-
- recipients = add_custom_notifications(recipients, project, custom_action)
- recipients = reject_mention_users(recipients, project)
-
- recipients = recipients.uniq
-
- # Re-assign is considered as a mention of the new assignee so we add the
- # new assignee to the list of recipients after we rejected users with
- # the "on mention" notification level
- if [:reassign_merge_request, :reassign_issue].include?(custom_action)
- recipients << previous_assignee if previous_assignee
- recipients << target.assignee
- end
-
- recipients = reject_muted_users(recipients, project)
- recipients = add_subscribed_users(recipients, project, target)
-
- if [:new_issue, :new_merge_request].include?(custom_action)
- recipients = add_labels_subscribers(recipients, project, target)
- end
-
- recipients = reject_unsubscribed_users(recipients, target)
- recipients = reject_users_without_access(recipients, target)
-
- recipients.delete(current_user) if skip_current_user
-
- recipients.uniq
- end
-
- def build_relabeled_recipients(target, project, current_user, labels:)
- recipients = add_labels_subscribers([], project, target, labels: labels)
- recipients = reject_unsubscribed_users(recipients, target)
- recipients = reject_users_without_access(recipients, target)
- recipients.delete(current_user)
- recipients.uniq
- end
-
def mailer
Notify
end
@@ -651,10 +372,4 @@ class NotificationService
end
end
end
-
- # Build event key to search on custom notification level
- # Check NotificationSetting::EMAIL_EVENTS
- def build_custom_key(action, object)
- "#{action}_#{object.class.model_name.name.underscore}".to_sym
- end
end
diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml
deleted file mode 100644
index 060b50ffc69..00000000000
--- a/app/views/notify/build_fail_email.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-- content_for :header do
- %h1{ style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" }
- GitLab (job failed)
-
-%h3
- Project:
- = link_to namespace_project_url(@project.namespace, @project) do
- = @project.name
-
-%p
- Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)}
-%p
- Author: #{@build.pipeline.git_author_name}
-%p
- Branch: #{@build.ref}
-%p
- Stage: #{@build.stage}
-%p
- Job: #{@build.name}
-%p
- Message: #{@build.pipeline.git_commit_message}
-
-%p
- Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
diff --git a/app/views/notify/build_fail_email.text.erb b/app/views/notify/build_fail_email.text.erb
deleted file mode 100644
index 2a94688a6b0..00000000000
--- a/app/views/notify/build_fail_email.text.erb
+++ /dev/null
@@ -1,11 +0,0 @@
-Job failed for <%= @project.name %>
-
-Status: <%= @build.status %>
-Commit: <%= @build.pipeline.short_sha %>
-Author: <%= @build.pipeline.git_author_name %>
-Branch: <%= @build.ref %>
-Stage: <%= @build.stage %>
-Job: <%= @build.name %>
-Message: <%= @build.pipeline.git_commit_message %>
-
-Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %>
diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml
deleted file mode 100644
index ca0eaa96a9d..00000000000
--- a/app/views/notify/build_success_email.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-- content_for :header do
- %h1{ style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" }
- GitLab (job successful)
-
-%h3
- Project:
- = link_to namespace_project_url(@project.namespace, @project) do
- = @project.name
-
-%p
- Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)}
-%p
- Author: #{@build.pipeline.git_author_name}
-%p
- Branch: #{@build.ref}
-%p
- Stage: #{@build.stage}
-%p
- Job: #{@build.name}
-%p
- Message: #{@build.pipeline.git_commit_message}
-
-%p
- Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
diff --git a/app/views/notify/build_success_email.text.erb b/app/views/notify/build_success_email.text.erb
deleted file mode 100644
index 445cd46e64f..00000000000
--- a/app/views/notify/build_success_email.text.erb
+++ /dev/null
@@ -1,11 +0,0 @@
-Job successful for <%= @project.name %>
-
-Status: <%= @build.status %>
-Commit: <%= @build.pipeline.short_sha %>
-Author: <%= @build.pipeline.git_author_name %>
-Branch: <%= @build.ref %>
-Stage: <%= @build.stage %>
-Job: <%= @build.name %>
-Message: <%= @build.pipeline.git_commit_message %>
-
-Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %>
diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml
index 188198c47d5..61420fd0fb6 100644
--- a/app/views/projects/_merge_request_merge_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_settings.html.haml
@@ -13,3 +13,7 @@
= form.label :only_allow_merge_if_all_discussions_are_resolved do
= form.check_box :only_allow_merge_if_all_discussions_are_resolved
%strong Only allow merge requests to be merged if all discussions are resolved
+ .checkbox
+ = form.label :printing_merge_request_link_enabled do
+ = form.check_box :printing_merge_request_link_enabled
+ %strong Show link to create/view merge request when pushing from the command line
diff --git a/app/views/projects/boards/components/_blank_state.html.haml b/app/views/projects/boards/components/_blank_state.html.haml
deleted file mode 100644
index 0af40ddf8fe..00000000000
--- a/app/views/projects/boards/components/_blank_state.html.haml
+++ /dev/null
@@ -1,15 +0,0 @@
-%board-blank-state{ "inline-template" => true,
- "v-if" => 'list.id == "blank"' }
- .board-blank-state
- %p
- Add the following default lists to your Issue Board with one click:
- %ul.board-blank-state-list
- %li{ "v-for" => "label in predefinedLabels" }
- %span.label-color{ ":style" => "{ backgroundColor: label.color } " }
- {{ label.title }}
- %p
- Starting out with the default set of lists will get you right on the way to making the most of your board.
- %button.btn.btn-create.btn-inverted.btn-block{ type: "button", "@click.stop" => "addDefaultLists" }
- Add default lists
- %button.btn.btn-default.btn-block{ type: "button", "@click.stop" => "clearBlankState" }
- Nevermind, I'll use my own
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index 72bce4049de..0bca6a786cb 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -32,4 +32,4 @@
":root-path" => "rootPath",
"ref" => "board-list" }
- if can?(current_user, :admin_list, @project)
- = render "projects/boards/components/blank_state"
+ %board-blank-state{ "v-if" => 'list.id == "blank"' }
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 58c085cdb9d..85e442e115c 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -5,6 +5,7 @@
= render 'shared/no_ssh'
= render 'shared/no_password'
+= render "projects/head"
= render "home_panel"
.row-content-block.second-block.center
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index f8e94ca98ae..b8c1782f050 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -1,5 +1,8 @@
- @no_container = true
- page_title "Metrics for environment", @environment.name
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_bundle_tag('common_d3')
+ = page_specific_javascript_bundle_tag('monitoring')
= render "projects/pipelines/head"
%div{ class: container_class }
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 7b7d7b1e00e..f3a429d12d9 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -19,15 +19,14 @@
.nav-controls
= link_to params.merge(rss_url_options), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do
= icon('rss')
- - if can? current_user, :create_issue, @project
- = link_to new_namespace_project_issue_path(@project.namespace,
- @project,
- issue: { assignee_id: issues_finder.assignee.try(:id),
- milestone_id: issues_finder.milestones.first.try(:id) }),
- class: "btn btn-new",
- title: "New Issue",
- id: "new_issue_link" do
- New Issue
+ = link_to new_namespace_project_issue_path(@project.namespace,
+ @project,
+ issue: { assignee_id: issues_finder.assignee.try(:id),
+ milestone_id: issues_finder.milestones.first.try(:id) }),
+ class: "btn btn-new",
+ title: "New Issue",
+ id: "new_issue_link" do
+ New Issue
= render 'shared/issuable/search_bar', type: :issues
.issues-holder
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index d39f36e94c7..6ac05bf3afe 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -20,37 +20,34 @@
= confidential_icon(@issue)
= issuable_meta(@issue, @project, "Issue")
- - if can?(current_user, :create_issue, @project) || can?(current_user, :update_issue, @issue)
- .issuable-actions
- .clearfix.issue-btn-group.dropdown
- %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
- Options
- = icon('caret-down')
- .dropdown-menu.dropdown-menu-align-right.hidden-lg
- %ul
- - if can?(current_user, :create_issue, @project)
- %li
- = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link'
- - if can?(current_user, :update_issue, @issue)
- %li
- = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- %li
- = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
- %li
- = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
- - if @issue.submittable_as_spam_by?(current_user)
- %li
- = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
-
- - if can?(current_user, :create_issue, @project)
- = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
- New issue
- - if can?(current_user, :update_issue, @issue)
- = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ .issuable-actions
+ .clearfix.issue-btn-group.dropdown
+ %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
+ Options
+ = icon('caret-down')
+ .dropdown-menu.dropdown-menu-align-right.hidden-lg
+ %ul
+ %li
+ = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link'
+ - if can?(current_user, :update_issue, @issue)
+ %li
+ = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ %li
+ = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ %li
+ = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
- if @issue.submittable_as_spam_by?(current_user)
- = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
- = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
+ %li
+ = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
+
+ = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
+ New issue
+ - if can?(current_user, :update_issue, @issue)
+ = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ - if @issue.submittable_as_spam_by?(current_user)
+ = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
+ = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
.issue-details.issuable-details
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index 367aa550a78..a212c714826 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -1,6 +1,5 @@
.dropdown.inline.prepend-left-10
%button.dropdown-toggle{ type: 'button', data: {toggle: 'dropdown' } }
- %span.light
- if @sort.present?
= sort_options_hash[@sort]
- else
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index e2033654018..7a7e3d46796 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -16,7 +16,6 @@
Also, issues are searchable and filterable.
- if project_select_button
= render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue'
- - else
- = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
- else
- %h4.text-center There are no issues to show.
+ %h4 There are no issues to show.
+ = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 048fc488207..25a4aec0a38 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -30,7 +30,7 @@
= icon('user', 'aria-hidden': 'true')
.title.hide-collapsed
Assignee
- = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true')
+ = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.hide-collapsed
@@ -64,7 +64,7 @@
None
.title.hide-collapsed
Milestone
- = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true')
+ = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.hide-collapsed
@@ -91,7 +91,7 @@
= issuable.due_date.try(:to_s, :medium) || 'None'
.title.hide-collapsed
Due date
- = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true')
+ = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.hide-collapsed
@@ -121,12 +121,12 @@
- selected_labels = issuable.labels
.block.labels
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
- = icon('tags', 'aria-hidden': 'true')
+ = icon('tags', class: 'hidden', 'aria-hidden': 'true')
%span
= selected_labels.size
.title.hide-collapsed
Labels
- = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true')
+ = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
.value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
diff --git a/app/workers/build_email_worker.rb b/app/workers/build_email_worker.rb
deleted file mode 100644
index 5fdb1f2baa0..00000000000
--- a/app/workers/build_email_worker.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-class BuildEmailWorker
- include Sidekiq::Worker
- include BuildQueue
-
- def perform(build_id, recipients, push_data)
- recipients.each do |recipient|
- begin
- case push_data['build_status']
- when 'success'
- Notify.build_success_email(build_id, recipient).deliver_now
- when 'failed'
- Notify.build_fail_email(build_id, recipient).deliver_now
- end
- # These are input errors and won't be corrected even if Sidekiq retries
- rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e
- logger.info("Failed to send e-mail for project '#{push_data['project_name']}' to #{recipient}: #{e}")
- end
- end
- end
-end
diff --git a/changelogs/unreleased/21451-allow-disable-mr-link.yml b/changelogs/unreleased/21451-allow-disable-mr-link.yml
new file mode 100644
index 00000000000..ef99970a7a2
--- /dev/null
+++ b/changelogs/unreleased/21451-allow-disable-mr-link.yml
@@ -0,0 +1,4 @@
+---
+title: Add ability to disable Merge Request URL on push
+merge_request: 9663
+author: Alex Sanford
diff --git a/changelogs/unreleased/23993-drop-ci_projects.yml b/changelogs/unreleased/23993-drop-ci_projects.yml
new file mode 100644
index 00000000000..ee9cf774e37
--- /dev/null
+++ b/changelogs/unreleased/23993-drop-ci_projects.yml
@@ -0,0 +1,6 @@
+---
+title: Drop unused ci_projects table and some unused project_id columns,
+ then rename gl_project_id to project_id. Stop exporting job trace when
+ exporting projects.
+merge_request: 9378
+author: David Wagner
diff --git a/changelogs/unreleased/24683-sidebar-spinners.yml b/changelogs/unreleased/24683-sidebar-spinners.yml
new file mode 100644
index 00000000000..3fec273152f
--- /dev/null
+++ b/changelogs/unreleased/24683-sidebar-spinners.yml
@@ -0,0 +1,4 @@
+---
+title: hide loading spinners for server-rendered sidebar fields
+merge_request:
+author:
diff --git a/changelogs/unreleased/26236-monospace-gfm.yml b/changelogs/unreleased/26236-monospace-gfm.yml
new file mode 100644
index 00000000000..c44f3d4d3dc
--- /dev/null
+++ b/changelogs/unreleased/26236-monospace-gfm.yml
@@ -0,0 +1,4 @@
+---
+title: Change gfm textarea to use monospace font
+merge_request:
+author:
diff --git a/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml b/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml
new file mode 100644
index 00000000000..e82cbf00cfb
--- /dev/null
+++ b/changelogs/unreleased/26470-branch-names-with-reference-prefixes-results-in-buggy-branches.yml
@@ -0,0 +1,4 @@
+---
+title: Strip reference prefixes on branch creation
+merge_request: 8498
+author: Matthieu Tardy
diff --git a/changelogs/unreleased/28499-fix-large-text-tooltip-in-diff-file-name.yml b/changelogs/unreleased/28499-fix-large-text-tooltip-in-diff-file-name.yml
new file mode 100644
index 00000000000..660a881e094
--- /dev/null
+++ b/changelogs/unreleased/28499-fix-large-text-tooltip-in-diff-file-name.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes large file name tooltip cutoff in diff header
+merge_request: 9529
+author:
diff --git a/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml b/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml
new file mode 100644
index 00000000000..8b592766bf3
--- /dev/null
+++ b/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes dismissable error close is not visible enough
+merge_request: 9516
+author:
diff --git a/changelogs/unreleased/28890-allow-creating-mr-without-target-branch-in-url.yml b/changelogs/unreleased/28890-allow-creating-mr-without-target-branch-in-url.yml
new file mode 100644
index 00000000000..114a14ec2df
--- /dev/null
+++ b/changelogs/unreleased/28890-allow-creating-mr-without-target-branch-in-url.yml
@@ -0,0 +1,5 @@
+---
+title: Allow creating merge request even if target branch is not specified in query
+ params
+merge_request: 9968
+author:
diff --git a/changelogs/unreleased/29405-fix-project-wiki-update.yml b/changelogs/unreleased/29405-fix-project-wiki-update.yml
new file mode 100644
index 00000000000..85be36f7902
--- /dev/null
+++ b/changelogs/unreleased/29405-fix-project-wiki-update.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Project Wiki update
+merge_request: 9990
+author: Dongqing Hu
diff --git a/changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml b/changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml
new file mode 100644
index 00000000000..61ffb64fa8f
--- /dev/null
+++ b/changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml
@@ -0,0 +1,4 @@
+---
+title: Fix trigger webhook for ref with a dot
+merge_request: 10001
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/29604-v3-fix-branch-creation.yml b/changelogs/unreleased/29604-v3-fix-branch-creation.yml
new file mode 100644
index 00000000000..25687e8be97
--- /dev/null
+++ b/changelogs/unreleased/29604-v3-fix-branch-creation.yml
@@ -0,0 +1,4 @@
+---
+title: Use "branch_name" instead "branch" on V3 branch creation API
+merge_request:
+author:
diff --git a/changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml b/changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml
new file mode 100644
index 00000000000..15d7b9dcafb
--- /dev/null
+++ b/changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml
@@ -0,0 +1,4 @@
+---
+title: Allow unauthenticated access to some Branch API GET endpoints
+merge_request:
+author:
diff --git a/changelogs/unreleased/add-labels-to-issue-hook.yml b/changelogs/unreleased/add-labels-to-issue-hook.yml
new file mode 100644
index 00000000000..967430ee09f
--- /dev/null
+++ b/changelogs/unreleased/add-labels-to-issue-hook.yml
@@ -0,0 +1,4 @@
+---
+title: Added labels array to the issue web hook returned object
+merge_request: 9972
+author:
diff --git a/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml b/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml
new file mode 100644
index 00000000000..4b668d994a1
--- /dev/null
+++ b/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml
@@ -0,0 +1,4 @@
+---
+title: Use Gitaly for CommitController#show
+merge_request: 9629
+author:
diff --git a/changelogs/unreleased/fix-prometheus-including-d3-main-bundle.yml b/changelogs/unreleased/fix-prometheus-including-d3-main-bundle.yml
new file mode 100644
index 00000000000..a42b0db3cfc
--- /dev/null
+++ b/changelogs/unreleased/fix-prometheus-including-d3-main-bundle.yml
@@ -0,0 +1,4 @@
+---
+title: Removed d3 from the main application.js bundle
+merge_request: 10062
+author:
diff --git a/changelogs/unreleased/fl-remove-ujs-pipelines.yml b/changelogs/unreleased/fl-remove-ujs-pipelines.yml
new file mode 100644
index 00000000000..f353400753a
--- /dev/null
+++ b/changelogs/unreleased/fl-remove-ujs-pipelines.yml
@@ -0,0 +1,4 @@
+---
+title: 'Removes UJS from pipelines tables'
+merge_request: 9929
+author:
diff --git a/changelogs/unreleased/issue_27212.yml b/changelogs/unreleased/issue_27212.yml
new file mode 100644
index 00000000000..7a7e04f7ca7
--- /dev/null
+++ b/changelogs/unreleased/issue_27212.yml
@@ -0,0 +1,4 @@
+---
+title: Add closed_at field to issues
+merge_request:
+author:
diff --git a/changelogs/unreleased/make-karma-fast-again.yml b/changelogs/unreleased/make-karma-fast-again.yml
new file mode 100644
index 00000000000..9b95e06954a
--- /dev/null
+++ b/changelogs/unreleased/make-karma-fast-again.yml
@@ -0,0 +1,4 @@
+---
+title: Only add code coverage instrumentation when generating coverage report
+merge_request: 9987
+author:
diff --git a/changelogs/unreleased/migrate-pipeline-events-and-email-service.yml b/changelogs/unreleased/migrate-pipeline-events-and-email-service.yml
new file mode 100644
index 00000000000..ce4d5092c17
--- /dev/null
+++ b/changelogs/unreleased/migrate-pipeline-events-and-email-service.yml
@@ -0,0 +1,4 @@
+---
+title: Migrate SlackService and MattermostService from build_events to pipeline_events, and migrate BuildsEmailService to PipelinesEmailService. Update Hipchat to use pipeline events rather than build events.
+merge_request: 8196
+author:
diff --git a/changelogs/unreleased/remove-unused-ci-tables.yml b/changelogs/unreleased/remove-unused-ci-tables.yml
new file mode 100644
index 00000000000..fccfb882bd9
--- /dev/null
+++ b/changelogs/unreleased/remove-unused-ci-tables.yml
@@ -0,0 +1,4 @@
+---
+title: Remove various unused CI tables and columns
+merge_request: 9639
+author:
diff --git a/changelogs/unreleased/rename-ci_commits-to-ci_pipeline.yml b/changelogs/unreleased/rename-ci_commits-to-ci_pipeline.yml
new file mode 100644
index 00000000000..4067b3de00c
--- /dev/null
+++ b/changelogs/unreleased/rename-ci_commits-to-ci_pipeline.yml
@@ -0,0 +1,4 @@
+---
+title: Rename table ci_commits to ci_pipelines
+merge_request: 9638
+author:
diff --git a/changelogs/unreleased/rename_all_issues.yml b/changelogs/unreleased/rename_all_issues.yml
new file mode 100644
index 00000000000..d3109bdb17e
--- /dev/null
+++ b/changelogs/unreleased/rename_all_issues.yml
@@ -0,0 +1,4 @@
+---
+title: Rename 'All issues' to 'Open issues' in Add issues modal
+merge_request: 10042
+author: blackst0ne
diff --git a/changelogs/unreleased/routes-lower-case.yml b/changelogs/unreleased/routes-lower-case.yml
new file mode 100644
index 00000000000..2110956680c
--- /dev/null
+++ b/changelogs/unreleased/routes-lower-case.yml
@@ -0,0 +1,4 @@
+---
+title: Remove repeated routes.path check for postgresql database
+merge_request:
+author: mhasbini
diff --git a/changelogs/unreleased/simplify-docs-trigger.yml b/changelogs/unreleased/simplify-docs-trigger.yml
new file mode 100644
index 00000000000..062626359ef
--- /dev/null
+++ b/changelogs/unreleased/simplify-docs-trigger.yml
@@ -0,0 +1,4 @@
+---
+title: Simplify trigger_docs build job for CE and EE
+merge_request: 9820
+author: winniehell
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 2b018c68703..ecd73956488 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -42,7 +42,7 @@ Sidekiq.configure_server do |config|
Gitlab::SidekiqThrottler.execute!
- config = ActiveRecord::Base.configurations[Rails.env] ||
+ config = Gitlab::Database.config ||
Rails.application.config.database_configuration[Rails.env]
config['pool'] = Sidekiq.options[:concurrency]
ActiveRecord::Base.establish_connection(config)
diff --git a/config/karma.config.js b/config/karma.config.js
index c1d3751d88f..eb082dd28bf 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -3,17 +3,6 @@ var webpack = require('webpack');
var webpackConfig = require('./webpack.config.js');
var ROOT_PATH = path.resolve(__dirname, '..');
-// add coverage instrumentation to babel config
-if (webpackConfig.module && webpackConfig.module.rules) {
- var babelConfig = webpackConfig.module.rules.find(function (rule) {
- return rule.loader === 'babel-loader';
- });
-
- babelConfig.options = babelConfig.options || {};
- babelConfig.options.plugins = babelConfig.options.plugins || [];
- babelConfig.options.plugins.push('istanbul');
-}
-
// remove problematic plugins
if (webpackConfig.plugins) {
webpackConfig.plugins = webpackConfig.plugins.filter(function (plugin) {
@@ -27,7 +16,8 @@ if (webpackConfig.plugins) {
// Karma configuration
module.exports = function(config) {
var progressReporter = process.env.CI ? 'mocha' : 'progress';
- config.set({
+
+ var karmaConfig = {
basePath: ROOT_PATH,
browsers: ['PhantomJS'],
frameworks: ['jasmine'],
@@ -38,14 +28,20 @@ module.exports = function(config) {
preprocessors: {
'spec/javascripts/**/*.js': ['webpack', 'sourcemap'],
},
- reporters: [progressReporter, 'coverage-istanbul'],
- coverageIstanbulReporter: {
+ reporters: [progressReporter],
+ webpack: webpackConfig,
+ webpackMiddleware: { stats: 'errors-only' },
+ };
+
+ if (process.env.BABEL_ENV === 'coverage' || process.env.NODE_ENV === 'coverage') {
+ karmaConfig.reporters.push('coverage-istanbul');
+ karmaConfig.coverageIstanbulReporter = {
reports: ['html', 'text-summary'],
dir: 'coverage-javascript/',
subdir: '.',
fixWebpackSourcePaths: true
- },
- webpack: webpackConfig,
- webpackMiddleware: { stats: 'errors-only' },
- });
+ };
+ }
+
+ config.set(karmaConfig);
};
diff --git a/config/webpack.config.js b/config/webpack.config.js
index cbcc9ac5aea..c6794d6b944 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -35,6 +35,7 @@ var config = {
issuable: './issuable/issuable_bundle.js',
merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js',
merge_request_widget: './merge_request_widget/ci_bundle.js',
+ monitoring: './monitoring/monitoring_bundle.js',
network: './network/network_bundle.js',
profile: './profile/profile_bundle.js',
protected_branches: './protected_branches/protected_branches_bundle.js',
@@ -58,13 +59,7 @@ var config = {
{
test: /\.js$/,
exclude: /(node_modules|vendor\/assets)/,
- loader: 'babel-loader',
- options: {
- presets: [
- ["es2015", {"modules": false}],
- 'stage-2'
- ]
- }
+ loader: 'babel-loader'
},
{
test: /\.svg$/,
@@ -120,7 +115,7 @@ var config = {
// create cacheable common library bundle for all d3 chunks
new webpack.optimize.CommonsChunkPlugin({
name: 'common_d3',
- chunks: ['graphs', 'users'],
+ chunks: ['graphs', 'users', 'monitoring'],
}),
// create cacheable common library bundles
diff --git a/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb
index 65adc90c2c1..8a96a784c97 100644
--- a/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb
+++ b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb
@@ -5,7 +5,7 @@ class AddIndexForLatestSuccessfulPipeline < ActiveRecord::Migration
disable_ddl_transaction!
def up
- add_concurrent_index :ci_commits, [:gl_project_id, :ref, :status]
+ add_concurrent_index(:ci_commits, [:gl_project_id, :ref, :status])
end
def down
diff --git a/db/migrate/20170222143317_drop_ci_projects.rb b/db/migrate/20170222143317_drop_ci_projects.rb
new file mode 100644
index 00000000000..4db8658f36f
--- /dev/null
+++ b/db/migrate/20170222143317_drop_ci_projects.rb
@@ -0,0 +1,34 @@
+class DropCiProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ drop_table :ci_projects
+ end
+
+ def down
+ create_table "ci_projects", force: :cascade do |t|
+ t.string "name"
+ t.integer "timeout", default: 3600, null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "token"
+ t.string "default_ref"
+ t.string "path"
+ t.boolean "always_build", default: false, null: false
+ t.integer "polling_interval"
+ t.boolean "public", default: false, null: false
+ t.string "ssh_url_to_repo"
+ t.integer "gitlab_id"
+ t.boolean "allow_git_fetch", default: true, null: false
+ t.string "email_recipients", default: "", null: false
+ t.boolean "email_add_pusher", default: true, null: false
+ t.boolean "email_only_broken_builds", default: true, null: false
+ t.string "skip_refs"
+ t.string "coverage_regex"
+ t.boolean "shared_runners_enabled", default: false
+ t.text "generated_yaml_config"
+ end
+ end
+end
diff --git a/db/migrate/20170222143500_remove_old_project_id_columns.rb b/db/migrate/20170222143500_remove_old_project_id_columns.rb
new file mode 100644
index 00000000000..eac93e8e407
--- /dev/null
+++ b/db/migrate/20170222143500_remove_old_project_id_columns.rb
@@ -0,0 +1,28 @@
+class RemoveOldProjectIdColumns < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Unused columns are being removed.'
+
+ def up
+ remove_index :ci_builds, :project_id if
+ index_exists?(:ci_builds, :project_id)
+
+ remove_column :ci_builds, :project_id
+ remove_column :ci_commits, :project_id
+ remove_column :ci_runner_projects, :project_id
+ remove_column :ci_triggers, :project_id
+ remove_column :ci_variables, :project_id
+ end
+
+ def down
+ add_column :ci_builds, :project_id, :integer
+ add_column :ci_commits, :project_id, :integer
+ add_column :ci_runner_projects, :project_id, :integer
+ add_column :ci_triggers, :project_id, :integer
+ add_column :ci_variables, :project_id, :integer
+
+ add_concurrent_index :ci_builds, :project_id
+ end
+end
diff --git a/db/migrate/20170222143603_rename_gl_project_id_to_project_id.rb b/db/migrate/20170222143603_rename_gl_project_id_to_project_id.rb
new file mode 100644
index 00000000000..7c19d471557
--- /dev/null
+++ b/db/migrate/20170222143603_rename_gl_project_id_to_project_id.rb
@@ -0,0 +1,14 @@
+class RenameGlProjectIdToProjectId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Renaming an actively used column.'
+
+ def change
+ rename_column :ci_builds, :gl_project_id, :project_id
+ rename_column :ci_commits, :gl_project_id, :project_id
+ rename_column :ci_runner_projects, :gl_project_id, :project_id
+ rename_column :ci_triggers, :gl_project_id, :project_id
+ rename_column :ci_variables, :gl_project_id, :project_id
+ end
+end
diff --git a/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb b/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb
new file mode 100644
index 00000000000..f54608ecceb
--- /dev/null
+++ b/db/migrate/20170301125302_add_printing_merge_request_link_enabled_to_project.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPrintingMergeRequestLinkEnabledToProject < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ add_column_with_default(:projects, :printing_merge_request_link_enabled, :boolean, default: true)
+ end
+
+ def down
+ remove_column(:projects, :printing_merge_request_link_enabled)
+ end
+end
diff --git a/db/migrate/20170301195939_rename_ci_commits_to_ci_pipelines.rb b/db/migrate/20170301195939_rename_ci_commits_to_ci_pipelines.rb
new file mode 100644
index 00000000000..4f061d96392
--- /dev/null
+++ b/db/migrate/20170301195939_rename_ci_commits_to_ci_pipelines.rb
@@ -0,0 +1,10 @@
+class RenameCiCommitsToCiPipelines < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'Rename table ci_commits to ci_pipelines'
+
+ def change
+ rename_table 'ci_commits', 'ci_pipelines'
+ end
+end
diff --git a/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
new file mode 100644
index 00000000000..1e2abea5254
--- /dev/null
+++ b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
@@ -0,0 +1,83 @@
+class RemoveUnusedCiTablesAndColumns < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON =
+ 'Remove unused columns in used tables.' \
+ ' Downtime required in case Rails caches them'
+
+ def up
+ %w[ci_application_settings
+ ci_events
+ ci_jobs
+ ci_sessions
+ ci_taggings
+ ci_tags].each do |table|
+ drop_table(table)
+ end
+
+ remove_column :ci_pipelines, :push_data, :text
+ remove_column :ci_builds, :job_id, :integer
+ remove_column :ci_builds, :deploy, :boolean
+ end
+
+ def down
+ add_column :ci_builds, :deploy, :boolean
+ add_column :ci_builds, :job_id, :integer
+ add_column :ci_pipelines, :push_data, :text
+
+ create_table "ci_tags", force: :cascade do |t|
+ t.string "name"
+ t.integer "taggings_count", default: 0
+ end
+
+ create_table "ci_taggings", force: :cascade do |t|
+ t.integer "tag_id"
+ t.integer "taggable_id"
+ t.string "taggable_type"
+ t.integer "tagger_id"
+ t.string "tagger_type"
+ t.string "context", limit: 128
+ t.datetime "created_at"
+ end
+
+ add_index "ci_taggings", %w[taggable_id taggable_type context]
+
+ create_table "ci_sessions", force: :cascade do |t|
+ t.string "session_id", null: false
+ t.text "data"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ create_table "ci_jobs", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.text "commands"
+ t.boolean "active", default: true, null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "name"
+ t.boolean "build_branches", default: true, null: false
+ t.boolean "build_tags", default: false, null: false
+ t.string "job_type", default: "parallel"
+ t.string "refs"
+ t.datetime "deleted_at"
+ end
+
+ create_table "ci_events", force: :cascade do |t|
+ t.integer "project_id"
+ t.integer "user_id"
+ t.integer "is_admin"
+ t.text "description"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ create_table "ci_application_settings", force: :cascade do |t|
+ t.boolean "all_broken_builds"
+ t.boolean "add_pusher"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+ end
+end
diff --git a/db/migrate/20170315194013_add_closed_at_to_issues.rb b/db/migrate/20170315194013_add_closed_at_to_issues.rb
new file mode 100644
index 00000000000..1326118cc8d
--- /dev/null
+++ b/db/migrate/20170315194013_add_closed_at_to_issues.rb
@@ -0,0 +1,7 @@
+class AddClosedAtToIssues < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ add_column :issues, :closed_at, :datetime
+ end
+end
diff --git a/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb b/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb
new file mode 100644
index 00000000000..2dd14ee5a78
--- /dev/null
+++ b/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb
@@ -0,0 +1,87 @@
+class MigrateBuildEventsToPipelineEvents < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ include Gitlab::Database
+
+ DOWNTIME = false
+
+ def up
+ Gitlab::Database.with_connection_pool(2) do |pool|
+ threads = []
+
+ threads << Thread.new do
+ pool.with_connection do |connection|
+ Thread.current[:foreign_key_connection] = connection
+
+ execute(<<-SQL.strip_heredoc)
+ UPDATE services
+ SET properties = replace(properties,
+ 'notify_only_broken_builds',
+ 'notify_only_broken_pipelines')
+ , pipeline_events = #{true_value}
+ , build_events = #{false_value}
+ WHERE type IN
+ ('SlackService', 'MattermostService', 'HipchatService')
+ AND build_events = #{true_value};
+ SQL
+ end
+ end
+
+ threads << Thread.new do
+ pool.with_connection do |connection|
+ Thread.current[:foreign_key_connection] = connection
+
+ execute(update_pipeline_services_sql)
+ end
+ end
+
+ threads.each(&:join)
+ end
+ end
+
+ def down
+ # Don't bother to migrate the data back
+ end
+
+ def connection
+ # Rails memoizes connection objects, but this causes them to be shared
+ # amongst threads; we don't want that.
+ Thread.current[:foreign_key_connection] || ActiveRecord::Base.connection
+ end
+
+ private
+
+ def update_pipeline_services_sql
+ if Gitlab::Database.postgresql?
+ <<-SQL
+ UPDATE services
+ SET type = 'PipelinesEmailService'
+ , properties = replace(properties,
+ 'notify_only_broken_builds',
+ 'notify_only_broken_pipelines')
+ , pipeline_events = #{true_value}
+ , build_events = #{false_value}
+ WHERE type = 'BuildsEmailService'
+ AND
+ (SELECT 1 FROM services pipeline_services
+ WHERE pipeline_services.project_id = services.project_id
+ AND pipeline_services.type = 'PipelinesEmailService' LIMIT 1)
+ IS NULL;
+ SQL
+ else
+ <<-SQL
+ UPDATE services build_services
+ LEFT OUTER JOIN services pipeline_services
+ ON build_services.project_id = pipeline_services.project_id
+ AND pipeline_services.type = 'PipelinesEmailService'
+ SET build_services.type = 'PipelinesEmailService'
+ , build_services.properties = replace(build_services.properties,
+ 'notify_only_broken_builds',
+ 'notify_only_broken_pipelines')
+ , build_services.pipeline_events = #{true_value}
+ , build_services.build_events = #{false_value}
+ WHERE build_services.type = 'BuildsEmailService'
+ AND pipeline_services.id IS NULL;
+ SQL
+ end.strip_heredoc
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 3bef910c1d6..ee5000ea64c 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: 20170315174634) do
+ActiveRecord::Schema.define(version: 20170315194013) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -185,15 +185,7 @@ ActiveRecord::Schema.define(version: 20170315174634) do
add_index "chat_teams", ["namespace_id"], name: "index_chat_teams_on_namespace_id", unique: true, using: :btree
- create_table "ci_application_settings", force: :cascade do |t|
- t.boolean "all_broken_builds"
- t.boolean "add_pusher"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
create_table "ci_builds", force: :cascade do |t|
- t.integer "project_id"
t.string "status"
t.datetime "finished_at"
t.text "trace"
@@ -204,9 +196,7 @@ ActiveRecord::Schema.define(version: 20170315174634) do
t.float "coverage"
t.integer "commit_id"
t.text "commands"
- t.integer "job_id"
t.string "name"
- t.boolean "deploy", default: false
t.text "options"
t.boolean "allow_failure", default: false, null: false
t.string "stage"
@@ -219,7 +209,7 @@ ActiveRecord::Schema.define(version: 20170315174634) do
t.string "target_url"
t.string "description"
t.text "artifacts_file"
- t.integer "gl_project_id"
+ t.integer "project_id"
t.text "artifacts_metadata"
t.integer "erased_by_id"
t.datetime "erased_at"
@@ -238,25 +228,22 @@ ActiveRecord::Schema.define(version: 20170315174634) do
add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree
add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree
add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
- add_index "ci_builds", ["gl_project_id"], name: "index_ci_builds_on_gl_project_id", using: :btree
add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
add_index "ci_builds", ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree
add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree
add_index "ci_builds", ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree
- create_table "ci_commits", force: :cascade do |t|
- t.integer "project_id"
+ create_table "ci_pipelines", force: :cascade do |t|
t.string "ref"
t.string "sha"
t.string "before_sha"
- t.text "push_data"
t.datetime "created_at"
t.datetime "updated_at"
t.boolean "tag", default: false
t.text "yaml_errors"
t.datetime "committed_at"
- t.integer "gl_project_id"
+ t.integer "project_id"
t.string "status"
t.datetime "started_at"
t.datetime "finished_at"
@@ -265,67 +252,20 @@ ActiveRecord::Schema.define(version: 20170315174634) do
t.integer "lock_version"
end
- add_index "ci_commits", ["gl_project_id", "ref", "status"], name: "index_ci_commits_on_gl_project_id_and_ref_and_status", using: :btree
- add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree
- add_index "ci_commits", ["gl_project_id"], name: "index_ci_commits_on_gl_project_id", using: :btree
- add_index "ci_commits", ["status"], name: "index_ci_commits_on_status", using: :btree
- add_index "ci_commits", ["user_id"], name: "index_ci_commits_on_user_id", using: :btree
-
- create_table "ci_events", force: :cascade do |t|
- t.integer "project_id"
- t.integer "user_id"
- t.integer "is_admin"
- t.text "description"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- create_table "ci_jobs", force: :cascade do |t|
- t.integer "project_id", null: false
- t.text "commands"
- t.boolean "active", default: true, null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.string "name"
- t.boolean "build_branches", default: true, null: false
- t.boolean "build_tags", default: false, null: false
- t.string "job_type", default: "parallel"
- t.string "refs"
- t.datetime "deleted_at"
- end
-
- create_table "ci_projects", force: :cascade do |t|
- t.string "name"
- t.integer "timeout", default: 3600, null: false
- t.datetime "created_at"
- t.datetime "updated_at"
- t.string "token"
- t.string "default_ref"
- t.string "path"
- t.boolean "always_build", default: false, null: false
- t.integer "polling_interval"
- t.boolean "public", default: false, null: false
- t.string "ssh_url_to_repo"
- t.integer "gitlab_id"
- t.boolean "allow_git_fetch", default: true, null: false
- t.string "email_recipients", default: "", null: false
- t.boolean "email_add_pusher", default: true, null: false
- t.boolean "email_only_broken_builds", default: true, null: false
- t.string "skip_refs"
- t.string "coverage_regex"
- t.boolean "shared_runners_enabled", default: false
- t.text "generated_yaml_config"
- end
+ add_index "ci_pipelines", ["project_id", "ref", "status"], name: "index_ci_pipelines_on_project_id_and_ref_and_status", using: :btree
+ add_index "ci_pipelines", ["project_id", "sha"], name: "index_ci_pipelines_on_project_id_and_sha", using: :btree
+ add_index "ci_pipelines", ["project_id"], name: "index_ci_pipelines_on_project_id", using: :btree
+ add_index "ci_pipelines", ["status"], name: "index_ci_pipelines_on_status", using: :btree
+ add_index "ci_pipelines", ["user_id"], name: "index_ci_pipelines_on_user_id", using: :btree
create_table "ci_runner_projects", force: :cascade do |t|
t.integer "runner_id", null: false
- t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "gl_project_id"
+ t.integer "project_id"
end
- add_index "ci_runner_projects", ["gl_project_id"], name: "index_ci_runner_projects_on_gl_project_id", using: :btree
+ add_index "ci_runner_projects", ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree
add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree
create_table "ci_runners", force: :cascade do |t|
@@ -349,30 +289,6 @@ ActiveRecord::Schema.define(version: 20170315174634) do
add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
- create_table "ci_sessions", force: :cascade do |t|
- t.string "session_id", null: false
- t.text "data"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- create_table "ci_taggings", force: :cascade do |t|
- t.integer "tag_id"
- t.integer "taggable_id"
- t.string "taggable_type"
- t.integer "tagger_id"
- t.string "tagger_type"
- t.string "context", limit: 128
- t.datetime "created_at"
- end
-
- add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
-
- create_table "ci_tags", force: :cascade do |t|
- t.string "name"
- t.integer "taggings_count", default: 0
- end
-
create_table "ci_trigger_requests", force: :cascade do |t|
t.integer "trigger_id", null: false
t.text "variables"
@@ -385,28 +301,26 @@ ActiveRecord::Schema.define(version: 20170315174634) do
create_table "ci_triggers", force: :cascade do |t|
t.string "token"
- t.integer "project_id"
t.datetime "deleted_at"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "gl_project_id"
+ t.integer "project_id"
t.integer "owner_id"
t.string "description"
end
- add_index "ci_triggers", ["gl_project_id"], name: "index_ci_triggers_on_gl_project_id", using: :btree
+ add_index "ci_triggers", ["project_id"], name: "index_ci_triggers_on_project_id", using: :btree
create_table "ci_variables", force: :cascade do |t|
- t.integer "project_id"
t.string "key"
t.text "value"
t.text "encrypted_value"
t.string "encrypted_value_salt"
t.string "encrypted_value_iv"
- t.integer "gl_project_id"
+ t.integer "project_id"
end
- add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree
+ add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree
create_table "deploy_keys_projects", force: :cascade do |t|
t.integer "deploy_key_id", null: false
@@ -531,6 +445,7 @@ ActiveRecord::Schema.define(version: 20170315174634) do
t.text "description_html"
t.integer "time_estimate"
t.integer "relative_position"
+ t.datetime "closed_at"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -1003,6 +918,7 @@ ActiveRecord::Schema.define(version: 20170315174634) do
t.boolean "lfs_enabled"
t.text "description_html"
t.boolean "only_allow_merge_if_all_discussions_are_resolved"
+ t.boolean "printing_merge_request_link_enabled", default: true, null: false
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -1377,7 +1293,7 @@ ActiveRecord::Schema.define(version: 20170315174634) do
add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "lists", "boards"
add_foreign_key "lists", "labels"
- add_foreign_key "merge_request_metrics", "ci_commits", column: "pipeline_id", on_delete: :cascade
+ add_foreign_key "merge_request_metrics", "ci_pipelines", column: "pipeline_id", on_delete: :cascade
add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md
index 69b16b7c483..b2445d1c0e5 100644
--- a/doc/administration/monitoring/prometheus/index.md
+++ b/doc/administration/monitoring/prometheus/index.md
@@ -96,21 +96,15 @@ Sample Prometheus queries:
> Introduced in GitLab 9.0.
-If your GitLab server is running within Kubernetes, an option is now available
-to monitor the health of each node in the cluster. This is particularly helpful
-if your CI/CD environments run in the same cluster, and you would like enable
-[Prometheus integration][] to monitor them.
+If your GitLab server is running within Kubernetes, Prometheus will collect metrics from the Nodes in the cluster including performance data on each container. This is particularly helpful if your CI/CD environments run in the same cluster, as you can use the [Prometheus project integration][] to monitor them.
-When enabled, the bundled Prometheus server monitors Kubernetes and automatically
-[collects metrics][prometheus-cadvisor-metrics] from each Node in the cluster.
-
-To enable the Kubernetes monitoring:
+To disable the monitoring of Kubernetes:
1. Edit `/etc/gitlab/gitlab.rb`
-1. Add or find and uncomment the following line:
+1. Add or find and uncomment the following line and set it to `false`:
```ruby
- prometheus['monitor_kubernetes'] = true
+ prometheus['monitor_kubernetes'] = false
```
1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
@@ -165,4 +159,4 @@ The GitLab monitor exporter allows you to measure various GitLab metrics.
[reconfigure]: ../../restart_gitlab.md#omnibus-gitlab-reconfigure
[1261]: https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/1261
[prometheus integration]: ../../../user/project/integrations/prometheus.md
-[rometheus-cadvisor-metrics]: https://github.com/google/cadvisor/blob/master/docs/storage/prometheus.md
+[prometheus-cadvisor-metrics]: https://github.com/google/cadvisor/blob/master/docs/storage/prometheus.md
diff --git a/doc/api/branches.md b/doc/api/branches.md
index 83705106160..815aabda8e3 100644
--- a/doc/api/branches.md
+++ b/doc/api/branches.md
@@ -3,6 +3,8 @@
## List repository branches
Get a list of repository branches from a project, sorted by name alphabetically.
+This endpoint can be accessed without authentication if the repository is
+publicly accessible.
```
GET /projects/:id/repository/branches
@@ -48,7 +50,8 @@ Example response:
## Get single repository branch
-Get a single project repository branch.
+Get a single project repository branch. This endpoint can be accessed without
+authentication if the repository is publicly accessible.
```
GET /projects/:id/repository/branches/:branch
diff --git a/doc/api/services.md b/doc/api/services.md
index 8e7afe41b0c..7d4779f1137 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -139,43 +139,6 @@ Get Buildkite service settings for a project.
GET /projects/:id/services/buildkite
```
-## Build-Emails
-
-Get emails for GitLab CI builds.
-
-### Create/Edit Build-Emails service
-
-Set Build-Emails service for a project.
-
-```
-PUT /projects/:id/services/jobs-email
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `recipients` | string | yes | Comma-separated list of recipient email addresses |
-| `add_pusher` | boolean | no | Add pusher to recipients list |
-| `notify_only_broken_jobs` | boolean | no | Notify only broken jobs |
-
-
-### Delete Job-Emails service
-
-Delete Build-Emails service for a project.
-
-```
-DELETE /projects/:id/services/jobs-email
-```
-
-### Get Job-Emails service settings
-
-Get Build-Emails service settings for a project.
-
-```
-GET /projects/:id/services/jobs-email
-```
-
## Campfire
Simple web-based real-time group chat
@@ -580,8 +543,7 @@ Parameters:
| --------- | ---- | -------- | ----------- |
| `recipients` | string | yes | Comma-separated list of recipient email addresses |
| `add_pusher` | boolean | no | Add pusher to recipients list |
-| `notify_only_broken_jobs` | boolean | no | Notify only broken pipelines |
-
+| `notify_only_broken_pipelines` | boolean | no | Notify only broken pipelines |
### Delete Pipeline-Emails service
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 76e86f3e3c3..30f209f80eb 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -207,15 +207,6 @@ you expected.
You are also able to view the status of any commit in the various pages in
GitLab, such as **Commits** and **Merge requests**.
-## Enabling build emails
-
-If you want to receive e-mail notifications about the result status of the
-jobs, you should explicitly enable the **Builds Emails** service under your
-project's settings.
-
-For more information read the
-[Builds emails service documentation](../../user/project/integrations/builds_emails.md).
-
## Examples
Visit the [examples README][examples] to see a list of examples using GitLab
diff --git a/doc/development/frontend.md b/doc/development/frontend.md
index 766b5f1f477..50105a486d0 100644
--- a/doc/development/frontend.md
+++ b/doc/development/frontend.md
@@ -36,16 +36,23 @@ You can find documentation about the desired architecture for a new feature buil
When writing code for realtime features we have to keep a couple of things in mind:
1. Do not overload the server with requests.
-1. It should feel realtime.
+1. It should feel realtime.
-Thus, we must strike a balance between sending requests and the feeling of realtime. Use the following rules when creating realtime solutions.
+Thus, we must strike a balance between sending requests and the feeling of realtime.
+Use the following rules when creating realtime solutions.
-1. The server will tell you how much to poll by sending `X-Poll-Interval` in the header. Use that as your polling interval. This way it is easy for system administrators to change the polling rate. A `X-Poll-Interval: -1` means you should disable polling, and this must be implemented.
-1. A response of `HTTP 429 Too Many Requests`, should disable polling as well. This must also be implemented.
+1. The server will tell you how much to poll by sending `Poll-Interval` in the header.
+Use that as your polling interval. This way it is easy for system administrators to change the
+polling rate.
+A `Poll-Interval: -1` means you should disable polling, and this must be implemented.
+1. A response with HTTP status `4XX` or `5XX` should disable polling as well.
1. Use a common library for polling.
-1. Poll on active tabs only. Use a common library to find out which tab currently has eyes on it. Please use [Focus](https://gitlab.com/andrewn/focus). Specifically [Eyeballs Detector](https://gitlab.com/andrewn/focus/blob/master/lib/eyeballs-detector.js).
-1. Use regular polling intervals, do not use backoff polling, or jitter, as the interval will be controlled by the server.
-1. The backend code will most likely be using etags. You do not and should not check for status `304 Not Modified`. The browser will transform it for you.
+1. Poll on active tabs only. Use a common library to find out which tab currently has eyes on it.
+Please use [Focus](https://gitlab.com/andrewn/focus). Specifically [Eyeballs Detector](https://gitlab.com/andrewn/focus/blob/master/lib/eyeballs-detector.js).
+1. Use regular polling intervals, do not use backoff polling, or jitter, as the interval will be
+controlled by the server.
+1. The backend code will most likely be using etags. You do not and should not check for status
+`304 Not Modified`. The browser will transform it for you.
### Vue
diff --git a/doc/project_services/builds_emails.md b/doc/project_services/builds_emails.md
deleted file mode 100644
index ee54d865225..00000000000
--- a/doc/project_services/builds_emails.md
+++ /dev/null
@@ -1 +0,0 @@
-This document was moved to [user/project/integrations/builds_emails.md](../user/project/integrations/builds_emails.md).
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index 678f5199b02..cf28f1a2eca 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -170,12 +170,12 @@ Integration (CI) server. By using deploy keys, you don't have to setup a
dummy user account.
If you are a project master or owner, you can add a deploy key in the
-project settings under the section 'Deploy Keys'. Press the 'New Deploy
-Key' button and upload a public SSH key. After this, the machine that uses
+project settings under the section 'Repository'. Specify a title for the new
+deploy key and paste a public SSH key. After this, the machine that uses
the corresponding private SSH key has read-only or read-write (if enabled)
access to the project.
-You can't add the same deploy key twice with the 'New Deploy Key' option.
+You can't add the same deploy key twice using the form.
If you want to add the same key to another project, please enable it in the
list that says 'Deploy keys from projects available to you'. All the deploy
keys of all the projects you have access to are available. This project
diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md
index 75956aeb360..ed0e668d854 100644
--- a/doc/update/8.12-to-8.13.md
+++ b/doc/update/8.12-to-8.13.md
@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-13-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v3.6.6
+sudo -u git -H git checkout v3.6.7
```
### 6. Update gitlab-workhorse
diff --git a/doc/user/award_emojis.md b/doc/user/award_emojis.md
new file mode 100644
index 00000000000..acbd2a66d37
--- /dev/null
+++ b/doc/user/award_emojis.md
@@ -0,0 +1,51 @@
+# Award emoji
+
+>**Notes:**
+- First [introduced][1825] in GitLab 8.2.
+- GitLab 9.0 [introduced][ce-9570] the usage of native emojis if the platform
+ supports them and falls back to images or CSS sprites. This change greatly
+ improved the award emoji performance overall.
+
+When you're collaborating online, you get fewer opportunities for high-fives
+and thumbs-ups. Emoji can be awarded to issues, merge requests, snippets, and
+virtually everywhere where you can have a discussion.
+
+![Award emoji](img/award_emoji_select.png)
+
+Award emoji make it much easier to give and receive feedback without a long
+comment thread. Comments that are only emoji will automatically become
+award emoji.
+
+## Sort issues and merge requests on vote count
+
+> [Introduced][2871] in GitLab 8.5.
+
+You can quickly sort issues and merge requests by the number of votes they
+have received. The sort options can be found in the dropdown menu as "Most
+popular" and "Least popular".
+
+![Votes sort options](img/award_emoji_votes_sort_options.png)
+
+The total number of votes is not summed up. An issue with 18 upvotes and 5
+downvotes is considered more popular than an issue with 17 upvotes and no
+downvotes.
+
+## Award emoji for comments
+
+> [Introduced][4291] in GitLab 8.9.
+
+Award emoji can also be applied to individual comments when you want to
+celebrate an accomplishment or agree with an opinion.
+
+To add an award emoji, click the smile in the top right of the comment and pick
+an emoji from the dropdown. If you want to remove an award emoji, just click
+the emoji again and the vote will be removed.
+
+![Picking an emoji for a comment](img/award_emoji_comment_picker.png)
+
+![An award emoji has been applied to a comment](img/award_emoji_comment_awarded.png)
+
+[2871]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2781
+[1825]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1825
+[4291]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4291
+[ce-9570]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9570
diff --git a/doc/workflow/img/award_emoji_comment_awarded.png b/doc/user/img/award_emoji_comment_awarded.png
index 111793ebf8a..111793ebf8a 100644
--- a/doc/workflow/img/award_emoji_comment_awarded.png
+++ b/doc/user/img/award_emoji_comment_awarded.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_comment_picker.png b/doc/user/img/award_emoji_comment_picker.png
index 3ad1bab3119..3ad1bab3119 100644
--- a/doc/workflow/img/award_emoji_comment_picker.png
+++ b/doc/user/img/award_emoji_comment_picker.png
Binary files differ
diff --git a/doc/user/img/award_emoji_select.png b/doc/user/img/award_emoji_select.png
new file mode 100644
index 00000000000..496acb29eec
--- /dev/null
+++ b/doc/user/img/award_emoji_select.png
Binary files differ
diff --git a/doc/user/img/award_emoji_votes_sort_options.png b/doc/user/img/award_emoji_votes_sort_options.png
new file mode 100644
index 00000000000..dd84b7f4f64
--- /dev/null
+++ b/doc/user/img/award_emoji_votes_sort_options.png
Binary files differ
diff --git a/doc/user/project/integrations/builds_emails.md b/doc/user/project/integrations/builds_emails.md
deleted file mode 100644
index f769dece242..00000000000
--- a/doc/user/project/integrations/builds_emails.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# Enabling build emails
-
-By enabling this service, you will be able to receive e-mail notifications about
-the result status of your builds.
-
-Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
-and select the **Builds emails** service to configure it.
-
-In the _Recipients_ area, provide a list of e-mails separated by comma.
-
-Check the _Add pusher_ checkbox if you want the committer to also receive
-e-mail notifications about each build's status.
-
-If you enable the _Notify only broken builds_ option, e-mail notifications will
-be sent only for failed builds.
diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md
index cfb0931273d..3e77823a6aa 100644
--- a/doc/user/project/integrations/mattermost.md
+++ b/doc/user/project/integrations/mattermost.md
@@ -28,7 +28,6 @@ There, you will see a checkbox with the following events that can be triggered:
- Merge request
- Note
- Tag push
-- Build
- Pipeline
- Wiki page
@@ -41,7 +40,6 @@ At the end, fill in your Mattermost details:
| ----- | ----------- |
| **Webhook** | The incoming webhook URL which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo… |
| **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. |
-| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
![Mattermost configuration](img/mattermost_configuration.png)
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index 3cf1cc704a2..25400633de5 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -32,7 +32,6 @@ Click on the service links to see further configuration instructions and details
| Assembla | Project Management Software (Source Commits Endpoint) |
| [Atlassian Bamboo CI](bamboo.md) | A continuous integration and build server |
| Buildkite | Continuous integration and deployments |
-| [Builds emails](builds_emails.md) | Email the builds status to a list of recipients |
| [Bugzilla](bugzilla.md) | Bugzilla issue tracker |
| Campfire | Simple web-based real-time group chat |
| Custom Issue Tracker | Custom issue tracker |
@@ -48,6 +47,7 @@ Click on the service links to see further configuration instructions and details
| [Kubernetes](kubernetes.md) | A containerized deployment service |
| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
+| Pipelines emails | Email the pipeline status to a list of recipients |
| [Slack Notifications](slack.md) | Receive event notifications in Slack |
| [Slack slash commands](slack_slash_commands.md) | Slack chat and ChatOps slash commands |
| PivotalTracker | Project Management Software (Source Commits Endpoint) |
diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md
index f27f9a726fc..e8b238351ca 100644
--- a/doc/user/project/integrations/slack.md
+++ b/doc/user/project/integrations/slack.md
@@ -25,7 +25,6 @@ There, you will see a checkbox with the following events that can be triggered:
- Merge request
- Note
- Tag push
-- Build
- Pipeline
- Wiki page
@@ -38,7 +37,6 @@ At the end, fill in your Slack details:
| ----- | ----------- |
| **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. |
| **Username** | Optional username which can be on messages sent to Slack. Fill this in if you want to change the username of the bot. |
-| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
After you are all done, click **Save changes** for the changes to take effect.
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index ed1e867f5fb..dbdc93a77a8 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -250,7 +250,19 @@ X-Gitlab-Event: Issue Hook
"name": "User1",
"username": "user1",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
- }
+ },
+ "labels": [{
+ "id": 206,
+ "title": "API",
+ "color": "#ffffff",
+ "project_id": 14,
+ "created_at": "2013-12-03T17:15:43Z",
+ "updated_at": "2013-12-03T17:15:43Z",
+ "template": false,
+ "description": "API related issues",
+ "type": "ProjectLabel",
+ "group_id": 41
+ }]
}
```
### Comment events
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index be042ddf623..7a4f9f408f1 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -34,7 +34,7 @@ with all their related data and be moved into a new GitLab instance.
| 8.10.0 | 0.1.2 |
| 8.9.5 | 0.1.1 |
| 8.9.0 | 0.1.0 |
-
+
> The table reflects what GitLab version we updated the Import/Export version at.
> For instance, 8.10.3 and 8.11 will have the same Import/Export version (0.1.3)
> and the exports between them will be compatible.
diff --git a/doc/workflow/award_emoji.md b/doc/workflow/award_emoji.md
index 1df0698afd0..d74378cc564 100644
--- a/doc/workflow/award_emoji.md
+++ b/doc/workflow/award_emoji.md
@@ -1,65 +1 @@
-# Award emoji
-
->**Note:**
-[Introduced][1825] in GitLab 8.2.
-
-When you're collaborating online, you get fewer opportunities for high-fives
-and thumbs-ups. Emoji can be awarded to issues and merge requests, making
-virtual celebrations easier.
-
-![Award emoji](img/award_emoji_select.png)
-
-Award emoji make it much easier to give and receive feedback without a long
-comment thread. Comments that are only emoji will automatically become
-award emoji.
-
-## Sort issues and merge requests on vote count
-
->**Note:**
-[Introduced][2871] in GitLab 8.5.
-
-You can quickly sort issues and merge requests by the number of votes they
-have received. The sort options can be found in the dropdown menu as "Most
-popular" and "Least popular".
-
-![Votes sort options](img/award_emoji_votes_sort_options.png)
-
----
-
-Sort by most popular issues/merge requests.
-
-![Votes sort by most popular](img/award_emoji_votes_most_popular.png)
-
----
-
-Sort by least popular issues/merge requests.
-
-![Votes sort by least popular](img/award_emoji_votes_least_popular.png)
-
----
-
-The total number of votes is not summed up. An issue with 18 upvotes and 5
-downvotes is considered more popular than an issue with 17 upvotes and no
-downvotes.
-
-## Award emoji for comments
-
->**Note:**
-[Introduced][4291] in GitLab 8.9.
-
-Award emoji can also be applied to individual comments when you want to
-celebrate an accomplishment or agree with an opinion.
-
-To add an award emoji, click the smile in the top right of the comment and pick
-an emoji from the dropdown.
-
-![Picking an emoji for a comment](img/award_emoji_comment_picker.png)
-
-![An award emoji has been applied to a comment](img/award_emoji_comment_awarded.png)
-
-If you want to remove an award emoji, just click the emoji again and the vote
-will be removed.
-
-[2871]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2781
-[1825]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1825
-[4291]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4291
+This document was moved to [another location](../user/award_emojis.md).
diff --git a/doc/workflow/img/award_emoji_select.png b/doc/workflow/img/award_emoji_select.png
deleted file mode 100644
index e1b37beaf62..00000000000
--- a/doc/workflow/img/award_emoji_select.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/img/award_emoji_votes_least_popular.png b/doc/workflow/img/award_emoji_votes_least_popular.png
deleted file mode 100644
index 86ede4b0c10..00000000000
--- a/doc/workflow/img/award_emoji_votes_least_popular.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/img/award_emoji_votes_most_popular.png b/doc/workflow/img/award_emoji_votes_most_popular.png
deleted file mode 100644
index 1d3e2e57aa0..00000000000
--- a/doc/workflow/img/award_emoji_votes_most_popular.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/img/award_emoji_votes_sort_options.png b/doc/workflow/img/award_emoji_votes_sort_options.png
deleted file mode 100644
index c6dc1b939c1..00000000000
--- a/doc/workflow/img/award_emoji_votes_sort_options.png
+++ /dev/null
Binary files differ
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 2cc64fc6712..f35084a582a 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -4,7 +4,6 @@ module API
class Branches < Grape::API
include PaginationParams
- before { authenticate! }
before { authorize! :download_code, user_project }
params do
@@ -102,6 +101,7 @@ module API
end
post ":id/repository/branches" do
authorize_push_project
+
result = CreateBranchService.new(user_project, current_user).
execute(params[:branch], params[:ref])
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 0a12ee72d49..5954aea8041 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -768,7 +768,7 @@ module API
end
class Dependency < Grape::Entity
- expose :id, :name
+ expose :id, :name, :token
expose :artifacts_file, using: ArtifactFile, if: ->(job, _) { job.artifacts? }
end
@@ -796,7 +796,7 @@ module API
expose :artifacts, using: Artifacts
expose :cache, using: Cache
expose :credentials, using: Credentials
- expose :depends_on_builds, as: :dependencies, using: Dependency
+ expose :dependencies, using: Dependency
end
end
end
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
index ec2bcaed929..74848a6e144 100644
--- a/lib/api/helpers/runner.rb
+++ b/lib/api/helpers/runner.rb
@@ -41,14 +41,6 @@ module API
(Time.now - current_runner.contacted_at) >= contacted_at_max_age
end
- def job_not_found!
- if headers['User-Agent'].to_s =~ /gitlab(-ci-multi)?-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /
- no_content!
- else
- not_found!
- end
- end
-
def validate_job!(job)
not_found! unless job
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index c700d2ef4a1..4c9db2c8716 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -47,11 +47,25 @@ module API
authenticate_runner!
Ci::Runner.find_by_token(params[:token]).destroy
end
+
+ desc 'Validates authentication credentials' do
+ http_codes [[200, 'Credentials are valid'], [403, 'Forbidden']]
+ end
+ params do
+ requires :token, type: String, desc: %q(Runner's authentication token)
+ end
+ post '/verify' do
+ authenticate_runner!
+ status 200
+ end
end
resource :jobs do
desc 'Request a job' do
success Entities::JobRequest::Response
+ http_codes [[201, 'Job was scheduled'],
+ [204, 'No job for Runner'],
+ [403, 'Forbidden']]
end
params do
requires :token, type: String, desc: %q(Runner's authentication token)
@@ -60,13 +74,13 @@ module API
end
post '/request' do
authenticate_runner!
- not_found! unless current_runner.active?
+ no_content! unless current_runner.active?
update_runner_info
if current_runner.is_runner_queue_value_latest?(params[:last_update])
header 'X-GitLab-Last-Update', params[:last_update]
Gitlab::Metrics.add_event(:build_not_found_cached)
- return job_not_found!
+ return no_content!
end
new_update = current_runner.ensure_runner_queue_value
@@ -80,7 +94,7 @@ module API
else
Gitlab::Metrics.add_event(:build_not_found)
header 'X-GitLab-Last-Update', new_update
- job_not_found!
+ no_content!
end
else
# We received build that is invalid due to concurrency conflict
diff --git a/lib/api/services.rb b/lib/api/services.rb
index be614bb8dc0..4e0c9cb1f63 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -107,26 +107,6 @@ module API
desc: 'Enable SSL verification for communication'
}
],
- 'builds-email' => [
- {
- required: true,
- name: :recipients,
- type: String,
- desc: 'Comma-separated list of recipient email addresses'
- },
- {
- required: false,
- name: :add_pusher,
- type: Boolean,
- desc: 'Add pusher to recipients list'
- },
- {
- required: false,
- name: :notify_only_broken_jobs,
- type: Boolean,
- desc: 'Notify only broken jobs'
- }
- ],
'campfire' => [
{
required: true,
@@ -403,9 +383,9 @@ module API
},
{
required: false,
- name: :notify_only_broken_jobs,
+ name: :notify_only_broken_pipelines,
type: Boolean,
- desc: 'Notify only broken jobs'
+ desc: 'Notify only broken pipelines'
}
],
'pivotaltracker' => [
@@ -550,7 +530,6 @@ module API
BambooService,
BugzillaService,
BuildkiteService,
- BuildsEmailService,
CampfireService,
CustomIssueTrackerService,
DroneCiService,
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index aa3c9a06ed5..a9f2ca2608e 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -14,7 +14,7 @@ module API
requires :token, type: String, desc: 'The unique token of trigger'
optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
end
- post ":id/(ref/:ref/)trigger/pipeline" do
+ post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do
project = find_project(params[:id])
trigger = Ci::Trigger.find_by_token(params[:token].to_s)
not_found! unless project && trigger
diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb
index 7d9d6246e46..0a877b960f6 100644
--- a/lib/api/v3/branches.rb
+++ b/lib/api/v3/branches.rb
@@ -45,6 +45,27 @@ module API
status(200)
end
+
+ desc 'Create branch' do
+ success ::API::Entities::RepoBranch
+ end
+ params do
+ requires :branch_name, type: String, desc: 'The name of the branch'
+ requires :ref, type: String, desc: 'Create branch from commit sha or existing branch'
+ end
+ post ":id/repository/branches" do
+ authorize_push_project
+ result = CreateBranchService.new(user_project, current_user).
+ execute(params[:branch_name], params[:ref])
+
+ if result[:status] == :success
+ present result[:branch],
+ with: ::API::Entities::RepoBranch,
+ project: user_project
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
end
end
end
diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb
index b46639a2205..a23d6b6b48c 100644
--- a/lib/api/v3/triggers.rb
+++ b/lib/api/v3/triggers.rb
@@ -15,7 +15,7 @@ module API
requires :token, type: String, desc: 'The unique token of trigger'
optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
end
- post ":id/(ref/:ref/)trigger/builds" do
+ post ":id/(ref/:ref/)trigger/builds", requirements: { ref: /.+/ } do
project = find_project(params[:id])
trigger = Ci::Trigger.find_by_token(params[:token].to_s)
not_found! unless project && trigger
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index f3f417c1a63..63b8d0d3b9d 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -5,8 +5,12 @@ module Gitlab
# http://dev.mysql.com/doc/refman/5.7/en/integer-types.html
MAX_INT_VALUE = 2147483647
+ def self.config
+ ActiveRecord::Base.configurations[Rails.env]
+ end
+
def self.adapter_name
- ActiveRecord::Base.configurations[Rails.env]['adapter']
+ config['adapter']
end
def self.mysql?
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index 2a017c93f57..019be151353 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -176,9 +176,13 @@ module Gitlab
def initialize(raw_diff, collapse: false)
case raw_diff
when Hash
- init_from_hash(raw_diff, collapse: collapse)
+ init_from_hash(raw_diff)
+ prune_diff_if_eligible(collapse)
when Rugged::Patch, Rugged::Diff::Delta
init_from_rugged(raw_diff, collapse: collapse)
+ when Gitaly::CommitDiffResponse
+ init_from_gitaly(raw_diff)
+ prune_diff_if_eligible(collapse)
when nil
raise "Nil as raw diff passed"
else
@@ -266,13 +270,26 @@ module Gitlab
@diff = encode!(strip_diff_headers(patch.to_s))
end
- def init_from_hash(hash, collapse: false)
+ def init_from_hash(hash)
raw_diff = hash.symbolize_keys
serialize_keys.each do |key|
send(:"#{key}=", raw_diff[key.to_sym])
end
+ end
+
+ def init_from_gitaly(diff_msg)
+ @diff = diff_msg.raw_chunks.join
+ @new_path = encode!(diff_msg.to_path.dup)
+ @old_path = encode!(diff_msg.from_path.dup)
+ @a_mode = diff_msg.old_mode.to_s(8)
+ @b_mode = diff_msg.new_mode.to_s(8)
+ @new_file = diff_msg.from_id == BLANK_SHA
+ @renamed_file = diff_msg.from_path != diff_msg.to_path
+ @deleted_file = diff_msg.to_id == BLANK_SHA
+ end
+ def prune_diff_if_eligible(collapse = false)
prune_large_diff! if too_large?
prune_collapsed_diff! if collapse && collapsible?
end
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index 65e06f5065d..4e45ec7c174 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -30,7 +30,9 @@ module Gitlab
elsif @deltas_only
each_delta(&block)
else
- each_patch(&block)
+ Gitlab::GitalyClient.migrate(:commit_raw_diffs) do
+ each_patch(&block)
+ end
end
end
diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb
index 4d83d8e72a8..0e87ee30c98 100644
--- a/lib/gitlab/git_ref_validator.rb
+++ b/lib/gitlab/git_ref_validator.rb
@@ -5,6 +5,9 @@ module Gitlab
#
# Returns true for a valid reference name, false otherwise
def validate(ref_name)
+ return false if ref_name.start_with?('refs/heads/')
+ return false if ref_name.start_with?('refs/remotes/')
+
Gitlab::Utils.system_silent(
%W(#{Gitlab.config.git.bin_path} check-ref-format refs/#{ref_name}))
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index b981a629fb0..5534d4af439 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -25,5 +25,19 @@ module Gitlab
def self.enabled?
gitaly_address.present?
end
+
+ def self.feature_enabled?(feature)
+ enabled? && ENV["GITALY_#{feature.upcase}"] == '1'
+ end
+
+ def self.migrate(feature)
+ is_enabled = feature_enabled?(feature)
+ metric_name = feature.to_s
+ metric_name += "_gitaly" if is_enabled
+
+ Gitlab::Metrics.measure(metric_name) do
+ yield is_enabled
+ end
+ end
end
end
diff --git a/lib/gitlab/gitaly_client/commit.rb b/lib/gitlab/gitaly_client/commit.rb
new file mode 100644
index 00000000000..525b8d680e9
--- /dev/null
+++ b/lib/gitlab/gitaly_client/commit.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module GitalyClient
+ class Commit
+ # The ID of empty tree.
+ # See http://stackoverflow.com/a/40884093/1856239 and https://github.com/git/git/blob/3ad8b5bf26362ac67c9020bf8c30eee54a84f56d/cache.h#L1011-L1012
+ EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'.freeze
+
+ class << self
+ def diff_from_parent(commit, options = {})
+ stub = Gitaly::Diff::Stub.new(nil, nil, channel_override: GitalyClient.channel)
+ repo = Gitaly::Repository.new(path: commit.project.repository.path_to_repo)
+ parent = commit.parents[0]
+ parent_id = parent ? parent.id : EMPTY_TREE_ID
+ request = Gitaly::CommitDiffRequest.new(
+ repository: repo,
+ left_commit_id: parent_id,
+ right_commit_id: commit.id
+ )
+
+ Gitlab::Git::DiffCollection.new(stub.commit_diff(request), options)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 416194e57d7..ab74c8782f6 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -73,6 +73,9 @@ excluded_attributes:
- :milestone_id
award_emoji:
- :awardable_id
+ statuses:
+ - :trace
+ - :token
methods:
labels:
@@ -81,6 +84,7 @@ methods:
- :type
statuses:
- :type
+ - :gl_project_id
services:
- :type
merge_request_diff:
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index fae792237d9..d44563333a5 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -15,7 +15,7 @@ module Gitlab
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id merge_user_id resolved_by_id].freeze
- PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze
+ PROJECT_REFERENCES = %w[project_id source_project_id target_project_id].freeze
BUILD_MODELS = %w[Ci::Build commit_status].freeze
@@ -98,12 +98,11 @@ module Gitlab
end
def generate_imported_object
- if BUILD_MODELS.include?(@relation_name) # call #trace= method after assigning the other attributes
- trace = @relation_hash.delete('trace')
+ if BUILD_MODELS.include?(@relation_name)
+ @relation_hash.delete('trace') # old export files have trace
@relation_hash.delete('token')
imported_object do |object|
- object.trace = trace
object.commit_id = nil
end
else
@@ -121,7 +120,6 @@ module Gitlab
# project_id may not be part of the export, but we always need to populate it if required.
@relation_hash['project_id'] = project_id
- @relation_hash['gl_project_id'] = project_id if @relation_hash['gl_project_id']
@relation_hash['target_project_id'] = project_id if @relation_hash['target_project_id']
end
diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab
index 2f7c34a3f31..f25e66d54c8 100644
--- a/lib/support/nginx/gitlab
+++ b/lib/support/nginx/gitlab
@@ -38,6 +38,13 @@ server {
## See app/controllers/application_controller.rb for headers set
+ ## Real IP Module Config
+ ## http://nginx.org/en/docs/http/ngx_http_realip_module.html
+ real_ip_header X-Real-IP; ## X-Real-IP or X-Forwarded-For or proxy_protocol
+ real_ip_recursive off; ## If you enable 'on'
+ ## If you have a trusted IP address, uncomment it and set it
+ # set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24
+
## Individual nginx logs for this GitLab vhost
access_log /var/log/nginx/gitlab_access.log;
error_log /var/log/nginx/gitlab_error.log;
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index 330031aaddc..2b40da18bab 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -85,6 +85,13 @@ server {
## [Optional] Enable HTTP Strict Transport Security
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
+ ## Real IP Module Config
+ ## http://nginx.org/en/docs/http/ngx_http_realip_module.html
+ real_ip_header X-Real-IP; ## X-Real-IP or X-Forwarded-For or proxy_protocol
+ real_ip_recursive off; ## If you enable 'on'
+ ## If you have a trusted IP address, uncomment it and set it
+ # set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24
+
## Individual nginx logs for this GitLab vhost
access_log /var/log/nginx/gitlab_access.log;
error_log /var/log/nginx/gitlab_error.log;
diff --git a/package.json b/package.json
index 1048e29d0ac..b3d038bd3d1 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"eslint-fix": "eslint --max-warnings 0 --ext .js --fix .",
"eslint-report": "eslint --max-warnings 0 --ext .js --format html --output-file ./eslint-report.html .",
"karma": "karma start config/karma.config.js --single-run",
+ "karma-coverage": "BABEL_ENV=coverage karma start config/karma.config.js --single-run",
"karma-start": "karma start config/karma.config.js",
"webpack": "webpack --config config/webpack.config.js",
"webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
@@ -13,7 +14,8 @@
"dependencies": {
"babel-core": "^6.22.1",
"babel-loader": "^6.2.10",
- "babel-preset-es2015": "^6.22.0",
+ "babel-plugin-transform-define": "^1.2.0",
+ "babel-preset-latest": "^6.24.0",
"babel-preset-stage-2": "^6.22.0",
"bootstrap-sass": "^3.3.6",
"compression-webpack-plugin": "^0.3.2",
@@ -57,12 +59,5 @@
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^2.0.2",
"webpack-dev-server": "^2.3.0"
- },
- "nyc": {
- "exclude": [
- "spec/javascripts/test_bundle.js",
- "spec/javascripts/**/*_spec.js",
- "app/assets/javascripts/droplab/**/*"
- ]
}
}
diff --git a/qa/qa/page/main/entry.rb b/qa/qa/page/main/entry.rb
index fe80deb6429..a9810beeb29 100644
--- a/qa/qa/page/main/entry.rb
+++ b/qa/qa/page/main/entry.rb
@@ -5,8 +5,14 @@ module QA
def initialize
visit('/')
- # This resolves cold boot problems with login page
- find('.application', wait: 120)
+ # This resolves cold boot / background tasks problems
+ #
+ start = Time.now
+
+ while Time.now - start < 240
+ break if page.has_css?('.application', wait: 10)
+ refresh
+ end
end
def sign_in_using_credentials
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 6ceaf96f78f..8263301c439 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -87,6 +87,12 @@ describe Projects::IssuesController do
end
describe 'GET #new' do
+ it 'redirects to signin if not logged in' do
+ get :new, namespace_id: project.namespace, project_id: project
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+
context 'internal issue tracker' do
before do
sign_in(user)
@@ -121,6 +127,11 @@ describe Projects::IssuesController do
end
context 'external issue tracker' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
it 'redirects to the external issue tracker' do
external = double(new_issue_path: 'https://example.com/issues/new')
allow(project).to receive(:external_issue_tracker).and_return(external)
diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb
index e3f3b4fe8eb..1ecfe48475c 100644
--- a/spec/controllers/projects/variables_controller_spec.rb
+++ b/spec/controllers/projects/variables_controller_spec.rb
@@ -35,7 +35,7 @@ describe Projects::VariablesController do
context 'updating a variable with valid characters' do
before do
- variable.gl_project_id = project.id
+ variable.project_id = project.id
project.variables << variable
end
diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb
index 3372e5ab685..6712dd5d82e 100644
--- a/spec/factories/ci/runner_projects.rb
+++ b/spec/factories/ci/runner_projects.rb
@@ -1,6 +1,6 @@
FactoryGirl.define do
factory :ci_runner_project, class: Ci::RunnerProject do
runner_id 1
- gl_project_id 1
+ project_id 1
end
end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index ae0bbbd6aeb..21487541507 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -4,6 +4,7 @@ FactoryGirl.define do
author
association :source_project, :repository, factory: :project
target_project { source_project }
+ project { target_project }
# $ git log --pretty=oneline feature..master
# 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index de42ab81fac..b4095095887 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -26,7 +26,7 @@ feature 'Admin updates settings', feature: true do
fill_in 'Webhook', with: 'http://localhost'
fill_in 'Username', with: 'test_user'
fill_in 'service_push_channel', with: '#test_channel'
- page.check('Notify only broken builds')
+ page.check('Notify only broken pipelines')
check_all_events
click_on 'Save'
@@ -50,7 +50,6 @@ feature 'Admin updates settings', feature: true do
page.check('Note')
page.check('Issue')
page.check('Merge request')
- page.check('Build')
page.check('Pipeline')
end
end
diff --git a/spec/features/groups/group_name_toggle.rb b/spec/features/groups/group_name_toggle_spec.rb
index ada4ac66e04..8528718a2f7 100644
--- a/spec/features/groups/group_name_toggle.rb
+++ b/spec/features/groups/group_name_toggle_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Group name toggle', js: true do
+feature 'Group name toggle', feature: true, js: true do
let(:group) { create(:group) }
let(:nested_group_1) { create(:group, parent: group) }
let(:nested_group_2) { create(:group, parent: nested_group_1) }
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 1c8267b1593..a58aedc924e 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -6,7 +6,7 @@ describe 'Issues', feature: true do
include SortingHelper
include WaitForAjax
- let(:project) { create(:project) }
+ let(:project) { create(:project, :public) }
before do
login_as :user
@@ -565,6 +565,24 @@ describe 'Issues', feature: true do
end
describe 'new issue' do
+ context 'by unauthenticated user' do
+ before do
+ logout
+ end
+
+ it 'redirects to signin then back to new issue after signin' do
+ visit namespace_project_issues_path(project.namespace, project)
+
+ click_link 'New issue'
+
+ expect(current_path).to eq new_user_session_path
+
+ login_as :user
+
+ expect(current_path).to eq new_namespace_project_issue_path(project.namespace, project)
+ end
+ end
+
context 'dropzone upload file', js: true do
before do
visit new_namespace_project_issue_path(project.namespace, project)
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
index 73c5ef31edc..18833ba7266 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -60,9 +60,6 @@ feature 'Merge request created from fork' do
expect(page).to have_content pipeline.status
expect(page).to have_content pipeline.id
end
-
- expect(page.find('a.btn-remove')[:href])
- .to include fork_project.path_with_namespace
end
end
diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb
index 6fed1568fcf..14511707af4 100644
--- a/spec/features/merge_requests/reset_filters_spec.rb
+++ b/spec/features/merge_requests/reset_filters_spec.rb
@@ -49,6 +49,26 @@ feature 'Merge requests filter clear button', feature: true, js: true do
end
end
+ context 'when multiple label filters have been applied' do
+ let!(:label) { create(:label, project: project, name: 'Frontend') }
+ let(:filter_dropdown) { find("#js-dropdown-label .filter-dropdown") }
+
+ before do
+ visit_merge_requests(project)
+ init_label_search
+ end
+
+ it 'filters bug label' do
+ filtered_search.set('~bug')
+
+ filter_dropdown.find('.filter-dropdown-item', text: bug.title).click
+ init_label_search
+
+ expect(filter_dropdown.find('.filter-dropdown-item', text: bug.title)).to be_visible
+ expect(filter_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
+ end
+ end
+
context 'when a text search has been conducted' do
it 'resets the text search filter' do
visit_merge_requests(project, search: 'Bug')
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
index 2f3c3e45ae6..a1f4eb2688b 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -133,7 +133,6 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
it 'changes target_branch in new merge_request' do
visit new_namespace_project_merge_request_path(another_project.namespace, another_project, new_url_opts)
- click_button "Compare branches and continue"
fill_in "merge_request_title", with: 'My brand new feature'
fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:"
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index 20cdfbae24f..399c1d478c5 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 22bf1bfbdf0..162056671e0 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -99,15 +99,18 @@ describe 'Pipelines', :feature, :js do
end
it 'indicates that pipeline can be canceled' do
- expect(page).to have_link('Cancel')
+ expect(page).to have_selector('.js-pipelines-cancel-button')
expect(page).to have_selector('.ci-running')
end
context 'when canceling' do
- before { click_link('Cancel') }
+ before do
+ find('.js-pipelines-cancel-button').click
+ wait_for_vue_resource
+ end
it 'indicated that pipelines was canceled' do
- expect(page).not_to have_link('Cancel')
+ expect(page).not_to have_selector('.js-pipelines-cancel-button')
expect(page).to have_selector('.ci-canceled')
end
end
@@ -126,15 +129,18 @@ describe 'Pipelines', :feature, :js do
end
it 'indicates that pipeline can be retried' do
- expect(page).to have_link('Retry')
+ expect(page).to have_selector('.js-pipelines-retry-button')
expect(page).to have_selector('.ci-failed')
end
context 'when retrying' do
- before { click_link('Retry') }
+ before do
+ find('.js-pipelines-retry-button').click
+ wait_for_vue_resource
+ end
it 'shows running pipeline that is not retryable' do
- expect(page).not_to have_link('Retry')
+ expect(page).not_to have_selector('.js-pipelines-retry-button')
expect(page).to have_selector('.ci-running')
end
end
@@ -176,17 +182,17 @@ describe 'Pipelines', :feature, :js do
it 'has link to the manual action' do
find('.js-pipeline-dropdown-manual-actions').click
- expect(page).to have_link('manual build')
+ expect(page).to have_button('manual build')
end
context 'when manual action was played' do
before do
find('.js-pipeline-dropdown-manual-actions').click
- click_link('manual build')
+ click_button('manual build')
end
it 'enqueues manual action job' do
- expect(manual.reload).to be_pending
+ expect(page).to have_selector('.js-pipeline-dropdown-manual-actions:disabled')
end
end
end
@@ -203,7 +209,7 @@ describe 'Pipelines', :feature, :js do
before { visit_project_pipelines }
it 'is cancelable' do
- expect(page).to have_link('Cancel')
+ expect(page).to have_selector('.js-pipelines-cancel-button')
end
it 'has pipeline running' do
@@ -211,10 +217,10 @@ describe 'Pipelines', :feature, :js do
end
context 'when canceling' do
- before { click_link('Cancel') }
+ before { find('.js-pipelines-cancel-button').trigger('click') }
it 'indicates that pipeline was canceled' do
- expect(page).not_to have_link('Cancel')
+ expect(page).not_to have_selector('.js-pipelines-cancel-button')
expect(page).to have_selector('.ci-canceled')
end
end
@@ -233,7 +239,7 @@ describe 'Pipelines', :feature, :js do
end
it 'is not retryable' do
- expect(page).not_to have_link('Retry')
+ expect(page).not_to have_selector('.js-pipelines-retry-button')
end
it 'has failed pipeline' do
diff --git a/spec/features/projects/services/slack_service_spec.rb b/spec/features/projects/services/slack_service_spec.rb
index 16541f51d98..c0a4a1e4bf5 100644
--- a/spec/features/projects/services/slack_service_spec.rb
+++ b/spec/features/projects/services/slack_service_spec.rb
@@ -7,7 +7,7 @@ feature 'Projects > Slack service > Setup events', feature: true do
background do
service.fields
- service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, build_channel: 6, wiki_page_channel: 7)
+ service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, pipeline_channel: 6, wiki_page_channel: 7)
project.team << [user, :master]
login_as(user)
end
@@ -20,7 +20,7 @@ feature 'Projects > Slack service > Setup events', feature: true do
expect(page.find_field("service_merge_request_channel").value).to have_content '3'
expect(page.find_field("service_note_channel").value).to have_content '4'
expect(page.find_field("service_tag_push_channel").value).to have_content '5'
- expect(page.find_field("service_build_channel").value).to have_content '6'
+ expect(page.find_field("service_pipeline_channel").value).to have_content '6'
expect(page.find_field("service_wiki_page_channel").value).to have_content '7'
end
end
diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb
index 6815039d5ed..321af416c91 100644
--- a/spec/features/projects/settings/merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/merge_requests_settings_spec.rb
@@ -62,4 +62,27 @@ feature 'Project settings > Merge Requests', feature: true, js: true do
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
end
end
+
+ describe 'Checkbox to enable merge request link' do
+ before do
+ visit edit_project_path(project)
+ end
+
+ scenario 'is initially checked' do
+ checkbox = find_field('project_printing_merge_request_link_enabled')
+ expect(checkbox).to be_checked
+ end
+
+ scenario 'when unchecked sets :printing_merge_request_link_enabled to false' do
+ uncheck('project_printing_merge_request_link_enabled')
+ click_on('Save')
+
+ # Wait for save to complete and page to reload
+ checkbox = find_field('project_printing_merge_request_link_enabled')
+ expect(checkbox).not_to be_checked
+
+ project.reload
+ expect(project.printing_merge_request_link_enabled).to be(false)
+ end
+ end
end
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index f842d14fa96..aedc0333cb9 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -15,15 +15,30 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do
context 'in the user namespace' do
let(:project) { create(:project, namespace: user.namespace) }
- scenario 'the home page' do
- click_link 'Edit'
-
- fill_in :wiki_content, with: 'My awesome wiki!'
- click_button 'Save changes'
-
- expect(page).to have_content('Home')
- expect(page).to have_content("Last edited by #{user.name}")
- expect(page).to have_content('My awesome wiki!')
+ context 'the home page' do
+ scenario 'success when the wiki content is not empty' do
+ click_link 'Edit'
+
+ fill_in :wiki_content, with: 'My awesome wiki!'
+ click_button 'Save changes'
+
+ expect(page).to have_content('Home')
+ expect(page).to have_content("Last edited by #{user.name}")
+ expect(page).to have_content('My awesome wiki!')
+ end
+
+ scenario 'failure when the wiki content is empty' do
+ click_link 'Edit'
+
+ fill_in :wiki_content, with: ''
+ click_button 'Save changes'
+
+ expect(page).to have_selector('.wiki-form')
+ expect(page).to have_content('Edit Page')
+ expect(page).to have_content('The form contains the following error:')
+ expect(page).to have_content('Content can\'t be blank')
+ expect(find('textarea#wiki_content').value).to eq ''
+ end
end
end
diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb
index 77a4ba305bb..3cb809d42b5 100644
--- a/spec/helpers/milestones_helper_spec.rb
+++ b/spec/helpers/milestones_helper_spec.rb
@@ -49,16 +49,20 @@ describe MilestonesHelper do
end
describe '#milestone_remaining_days' do
+ around do |example|
+ Timecop.freeze(Time.utc(2017, 3, 17)) { example.run }
+ end
+
context 'when less than 31 days remaining' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 12.days.from_now)) }
+ let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 12.days.from_now.utc)) }
it 'returns days remaining' do
- expect(milestone_remaining).to eq("<strong>11</strong> days remaining")
+ expect(milestone_remaining).to eq("<strong>12</strong> days remaining")
end
end
context 'when less than 1 year and more than 30 days remaining' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.months.from_now)) }
+ let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.months.from_now.utc)) }
it 'returns months remaining' do
expect(milestone_remaining).to eq("<strong>2</strong> months remaining")
@@ -66,7 +70,7 @@ describe MilestonesHelper do
end
context 'when more than 1 year remaining' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 1.year.from_now + 2.days)) }
+ let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: (1.year.from_now + 2.days).utc)) }
it 'returns years remaining' do
expect(milestone_remaining).to eq("<strong>1</strong> year remaining")
@@ -74,7 +78,7 @@ describe MilestonesHelper do
end
context 'when milestone is expired' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.days.ago)) }
+ let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, due_date: 2.days.ago.utc)) }
it 'returns "Past due"' do
expect(milestone_remaining).to eq("<strong>Past due</strong>")
@@ -82,7 +86,7 @@ describe MilestonesHelper do
end
context 'when milestone has start_date in the future' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.from_now)) }
+ let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.from_now.utc)) }
it 'returns "Upcoming"' do
expect(milestone_remaining).to eq("<strong>Upcoming</strong>")
@@ -90,7 +94,7 @@ describe MilestonesHelper do
end
context 'when milestone has start_date in the past' do
- let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.ago)) }
+ let(:milestone_remaining) { milestone_remaining_days(build_stubbed(:milestone, start_date: 2.days.ago.utc)) }
it 'returns days elapsed' do
expect(milestone_remaining).to eq("<strong>2</strong> days elapsed")
diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb
index 50060a0925d..21e0e74e008 100644
--- a/spec/helpers/todos_helper_spec.rb
+++ b/spec/helpers/todos_helper_spec.rb
@@ -1,6 +1,40 @@
require "spec_helper"
describe TodosHelper do
+ include GitlabRoutingHelper
+
+ describe '#todo_target_path' do
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request, target_project: project, source_project: project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:note) { create(:note_on_issue, noteable: issue, project: project) }
+
+ let(:mr_todo) { build(:todo, project: project, target: merge_request) }
+ let(:issue_todo) { build(:todo, project: project, target: issue) }
+ let(:note_todo) { build(:todo, project: project, target: issue, note: note) }
+ let(:build_failed_todo) { build(:todo, :build_failed, project: project, target: merge_request) }
+
+ it 'returns correct path to the todo MR' do
+ expect(todo_target_path(mr_todo)).
+ to eq("/#{project.full_path}/merge_requests/#{merge_request.iid}")
+ end
+
+ it 'returns correct path to the todo issue' do
+ expect(todo_target_path(issue_todo)).
+ to eq("/#{project.full_path}/issues/#{issue.iid}")
+ end
+
+ it 'returns correct path to the todo note' do
+ expect(todo_target_path(note_todo)).
+ to eq("/#{project.full_path}/issues/#{issue.iid}#note_#{note.id}")
+ end
+
+ it 'returns correct path to build_todo MR when pipeline failed' do
+ expect(todo_target_path(build_failed_todo)).
+ to eq("/#{project.full_path}/merge_requests/#{merge_request.iid}/pipelines")
+ end
+ end
+
describe '#todo_projects_options' do
let(:projects) { create_list(:empty_project, 3) }
let(:user) { create(:user) }
diff --git a/spec/javascripts/boards/board_blank_state_spec.js b/spec/javascripts/boards/board_blank_state_spec.js
new file mode 100644
index 00000000000..47baf83512f
--- /dev/null
+++ b/spec/javascripts/boards/board_blank_state_spec.js
@@ -0,0 +1,93 @@
+/* global BoardService */
+import Vue from 'vue';
+import '~/boards/stores/boards_store';
+import boardBlankState from '~/boards/components/board_blank_state';
+import './mock_data';
+
+describe('Boards blank state', () => {
+ let vm;
+ let fail = false;
+
+ beforeEach((done) => {
+ const Comp = Vue.extend(boardBlankState);
+
+ gl.issueBoards.BoardsStore.create();
+ gl.boardService = new BoardService('/test/issue-boards/board', '', '1');
+
+ spyOn(gl.boardService, 'generateDefaultLists').and.callFake(() => new Promise((resolve, reject) => {
+ if (fail) {
+ reject();
+ } else {
+ resolve({
+ json() {
+ return [{
+ id: 1,
+ title: 'To Do',
+ label: { id: 1 },
+ }, {
+ id: 2,
+ title: 'Doing',
+ label: { id: 2 },
+ }];
+ },
+ });
+ }
+ }));
+
+ vm = new Comp();
+
+ setTimeout(() => {
+ vm.$mount();
+ done();
+ });
+ });
+
+ it('renders pre-defined labels', () => {
+ expect(
+ vm.$el.querySelectorAll('.board-blank-state-list li').length,
+ ).toBe(2);
+
+ expect(
+ vm.$el.querySelectorAll('.board-blank-state-list li')[0].textContent.trim(),
+ ).toEqual('To Do');
+
+ expect(
+ vm.$el.querySelectorAll('.board-blank-state-list li')[1].textContent.trim(),
+ ).toEqual('Doing');
+ });
+
+ it('clears blank state', (done) => {
+ vm.$el.querySelector('.btn-default').click();
+
+ setTimeout(() => {
+ expect(gl.issueBoards.BoardsStore.welcomeIsHidden()).toBeTruthy();
+
+ done();
+ });
+ });
+
+ it('creates pre-defined labels', (done) => {
+ vm.$el.querySelector('.btn-create').click();
+
+ setTimeout(() => {
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2);
+ expect(gl.issueBoards.BoardsStore.state.lists[0].title).toEqual('To Do');
+ expect(gl.issueBoards.BoardsStore.state.lists[1].title).toEqual('Doing');
+
+ done();
+ });
+ });
+
+ it('resets the store if request fails', (done) => {
+ fail = true;
+
+ vm.$el.querySelector('.btn-create').click();
+
+ setTimeout(() => {
+ expect(gl.issueBoards.BoardsStore.welcomeIsHidden()).toBeFalsy();
+ expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(1);
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/commit/pipelines/mock_data.js b/spec/javascripts/commit/pipelines/mock_data.js
index 188908d66bd..82b00b4c1ec 100644
--- a/spec/javascripts/commit/pipelines/mock_data.js
+++ b/spec/javascripts/commit/pipelines/mock_data.js
@@ -1,5 +1,4 @@
-/* eslint-disable no-unused-vars */
-const pipeline = {
+export default {
id: 73,
user: {
name: 'Administrator',
@@ -88,5 +87,3 @@ const pipeline = {
created_at: '2017-01-16T17:13:59.800Z',
updated_at: '2017-01-25T00:00:17.132Z',
};
-
-module.exports = pipeline;
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index f09c57978a1..75efcc06585 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -1,11 +1,6 @@
-/* global pipeline, Vue */
-
-require('~/flash');
-require('~/commit/pipelines/pipelines_store');
-require('~/commit/pipelines/pipelines_service');
-require('~/commit/pipelines/pipelines_table');
-require('~/vue_shared/vue_resource_interceptor');
-const pipeline = require('./mock_data');
+import Vue from 'vue';
+import PipelinesTable from '~/commit/pipelines/pipelines_table';
+import pipeline from './mock_data';
describe('Pipelines table in Commits and Merge requests', () => {
preloadFixtures('static/pipelines_table.html.raw');
@@ -33,7 +28,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
});
it('should render the empty state', (done) => {
- const component = new gl.commits.pipelines.PipelinesTableView({
+ const component = new PipelinesTable({
el: document.querySelector('#commit-pipeline-table-view'),
});
@@ -62,7 +57,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
});
it('should render a table with the received pipelines', (done) => {
- const component = new gl.commits.pipelines.PipelinesTableView({
+ const component = new PipelinesTable({
el: document.querySelector('#commit-pipeline-table-view'),
});
@@ -92,7 +87,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
});
it('should render empty state', (done) => {
- const component = new gl.commits.pipelines.PipelinesTableView({
+ const component = new PipelinesTable({
el: document.querySelector('#commit-pipeline-table-view'),
});
diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js b/spec/javascripts/commit/pipelines/pipelines_store_spec.js
deleted file mode 100644
index 94973419979..00000000000
--- a/spec/javascripts/commit/pipelines/pipelines_store_spec.js
+++ /dev/null
@@ -1,33 +0,0 @@
-const PipelinesStore = require('~/commit/pipelines/pipelines_store');
-
-describe('Store', () => {
- let store;
-
- beforeEach(() => {
- store = new PipelinesStore();
- });
-
- // unregister intervals and event handlers
- afterEach(() => gl.VueRealtimeListener.reset());
-
- it('should start with a blank state', () => {
- expect(store.state.pipelines.length).toBe(0);
- });
-
- it('should store an array of pipelines', () => {
- const pipelines = [
- {
- id: '1',
- name: 'pipeline',
- },
- {
- id: '2',
- name: 'pipeline_2',
- },
- ];
-
- store.storePipelines(pipelines);
-
- expect(store.state.pipelines.length).toBe(pipelines.length);
- });
-});
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 8d25500b9fd..aabc8bea12f 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -136,6 +136,21 @@ describe('Issue', function() {
expectErrorMessage();
expect($('.issue_counter')).toHaveText(1);
});
+
+ it('updates counter', () => {
+ spyOn(jQuery, 'ajax').and.callFake(function(req) {
+ expectPendingRequest(req, $btnClose);
+ req.success({
+ id: 34
+ });
+ });
+
+ expect($('.issue_counter')).toHaveText(1);
+ $('.issue_counter').text('1,001');
+ expect($('.issue_counter').text()).toEqual('1,001');
+ $btnClose.trigger('click');
+ expect($('.issue_counter').text()).toEqual('1,000');
+ });
});
describe('reopen issue', function() {
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index c12b44cea89..5cdb6473eda 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -32,10 +32,11 @@ testsContext.keys().forEach(function (path) {
}
});
-// workaround: include all source files to find files with 0% coverage
-// see also https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15
-describe('Uncovered files', function () {
- // the following files throw errors because of undefined variables
+// if we're generating coverage reports, make sure to include all files so
+// that we can catch files with 0% coverage
+// see: https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15
+if (process.env.BABEL_ENV === 'coverage') {
+ // exempt these files from the coverage report
const troubleMakers = [
'./blob_edit/blob_edit_bundle.js',
'./cycle_analytics/components/stage_plan_component.js',
@@ -48,21 +49,23 @@ describe('Uncovered files', function () {
'./network/branch_graph.js',
];
- const sourceFiles = require.context('~', true, /^\.\/(?!application\.js).*\.js$/);
- sourceFiles.keys().forEach(function (path) {
- // ignore if there is a matching spec file
- if (testsContext.keys().indexOf(`${path.replace(/\.js$/, '')}_spec`) > -1) {
- return;
- }
+ describe('Uncovered files', function () {
+ const sourceFiles = require.context('~', true, /\.js$/);
+ sourceFiles.keys().forEach(function (path) {
+ // ignore if there is a matching spec file
+ if (testsContext.keys().indexOf(`${path.replace(/\.js$/, '')}_spec`) > -1) {
+ return;
+ }
- it(`includes '${path}'`, function () {
- try {
- sourceFiles(path);
- } catch (err) {
- if (troubleMakers.indexOf(path) === -1) {
- expect(err).toBeNull();
+ it(`includes '${path}'`, function () {
+ try {
+ sourceFiles(path);
+ } catch (err) {
+ if (troubleMakers.indexOf(path) === -1) {
+ expect(err).toBeNull();
+ }
}
- }
+ });
});
});
-});
+}
diff --git a/spec/javascripts/vue_pipelines_index/async_button_spec.js b/spec/javascripts/vue_pipelines_index/async_button_spec.js
new file mode 100644
index 00000000000..bc8e504c413
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/async_button_spec.js
@@ -0,0 +1,93 @@
+import Vue from 'vue';
+import asyncButtonComp from '~/vue_pipelines_index/components/async_button';
+
+describe('Pipelines Async Button', () => {
+ let component;
+ let spy;
+ let AsyncButtonComponent;
+
+ beforeEach(() => {
+ AsyncButtonComponent = Vue.extend(asyncButtonComp);
+
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+
+ component = new AsyncButtonComponent({
+ propsData: {
+ endpoint: '/foo',
+ title: 'Foo',
+ icon: 'fa fa-foo',
+ cssClass: 'bar',
+ service: {
+ postAction: spy,
+ },
+ },
+ }).$mount();
+ });
+
+ it('should render a button', () => {
+ expect(component.$el.tagName).toEqual('BUTTON');
+ });
+
+ it('should render the provided icon', () => {
+ expect(component.$el.querySelector('i').getAttribute('class')).toContain('fa fa-foo');
+ });
+
+ it('should render the provided title', () => {
+ expect(component.$el.getAttribute('title')).toContain('Foo');
+ expect(component.$el.getAttribute('aria-label')).toContain('Foo');
+ });
+
+ it('should render the provided cssClass', () => {
+ expect(component.$el.getAttribute('class')).toContain('bar');
+ });
+
+ it('should call the service when it is clicked with the provided endpoint', () => {
+ component.$el.click();
+ expect(spy).toHaveBeenCalledWith('/foo');
+ });
+
+ it('should hide loading if request fails', () => {
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.reject());
+
+ component = new AsyncButtonComponent({
+ propsData: {
+ endpoint: '/foo',
+ title: 'Foo',
+ icon: 'fa fa-foo',
+ cssClass: 'bar',
+ dataAttributes: {
+ 'data-foo': 'foo',
+ },
+ service: {
+ postAction: spy,
+ },
+ },
+ }).$mount();
+
+ component.$el.click();
+ expect(component.$el.querySelector('.fa-spinner')).toBe(null);
+ });
+
+ describe('With confirm dialog', () => {
+ it('should call the service when confimation is positive', () => {
+ spyOn(window, 'confirm').and.returnValue(true);
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+
+ component = new AsyncButtonComponent({
+ propsData: {
+ endpoint: '/foo',
+ title: 'Foo',
+ icon: 'fa fa-foo',
+ cssClass: 'bar',
+ service: {
+ postAction: spy,
+ },
+ confirmActionMessage: 'bar',
+ },
+ }).$mount();
+
+ component.$el.click();
+ expect(spy).toHaveBeenCalledWith('/foo');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js b/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js
new file mode 100644
index 00000000000..96a2a37b5f7
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js
@@ -0,0 +1,100 @@
+import Vue from 'vue';
+import pipelineUrlComp from '~/vue_pipelines_index/components/pipeline_url';
+
+describe('Pipeline Url Component', () => {
+ let PipelineUrlComponent;
+
+ beforeEach(() => {
+ PipelineUrlComponent = Vue.extend(pipelineUrlComp);
+ });
+
+ it('should render a table cell', () => {
+ const component = new PipelineUrlComponent({
+ propsData: {
+ pipeline: {
+ id: 1,
+ path: 'foo',
+ flags: {},
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.tagName).toEqual('TD');
+ });
+
+ it('should render a link the provided path and id', () => {
+ const component = new PipelineUrlComponent({
+ propsData: {
+ pipeline: {
+ id: 1,
+ path: 'foo',
+ flags: {},
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.js-pipeline-url-link').getAttribute('href')).toEqual('foo');
+ expect(component.$el.querySelector('.js-pipeline-url-link span').textContent).toEqual('#1');
+ });
+
+ it('should render user information when a user is provided', () => {
+ const mockData = {
+ pipeline: {
+ id: 1,
+ path: 'foo',
+ flags: {},
+ user: {
+ web_url: '/',
+ name: 'foo',
+ avatar_url: '/',
+ },
+ },
+ };
+
+ const component = new PipelineUrlComponent({
+ propsData: mockData,
+ }).$mount();
+
+ const image = component.$el.querySelector('.js-pipeline-url-user img');
+
+ expect(
+ component.$el.querySelector('.js-pipeline-url-user').getAttribute('href'),
+ ).toEqual(mockData.pipeline.user.web_url);
+ expect(image.getAttribute('title')).toEqual(mockData.pipeline.user.name);
+ expect(image.getAttribute('src')).toEqual(mockData.pipeline.user.avatar_url);
+ });
+
+ it('should render "API" when no user is provided', () => {
+ const component = new PipelineUrlComponent({
+ propsData: {
+ pipeline: {
+ id: 1,
+ path: 'foo',
+ flags: {},
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.js-pipeline-url-api').textContent).toContain('API');
+ });
+
+ it('should render latest, yaml invalid and stuck flags when provided', () => {
+ const component = new PipelineUrlComponent({
+ propsData: {
+ pipeline: {
+ id: 1,
+ path: 'foo',
+ flags: {
+ latest: true,
+ yaml_errors: true,
+ stuck: true,
+ },
+ },
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.js-pipeline-url-lastest').textContent).toContain('latest');
+ expect(component.$el.querySelector('.js-pipeline-url-yaml').textContent).toContain('yaml invalid');
+ expect(component.$el.querySelector('.js-pipeline-url-stuck').textContent).toContain('stuck');
+ });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js
new file mode 100644
index 00000000000..dba998c7688
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js
@@ -0,0 +1,62 @@
+import Vue from 'vue';
+import pipelinesActionsComp from '~/vue_pipelines_index/components/pipelines_actions';
+
+describe('Pipelines Actions dropdown', () => {
+ let component;
+ let spy;
+ let actions;
+ let ActionsComponent;
+
+ beforeEach(() => {
+ ActionsComponent = Vue.extend(pipelinesActionsComp);
+
+ actions = [
+ {
+ name: 'stop_review',
+ path: '/root/review-app/builds/1893/play',
+ },
+ ];
+
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+
+ component = new ActionsComponent({
+ propsData: {
+ actions,
+ service: {
+ postAction: spy,
+ },
+ },
+ }).$mount();
+ });
+
+ it('should render a dropdown with the provided actions', () => {
+ expect(
+ component.$el.querySelectorAll('.dropdown-menu li').length,
+ ).toEqual(actions.length);
+ });
+
+ it('should call the service when an action is clicked', () => {
+ component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click();
+ component.$el.querySelector('.js-pipeline-action-link').click();
+
+ expect(spy).toHaveBeenCalledWith(actions[0].path);
+ });
+
+ it('should hide loading if request fails', () => {
+ spy = jasmine.createSpy('spy').and.returnValue(Promise.reject());
+
+ component = new ActionsComponent({
+ propsData: {
+ actions,
+ service: {
+ postAction: spy,
+ },
+ },
+ }).$mount();
+
+ component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click();
+ component.$el.querySelector('.js-pipeline-action-link').click();
+
+ expect(component.$el.querySelector('.fa-spinner')).toEqual(null);
+ });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js
new file mode 100644
index 00000000000..f7f49649c1c
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js
@@ -0,0 +1,40 @@
+import Vue from 'vue';
+import artifactsComp from '~/vue_pipelines_index/components/pipelines_artifacts';
+
+describe('Pipelines Artifacts dropdown', () => {
+ let component;
+ let artifacts;
+
+ beforeEach(() => {
+ const ArtifactsComponent = Vue.extend(artifactsComp);
+
+ artifacts = [
+ {
+ name: 'artifact',
+ path: '/download/path',
+ },
+ ];
+
+ component = new ArtifactsComponent({
+ propsData: {
+ artifacts,
+ },
+ }).$mount();
+ });
+
+ it('should render a dropdown with the provided artifacts', () => {
+ expect(
+ component.$el.querySelectorAll('.dropdown-menu li').length,
+ ).toEqual(artifacts.length);
+ });
+
+ it('should render a link with the provided path', () => {
+ expect(
+ component.$el.querySelector('.dropdown-menu li a').getAttribute('href'),
+ ).toEqual(artifacts[0].path);
+
+ expect(
+ component.$el.querySelector('.dropdown-menu li a span').textContent,
+ ).toContain(artifacts[0].name);
+ });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js
new file mode 100644
index 00000000000..5c0934404bb
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js
@@ -0,0 +1,72 @@
+import PipelineStore from '~/vue_pipelines_index/stores/pipelines_store';
+
+describe('Pipelines Store', () => {
+ let store;
+
+ beforeEach(() => {
+ store = new PipelineStore();
+ });
+
+ it('should be initialized with an empty state', () => {
+ expect(store.state.pipelines).toEqual([]);
+ expect(store.state.count).toEqual({});
+ expect(store.state.pageInfo).toEqual({});
+ });
+
+ describe('storePipelines', () => {
+ it('should use the default parameter if none is provided', () => {
+ store.storePipelines();
+ expect(store.state.pipelines).toEqual([]);
+ });
+
+ it('should store the provided array', () => {
+ const array = [{ id: 1, status: 'running' }, { id: 2, status: 'success' }];
+ store.storePipelines(array);
+ expect(store.state.pipelines).toEqual(array);
+ });
+ });
+
+ describe('storeCount', () => {
+ it('should use the default parameter if none is provided', () => {
+ store.storeCount();
+ expect(store.state.count).toEqual({});
+ });
+
+ it('should store the provided count', () => {
+ const count = { all: 20, finished: 10 };
+ store.storeCount(count);
+
+ expect(store.state.count).toEqual(count);
+ });
+ });
+
+ describe('storePagination', () => {
+ it('should use the default parameter if none is provided', () => {
+ store.storePagination();
+ expect(store.state.pageInfo).toEqual({});
+ });
+
+ it('should store pagination information normalized and parsed', () => {
+ const pagination = {
+ 'X-nExt-pAge': '2',
+ 'X-page': '1',
+ 'X-Per-Page': '1',
+ 'X-Prev-Page': '2',
+ 'X-TOTAL': '37',
+ 'X-Total-Pages': '2',
+ };
+
+ const expectedResult = {
+ perPage: 1,
+ page: 1,
+ total: 37,
+ totalPages: 2,
+ nextPage: 2,
+ previousPage: 2,
+ };
+
+ store.storePagination(pagination);
+ expect(store.state.pageInfo).toEqual(expectedResult);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js
index 15ab10b9b69..df547299d75 100644
--- a/spec/javascripts/vue_shared/components/commit_spec.js
+++ b/spec/javascripts/vue_shared/components/commit_spec.js
@@ -1,13 +1,17 @@
-require('~/vue_shared/components/commit');
+import Vue from 'vue';
+import commitComp from '~/vue_shared/components/commit';
describe('Commit component', () => {
let props;
let component;
+ let CommitComponent;
+
+ beforeEach(() => {
+ CommitComponent = Vue.extend(commitComp);
+ });
it('should render a code-fork icon if it does not represent a tag', () => {
- setFixtures('<div class="test-commit-container"></div>');
- component = new window.gl.CommitComponent({
- el: document.querySelector('.test-commit-container'),
+ component = new CommitComponent({
propsData: {
tag: false,
commitRef: {
@@ -23,15 +27,13 @@ describe('Commit component', () => {
username: 'jschatz1',
},
},
- });
+ }).$mount();
expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-code-fork');
});
describe('Given all the props', () => {
beforeEach(() => {
- setFixtures('<div class="test-commit-container"></div>');
-
props = {
tag: true,
commitRef: {
@@ -49,10 +51,9 @@ describe('Commit component', () => {
commitIconSvg: '<svg></svg>',
};
- component = new window.gl.CommitComponent({
- el: document.querySelector('.test-commit-container'),
+ component = new CommitComponent({
propsData: props,
- });
+ }).$mount();
});
it('should render a tag icon if it represents a tag', () => {
@@ -105,7 +106,6 @@ describe('Commit component', () => {
describe('When commit title is not provided', () => {
it('should render default message', () => {
- setFixtures('<div class="test-commit-container"></div>');
props = {
tag: false,
commitRef: {
@@ -118,10 +118,9 @@ describe('Commit component', () => {
author: {},
};
- component = new window.gl.CommitComponent({
- el: document.querySelector('.test-commit-container'),
+ component = new CommitComponent({
propsData: props,
- });
+ }).$mount();
expect(
component.$el.querySelector('.commit-title span').textContent,
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
index 412abfd5e41..699625cdbb7 100644
--- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
+++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
@@ -1,20 +1,20 @@
-require('~/vue_shared/components/pipelines_table_row');
-const pipeline = require('../../commit/pipelines/mock_data');
+import Vue from 'vue';
+import tableRowComp from '~/vue_shared/components/pipelines_table_row';
+import pipeline from '../../commit/pipelines/mock_data';
describe('Pipelines Table Row', () => {
let component;
- preloadFixtures('static/environments/element.html.raw');
beforeEach(() => {
- loadFixtures('static/environments/element.html.raw');
+ const PipelinesTableRowComponent = Vue.extend(tableRowComp);
- component = new gl.pipelines.PipelinesTableRowComponent({
+ component = new PipelinesTableRowComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
pipeline,
- svgs: {},
+ service: {},
},
- });
+ }).$mount();
});
it('should render a table row', () => {
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_spec.js
index 54d81e2ea7d..b0b1df5a753 100644
--- a/spec/javascripts/vue_shared/components/pipelines_table_spec.js
+++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js
@@ -1,24 +1,24 @@
-require('~/vue_shared/components/pipelines_table');
-require('~/lib/utils/datetime_utility');
-const pipeline = require('../../commit/pipelines/mock_data');
+import Vue from 'vue';
+import pipelinesTableComp from '~/vue_shared/components/pipelines_table';
+import '~/lib/utils/datetime_utility';
+import pipeline from '../../commit/pipelines/mock_data';
describe('Pipelines Table', () => {
- preloadFixtures('static/environments/element.html.raw');
+ let PipelinesTableComponent;
beforeEach(() => {
- loadFixtures('static/environments/element.html.raw');
+ PipelinesTableComponent = Vue.extend(pipelinesTableComp);
});
describe('table', () => {
let component;
beforeEach(() => {
- component = new gl.pipelines.PipelinesTableComponent({
- el: document.querySelector('.test-dom-element'),
+ component = new PipelinesTableComponent({
propsData: {
pipelines: [],
- svgs: {},
+ service: {},
},
- });
+ }).$mount();
});
it('should render a table', () => {
@@ -37,26 +37,25 @@ describe('Pipelines Table', () => {
describe('without data', () => {
it('should render an empty table', () => {
- const component = new gl.pipelines.PipelinesTableComponent({
- el: document.querySelector('.test-dom-element'),
+ const component = new PipelinesTableComponent({
propsData: {
pipelines: [],
- svgs: {},
+ service: {},
},
- });
+ }).$mount();
expect(component.$el.querySelectorAll('tbody tr').length).toEqual(0);
});
});
describe('with data', () => {
it('should render rows', () => {
- const component = new gl.pipelines.PipelinesTableComponent({
+ const component = new PipelinesTableComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
pipelines: [pipeline],
- svgs: {},
+ service: {},
},
- });
+ }).$mount();
expect(component.$el.querySelectorAll('tbody tr').length).toEqual(1);
});
diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/javascripts/vue_shared/components/table_pagination_spec.js
index 9cb067921a7..a5c3870b3ac 100644
--- a/spec/javascripts/vue_shared/components/table_pagination_spec.js
+++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js
@@ -1,8 +1,10 @@
-require('~/lib/utils/common_utils');
-require('~/vue_shared/components/table_pagination');
+import Vue from 'vue';
+import paginationComp from '~/vue_shared/components/table_pagination';
+import '~/lib/utils/common_utils';
describe('Pagination component', () => {
let component;
+ let PaginationComponent;
const changeChanges = {
one: '',
@@ -12,11 +14,12 @@ describe('Pagination component', () => {
changeChanges.one = one;
};
- it('should render and start at page 1', () => {
- setFixtures('<div class="test-pagination-container"></div>');
+ beforeEach(() => {
+ PaginationComponent = Vue.extend(paginationComp);
+ });
- component = new window.gl.VueGlPagination({
- el: document.querySelector('.test-pagination-container'),
+ it('should render and start at page 1', () => {
+ component = new PaginationComponent({
propsData: {
pageInfo: {
totalPages: 10,
@@ -25,7 +28,7 @@ describe('Pagination component', () => {
},
change,
},
- });
+ }).$mount();
expect(component.$el.classList).toContain('gl-pagination');
@@ -35,10 +38,7 @@ describe('Pagination component', () => {
});
it('should go to the previous page', () => {
- setFixtures('<div class="test-pagination-container"></div>');
-
- component = new window.gl.VueGlPagination({
- el: document.querySelector('.test-pagination-container'),
+ component = new PaginationComponent({
propsData: {
pageInfo: {
totalPages: 10,
@@ -47,7 +47,7 @@ describe('Pagination component', () => {
},
change,
},
- });
+ }).$mount();
component.changePage({ target: { innerText: 'Prev' } });
@@ -55,10 +55,7 @@ describe('Pagination component', () => {
});
it('should go to the next page', () => {
- setFixtures('<div class="test-pagination-container"></div>');
-
- component = new window.gl.VueGlPagination({
- el: document.querySelector('.test-pagination-container'),
+ component = new PaginationComponent({
propsData: {
pageInfo: {
totalPages: 10,
@@ -67,7 +64,7 @@ describe('Pagination component', () => {
},
change,
},
- });
+ }).$mount();
component.changePage({ target: { innerText: 'Next' } });
@@ -75,10 +72,7 @@ describe('Pagination component', () => {
});
it('should go to the last page', () => {
- setFixtures('<div class="test-pagination-container"></div>');
-
- component = new window.gl.VueGlPagination({
- el: document.querySelector('.test-pagination-container'),
+ component = new PaginationComponent({
propsData: {
pageInfo: {
totalPages: 10,
@@ -87,7 +81,7 @@ describe('Pagination component', () => {
},
change,
},
- });
+ }).$mount();
component.changePage({ target: { innerText: 'Last >>' } });
@@ -95,10 +89,7 @@ describe('Pagination component', () => {
});
it('should go to the first page', () => {
- setFixtures('<div class="test-pagination-container"></div>');
-
- component = new window.gl.VueGlPagination({
- el: document.querySelector('.test-pagination-container'),
+ component = new PaginationComponent({
propsData: {
pageInfo: {
totalPages: 10,
@@ -107,7 +98,7 @@ describe('Pagination component', () => {
},
change,
},
- });
+ }).$mount();
component.changePage({ target: { innerText: '<< First' } });
@@ -115,10 +106,7 @@ describe('Pagination component', () => {
});
it('should do nothing', () => {
- setFixtures('<div class="test-pagination-container"></div>');
-
- component = new window.gl.VueGlPagination({
- el: document.querySelector('.test-pagination-container'),
+ component = new PaginationComponent({
propsData: {
pageInfo: {
totalPages: 10,
@@ -127,7 +115,7 @@ describe('Pagination component', () => {
},
change,
},
- });
+ }).$mount();
component.changePage({ target: { innerText: '...' } });
diff --git a/spec/lib/git_ref_validator_spec.rb b/spec/lib/git_ref_validator_spec.rb
index dc57e94f193..cc8daa535d6 100644
--- a/spec/lib/git_ref_validator_spec.rb
+++ b/spec/lib/git_ref_validator_spec.rb
@@ -5,6 +5,7 @@ describe Gitlab::GitRefValidator, lib: true do
it { expect(Gitlab::GitRefValidator.validate('implement_@all')).to be_truthy }
it { expect(Gitlab::GitRefValidator.validate('my_new_feature')).to be_truthy }
it { expect(Gitlab::GitRefValidator.validate('#1')).to be_truthy }
+ it { expect(Gitlab::GitRefValidator.validate('feature/refs/heads/foo')).to be_truthy }
it { expect(Gitlab::GitRefValidator.validate('feature/~new/')).to be_falsey }
it { expect(Gitlab::GitRefValidator.validate('feature/^new/')).to be_falsey }
it { expect(Gitlab::GitRefValidator.validate('feature/:new/')).to be_falsey }
@@ -17,4 +18,8 @@ describe Gitlab::GitRefValidator, lib: true do
it { expect(Gitlab::GitRefValidator.validate('feature\new')).to be_falsey }
it { expect(Gitlab::GitRefValidator.validate('feature//new')).to be_falsey }
it { expect(Gitlab::GitRefValidator.validate('feature new')).to be_falsey }
+ it { expect(Gitlab::GitRefValidator.validate('refs/heads/')).to be_falsey }
+ it { expect(Gitlab::GitRefValidator.validate('refs/remotes/')).to be_falsey }
+ it { expect(Gitlab::GitRefValidator.validate('refs/heads/feature')).to be_falsey }
+ it { expect(Gitlab::GitRefValidator.validate('refs/remotes/origin')).to be_falsey }
end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index edd01d032c8..4ce4e6e1034 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -1,10 +1,16 @@
require 'spec_helper'
-class MigrationTest
- include Gitlab::Database
-end
-
describe Gitlab::Database, lib: true do
+ before do
+ stub_const('MigrationTest', Class.new { include Gitlab::Database })
+ end
+
+ describe '.config' do
+ it 'returns a Hash' do
+ expect(described_class.config).to be_an_instance_of(Hash)
+ end
+ end
+
describe '.adapter_name' do
it 'returns the name of the adapter' do
expect(described_class.adapter_name).to be_an_instance_of(String)
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index 4c55532d165..992126ef153 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -109,6 +109,43 @@ EOT
end
end
end
+
+ context 'using a Gitaly::CommitDiffResponse' do
+ let(:diff) do
+ described_class.new(
+ Gitaly::CommitDiffResponse.new(
+ to_path: ".gitmodules",
+ from_path: ".gitmodules",
+ old_mode: 0100644,
+ new_mode: 0100644,
+ from_id: '357406f3075a57708d0163752905cc1576fceacc',
+ to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0',
+ raw_chunks: raw_chunks,
+ )
+ )
+ end
+
+ context 'with a small diff' do
+ let(:raw_chunks) { [@raw_diff_hash[:diff]] }
+
+ it 'initializes the diff' do
+ expect(diff.to_hash).to eq(@raw_diff_hash)
+ end
+
+ it 'does not prune the diff' do
+ expect(diff).not_to be_too_large
+ end
+ end
+
+ context 'using a diff that is too large' do
+ let(:raw_chunks) { ['a' * 204800] }
+
+ it 'prunes the diff' do
+ expect(diff.diff).to be_empty
+ expect(diff).to be_too_large
+ end
+ end
+ end
end
describe 'straight diffs' do
diff --git a/spec/lib/gitlab/gitaly_client/commit_spec.rb b/spec/lib/gitlab/gitaly_client/commit_spec.rb
new file mode 100644
index 00000000000..4684b1d1ac0
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/commit_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::Commit do
+ describe '.diff_from_parent' do
+ let(:diff_stub) { double('Gitaly::Diff::Stub') }
+ let(:project) { create(:project, :repository) }
+ let(:repository_message) { Gitaly::Repository.new(path: project.repository.path) }
+ let(:commit) { project.commit('913c66a37b4a45b9769037c55c2d238bd0942d2e') }
+
+ before do
+ allow(Gitaly::Diff::Stub).to receive(:new).and_return(diff_stub)
+ allow(diff_stub).to receive(:commit_diff).and_return([])
+ end
+
+ context 'when a commit has a parent' do
+ it 'sends an RPC request with the parent ID as left commit' do
+ request = Gitaly::CommitDiffRequest.new(
+ repository: repository_message,
+ left_commit_id: 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660',
+ right_commit_id: commit.id,
+ )
+
+ expect(diff_stub).to receive(:commit_diff).with(request)
+
+ described_class.diff_from_parent(commit)
+ end
+ end
+
+ context 'when a commit does not have a parent' do
+ it 'sends an RPC request with empty tree ref as left commit' do
+ initial_commit = project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863')
+ request = Gitaly::CommitDiffRequest.new(
+ repository: repository_message,
+ left_commit_id: '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
+ right_commit_id: initial_commit.id,
+ )
+
+ expect(diff_stub).to receive(:commit_diff).with(request)
+
+ described_class.diff_from_parent(initial_commit)
+ end
+ end
+
+ it 'returns a Gitlab::Git::DiffCollection' do
+ ret = described_class.diff_from_parent(commit)
+
+ expect(ret).to be_kind_of(Gitlab::Git::DiffCollection)
+ end
+
+ it 'passes options to Gitlab::Git::DiffCollection' do
+ options = { max_files: 31, max_lines: 13 }
+
+ expect(Gitlab::Git::DiffCollection).to receive(:new).with([], options)
+
+ described_class.diff_from_parent(commit, options)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index e47956a365f..ddeb71730e7 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -130,7 +130,6 @@ project:
- campfire_service
- drone_ci_service
- emails_on_push_service
-- builds_email_service
- pipelines_email_service
- mattermost_slash_commands_service
- slack_slash_commands_service
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index c3d5c451a3c..d9b67426818 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -6507,7 +6507,6 @@
"tag": null,
"yaml_errors": null,
"committed_at": null,
- "gl_project_id": 5,
"status": "failed",
"started_at": null,
"finished_at": null,
@@ -6565,7 +6564,6 @@
"artifacts_file": {
"url": null
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": null
},
@@ -6603,7 +6601,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts_metadata.gz"
},
@@ -6624,7 +6621,6 @@
"tag": null,
"yaml_errors": null,
"committed_at": null,
- "gl_project_id": 5,
"status": "failed",
"started_at": null,
"finished_at": null,
@@ -6659,7 +6655,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts_metadata.gz"
},
@@ -6695,7 +6690,6 @@
"artifacts_file": {
"url": null
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": null
},
@@ -6716,7 +6710,6 @@
"tag": null,
"yaml_errors": null,
"committed_at": null,
- "gl_project_id": 5,
"status": "failed",
"started_at": null,
"finished_at": null,
@@ -6751,7 +6744,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts_metadata.gz"
},
@@ -6787,7 +6779,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts_metadata.gz"
},
@@ -6808,7 +6799,6 @@
"tag": null,
"yaml_errors": null,
"committed_at": null,
- "gl_project_id": 5,
"status": "failed",
"started_at": null,
"finished_at": null,
@@ -6843,7 +6833,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts_metadata.gz"
},
@@ -6879,7 +6868,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts_metadata.gz"
},
@@ -6900,7 +6888,6 @@
"tag": null,
"yaml_errors": null,
"committed_at": null,
- "gl_project_id": 5,
"status": "failed",
"started_at": null,
"finished_at": null,
@@ -6935,7 +6922,6 @@
"artifacts_file": {
"url": null
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": null
},
@@ -6971,7 +6957,6 @@
"artifacts_file": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts.zip"
},
- "gl_project_id": 5,
"artifacts_metadata": {
"url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts_metadata.gz"
},
@@ -6985,11 +6970,10 @@
{
"id": 123,
"token": "cdbfasdf44a5958c83654733449e585",
- "project_id": null,
+ "project_id": 5,
"deleted_at": null,
"created_at": "2017-01-16T15:25:28.637Z",
- "updated_at": "2017-01-16T15:25:28.637Z",
- "gl_project_id": 123
+ "updated_at": "2017-01-16T15:25:28.637Z"
}
],
"deploy_keys": [
@@ -7047,7 +7031,7 @@
"updated_at": "2016-06-14T15:01:51.303Z",
"active": false,
"properties": {
- "notify_only_broken_builds": true
+ "notify_only_broken_pipelines": true
},
"template": false,
"push_events": true,
@@ -7055,7 +7039,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "pipeline_events": true,
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7174,7 +7158,7 @@
"updated_at": "2016-06-14T15:01:51.219Z",
"active": false,
"properties": {
- "notify_only_broken_builds": true
+ "notify_only_broken_pipelines": true
},
"template": false,
"push_events": true,
@@ -7182,7 +7166,7 @@
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
- "build_events": true,
+ "pipeline_events": true,
"category": "common",
"default": false,
"wiki_page_events": true
@@ -7335,27 +7319,6 @@
"wiki_page_events": true
},
{
- "id": 85,
- "title": "Builds emails",
- "project_id": 5,
- "created_at": "2016-06-14T15:01:51.090Z",
- "updated_at": "2016-06-14T15:01:51.090Z",
- "active": false,
- "properties": {
- "notify_only_broken_builds": true
- },
- "template": false,
- "push_events": true,
- "issues_events": true,
- "merge_requests_events": true,
- "tag_push_events": true,
- "note_events": true,
- "build_events": true,
- "category": "common",
- "default": false,
- "wiki_page_events": true
- },
- {
"id": 84,
"title": "Buildkite",
"project_id": 5,
@@ -7503,4 +7466,4 @@
"updated_at": "2016-09-23T11:58:28.000Z",
"wiki_access_level": 20
}
-} \ No newline at end of file
+}
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index f4a21c24fa1..c36f12dbd82 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -129,6 +129,25 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(Ci::Build.where(token: 'abcd')).to be_empty
end
end
+
+ context 'has restored the correct number of records' do
+ it 'has the correct number of merge requests' do
+ expect(@project.merge_requests.size).to eq(9)
+ end
+
+ it 'has the correct number of triggers' do
+ expect(@project.triggers.size).to eq(1)
+ end
+
+ it 'has the correct number of pipelines and statuses' do
+ expect(@project.pipelines.size).to eq(5)
+
+ @project.pipelines.zip([2, 2, 2, 2, 2])
+ .each do |(pipeline, expected_status_size)|
+ expect(pipeline.statuses.size).to eq(expected_status_size)
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index c718e792461..1ad16a9b57d 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -15,6 +15,7 @@ Issue:
- updated_by_id
- confidential
- deleted_at
+- closed_at
- due_date
- moved_to_id
- lock_version
@@ -176,7 +177,6 @@ Ci::Pipeline:
- tag
- yaml_errors
- committed_at
-- gl_project_id
- status
- started_at
- finished_at
@@ -211,7 +211,6 @@ CommitStatus:
- target_url
- description
- artifacts_file
-- gl_project_id
- artifacts_metadata
- erased_by_id
- erased_at
@@ -232,7 +231,6 @@ Ci::Variable:
- encrypted_value
- encrypted_value_salt
- encrypted_value_iv
-- gl_project_id
Ci::Trigger:
- id
- token
@@ -240,7 +238,6 @@ Ci::Trigger:
- deleted_at
- created_at
- updated_at
-- gl_project_id
- owner_id
- description
DeployKey:
diff --git a/spec/mailers/emails/builds_spec.rb b/spec/mailers/emails/builds_spec.rb
deleted file mode 100644
index d968096783c..00000000000
--- a/spec/mailers/emails/builds_spec.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-require 'spec_helper'
-require 'email_spec'
-
-describe Notify do
- include EmailSpec::Matchers
-
- include_context 'gitlab email notification'
-
- describe 'build notification email' do
- let(:build) { create(:ci_build) }
- let(:project) { build.project }
-
- shared_examples 'build email' do
- it 'contains name of project' do
- is_expected.to have_body_text build.project_name
- end
-
- it 'contains link to project' do
- is_expected.to have_body_text namespace_project_path(project.namespace, project)
- end
- end
-
- shared_examples 'an email with X-GitLab headers containing build details' do
- it 'has X-GitLab-Build* headers' do
- is_expected.to have_header 'X-GitLab-Build-Id', /#{build.id}/
- is_expected.to have_header 'X-GitLab-Build-Ref', /#{build.ref}/
- end
- end
-
- describe 'build success' do
- subject { Notify.build_success_email(build.id, 'wow@example.com') }
- before { build.success }
-
- it_behaves_like 'build email'
- it_behaves_like 'an email with X-GitLab headers containing build details'
- it_behaves_like 'an email with X-GitLab headers containing project details'
-
- it 'has header indicating build status' do
- is_expected.to have_header 'X-GitLab-Build-Status', 'success'
- end
-
- it 'has the correct subject' do
- is_expected.to have_subject /Build success for/
- end
- end
-
- describe 'build fail' do
- subject { Notify.build_fail_email(build.id, 'wow@example.com') }
- before { build.drop }
-
- it_behaves_like 'build email'
- it_behaves_like 'an email with X-GitLab headers containing build details'
- it_behaves_like 'an email with X-GitLab headers containing project details'
-
- it 'has header indicating build status' do
- is_expected.to have_header 'X-GitLab-Build-Status', 'failed'
- end
-
- it 'has the correct subject' do
- is_expected.to have_subject /Build failed for/
- end
- end
- end
-end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index e822d7eb348..6ee91576676 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -63,7 +63,7 @@ describe Notify do
end
it 'contains a link to note author' do
- is_expected.to have_body_text issue.author_name
+ is_expected.to have_html_escaped_body_text issue.author_name
is_expected.to have_body_text 'wrote:'
end
end
@@ -75,7 +75,7 @@ describe Notify do
it_behaves_like 'it should show Gmail Actions View Issue link'
it 'contains the description' do
- is_expected.to have_body_text issue_with_description.description
+ is_expected.to have_html_escaped_body_text issue_with_description.description
end
end
@@ -100,11 +100,11 @@ describe Notify do
end
it 'contains the name of the previous assignee' do
- is_expected.to have_body_text previous_assignee.name
+ is_expected.to have_html_escaped_body_text previous_assignee.name
end
it 'contains the name of the new assignee' do
- is_expected.to have_body_text assignee.name
+ is_expected.to have_html_escaped_body_text assignee.name
end
it 'contains a link to the issue' do
@@ -167,7 +167,7 @@ describe Notify do
end
it 'contains the user name' do
- is_expected.to have_body_text current_user.name
+ is_expected.to have_html_escaped_body_text current_user.name
end
it 'contains a link to the issue' do
@@ -242,7 +242,7 @@ describe Notify do
end
it 'contains a link to note author' do
- is_expected.to have_body_text merge_request.author_name
+ is_expected.to have_html_escaped_body_text merge_request.author_name
is_expected.to have_body_text 'wrote:'
end
end
@@ -255,7 +255,7 @@ describe Notify do
it_behaves_like "an unsubscribeable thread"
it 'contains the description' do
- is_expected.to have_body_text merge_request_with_description.description
+ is_expected.to have_html_escaped_body_text merge_request_with_description.description
end
end
@@ -280,11 +280,11 @@ describe Notify do
end
it 'contains the name of the previous assignee' do
- is_expected.to have_body_text previous_assignee.name
+ is_expected.to have_html_escaped_body_text previous_assignee.name
end
it 'contains the name of the new assignee' do
- is_expected.to have_body_text assignee.name
+ is_expected.to have_html_escaped_body_text assignee.name
end
it 'contains a link to the merge request' do
@@ -347,7 +347,7 @@ describe Notify do
end
it 'contains the user name' do
- is_expected.to have_body_text current_user.name
+ is_expected.to have_html_escaped_body_text current_user.name
end
it 'contains a link to the merge request' do
@@ -400,7 +400,7 @@ describe Notify do
end
it 'contains name of project' do
- is_expected.to have_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
end
it 'contains new user role' do
@@ -433,7 +433,7 @@ describe Notify do
expect(to_emails[0].address).to eq(project.members.owners_and_masters.first.user.notification_email)
is_expected.to have_subject "Request to join the #{project.name_with_namespace} project"
- is_expected.to have_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
is_expected.to have_body_text namespace_project_project_members_url(project.namespace, project)
is_expected.to have_body_text project_member.human_access
end
@@ -460,7 +460,7 @@ describe Notify do
expect(to_emails[0].address).to eq(group.members.owners_and_masters.first.user.notification_email)
is_expected.to have_subject "Request to join the #{project.name_with_namespace} project"
- is_expected.to have_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
is_expected.to have_body_text namespace_project_project_members_url(project.namespace, project)
is_expected.to have_body_text project_member.human_access
end
@@ -482,13 +482,14 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{project.name_with_namespace} project was denied"
- is_expected.to have_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
is_expected.to have_body_text project.web_url
end
end
describe 'project access changed' do
- let(:project) { create(:empty_project, :public, :access_requestable) }
+ let(:owner) { create(:user, name: "Chang O'Keefe") }
+ let(:project) { create(:empty_project, :public, :access_requestable, namespace: owner.namespace) }
let(:user) { create(:user) }
let(:project_member) { create(:project_member, project: project, user: user) }
subject { Notify.member_access_granted_email('project', project_member.id) }
@@ -499,7 +500,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{project.name_with_namespace} project was granted"
- is_expected.to have_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project_member.human_access
end
@@ -530,7 +531,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Invitation to join the #{project.name_with_namespace} project"
- is_expected.to have_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project_member.human_access
is_expected.to have_body_text project_member.invite_token
@@ -555,10 +556,10 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation accepted'
- is_expected.to have_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project_member.invite_email
- is_expected.to have_body_text invited_user.name
+ is_expected.to have_html_escaped_body_text invited_user.name
end
end
@@ -579,7 +580,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation declined'
- is_expected.to have_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.name_with_namespace
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project_member.invite_email
end
@@ -607,7 +608,7 @@ describe Notify do
end
it 'contains the message from the note' do
- is_expected.to have_body_text note.note
+ is_expected.to have_html_escaped_body_text note.note
end
it 'does not contain note author' do
@@ -620,7 +621,7 @@ describe Notify do
end
it 'contains a link to note author' do
- is_expected.to have_body_text note.author_name
+ is_expected.to have_html_escaped_body_text note.author_name
is_expected.to have_body_text 'wrote:'
end
end
@@ -727,7 +728,7 @@ describe Notify do
end
it 'contains the message from the note' do
- is_expected.to have_body_text note.note
+ is_expected.to have_html_escaped_body_text note.note
end
it 'does not contain note author' do
@@ -740,7 +741,7 @@ describe Notify do
end
it 'contains a link to note author' do
- is_expected.to have_body_text note.author_name
+ is_expected.to have_html_escaped_body_text note.author_name
is_expected.to have_body_text 'wrote:'
end
end
@@ -786,7 +787,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Request to join the #{group.name} group"
- is_expected.to have_body_text group.name
+ is_expected.to have_html_escaped_body_text group.name
is_expected.to have_body_text group_group_members_url(group)
is_expected.to have_body_text group_member.human_access
end
@@ -807,7 +808,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{group.name} group was denied"
- is_expected.to have_body_text group.name
+ is_expected.to have_html_escaped_body_text group.name
is_expected.to have_body_text group.web_url
end
end
@@ -825,7 +826,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{group.name} group was granted"
- is_expected.to have_body_text group.name
+ is_expected.to have_html_escaped_body_text group.name
is_expected.to have_body_text group.web_url
is_expected.to have_body_text group_member.human_access
end
@@ -856,7 +857,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject "Invitation to join the #{group.name} group"
- is_expected.to have_body_text group.name
+ is_expected.to have_html_escaped_body_text group.name
is_expected.to have_body_text group.web_url
is_expected.to have_body_text group_member.human_access
is_expected.to have_body_text group_member.invite_token
@@ -881,10 +882,10 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation accepted'
- is_expected.to have_body_text group.name
+ is_expected.to have_html_escaped_body_text group.name
is_expected.to have_body_text group.web_url
is_expected.to have_body_text group_member.invite_email
- is_expected.to have_body_text invited_user.name
+ is_expected.to have_html_escaped_body_text invited_user.name
end
end
@@ -905,7 +906,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation declined'
- is_expected.to have_body_text group.name
+ is_expected.to have_html_escaped_body_text group.name
is_expected.to have_body_text group.web_url
is_expected.to have_body_text group_member.invite_email
end
diff --git a/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb b/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb
new file mode 100644
index 00000000000..57eb03e3c80
--- /dev/null
+++ b/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170301205640_migrate_build_events_to_pipeline_events.rb')
+
+# This migration uses multiple threads, and thus different transactions. This
+# means data created in this spec may not be visible to some threads. To work
+# around this we use the TRUNCATE cleaning strategy.
+describe MigrateBuildEventsToPipelineEvents, truncate: true do
+ let(:migration) { described_class.new }
+ let(:project_with_pipeline_service) { create(:empty_project) }
+ let(:project_with_build_service) { create(:empty_project) }
+
+ before do
+ ActiveRecord::Base.connection.execute <<-SQL
+ INSERT INTO services (properties, build_events, pipeline_events, type)
+ VALUES
+ ('{"notify_only_broken_builds":true}', true, false, 'SlackService')
+ , ('{"notify_only_broken_builds":true}', true, false, 'MattermostService')
+ , ('{"notify_only_broken_builds":true}', true, false, 'HipchatService')
+ ;
+ SQL
+
+ ActiveRecord::Base.connection.execute <<-SQL
+ INSERT INTO services
+ (properties, build_events, pipeline_events, type, project_id)
+ VALUES
+ ('{"notify_only_broken_builds":true}', true, false,
+ 'BuildsEmailService', #{project_with_pipeline_service.id})
+ , ('{"notify_only_broken_pipelines":true}', false, true,
+ 'PipelinesEmailService', #{project_with_pipeline_service.id})
+ , ('{"notify_only_broken_builds":true}', true, false,
+ 'BuildsEmailService', #{project_with_build_service.id})
+ ;
+ SQL
+ end
+
+ describe '#up' do
+ before do
+ silence_migration = Module.new do
+ # rubocop:disable Rails/Delegate
+ def execute(query)
+ connection.execute(query)
+ end
+ end
+
+ migration.extend(silence_migration)
+ migration.up
+ end
+
+ it 'migrates chat service properly' do
+ [SlackService, MattermostService, HipchatService].each do |service|
+ expect(service.count).to eq(1)
+
+ verify_service_record(service.first)
+ end
+ end
+
+ it 'migrates pipelines email service only if it has none before' do
+ Project.find_each do |project|
+ pipeline_service_count =
+ project.services.where(type: 'PipelinesEmailService').count
+
+ expect(pipeline_service_count).to eq(1)
+
+ verify_service_record(project.pipelines_email_service)
+ end
+ end
+
+ def verify_service_record(service)
+ expect(service.notify_only_broken_pipelines).to be(true)
+ expect(service.build_events).to be(false)
+ expect(service.pipeline_events).to be(true)
+ end
+ end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index fd6ea2d6722..8dbcf50ee0c 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -795,8 +795,8 @@ describe Ci::Build, :models do
describe '#merge_request' do
def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
- create(factory, source_project_id: pipeline.gl_project_id,
- target_project_id: pipeline.gl_project_id,
+ create(factory, source_project: pipeline.project,
+ target_project: pipeline.project,
source_branch: build.ref,
created_at: created_at)
end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index bee9f714849..048d25869bc 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -6,7 +6,7 @@ describe Ci::Variable, models: true do
let(:secret_value) { 'secret' }
it { is_expected.to validate_presence_of(:key) }
- it { is_expected.to validate_uniqueness_of(:key).scoped_to(:gl_project_id) }
+ it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id) }
it { is_expected.to validate_length_of(:key).is_at_most(255) }
it { is_expected.to allow_value('foo').for(:key) }
it { is_expected.not_to allow_value('foo bar').for(:key) }
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 4b449546a30..980a1b70ef5 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -388,4 +388,32 @@ eos
expect(described_class.valid_hash?('a' * 41)).to be false
end
end
+
+ describe '#raw_diffs' do
+ context 'Gitaly commit_raw_diffs feature enabled' do
+ before do
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:commit_raw_diffs).and_return(true)
+ end
+
+ context 'when a truthy deltas_only is not passed to args' do
+ it 'fetches diffs from Gitaly server' do
+ expect(Gitlab::GitalyClient::Commit).to receive(:diff_from_parent).
+ with(commit)
+
+ commit.raw_diffs
+ end
+ end
+
+ context 'when a truthy deltas_only is passed to args' do
+ it 'fetches diffs using Rugged' do
+ opts = { deltas_only: true }
+
+ expect(Gitlab::GitalyClient::Commit).not_to receive(:diff_from_parent)
+ expect(commit.raw).to receive(:diffs).with(opts)
+
+ commit.raw_diffs(opts)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 31ae0dce140..9574796a945 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -278,6 +278,16 @@ describe Issue, "Issuable" do
end
end
+ context 'issue has labels' do
+ let(:labels) { [create(:label), create(:label)] }
+
+ before { issue.update_attribute(:labels, labels)}
+
+ it 'includes labels in the hook data' do
+ expect(data[:labels]).to eq(labels.map(&:hook_attrs))
+ end
+ end
+
include_examples 'project hook data'
include_examples 'deprecated repository hook data'
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 9ffcb88bafd..73977d031f9 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -37,6 +37,30 @@ describe Issue, models: true do
end
end
+ describe '#closed_at' do
+ after do
+ Timecop.return
+ end
+
+ let!(:now) { Timecop.freeze(Time.now) }
+
+ it 'sets closed_at to Time.now when issue is closed' do
+ issue = create(:issue, state: 'opened')
+
+ issue.close
+
+ expect(issue.closed_at).to eq(now)
+ end
+
+ it 'sets closed_at to nil when issue is reopened' do
+ issue = create(:issue, state: 'closed')
+
+ issue.reopen
+
+ expect(issue.closed_at).to be_nil
+ end
+ end
+
describe '#to_reference' do
let(:namespace) { build(:namespace, path: 'sample-namespace') }
let(:project) { build(:empty_project, name: 'sample-project', namespace: namespace) }
diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb
deleted file mode 100644
index 0194f9e2563..00000000000
--- a/spec/models/project_services/builds_email_service_spec.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-require 'spec_helper'
-
-describe BuildsEmailService do
- let(:data) do
- Gitlab::DataBuilder::Build.build(create(:ci_build))
- end
-
- describe 'Validations' do
- context 'when service is active' do
- before { subject.active = true }
-
- it { is_expected.to validate_presence_of(:recipients) }
-
- context 'when pusher is added' do
- before { subject.add_pusher = true }
-
- it { is_expected.not_to validate_presence_of(:recipients) }
- end
- end
-
- context 'when service is inactive' do
- before { subject.active = false }
-
- it { is_expected.not_to validate_presence_of(:recipients) }
- end
- end
-
- describe '#test_data' do
- let(:build) { create(:ci_build) }
- let(:project) { build.project }
- let(:user) { create(:user) }
-
- before { project.team << [user, :developer] }
-
- it 'builds test data' do
- data = subject.test_data(project)
-
- expect(data[:object_kind]).to eq("build")
- end
- end
-
- describe '#test' do
- it 'sends email' do
- data = Gitlab::DataBuilder::Build.build(create(:ci_build))
- subject.recipients = 'test@gitlab.com'
-
- expect(BuildEmailWorker).to receive(:perform_async)
-
- subject.test(data)
- end
-
- context 'notify only failed builds is true' do
- it 'sends email' do
- data = Gitlab::DataBuilder::Build.build(create(:ci_build))
- data[:build_status] = "success"
- subject.recipients = 'test@gitlab.com'
-
- expect(subject).not_to receive(:notify_only_broken_builds)
- expect(BuildEmailWorker).to receive(:perform_async)
-
- subject.test(data)
- end
- end
- end
-
- describe '#execute' do
- it 'sends email' do
- subject.recipients = 'test@gitlab.com'
- data[:build_status] = 'failed'
-
- expect(BuildEmailWorker).to receive(:perform_async)
-
- subject.execute(data)
- end
-
- it 'does not send email with succeeded build and notify_only_broken_builds on' do
- expect(subject).to receive(:notify_only_broken_builds).and_return(true)
- data[:build_status] = 'success'
-
- expect(BuildEmailWorker).not_to receive(:perform_async)
-
- subject.execute(data)
- end
-
- it 'does not send email with failed build and build_allow_failure is true' do
- data[:build_status] = 'failed'
- data[:build_allow_failure] = true
-
- expect(BuildEmailWorker).not_to receive(:perform_async)
-
- subject.execute(data)
- end
-
- it 'does not send email with unknown build status' do
- data[:build_status] = 'foo'
-
- expect(BuildEmailWorker).not_to receive(:perform_async)
-
- subject.execute(data)
- end
-
- it 'does not send email when recipients list is empty' do
- subject.recipients = ' ,, '
- data[:build_status] = 'failed'
-
- expect(BuildEmailWorker).not_to receive(:perform_async)
-
- subject.execute(data)
- end
- end
-end
diff --git a/spec/models/project_services/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb
deleted file mode 100644
index 3bd7ec18ae0..00000000000
--- a/spec/models/project_services/chat_message/build_message_spec.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-require 'spec_helper'
-
-describe ChatMessage::BuildMessage do
- subject { described_class.new(args) }
-
- let(:args) do
- {
- sha: '97de212e80737a608d939f648d959671fb0a0142',
- ref: 'develop',
- tag: false,
-
- project_name: 'project_name',
- project_url: 'http://example.gitlab.com',
- build_id: 1,
- build_name: build_name,
- build_stage: stage,
-
- commit: {
- status: status,
- author_name: 'hacker',
- author_url: 'http://example.gitlab.com/hacker',
- duration: duration,
- },
- }
- end
-
- let(:message) { build_message }
- let(:stage) { 'test' }
- let(:status) { 'success' }
- let(:build_name) { 'rspec' }
- let(:duration) { 10 }
-
- context 'build succeeded' do
- let(:status) { 'success' }
- let(:color) { 'good' }
- let(:message) { build_message('passed') }
-
- it 'returns a message with information about succeeded build' do
- expect(subject.pretext).to be_empty
- expect(subject.fallback).to eq(message)
- expect(subject.attachments).to eq([text: message, color: color])
- end
- end
-
- context 'build failed' do
- let(:status) { 'failed' }
- let(:color) { 'danger' }
-
- it 'returns a message with information about failed build' do
- expect(subject.pretext).to be_empty
- expect(subject.fallback).to eq(message)
- expect(subject.attachments).to eq([text: message, color: color])
- end
- end
-
- it 'returns a message with information on build' do
- expect(subject.fallback).to include("on build <http://example.gitlab.com/builds/1|#{build_name}>")
- end
-
- it 'returns a message with stage name' do
- expect(subject.fallback).to include("of stage #{stage}")
- end
-
- it 'returns a message with link to author' do
- expect(subject.fallback).to include("by <http://example.gitlab.com/hacker|hacker>")
- end
-
- def build_message(status_text = status, stage_text = stage, build_text = build_name)
- "<http://example.gitlab.com|project_name>:" \
- " Commit <http://example.gitlab.com/commit/" \
- "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \
- " of <http://example.gitlab.com/commits/develop|develop> branch" \
- " by <http://example.gitlab.com/hacker|hacker> #{status_text}" \
- " on build <http://example.gitlab.com/builds/1|#{build_text}>" \
- " of stage #{stage_text} in #{duration} #{'second'.pluralize(duration)}"
- end
-end
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index bf422ac7ce1..1200ae7eb22 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -280,13 +280,14 @@ describe HipchatService, models: true do
end
end
- context 'build events' do
- let(:pipeline) { create(:ci_empty_pipeline) }
- let(:build) { create(:ci_build, pipeline: pipeline) }
- let(:data) { Gitlab::DataBuilder::Build.build(build.reload) }
+ context 'pipeline events' do
+ let(:pipeline) { create(:ci_empty_pipeline, user: create(:user)) }
+ let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
context 'for failed' do
- before { build.drop }
+ before do
+ pipeline.drop
+ end
it "calls Hipchat API" do
hipchat.execute(data)
@@ -295,35 +296,36 @@ describe HipchatService, models: true do
end
it "creates a build message" do
- message = hipchat.send(:create_build_message, data)
+ message = hipchat.__send__(:create_pipeline_message, data)
project_url = project.web_url
project_name = project.name_with_namespace.gsub(/\s/, '')
- sha = data[:sha]
- ref = data[:ref]
- ref_type = data[:tag] ? 'tag' : 'branch'
- duration = data[:commit][:duration]
+ pipeline_attributes = data[:object_attributes]
+ ref = pipeline_attributes[:ref]
+ ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+ duration = pipeline_attributes[:duration]
+ user_name = data[:user][:name]
expect(message).to eq("<a href=\"#{project_url}\">#{project_name}</a>: " \
- "Commit <a href=\"#{project_url}/commit/#{sha}/builds\">#{Commit.truncate_sha(sha)}</a> " \
+ "Pipeline <a href=\"#{project_url}/pipelines/#{pipeline.id}\">##{pipeline.id}</a> " \
"of <a href=\"#{project_url}/commits/#{ref}\">#{ref}</a> #{ref_type} " \
- "by #{data[:commit][:author_name]} failed in #{duration} second(s)")
+ "by #{user_name} failed in #{duration} second(s)")
end
end
context 'for succeeded' do
before do
- build.success
+ pipeline.succeed
end
it "calls Hipchat API" do
- hipchat.notify_only_broken_builds = false
+ hipchat.notify_only_broken_pipelines = false
hipchat.execute(data)
expect(WebMock).to have_requested(:post, api_url).once
end
it "notifies only broken" do
- hipchat.notify_only_broken_builds = true
+ hipchat.notify_only_broken_pipelines = true
hipchat.execute(data)
expect(WebMock).not_to have_requested(:post, api_url).once
end
@@ -349,17 +351,19 @@ describe HipchatService, models: true do
context 'with a successful build' do
it 'uses the green color' do
- build_data = { object_kind: 'build', commit: { status: 'success' } }
+ data = { object_kind: 'pipeline',
+ object_attributes: { status: 'success' } }
- expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'green' })
+ expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'green' })
end
end
context 'with a failed build' do
it 'uses the red color' do
- build_data = { object_kind: 'build', commit: { status: 'failed' } }
+ data = { object_kind: 'pipeline',
+ object_attributes: { status: 'failed' } }
- expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'red' })
+ expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'red' })
end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index ff1defcd32d..618ce2b6d53 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -29,8 +29,7 @@ describe Project, models: true do
it { is_expected.to have_one(:campfire_service).dependent(:destroy) }
it { is_expected.to have_one(:drone_ci_service).dependent(:destroy) }
it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) }
- it { is_expected.to have_one(:builds_email_service).dependent(:destroy) }
- it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) }
+ it { is_expected.to have_one(:pipelines_email_service).dependent(:destroy) }
it { is_expected.to have_one(:irker_service).dependent(:destroy) }
it { is_expected.to have_one(:pivotaltracker_service).dependent(:destroy) }
it { is_expected.to have_one(:hipchat_service).dependent(:destroy) }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 9da4140f3ce..90378179e32 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -699,7 +699,9 @@ describe User, models: true do
let!(:user) { create(:user, name: 'John Doe', username: 'john.doe', email: 'john.doe@example.com' ) }
let!(:another_user) { create(:user, name: 'Albert Smith', username: 'albert.smith', email: 'albert.smith@example.com' ) }
- let!(:email) { create(:email, user: another_user) }
+ let!(:email) do
+ create(:email, user: another_user, email: 'alias@example.com')
+ end
it 'returns users with a matching name' do
expect(search_with_secondary_emails(user.name)).to eq([user])
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index ab5a7e4d3de..a70f7beaae0 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -5,77 +5,146 @@ describe API::Branches, api: true do
include ApiHelpers
let(:user) { create(:user) }
- let(:user2) { create(:user) }
let!(:project) { create(:project, :repository, creator: user) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
- let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
+ let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } }
let!(:branch_name) { 'feature' }
let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
- let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") }
+ let(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master")[:branch] }
describe "GET /projects/:id/repository/branches" do
- it "returns an array of project branches" do
- project.repository.expire_all_method_caches
+ let(:route) { "/projects/#{project.id}/repository/branches" }
- get api("/projects/#{project.id}/repository/branches", user), per_page: 100
+ shared_examples_for 'repository branches' do
+ it 'returns the repository branches' do
+ get api(route, current_user), per_page: 100
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- branch_names = json_response.map { |x| x['name'] }
- expect(branch_names).to match_array(project.repository.branch_names)
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ branch_names = json_response.map { |x| x['name'] }
+ expect(branch_names).to match_array(project.repository.branch_names)
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, current_user) }
+ end
+ end
end
- end
- describe "GET /projects/:id/repository/branches/:branch" do
- it "returns the branch information for a single branch" do
- get api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
- expect(response).to have_http_status(200)
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository branches' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
+ end
- expect(json_response['name']).to eq(branch_name)
- json_commit = json_response['commit']
- expect(json_commit['id']).to eq(branch_sha)
- expect(json_commit).to have_key('short_id')
- expect(json_commit).to have_key('title')
- expect(json_commit).to have_key('message')
- expect(json_commit).to have_key('author_name')
- expect(json_commit).to have_key('author_email')
- expect(json_commit).to have_key('authored_date')
- expect(json_commit).to have_key('committer_name')
- expect(json_commit).to have_key('committer_email')
- expect(json_commit).to have_key('committed_date')
- expect(json_commit).to have_key('parent_ids')
- expect(json_response['merged']).to eq(false)
- expect(json_response['protected']).to eq(false)
- expect(json_response['developers_can_push']).to eq(false)
- expect(json_response['developers_can_merge']).to eq(false)
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
end
- it "returns the branch information for a single branch with dots in the name" do
- get api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+ context 'when authenticated', 'as a developer' do
+ it_behaves_like 'repository branches' do
+ let(:current_user) { user }
+ end
+ end
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq("with.1.2.3")
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, guest) }
+ end
end
+ end
+
+ describe "GET /projects/:id/repository/branches/:branch" do
+ let(:route) { "/projects/#{project.id}/repository/branches/#{branch_name}" }
- context 'on a merged branch' do
- it "returns the branch information for a single branch" do
- get api("/projects/#{project.id}/repository/branches/merge-test", user)
+ shared_examples_for 'repository branch' do |merged: false|
+ it 'returns the repository branch' do
+ get api(route, current_user)
expect(response).to have_http_status(200)
- expect(json_response['name']).to eq('merge-test')
- expect(json_response['merged']).to eq(true)
+ expect(json_response['name']).to eq(branch_name)
+ expect(json_response['merged']).to eq(merged)
+ expect(json_response['protected']).to eq(false)
+ expect(json_response['developers_can_push']).to eq(false)
+ expect(json_response['developers_can_merge']).to eq(false)
+
+ json_commit = json_response['commit']
+ expect(json_commit['id']).to eq(branch_sha)
+ expect(json_commit).to have_key('short_id')
+ expect(json_commit).to have_key('title')
+ expect(json_commit).to have_key('message')
+ expect(json_commit).to have_key('author_name')
+ expect(json_commit).to have_key('author_email')
+ expect(json_commit).to have_key('authored_date')
+ expect(json_commit).to have_key('committer_name')
+ expect(json_commit).to have_key('committer_email')
+ expect(json_commit).to have_key('committed_date')
+ expect(json_commit).to have_key('parent_ids')
+ end
+
+ context 'when branch does not exist' do
+ let(:branch_name) { 'unknown' }
+
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, current_user) }
+ let(:message) { '404 Branch Not Found' }
+ end
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, current_user) }
+ end
end
end
- it "returns a 403 error if guest" do
- get api("/projects/#{project.id}/repository/branches", user2)
- expect(response).to have_http_status(403)
+ context 'when unauthenticated', 'and project is public' do
+ it_behaves_like 'repository branch' do
+ let(:project) { create(:project, :public, :repository) }
+ let(:current_user) { nil }
+ end
end
- it "returns a 404 error if branch is not available" do
- get api("/projects/#{project.id}/repository/branches/unknown", user)
- expect(response).to have_http_status(404)
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+
+ context 'when authenticated', 'as a developer' do
+ let(:current_user) { user }
+ it_behaves_like 'repository branch'
+
+ context 'when branch contains a dot' do
+ let(:branch_name) { branch_with_dot.name }
+ let(:branch_sha) { project.commit('master').sha }
+
+ it_behaves_like 'repository branch'
+ end
+
+ context 'when branch is merged' do
+ let(:branch_name) { 'merge-test' }
+ let(:branch_sha) { project.commit('merge-test').sha }
+
+ it_behaves_like 'repository branch', merged: true
+ end
+ end
+
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, guest) }
+ end
end
end
@@ -93,10 +162,10 @@ describe API::Branches, api: true do
end
it "protects a single branch with dots in the name" do
- put api("/projects/#{project.id}/repository/branches/with.1.2.3/protect", user)
+ put api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}/protect", user)
expect(response).to have_http_status(200)
- expect(json_response['name']).to eq("with.1.2.3")
+ expect(json_response['name']).to eq(branch_with_dot.name)
expect(json_response['protected']).to eq(true)
end
@@ -234,7 +303,7 @@ describe API::Branches, api: true do
end
it "returns a 403 error if guest" do
- put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2)
+ put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", guest)
expect(response).to have_http_status(403)
end
end
@@ -250,10 +319,10 @@ describe API::Branches, api: true do
end
it "update branches with dots in branch name" do
- put api("/projects/#{project.id}/repository/branches/with.1.2.3/unprotect", user)
+ put api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}/unprotect", user)
expect(response).to have_http_status(200)
- expect(json_response['name']).to eq("with.1.2.3")
+ expect(json_response['name']).to eq(branch_with_dot.name)
expect(json_response['protected']).to eq(false)
end
@@ -282,7 +351,7 @@ describe API::Branches, api: true do
end
it "denies for user without push access" do
- post api("/projects/#{project.id}/repository/branches", user2),
+ post api("/projects/#{project.id}/repository/branches", guest),
branch: branch_name,
ref: branch_sha
expect(response).to have_http_status(403)
@@ -330,7 +399,7 @@ describe API::Branches, api: true do
end
it "removes a branch with dots in the branch name" do
- delete api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+ delete api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}", user)
expect(response).to have_http_status(204)
end
@@ -367,7 +436,7 @@ describe API::Branches, api: true do
end
it 'returns a 403 error if guest' do
- delete api("/projects/#{project.id}/repository/merged_branches", user2)
+ delete api("/projects/#{project.id}/repository/merged_branches", guest)
expect(response).to have_http_status(403)
end
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index f18b8e98707..63ec00cdf04 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -397,16 +397,25 @@ describe API::Internal, api: true do
before do
project.team << [user, :developer]
- get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token
end
it 'returns link to create new merge request' do
+ get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token
+
expect(json_response).to match [{
"branch_name" => "new_branch",
"url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
"new_merge_request" => true
}]
end
+
+ it 'returns empty array if printing_merge_request_link_enabled is false' do
+ project.update!(printing_merge_request_link_enabled: false)
+
+ get api("/internal/merge_request_urls?project=#{repo_name}&changes=#{changes}"), secret_token: secret_token
+
+ expect(json_response).to eq([])
+ end
end
describe 'POST /notify_post_receive' do
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 442b2df1952..044b989e5ba 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -152,6 +152,34 @@ describe API::Runner do
end
end
end
+
+ describe 'POST /api/v4/runners/verify' do
+ let(:runner) { create(:ci_runner) }
+
+ context 'when no token is provided' do
+ it 'returns 400 error' do
+ post api('/runners/verify')
+
+ expect(response).to have_http_status :bad_request
+ end
+ end
+
+ context 'when invalid token is provided' do
+ it 'returns 403 error' do
+ post api('/runners/verify'), token: 'invalid-token'
+
+ expect(response).to have_http_status 403
+ end
+ end
+
+ context 'when valid token is provided' do
+ it 'verifies Runner credentials' do
+ post api('/runners/verify'), token: runner.token
+
+ expect(response).to have_http_status 200
+ end
+ end
+ end
end
describe '/api/v4/jobs' do
@@ -220,18 +248,6 @@ describe API::Runner do
it { expect(response).to have_http_status(204) }
end
end
-
- context "when runner doesn't send version in User-Agent" do
- let(:user_agent) { 'Go-http-client/1.1' }
-
- it { expect(response).to have_http_status(404) }
- end
-
- context "when runner doesn't have a User-Agent" do
- let(:user_agent) { nil }
-
- it { expect(response).to have_http_status(404) }
- end
end
context 'when no token is provided' do
@@ -254,10 +270,10 @@ describe API::Runner do
context 'when Runner is not active' do
let(:runner) { create(:ci_runner, :inactive) }
- it 'returns 404 error' do
+ it 'returns 204 error' do
request_job
- expect(response).to have_http_status 404
+ expect(response).to have_http_status 204
end
end
@@ -401,9 +417,39 @@ describe API::Runner do
end
context 'when project and pipeline have multiple jobs' do
+ let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
- before { job.success }
+ before do
+ job.success
+ job2.success
+ end
+
+ it 'returns dependent jobs' do
+ request_job
+
+ expect(response).to have_http_status(201)
+ expect(json_response['id']).to eq(test_job.id)
+ expect(json_response['dependencies'].count).to eq(2)
+ expect(json_response['dependencies']).to include({ 'id' => job.id, 'name' => job.name, 'token' => job.token },
+ { 'id' => job2.id, 'name' => job2.name, 'token' => job2.token })
+ end
+ end
+
+ context 'when explicit dependencies are defined' do
+ let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+ let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
+ let!(:test_job) do
+ create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy',
+ stage: 'deploy', stage_idx: 1,
+ options: { dependencies: [job2.name] })
+ end
+
+ before do
+ job.success
+ job2.success
+ end
it 'returns dependent jobs' do
request_job
@@ -411,7 +457,7 @@ describe API::Runner do
expect(response).to have_http_status(201)
expect(json_response['id']).to eq(test_job.id)
expect(json_response['dependencies'].count).to eq(1)
- expect(json_response['dependencies'][0]).to include('id' => job.id, 'name' => 'spinach')
+ expect(json_response['dependencies'][0]).to include('id' => job2.id, 'name' => job2.name, 'token' => job2.token)
end
end
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 424c02932ab..d93a734f5b6 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -59,14 +59,6 @@ describe API::Triggers do
expect(pipeline.builds.size).to eq(5)
end
- it 'creates builds on webhook from other gitlab repository and branch' do
- expect do
- post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
- end.to change(project.builds, :count).by(5)
-
- expect(response).to have_http_status(201)
- end
-
it 'returns bad request with no pipeline created if there\'s no commit for that ref' do
post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'other-branch')
@@ -101,6 +93,28 @@ describe API::Triggers do
end
end
end
+
+ context 'when triggering a pipeline from a trigger token' do
+ it 'creates builds from the ref given in the URL, not in the body' do
+ expect do
+ post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+ end.to change(project.builds, :count).by(5)
+
+ expect(response).to have_http_status(201)
+ end
+
+ context 'when ref contains a dot' do
+ it 'creates builds from the ref given in the URL, not in the body' do
+ project.repository.create_file(user, '.gitlab/gitlabhq/new_feature.md', 'something valid', message: 'new_feature', branch_name: 'v.1-branch')
+
+ expect do
+ post api("/projects/#{project.id}/ref/v.1-branch/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+ end.to change(project.builds, :count).by(4)
+
+ expect(response).to have_http_status(201)
+ end
+ end
+ end
end
describe 'GET /projects/:id/triggers' do
diff --git a/spec/requests/api/v3/branches_spec.rb b/spec/requests/api/v3/branches_spec.rb
index e4cedf98e64..5dcd4f21f4e 100644
--- a/spec/requests/api/v3/branches_spec.rb
+++ b/spec/requests/api/v3/branches_spec.rb
@@ -10,6 +10,7 @@ describe API::V3::Branches, api: true do
let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:branch_name) { 'feature' }
+ let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") }
describe "GET /projects/:id/repository/branches" do
@@ -80,4 +81,55 @@ describe API::V3::Branches, api: true do
expect(response).to have_http_status(403)
end
end
+
+ describe "POST /projects/:id/repository/branches" do
+ it "creates a new branch" do
+ post v3_api("/projects/#{project.id}/repository/branches", user),
+ branch_name: 'feature1',
+ ref: branch_sha
+
+ expect(response).to have_http_status(201)
+
+ expect(json_response['name']).to eq('feature1')
+ expect(json_response['commit']['id']).to eq(branch_sha)
+ end
+
+ it "denies for user without push access" do
+ post v3_api("/projects/#{project.id}/repository/branches", user2),
+ branch_name: branch_name,
+ ref: branch_sha
+ expect(response).to have_http_status(403)
+ end
+
+ it 'returns 400 if branch name is invalid' do
+ post v3_api("/projects/#{project.id}/repository/branches", user),
+ branch_name: 'new design',
+ ref: branch_sha
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('Branch name is invalid')
+ end
+
+ it 'returns 400 if branch already exists' do
+ post v3_api("/projects/#{project.id}/repository/branches", user),
+ branch_name: 'new_design1',
+ ref: branch_sha
+ expect(response).to have_http_status(201)
+
+ post v3_api("/projects/#{project.id}/repository/branches", user),
+ branch_name: 'new_design1',
+ ref: branch_sha
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('Branch already exists')
+ end
+
+ it 'returns 400 if ref name is invalid' do
+ post v3_api("/projects/#{project.id}/repository/branches", user),
+ branch_name: 'new_design3',
+ ref: 'foo'
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to eq('Invalid reference name')
+ end
+ end
end
diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb
index 4819269d69f..9233e9621bf 100644
--- a/spec/requests/api/v3/triggers_spec.rb
+++ b/spec/requests/api/v3/triggers_spec.rb
@@ -51,13 +51,6 @@ describe API::V3::Triggers do
expect(pipeline.builds.size).to eq(5)
end
- it 'creates builds on webhook from other gitlab repository and branch' do
- expect do
- post v3_api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
- end.to change(project.builds, :count).by(5)
- expect(response).to have_http_status(201)
- end
-
it 'returns bad request with no builds created if there\'s no commit for that ref' do
post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch')
expect(response).to have_http_status(400)
@@ -89,6 +82,27 @@ describe API::V3::Triggers do
end
end
end
+
+ context 'when triggering a pipeline from a trigger token' do
+ it 'creates builds from the ref given in the URL, not in the body' do
+ expect do
+ post v3_api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+ end.to change(project.builds, :count).by(5)
+ expect(response).to have_http_status(201)
+ end
+
+ context 'when ref contains a dot' do
+ it 'creates builds from the ref given in the URL, not in the body' do
+ project.repository.create_file(user, '.gitlab/gitlabhq/new_feature.md', 'something valid', message: 'new_feature', branch_name: 'v.1-branch')
+
+ expect do
+ post v3_api("/projects/#{project.id}/ref/v.1-branch/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+ end.to change(project.builds, :count).by(4)
+
+ expect(response).to have_http_status(201)
+ end
+ end
+ end
end
describe 'GET /projects/:id/triggers' do
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 65af4e13118..8567817147b 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -19,7 +19,7 @@ describe Ci::RetryBuildService, :services do
erased_at].freeze
IGNORE_ACCESSORS =
- %i[type lock_version target_url gl_project_id deploy job_id base_tags
+ %i[type lock_version target_url base_tags
commit_id deployments erased_by_id last_deployment project_id
runner_id tag_taggings taggings tags trigger_request_id
user_id].freeze
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 0768f644036..adfa75a524f 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -49,10 +49,13 @@ describe MergeRequests::BuildService, services: true do
let(:commits) { Commit.decorate([commit_1], project) }
it 'creates compare object with target branch as default branch' do
- expect(merge_request.can_be_created).to eq(false)
expect(merge_request.compare).to be_present
expect(merge_request.target_branch).to eq(project.default_branch)
end
+
+ it 'allows the merge request to be created' do
+ expect(merge_request.can_be_created).to eq(true)
+ end
end
context 'same source and target branch' do
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
index 08829e4be70..b7a05907208 100644
--- a/spec/services/merge_requests/get_urls_service_spec.rb
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -130,5 +130,15 @@ describe MergeRequests::GetUrlsService do
}])
end
end
+
+ context 'when printing_merge_request_link_enabled is false' do
+ it 'returns empty array' do
+ project.update!(printing_merge_request_link_enabled: false)
+
+ result = service.execute(existing_branch_changes)
+
+ expect(result).to eq([])
+ end
+ end
end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 82a4ec3f581..f7240969588 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -758,7 +758,7 @@ describe NotificationService, services: true do
update_custom_notification(:reopen_issue, @u_custom_global)
end
- it 'sends email to issue assignee and issue author' do
+ it 'sends email to issue notification recipients' do
notification.reopen_issue(issue, @u_disabled)
should_email(issue.assignee)
@@ -772,6 +772,7 @@ describe NotificationService, services: true do
should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
+ should_not_email(@u_disabled)
should_not_email(@u_lazy_participant)
end
@@ -781,6 +782,32 @@ describe NotificationService, services: true do
let(:notification_trigger) { notification.reopen_issue(issue, @u_disabled) }
end
end
+
+ describe '#issue_moved' do
+ let(:new_issue) { create(:issue) }
+
+ it 'sends email to issue notification recipients' do
+ notification.issue_moved(issue, new_issue, @u_disabled)
+
+ should_email(issue.assignee)
+ should_email(issue.author)
+ should_email(@u_watcher)
+ should_email(@u_guest_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
+ should_not_email(@u_lazy_participant)
+ end
+
+ it_behaves_like 'participating notifications' do
+ let(:participant) { create(:user, username: 'user-participant') }
+ let(:issuable) { issue }
+ let(:notification_trigger) { notification.issue_moved(issue, new_issue, @u_disabled) }
+ end
+ end
end
describe 'Merge Requests' do
@@ -1192,6 +1219,48 @@ describe NotificationService, services: true do
end
end
+ describe 'Pipelines' do
+ describe '#pipeline_finished' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { create(:user) }
+ let(:u_member) { create(:user) }
+ let(:u_other) { create(:user) }
+
+ let(:commit) { project.commit }
+ let(:pipeline) do
+ create(:ci_pipeline, :success,
+ project: project,
+ user: current_user,
+ ref: 'refs/heads/master',
+ sha: commit.id,
+ before_sha: '00000000')
+ end
+
+ before do
+ project.add_master(current_user)
+ project.add_master(u_member)
+ reset_delivered_emails!
+ end
+
+ context 'without custom recipients' do
+ it 'notifies the pipeline user' do
+ notification.pipeline_finished(pipeline)
+
+ should_only_email(current_user, kind: :bcc)
+ end
+ end
+
+ context 'with custom recipients' do
+ it 'notifies the custom recipients' do
+ users = [u_member, u_other]
+ notification.pipeline_finished(pipeline, users.map(&:notification_email))
+
+ should_only_email(*users, kind: :bcc)
+ end
+ end
+ end
+ end
+
def build_team(project)
@u_watcher = create_global_setting_for(create(:user), :watch)
@u_participating = create_global_setting_for(create(:user), :participating)
diff --git a/spec/support/matchers/email_matchers.rb b/spec/support/matchers/email_matchers.rb
new file mode 100644
index 00000000000..d9d59ec12ec
--- /dev/null
+++ b/spec/support/matchers/email_matchers.rb
@@ -0,0 +1,5 @@
+RSpec::Matchers.define :have_html_escaped_body_text do |expected|
+ match do |actual|
+ expect(actual).to have_body_text(ERB::Util.html_escape(expected))
+ end
+end
diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb
deleted file mode 100644
index 542e674c150..00000000000
--- a/spec/workers/build_email_worker_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-require 'spec_helper'
-
-describe BuildEmailWorker do
- include EmailHelpers
- include RepoHelpers
-
- let(:build) { create(:ci_build) }
- let(:user) { create(:user) }
- let(:data) { Gitlab::DataBuilder::Build.build(build) }
-
- subject { BuildEmailWorker.new }
-
- before do
- allow(build).to receive(:execute_hooks).and_return(false)
- build.success
- end
-
- describe "#perform" do
- it "sends mail" do
- subject.perform(build.id, [user.email], data.stringify_keys)
-
- email = ActionMailer::Base.deliveries.last
- expect(email.subject).to include('Build success for')
- expect(email.to).to eq([user.email])
- end
-
- it "gracefully handles an input SMTP error" do
- reset_delivered_emails!
- allow(Notify).to receive(:build_success_email).and_raise(Net::SMTPFatalError)
-
- subject.perform(build.id, [user.email], data.stringify_keys)
-
- expect(ActionMailer::Base.deliveries.count).to eq(0)
- end
- end
-end
diff --git a/yarn.lock b/yarn.lock
index 391b1c7eccf..2500ddc6f6b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -473,6 +473,13 @@ babel-plugin-transform-decorators@^6.22.0:
babel-template "^6.22.0"
babel-types "^6.22.0"
+babel-plugin-transform-define@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-define/-/babel-plugin-transform-define-1.2.0.tgz#f036bda05162f29a542e434f585da1ccf1e7ec6a"
+ dependencies:
+ lodash.get "4.4.2"
+ traverse "0.6.6"
+
babel-plugin-transform-es2015-arrow-functions@^6.22.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221"
@@ -549,17 +556,17 @@ babel-plugin-transform-es2015-literals@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-modules-amd@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.22.0.tgz#bf69cd34889a41c33d90dfb740e0091ccff52f21"
+babel-plugin-transform-es2015-modules-amd@^6.24.0:
+ version "6.24.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.0.tgz#a1911fb9b7ec7e05a43a63c5995007557bcf6a2e"
dependencies:
- babel-plugin-transform-es2015-modules-commonjs "^6.22.0"
+ babel-plugin-transform-es2015-modules-commonjs "^6.24.0"
babel-runtime "^6.22.0"
babel-template "^6.22.0"
-babel-plugin-transform-es2015-modules-commonjs@^6.22.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.23.0.tgz#cba7aa6379fb7ec99250e6d46de2973aaffa7b92"
+babel-plugin-transform-es2015-modules-commonjs@^6.24.0:
+ version "6.24.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.0.tgz#e921aefb72c2cc26cb03d107626156413222134f"
dependencies:
babel-plugin-transform-strict-mode "^6.22.0"
babel-runtime "^6.22.0"
@@ -574,11 +581,11 @@ babel-plugin-transform-es2015-modules-systemjs@^6.22.0:
babel-runtime "^6.22.0"
babel-template "^6.23.0"
-babel-plugin-transform-es2015-modules-umd@^6.22.0:
- version "6.23.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.23.0.tgz#8d284ae2e19ed8fe21d2b1b26d6e7e0fcd94f0f1"
+babel-plugin-transform-es2015-modules-umd@^6.24.0:
+ version "6.24.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.0.tgz#fd5fa63521cae8d273927c3958afd7c067733450"
dependencies:
- babel-plugin-transform-es2015-modules-amd "^6.22.0"
+ babel-plugin-transform-es2015-modules-amd "^6.24.0"
babel-runtime "^6.22.0"
babel-template "^6.23.0"
@@ -669,9 +676,9 @@ babel-plugin-transform-strict-mode@^6.22.0:
babel-runtime "^6.22.0"
babel-types "^6.22.0"
-babel-preset-es2015@^6.22.0:
- version "6.22.0"
- resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.22.0.tgz#af5a98ecb35eb8af764ad8a5a05eb36dc4386835"
+babel-preset-es2015@^6.24.0:
+ version "6.24.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.0.tgz#c162d68b1932696e036cd3110dc1ccd303d2673a"
dependencies:
babel-plugin-check-es2015-constants "^6.22.0"
babel-plugin-transform-es2015-arrow-functions "^6.22.0"
@@ -684,10 +691,10 @@ babel-preset-es2015@^6.22.0:
babel-plugin-transform-es2015-for-of "^6.22.0"
babel-plugin-transform-es2015-function-name "^6.22.0"
babel-plugin-transform-es2015-literals "^6.22.0"
- babel-plugin-transform-es2015-modules-amd "^6.22.0"
- babel-plugin-transform-es2015-modules-commonjs "^6.22.0"
+ babel-plugin-transform-es2015-modules-amd "^6.24.0"
+ babel-plugin-transform-es2015-modules-commonjs "^6.24.0"
babel-plugin-transform-es2015-modules-systemjs "^6.22.0"
- babel-plugin-transform-es2015-modules-umd "^6.22.0"
+ babel-plugin-transform-es2015-modules-umd "^6.24.0"
babel-plugin-transform-es2015-object-super "^6.22.0"
babel-plugin-transform-es2015-parameters "^6.22.0"
babel-plugin-transform-es2015-shorthand-properties "^6.22.0"
@@ -698,6 +705,27 @@ babel-preset-es2015@^6.22.0:
babel-plugin-transform-es2015-unicode-regex "^6.22.0"
babel-plugin-transform-regenerator "^6.22.0"
+babel-preset-es2016@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-es2016/-/babel-preset-es2016-6.22.0.tgz#b061aaa3983d40c9fbacfa3743b5df37f336156c"
+ dependencies:
+ babel-plugin-transform-exponentiation-operator "^6.22.0"
+
+babel-preset-es2017@^6.22.0:
+ version "6.22.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-es2017/-/babel-preset-es2017-6.22.0.tgz#de2f9da5a30c50d293fb54a0ba15d6ddc573f0f2"
+ dependencies:
+ babel-plugin-syntax-trailing-function-commas "^6.22.0"
+ babel-plugin-transform-async-to-generator "^6.22.0"
+
+babel-preset-latest@^6.24.0:
+ version "6.24.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-latest/-/babel-preset-latest-6.24.0.tgz#a68d20f509edcc5d7433a48dfaebf7e4f2cd4cb7"
+ dependencies:
+ babel-preset-es2015 "^6.24.0"
+ babel-preset-es2016 "^6.22.0"
+ babel-preset-es2017 "^6.22.0"
+
babel-preset-stage-2@^6.22.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.22.0.tgz#ccd565f19c245cade394b21216df704a73b27c07"
@@ -2900,6 +2928,10 @@ lodash.deburr@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-4.1.0.tgz#ddb1bbb3ef07458c0177ba07de14422cb033ff9b"
+lodash.get@4.4.2:
+ version "4.4.2"
+ resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
+
lodash.get@^3.7.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-3.7.0.tgz#3ce68ae2c91683b281cc5394128303cbf75e691f"
@@ -4271,6 +4303,10 @@ tough-cookie@~2.3.0:
dependencies:
punycode "^1.4.1"
+traverse@0.6.6:
+ version "0.6.6"
+ resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137"
+
trim-right@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"