summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock9
-rw-r--r--Gemfile.rails5.lock9
-rw-r--r--app/assets/images/cluster_app_logos/knative.pngbin0 -> 11259 bytes
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue142
-rw-r--r--app/assets/javascripts/boards/components/issue_due_date.vue90
-rw-r--r--app/assets/javascripts/boards/components/issue_time_estimate.vue48
-rw-r--r--app/assets/javascripts/boards/models/issue.js1
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js2
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue58
-rw-r--r--app/assets/javascripts/clusters/constants.js1
-rw-r--r--app/assets/javascripts/clusters/services/clusters_service.js1
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js13
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue1
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_gutter_content.vue6
-rw-r--r--app/assets/javascripts/diffs/components/diff_table_cell.vue10
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_table_row.vue6
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue10
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_table_row.vue6
-rw-r--r--app/assets/javascripts/diffs/constants.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js8
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue6
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js12
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue8
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue30
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue63
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue27
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue12
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue170
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue3
-rw-r--r--app/assets/javascripts/notes/components/toggle_replies_widget.vue94
-rw-r--r--app/assets/javascripts/users_select.js28
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue53
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue12
-rw-r--r--app/assets/stylesheets/framework/awards.scss5
-rw-r--r--app/assets/stylesheets/framework/buttons.scss42
-rw-r--r--app/assets/stylesheets/framework/common.scss7
-rw-r--r--app/assets/stylesheets/framework/files.scss1
-rw-r--r--app/assets/stylesheets/framework/selects.scss2
-rw-r--r--app/assets/stylesheets/framework/timeline.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss8
-rw-r--r--app/assets/stylesheets/pages/boards.scss197
-rw-r--r--app/assets/stylesheets/pages/diff.scss3
-rw-r--r--app/assets/stylesheets/pages/note_form.scss14
-rw-r--r--app/assets/stylesheets/pages/notes.scss293
-rw-r--r--app/controllers/chaos_controller.rb56
-rw-r--r--app/controllers/concerns/creates_commit.rb2
-rw-r--r--app/controllers/import/bitbucket_server_controller.rb24
-rw-r--r--app/controllers/projects/blob_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb6
-rw-r--r--app/helpers/compare_helper.rb2
-rw-r--r--app/helpers/merge_requests_helper.rb4
-rw-r--r--app/helpers/projects_helper.rb9
-rw-r--r--app/models/ci/build.rb26
-rw-r--r--app/models/clusters/applications/knative.rb51
-rw-r--r--app/models/clusters/cluster.rb7
-rw-r--r--app/models/clusters/platforms/kubernetes.rb6
-rw-r--r--app/models/diff_note.rb45
-rw-r--r--app/models/project_import_state.rb13
-rw-r--r--app/models/repository.rb12
-rw-r--r--app/serializers/issue_board_entity.rb1
-rw-r--r--app/services/clusters/applications/check_installation_progress_service.rb6
-rw-r--r--app/services/clusters/applications/create_service.rb3
-rw-r--r--app/services/clusters/applications/install_service.rb6
-rw-r--r--app/services/commits/commit_patch_service.rb61
-rw-r--r--app/services/commits/create_service.rb7
-rw-r--r--app/services/issuable_base_service.rb4
-rw-r--r--app/services/merge_requests/build_service.rb6
-rw-r--r--app/services/merge_requests/get_urls_service.rb4
-rw-r--r--app/services/merge_requests/reload_diffs_service.rb4
-rw-r--r--app/services/notes/base_service.rb15
-rw-r--r--app/services/notes/create_service.rb3
-rw-r--r--app/services/notes/destroy_service.rb4
-rw-r--r--app/services/quick_actions/interpret_service.rb4
-rw-r--r--app/services/submodules/update_service.rb38
-rw-r--r--app/views/clusters/clusters/show.html.haml1
-rw-r--r--app/views/import/bitbucket_server/status.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml7
-rw-r--r--app/views/shared/boards/_show.html.haml2
-rw-r--r--app/views/shared/empty_states/_labels.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml3
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml4
-rw-r--r--app/workers/email_receiver_worker.rb2
-rw-r--r--app/workers/stuck_import_jobs_worker.rb46
-rw-r--r--changelogs/unreleased/47008-issue-board-card-design.yml5
-rw-r--r--changelogs/unreleased/52767-more-chaos-for-gitlab.yml5
-rw-r--r--changelogs/unreleased/6500-fix-misaligned-approvers-dropdown.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml5
-rw-r--r--changelogs/unreleased/bvl-patches-via-mail.yml5
-rw-r--r--changelogs/unreleased/fix-stuck-import-jobs-query-performance-issue.yml5
-rw-r--r--changelogs/unreleased/fj-41213-api-update-submodule-commit.yml5
-rw-r--r--changelogs/unreleased/gt-align-sign-in-button.yml5
-rw-r--r--changelogs/unreleased/introduce-knative-support.yml5
-rw-r--r--changelogs/unreleased/max_retries_when.yml5
-rw-r--r--changelogs/unreleased/osw-comment-on-any-line-on-diffs.yml5
-rw-r--r--changelogs/unreleased/sh-paginate-bitbucket-server-imports.yml5
-rw-r--r--config/initializers/fill_shards.rb7
-rw-r--r--config/routes.rb7
-rw-r--r--config/routes/project.rb5
-rw-r--r--danger/metadata/Dangerfile7
-rw-r--r--db/fixtures/development/09_issues.rb3
-rw-r--r--db/migrate/20180912111628_add_knative_application.rb20
-rw-r--r--db/schema.rb11
-rw-r--r--doc/administration/auth/okta.md2
-rw-r--r--doc/administration/container_registry.md5
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md2
-rw-r--r--doc/api/README.md3
-rw-r--r--doc/api/repository_submodules.md49
-rw-r--r--doc/ci/yaml/README.md59
-rw-r--r--doc/development/chaos_endpoints.md117
-rw-r--r--doc/development/documentation/index.md115
-rw-r--r--doc/development/documentation/styleguide.md64
-rw-r--r--doc/development/documentation/workflow.md33
-rw-r--r--doc/development/performance.md3
-rw-r--r--doc/install/database_mysql.md48
-rw-r--r--doc/install/installation.md6
-rw-r--r--doc/raketasks/import.md72
-rw-r--r--doc/topics/authentication/index.md2
-rw-r--r--doc/university/training/topics/additional_resources.md12
-rw-r--r--doc/university/training/topics/tags.md2
-rw-r--r--doc/university/training/user_training.md2
-rw-r--r--doc/update/11.4-to-11.5.md28
-rw-r--r--doc/user/project/clusters/eks_and_gitlab/index.md5
-rw-r--r--doc/user/project/clusters/index.md132
-rw-r--r--doc/user/project/merge_requests/index.md17
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/submodules.rb47
-rw-r--r--lib/bitbucket_server/client.rb8
-rw-r--r--lib/bitbucket_server/collection.rb24
-rw-r--r--lib/bitbucket_server/paginator.rb27
-rw-r--r--lib/gitlab/ci/config/entry/job.rb9
-rw-r--r--lib/gitlab/ci/config/entry/retry.rb90
-rw-r--r--lib/gitlab/ci/config/entry/validators.rb24
-rw-r--r--lib/gitlab/diff/file.rb19
-rw-r--r--lib/gitlab/diff/line.rb13
-rw-r--r--lib/gitlab/diff/lines_unfolder.rb235
-rw-r--r--lib/gitlab/diff/position.rb12
-rw-r--r--lib/gitlab/email/handler/create_merge_request_handler.rb50
-rw-r--r--lib/gitlab/email/receiver.rb1
-rw-r--r--lib/gitlab/git/patches/collection.rb33
-rw-r--r--lib/gitlab/git/patches/commit_patches.rb31
-rw-r--r--lib/gitlab/git/patches/patch.rb19
-rw-r--r--lib/gitlab/git/repository.rb14
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb49
-rw-r--r--lib/gitlab/kubernetes/helm/install_command.rb25
-rw-r--r--lib/gitlab/quick_actions/extractor.rb14
-rw-r--r--lib/gitlab/usage_data.rb19
-rw-r--r--locale/gitlab.pot35
-rw-r--r--qa/qa/git/repository.rb26
-rw-r--r--qa/qa/page/project/issue/show.rb5
-rw-r--r--qa/qa/runtime/env.rb19
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb1
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb5
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb84
-rw-r--r--qa/qa/specs/runner.rb4
-rw-r--r--qa/spec/git/repository_spec.rb30
-rw-r--r--qa/spec/runtime/env_spec.rb47
-rw-r--r--qa/spec/specs/runner_spec.rb14
-rw-r--r--spec/controllers/import/bitbucket_server_controller_spec.rb11
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb6
-rw-r--r--spec/factories/clusters/applications/helm.rb5
-rw-r--r--spec/factories/services.rb11
-rw-r--r--spec/features/boards/add_issues_modal_spec.rb2
-rw-r--r--spec/features/boards/issue_ordering_spec.rb4
-rw-r--r--spec/features/dashboard/projects_spec.rb8
-rw-r--r--spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb2
-rw-r--r--spec/features/merge_request/user_posts_diff_notes_spec.rb13
-rw-r--r--spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb15
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb2
-rw-r--r--spec/features/merge_request/user_sees_wip_help_message_spec.rb4
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb11
-rw-r--r--spec/features/merge_request/user_uses_quick_actions_spec.rb2
-rw-r--r--spec/features/merge_requests/user_squashes_merge_request_spec.rb4
-rw-r--r--spec/features/projects/files/user_creates_directory_spec.rb5
-rw-r--r--spec/features/projects/files/user_creates_files_spec.rb4
-rw-r--r--spec/features/projects/files/user_deletes_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_edits_files_spec.rb6
-rw-r--r--spec/features/projects/files/user_replaces_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_uploads_files_spec.rb4
-rw-r--r--spec/features/projects/merge_request_button_spec.rb8
-rw-r--r--spec/fixtures/api/schemas/entities/issue_board.json1
-rw-r--r--spec/fixtures/api/schemas/issue.json1
-rw-r--r--spec/fixtures/emails/merge_request_multiple_patches.eml181
-rw-r--r--spec/fixtures/emails/merge_request_with_conflicting_patch.eml45
-rw-r--r--spec/fixtures/emails/merge_request_with_patch_and_target_branch.eml44
-rw-r--r--spec/fixtures/emails/valid_merge_request_with_patch.eml151
-rw-r--r--spec/fixtures/patchfiles/0001-A-commit-from-a-patch.patch19
-rw-r--r--spec/fixtures/patchfiles/0001-This-does-not-apply-to-the-feature-branch.patch23
-rw-r--r--spec/javascripts/boards/components/issue_due_date_spec.js64
-rw-r--r--spec/javascripts/boards/components/issue_time_estimate_spec.js40
-rw-r--r--spec/javascripts/boards/issue_card_spec.js25
-rw-r--r--spec/javascripts/clusters/components/applications_spec.js12
-rw-r--r--spec/javascripts/clusters/services/mock_data.js11
-rw-r--r--spec/javascripts/clusters/stores/clusters_store_spec.js8
-rw-r--r--spec/javascripts/diffs/store/mutations_spec.js4
-rw-r--r--spec/javascripts/jobs/components/job_app_spec.js4
-rw-r--r--spec/javascripts/lib/utils/datetime_utility_spec.js6
-rw-r--r--spec/javascripts/notes/components/noteable_discussion_spec.js16
-rw-r--r--spec/javascripts/notes/components/toggle_replies_widget_spec.js78
-rw-r--r--spec/javascripts/pipelines/header_component_spec.js2
-rw-r--r--spec/javascripts/pipelines/pipeline_url_spec.js5
-rw-r--r--spec/javascripts/pipelines/pipelines_table_row_spec.js8
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/header_ci_component_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js57
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js28
-rw-r--r--spec/lib/bitbucket_server/client_spec.rb12
-rw-r--r--spec/lib/bitbucket_server/collection_spec.rb29
-rw-r--r--spec/lib/bitbucket_server/paginator_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb41
-rw-r--r--spec/lib/gitlab/ci/config/entry/retry_spec.rb236
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb4
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb46
-rw-r--r--spec/lib/gitlab/diff/lines_unfolder_spec.rb750
-rw-r--r--spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb69
-rw-r--r--spec/lib/gitlab/git/patches/collection_spec.rb28
-rw-r--r--spec/lib/gitlab/git/patches/commit_patches_spec.rb49
-rw-r--r--spec/lib/gitlab/git/patches/patch_spec.rb16
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb33
-rw-r--r--spec/lib/gitlab/import_export/attribute_configuration_spec.rb15
-rw-r--r--spec/lib/gitlab/kubernetes/helm/install_command_spec.rb57
-rw-r--r--spec/lib/gitlab/quick_actions/extractor_spec.rb19
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb10
-rw-r--r--spec/models/ci/build_spec.rb108
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb24
-rw-r--r--spec/models/clusters/applications/jupyter_spec.rb24
-rw-r--r--spec/models/clusters/applications/knative_spec.rb77
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb24
-rw-r--r--spec/models/clusters/applications/runner_spec.rb24
-rw-r--r--spec/models/clusters/cluster_spec.rb3
-rw-r--r--spec/requests/api/internal_spec.rb6
-rw-r--r--spec/requests/api/submodules_spec.rb99
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb32
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb4
-rw-r--r--spec/services/clusters/applications/create_service_spec.rb33
-rw-r--r--spec/services/clusters/gcp/finalize_creation_service_spec.rb2
-rw-r--r--spec/services/commits/commit_patch_service_spec.rb92
-rw-r--r--spec/services/merge_requests/build_service_spec.rb8
-rw-r--r--spec/services/merge_requests/get_urls_service_spec.rb4
-rw-r--r--spec/services/merge_requests/reload_diffs_service_spec.rb21
-rw-r--r--spec/services/notes/create_service_spec.rb51
-rw-r--r--spec/services/notes/destroy_service_spec.rb33
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb9
-rw-r--r--spec/services/submodules/update_service_spec.rb212
-rw-r--r--spec/support/features/discussion_comments_shared_example.rb14
-rw-r--r--spec/support/helpers/migrations_helpers.rb22
-rw-r--r--spec/support/helpers/test_env.rb8
-rw-r--r--spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb25
-rw-r--r--spec/workers/email_receiver_worker_spec.rb15
-rw-r--r--spec/workers/stuck_import_jobs_worker_spec.rb22
-rw-r--r--vendor/gitignore/Android.gitignore4
-rw-r--r--vendor/gitignore/Delphi.gitignore3
-rw-r--r--vendor/gitignore/Elixir.gitignore1
-rw-r--r--vendor/gitignore/Global/Images.gitignore63
-rw-r--r--vendor/gitignore/Global/NetBeans.gitignore2
-rw-r--r--vendor/gitignore/Global/PSoCCreator.gitignore18
-rw-r--r--vendor/gitignore/Global/Xcode.gitignore74
-rw-r--r--vendor/gitignore/Laravel.gitignore2
-rw-r--r--vendor/gitignore/Magento.gitignore2
-rw-r--r--vendor/gitignore/Node.gitignore3
-rw-r--r--vendor/gitignore/Python.gitignore3
-rw-r--r--vendor/gitignore/Rails.gitignore1
-rw-r--r--vendor/gitignore/Unity.gitignore1
270 files changed, 5205 insertions, 2328 deletions
diff --git a/Gemfile b/Gemfile
index 5952964a9c3..c7efa790cfd 100644
--- a/Gemfile
+++ b/Gemfile
@@ -416,7 +416,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.118.1', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.123.0', require: 'gitaly'
gem 'grpc', '~> 1.15.0'
gem 'google-protobuf', '~> 3.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index 4ad9613989d..50e3ddef1e1 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -269,9 +269,8 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (0.118.1)
- google-protobuf (~> 3.1)
- grpc (~> 1.10)
+ gitaly-proto (0.123.0)
+ grpc (~> 1.0)
github-markup (1.7.0)
gitlab-markup (1.6.4)
gitlab-sidekiq-fetcher (0.3.0)
@@ -994,7 +993,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.118.1)
+ gitaly-proto (~> 0.123.0)
github-markup (~> 1.7.0)
gitlab-markup (~> 1.6.4)
gitlab-sidekiq-fetcher
@@ -1152,4 +1151,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.16.6
+ 1.17.1
diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock
index 9674fc27310..181f2db95b0 100644
--- a/Gemfile.rails5.lock
+++ b/Gemfile.rails5.lock
@@ -272,9 +272,8 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (0.118.1)
- google-protobuf (~> 3.1)
- grpc (~> 1.10)
+ gitaly-proto (0.123.0)
+ grpc (~> 1.0)
github-markup (1.7.0)
gitlab-markup (1.6.4)
gitlab-sidekiq-fetcher (0.3.0)
@@ -1003,7 +1002,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.118.1)
+ gitaly-proto (~> 0.123.0)
github-markup (~> 1.7.0)
gitlab-markup (~> 1.6.4)
gitlab-sidekiq-fetcher
@@ -1161,4 +1160,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.16.6
+ 1.17.1
diff --git a/app/assets/images/cluster_app_logos/knative.png b/app/assets/images/cluster_app_logos/knative.png
new file mode 100644
index 00000000000..0a2510c8549
--- /dev/null
+++ b/app/assets/images/cluster_app_logos/knative.png
Binary files differ
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index d956777a86b..2315a48a306 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -1,18 +1,24 @@
<script>
-import $ from 'jquery';
+import { GlTooltipDirective } from '@gitlab-org/gitlab-ui';
+import { sprintf, __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
+import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import eventHub from '../eventhub';
-import tooltip from '../../vue_shared/directives/tooltip';
+import IssueDueDate from './issue_due_date.vue';
+import IssueTimeEstimate from './issue_time_estimate.vue';
import boardsStore from '../stores/boards_store';
export default {
components: {
- UserAvatarLink,
Icon,
+ UserAvatarLink,
+ TooltipOnTruncate,
+ IssueDueDate,
+ IssueTimeEstimate,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
issue: {
@@ -45,8 +51,8 @@ export default {
},
data() {
return {
- limitBeforeCounter: 3,
- maxRender: 4,
+ limitBeforeCounter: 2,
+ maxRender: 3,
maxCounter: 99,
};
},
@@ -55,7 +61,9 @@ export default {
return this.issue.assignees.length - this.limitBeforeCounter;
},
assigneeCounterTooltip() {
- return `${this.assigneeCounterLabel} more`;
+ const { numberOverLimit, maxCounter } = this;
+ const count = numberOverLimit > maxCounter ? maxCounter : numberOverLimit;
+ return sprintf(__('%{count} more assignees'), { count });
},
assigneeCounterLabel() {
if (this.numberOverLimit > this.maxCounter) {
@@ -80,6 +88,10 @@ export default {
showLabelFooter() {
return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
},
+ issueReferencePath() {
+ const { referencePath, groupId } = this.issue;
+ return !groupId ? referencePath.split('#')[0] : null;
+ },
},
methods: {
isIndexLessThanlimit(index) {
@@ -96,11 +108,9 @@ export default {
return index < this.limitBeforeCounter;
},
assigneeUrl(assignee) {
+ if (!assignee) return '';
return `${this.rootPath}${assignee.username}`;
},
- assigneeUrlTitle(assignee) {
- return `Assigned to ${assignee.name}`;
- },
avatarUrlTitle(assignee) {
return `Avatar for ${assignee.name}`;
},
@@ -108,19 +118,29 @@ export default {
if (!label.id) return false;
return true;
},
- filterByLabel(label, e) {
+ filterByLabel(label) {
+ if (!this.updateFilters) return;
+ const labelTitle = encodeURIComponent(label.title);
+ const filter = `label_name[]=${labelTitle}`;
+
+ this.applyFilter(filter);
+ },
+ filterByWeight(weight) {
if (!this.updateFilters) return;
+ const issueWeight = encodeURIComponent(weight);
+ const filter = `weight=${issueWeight}`;
+
+ this.applyFilter(filter);
+ },
+ applyFilter(filter) {
const filterPath = boardsStore.filter.path.split('&');
- const labelTitle = encodeURIComponent(label.title);
- const param = `label_name[]=${labelTitle}`;
- const labelIndex = filterPath.indexOf(param);
- $(e.currentTarget).tooltip('hide');
+ const filterIndex = filterPath.indexOf(filter);
- if (labelIndex === -1) {
- filterPath.push(param);
+ if (filterIndex === -1) {
+ filterPath.push(filter);
} else {
- filterPath.splice(labelIndex, 1);
+ filterPath.splice(filterIndex, 1);
}
boardsStore.filter.path = filterPath.join('&');
@@ -141,24 +161,62 @@ export default {
<template>
<div>
<div class="board-card-header">
- <h4 class="board-card-title">
+ <h4 class="board-card-title append-bottom-0 prepend-top-0">
<icon
v-if="issue.confidential"
+ v-gl-tooltip
name="eye-slash"
- class="confidential-icon"
- />
- <a
+ :title="__('Confidential')"
+ class="confidential-icon append-right-4"
+ :aria-label="__('Confidential')"
+ /><a
:href="issue.path"
:title="issue.title"
class="js-no-trigger"
@mousemove.stop>{{ issue.title }}</a>
+ </h4>
+ </div>
+ <div
+ v-if="showLabelFooter"
+ class="board-card-labels prepend-top-4 d-flex flex-wrap"
+ >
+ <button
+ v-for="label in issue.labels"
+ v-if="showLabel(label)"
+ :key="label.id"
+ v-gl-tooltip
+ :style="labelStyle(label)"
+ :title="label.description"
+ class="badge color-label append-right-4 prepend-top-4"
+ type="button"
+ @click="filterByLabel(label)"
+ >
+ {{ label.title }}
+ </button>
+ </div>
+ <div class="board-card-footer d-flex justify-content-between align-items-end">
+ <div class="d-flex align-items-start flex-wrap-reverse board-card-number-container js-board-card-number-container">
<span
- v-if="issueId"
- class="board-card-number append-right-5"
+ v-if="issue.referencePath"
+ class="board-card-number d-flex append-right-8 prepend-top-8"
>
- {{ issue.referencePath }}
+ <tooltip-on-truncate
+ v-if="issueReferencePath"
+ :title="issueReferencePath"
+ placement="bottom"
+ class="board-issue-path block-truncated bold"
+ >{{ issueReferencePath }}</tooltip-on-truncate>#{{ issue.iid }}
</span>
- </h4>
+ <span class="board-info-items prepend-top-8 d-inline-block">
+ <issue-due-date
+ v-if="issue.dueDate"
+ :date="issue.dueDate"
+ /><issue-time-estimate
+ v-if="issue.timeEstimate"
+ :estimate="issue.timeEstimate"
+ />
+ </span>
+ </div>
<div class="board-card-assignee">
<user-avatar-link
v-for="(assignee, index) in issue.assignees"
@@ -167,38 +225,26 @@ export default {
:link-href="assigneeUrl(assignee)"
:img-alt="avatarUrlTitle(assignee)"
:img-src="assignee.avatar"
- :tooltip-text="assigneeUrlTitle(assignee)"
+ :img-size="24"
class="js-no-trigger"
tooltip-placement="bottom"
- />
+ >
+ <span class="js-assignee-tooltip">
+ <span class="bold d-block">Assignee</span>
+ {{ assignee.name }}
+ <span class="text-white-50">@{{ assignee.username }}</span>
+ </span>
+ </user-avatar-link>
<span
v-if="shouldRenderCounter"
- v-tooltip
+ v-gl-tooltip
:title="assigneeCounterTooltip"
class="avatar-counter"
+ data-placement="bottom"
>
{{ assigneeCounterLabel }}
</span>
</div>
</div>
- <div
- v-if="showLabelFooter"
- class="board-card-footer"
- >
- <button
- v-for="label in issue.labels"
- v-if="showLabel(label)"
- :key="label.id"
- v-tooltip
- :style="labelStyle(label)"
- :title="label.description"
- class="badge color-label"
- type="button"
- data-container="body"
- @click="filterByLabel(label, $event)"
- >
- {{ label.title }}
- </button>
- </div>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/issue_due_date.vue b/app/assets/javascripts/boards/components/issue_due_date.vue
new file mode 100644
index 00000000000..025ef7e9743
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_due_date.vue
@@ -0,0 +1,90 @@
+<script>
+import dateFormat from 'dateformat';
+import { GlTooltip } from '@gitlab-org/gitlab-ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import { __ } from '~/locale';
+import { getDayDifference, getTimeago, dateInWords } from '~/lib/utils/datetime_utility';
+
+export default {
+ components: {
+ Icon,
+ GlTooltip,
+ },
+ props: {
+ date: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ title() {
+ const timeago = getTimeago();
+ const { timeDifference, standardDateFormat } = this;
+ const formatedDate = standardDateFormat;
+
+ if (timeDifference >= -1 && timeDifference < 7) {
+ return `${timeago.format(this.issueDueDate)} (${formatedDate})`;
+ }
+
+ return timeago.format(this.issueDueDate);
+ },
+ body() {
+ const { timeDifference, issueDueDate, standardDateFormat } = this;
+
+ if (timeDifference === 0) {
+ return __('Today');
+ } else if (timeDifference === 1) {
+ return __('Tomorrow');
+ } else if (timeDifference === -1) {
+ return __('Yesterday');
+ } else if (timeDifference > 0 && timeDifference < 7) {
+ return dateFormat(issueDueDate, 'dddd', true);
+ }
+
+ return standardDateFormat;
+ },
+ issueDueDate() {
+ return new Date(this.date);
+ },
+ timeDifference() {
+ const today = new Date();
+ return getDayDifference(today, this.issueDueDate);
+ },
+ isPastDue() {
+ if (this.timeDifference >= 0) return false;
+ return true;
+ },
+ standardDateFormat() {
+ const today = new Date();
+ const isDueInCurrentYear = today.getFullYear() === this.issueDueDate.getFullYear();
+
+ return dateInWords(this.issueDueDate, true, isDueInCurrentYear);
+ },
+ },
+};
+</script>
+
+<template>
+ <span>
+ <span
+ ref="issueDueDate"
+ class="board-card-info card-number"
+ >
+ <icon
+ :class="{'text-danger': isPastDue, 'board-card-info-icon': true}"
+ name="calendar"
+ /><time
+ :class="{'text-danger': isPastDue}"
+ datetime="date"
+ class="board-card-info-text">{{ body }}</time>
+ </span>
+ <gl-tooltip
+ :target="() => $refs.issueDueDate"
+ placement="bottom"
+ >
+ <span class="bold">{{ __('Due date') }}</span>
+ <br />
+ <span :class="{'text-danger-muted': isPastDue}">{{ title }}</span>
+ </gl-tooltip>
+ </span>
+</template>
diff --git a/app/assets/javascripts/boards/components/issue_time_estimate.vue b/app/assets/javascripts/boards/components/issue_time_estimate.vue
new file mode 100644
index 00000000000..efc7daf7812
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_time_estimate.vue
@@ -0,0 +1,48 @@
+<script>
+import { GlTooltip } from '@gitlab-org/gitlab-ui';
+import Icon from '~/vue_shared/components/icon.vue';
+import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
+
+export default {
+ components: {
+ Icon,
+ GlTooltip,
+ },
+ props: {
+ estimate: {
+ type: Number,
+ required: true,
+ },
+ },
+ computed: {
+ title() {
+ return stringifyTime(parseSeconds(this.estimate), true);
+ },
+ timeEstimate() {
+ return stringifyTime(parseSeconds(this.estimate));
+ },
+ },
+};
+</script>
+
+<template>
+ <span>
+ <span
+ ref="issueTimeEstimate"
+ class="board-card-info card-number"
+ >
+ <icon
+ name="hourglass"
+ css-classes="board-card-info-icon"
+ /><time class="board-card-info-text">{{ timeEstimate }}</time>
+ </span>
+ <gl-tooltip
+ :target="() => $refs.issueTimeEstimate"
+ placement="bottom"
+ class="js-issue-time-estimate"
+ >
+ <span class="bold d-block">{{ __('Time estimate') }}</span>
+ {{ title }}
+ </gl-tooltip>
+ </span>
+</template>
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 669630edcab..5e0f0b07247 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -30,6 +30,7 @@ class ListIssue {
this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint;
this.milestone_id = obj.milestone_id;
this.project_id = obj.project_id;
+ this.timeEstimate = obj.time_estimate;
this.assignableLabelsEndpoint = obj.assignable_labels_endpoint;
if (obj.project) {
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index ebf76af5966..02dfe1c7d6f 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -28,6 +28,7 @@ export default class Clusters {
installIngressPath,
installRunnerPath,
installJupyterPath,
+ installKnativePath,
installPrometheusPath,
managePrometheusPath,
clusterStatus,
@@ -49,6 +50,7 @@ export default class Clusters {
installRunnerEndpoint: installRunnerPath,
installPrometheusEndpoint: installPrometheusPath,
installJupyterEndpoint: installJupyterPath,
+ installKnativeEndpoint: installKnativePath,
});
this.installApplication = this.installApplication.bind(this);
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 6d7f45a35d8..c7ffb470d4d 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -7,6 +7,7 @@ import helmLogo from 'images/cluster_app_logos/helm.png';
import jeagerLogo from 'images/cluster_app_logos/jeager.png';
import jupyterhubLogo from 'images/cluster_app_logos/jupyterhub.png';
import kubernetesLogo from 'images/cluster_app_logos/kubernetes.png';
+import knativeLogo from 'images/cluster_app_logos/knative.png';
import meltanoLogo from 'images/cluster_app_logos/meltano.png';
import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
import { s__, sprintf } from '../../locale';
@@ -53,6 +54,7 @@ export default {
jeagerLogo,
jupyterhubLogo,
kubernetesLogo,
+ knativeLogo,
meltanoLogo,
prometheusLogo,
}),
@@ -136,6 +138,9 @@ export default {
jupyterHostname() {
return this.applications.jupyter.hostname;
},
+ knativeInstalled() {
+ return this.applications.knative.status === APPLICATION_STATUS.INSTALLED;
+ },
},
created() {
this.helmInstallIllustration = helmInstallIllustration;
@@ -321,7 +326,6 @@ export default {
:request-reason="applications.jupyter.requestReason"
:install-application-request-params="{ hostname: applications.jupyter.hostname }"
:disabled="!helmInstalled"
- class="hide-bottom-border rounded-bottom"
title-link="https://jupyterhub.readthedocs.io/en/stable/"
>
<div slot="description">
@@ -371,6 +375,58 @@ export default {
</template>
</div>
</application-row>
+ <application-row
+ id="knative"
+ :logo-url="knativeLogo"
+ :title="applications.knative.title"
+ :status="applications.knative.status"
+ :status-reason="applications.knative.statusReason"
+ :request-status="applications.knative.requestStatus"
+ :request-reason="applications.knative.requestReason"
+ :install-application-request-params="{ hostname: applications.knative.hostname}"
+ :disabled="!helmInstalled"
+ class="hide-bottom-border rounded-bottom"
+ title-link="https://github.com/knative/docs"
+ >
+ <div slot="description">
+ <p>
+ {{ s__(`ClusterIntegration|A Knative build extends Kubernetes
+ and utilizes existing Kubernetes primitives to provide you with
+ the ability to run on-cluster container builds from source.
+ For example, you can write a build that uses Kubernetes-native
+ resources to obtain your source code from a repository,
+ build it into container a image, and then run that image.`) }}
+ </p>
+
+ <template v-if="knativeInstalled">
+ <div class="form-group">
+ <label for="knative-domainname">
+ {{ s__('ClusterIntegration|Knative Domain Name:') }}
+ </label>
+ <input
+ id="knative-domainname"
+ v-model="applications.knative.hostname"
+ type="text"
+ class="form-control js-domainname"
+ readonly
+ />
+ </div>
+ </template>
+ <template v-else>
+ <div class="form-group">
+ <label for="knative-domainname">
+ {{ s__('ClusterIntegration|Knative Domain Name:') }}
+ </label>
+ <input
+ id="knative-domainname"
+ v-model="applications.knative.hostname"
+ type="text"
+ class="form-control js-domainname"
+ />
+ </div>
+ </template>
+ </div>
+ </application-row>
</div>
</section>
</template>
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index 24a49624583..d707420c845 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -16,3 +16,4 @@ export const REQUEST_SUCCESS = 'request-success';
export const REQUEST_FAILURE = 'request-failure';
export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter';
+export const KNATIVE = 'knative';
diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js
index a7d82292ba9..da562b09ee5 100644
--- a/app/assets/javascripts/clusters/services/clusters_service.js
+++ b/app/assets/javascripts/clusters/services/clusters_service.js
@@ -9,6 +9,7 @@ export default class ClusterService {
runner: this.options.installRunnerEndpoint,
prometheus: this.options.installPrometheusEndpoint,
jupyter: this.options.installJupyterEndpoint,
+ knative: this.options.installKnativeEndpoint,
};
}
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 106ac3cb516..e45da967392 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -1,5 +1,5 @@
import { s__ } from '../../locale';
-import { INGRESS, JUPYTER } from '../constants';
+import { INGRESS, JUPYTER, KNATIVE } from '../constants';
export default class ClusterStore {
constructor() {
@@ -46,6 +46,14 @@ export default class ClusterStore {
requestReason: null,
hostname: null,
},
+ knative: {
+ title: s__('ClusterIntegration|Knative'),
+ status: null,
+ statusReason: null,
+ requestStatus: null,
+ requestReason: null,
+ hostname: null,
+ },
},
};
}
@@ -93,6 +101,9 @@ export default class ClusterStore {
(this.state.applications.ingress.externalIp
? `jupyter.${this.state.applications.ingress.externalIp}.nip.io`
: '');
+ } else if (appId === KNATIVE) {
+ this.state.applications.knative.hostname =
+ serverAppEntry.hostname || this.state.applications.knative.hostname;
}
});
}
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index 539d0d29e0d..bffc025ced3 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -5,6 +5,7 @@ import 'core-js/fn/array/find-index';
import 'core-js/fn/array/from';
import 'core-js/fn/array/includes';
import 'core-js/fn/object/assign';
+import 'core-js/fn/object/values';
import 'core-js/fn/promise';
import 'core-js/fn/string/code-point-at';
import 'core-js/fn/string/from-code-point';
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index e19207bdc95..b9de487a737 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -76,7 +76,6 @@ export default {
<noteable-discussion
v-show="isExpanded(discussion)"
:discussion="discussion"
- :render-header="false"
:render-diff-file="false"
:always-expanded="true"
:discussions-by-diff-order="true"
diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
index e31a3546b69..f4a9be19496 100644
--- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
@@ -55,6 +55,11 @@ export default {
required: false,
default: false,
},
+ isContextLine: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
isHover: {
type: Boolean,
required: false,
@@ -76,6 +81,7 @@ export default {
this.showCommentButton &&
this.isHover &&
!this.isMatchLine &&
+ !this.isContextLine &&
!this.isMetaLine &&
!this.hasDiscussions
);
diff --git a/app/assets/javascripts/diffs/components/diff_table_cell.vue b/app/assets/javascripts/diffs/components/diff_table_cell.vue
index e26aa9c9b00..5d9a0b123fe 100644
--- a/app/assets/javascripts/diffs/components/diff_table_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue
@@ -3,6 +3,7 @@ import { mapGetters } from 'vuex';
import DiffLineGutterContent from './diff_line_gutter_content.vue';
import {
MATCH_LINE_TYPE,
+ CONTEXT_LINE_TYPE,
EMPTY_CELL_TYPE,
OLD_LINE_TYPE,
OLD_NO_NEW_LINE_TYPE,
@@ -70,6 +71,9 @@ export default {
isMatchLine() {
return this.line.type === MATCH_LINE_TYPE;
},
+ isContextLine() {
+ return this.line.type === CONTEXT_LINE_TYPE;
+ },
isMetaLine() {
const { type } = this.line;
@@ -84,7 +88,11 @@ export default {
[type]: type,
[LINE_UNFOLD_CLASS_NAME]: this.isMatchLine,
[LINE_HOVER_CLASS_NAME]:
- this.isLoggedIn && this.isHover && !this.isMatchLine && !this.isMetaLine,
+ this.isLoggedIn &&
+ this.isHover &&
+ !this.isMatchLine &&
+ !this.isContextLine &&
+ !this.isMetaLine,
};
},
lineNumber() {
diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
index 44c05e4b634..542acd3d930 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
@@ -4,6 +4,8 @@ import DiffTableCell from './diff_table_cell.vue';
import {
NEW_LINE_TYPE,
OLD_LINE_TYPE,
+ CONTEXT_LINE_TYPE,
+ CONTEXT_LINE_CLASS_NAME,
PARALLEL_DIFF_VIEW_TYPE,
LINE_POSITION_LEFT,
LINE_POSITION_RIGHT,
@@ -39,9 +41,13 @@ export default {
},
computed: {
...mapGetters('diffs', ['isInlineView']),
+ isContextLine() {
+ return this.line.type === CONTEXT_LINE_TYPE;
+ },
classNameMap() {
return {
[this.line.type]: this.line.type,
+ [CONTEXT_LINE_CLASS_NAME]: this.isContextLine,
[PARALLEL_DIFF_VIEW_TYPE]: this.isParallelView,
};
},
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
index 3339c56cbb6..3b71c0a1fd4 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
@@ -76,8 +76,9 @@ export default {
:class="className"
class="notes_holder"
>
- <td class="notes_line old"></td>
- <td class="notes_content parallel old">
+ <td
+ class="notes_content parallel old"
+ colspan="2">
<div
v-if="shouldRenderDiscussionsOnLeft"
class="content"
@@ -95,8 +96,9 @@ export default {
line-position="left"
/>
</td>
- <td class="notes_line new"></td>
- <td class="notes_content parallel new">
+ <td
+ class="notes_content parallel new"
+ colspan="2">
<div
v-if="shouldRenderDiscussionsOnRight"
class="content"
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
index 39312cddfce..fcc3b3e9117 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
@@ -5,6 +5,8 @@ import DiffTableCell from './diff_table_cell.vue';
import {
NEW_LINE_TYPE,
OLD_LINE_TYPE,
+ CONTEXT_LINE_TYPE,
+ CONTEXT_LINE_CLASS_NAME,
OLD_NO_NEW_LINE_TYPE,
PARALLEL_DIFF_VIEW_TYPE,
NEW_NO_NEW_LINE_TYPE,
@@ -41,8 +43,12 @@ export default {
};
},
computed: {
+ isContextLine() {
+ return this.line.left && this.line.left.type === CONTEXT_LINE_TYPE;
+ },
classNameMap() {
return {
+ [CONTEXT_LINE_CLASS_NAME]: this.isContextLine,
[PARALLEL_DIFF_VIEW_TYPE]: true,
};
},
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index f5f5c0ffc29..78a39baa4cb 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -3,6 +3,7 @@ export const PARALLEL_DIFF_VIEW_TYPE = 'parallel';
export const MATCH_LINE_TYPE = 'match';
export const OLD_NO_NEW_LINE_TYPE = 'old-nonewline';
export const NEW_NO_NEW_LINE_TYPE = 'new-nonewline';
+export const CONTEXT_LINE_TYPE = 'context';
export const EMPTY_CELL_TYPE = 'empty-cell';
export const COMMENT_FORM_TYPE = 'commentForm';
export const DIFF_NOTE_TYPE = 'DiffNote';
@@ -21,6 +22,7 @@ export const LINE_SIDE_RIGHT = 'right-side';
export const DIFF_VIEW_COOKIE_NAME = 'diff_view';
export const LINE_HOVER_CLASS_NAME = 'is-over';
export const LINE_UNFOLD_CLASS_NAME = 'unfold js-unfold';
+export const CONTEXT_LINE_CLASS_NAME = 'diff-expanded';
export const UNFOLD_COUNT = 20;
export const COUNT_OF_AVATARS_IN_GUTTER = 3;
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index e651c197968..a7eea2c1449 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -65,13 +65,7 @@ export default {
const { highlightedDiffLines, parallelDiffLines } = diffFile;
removeMatchLine(diffFile, lineNumbers, bottom);
-
- const lines = addLineReferences(contextLines, lineNumbers, bottom).map(line => ({
- ...line,
- lineCode: line.lineCode || `${fileHash}_${line.oldLine}_${line.newLine}`,
- discussions: line.discussions || [],
- }));
-
+ const lines = addLineReferences(contextLines, lineNumbers, bottom);
addContextLines({
inlineLines: highlightedDiffLines,
parallelLines: parallelDiffLines,
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index 35104c80694..90216b04e92 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -240,16 +240,16 @@ export default {
:erased-at="job.erased_at"
/>
- <div
+ <div
v-if="job.archived"
ref="sticky"
class="js-archived-job prepend-top-default archived-sticky sticky-top"
>
- <icon
+ <icon
name="lock"
class="align-text-bottom"
/>
-
+
{{ __('This job is archived. Only the complete pipeline can be retried.') }}
</div>
<!--job log -->
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 46740308f17..59007d5950e 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -14,7 +14,7 @@ window.timeago = timeago;
*
* @param {Boolean} abbreviated
*/
-const getMonthNames = abbreviated => {
+export const getMonthNames = abbreviated => {
if (abbreviated) {
return [
s__('Jan'),
@@ -454,12 +454,20 @@ export const parseSeconds = (seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {})
/**
* Accepts a timeObject (see parseSeconds) and returns a condensed string representation of it
* (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
+ * If the 'fullNameFormat' param is passed it returns a non condensed string eg '1 week 3 days'
*/
-export const stringifyTime = timeObject => {
+export const stringifyTime = (timeObject, fullNameFormat = false) => {
const reducedTime = _.reduce(
timeObject,
(memo, unitValue, unitName) => {
const isNonZero = !!unitValue;
+
+ if (fullNameFormat && isNonZero) {
+ // Remove traling 's' if unit value is singular
+ const formatedUnitName = unitValue > 1 ? unitName : unitName.replace(/s$/, '');
+ return `${memo} ${unitValue} ${formatedUnitName}`;
+ }
+
return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
},
'',
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 554db102027..754c6e79ee4 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -321,10 +321,10 @@ Please check your network connection and try again.`;
v-else-if="!canCreateNote"
:issuable-type="issuableTypeTitle"
/>
- <ul
+ <div
v-else-if="canCreateNote"
class="notes notes-form timeline">
- <li class="timeline-entry">
+ <div class="timeline-entry note-form">
<div class="timeline-entry-inner">
<div class="flash-container error-alert timeline-content"></div>
<div class="timeline-icon d-none d-sm-none d-md-block">
@@ -462,7 +462,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
</form>
</div>
</div>
- </li>
- </ul>
+ </div>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index 1f80f24e045..a4d76a70696 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -1,9 +1,6 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import resolveSvg from 'icons/_icon_resolve_discussion.svg';
-import resolvedSvg from 'icons/_icon_status_success_solid.svg';
-import mrIssueSvg from 'icons/_icon_mr_issue.svg';
-import nextDiscussionSvg from 'icons/_next_discussion.svg';
+import Icon from '~/vue_shared/components/icon.vue';
import { pluralize } from '../../lib/utils/text_utility';
import discussionNavigation from '../mixins/discussion_navigation';
import tooltip from '../../vue_shared/directives/tooltip';
@@ -12,6 +9,9 @@ export default {
directives: {
tooltip,
},
+ components: {
+ Icon,
+ },
mixins: [discussionNavigation],
computed: {
...mapGetters([
@@ -37,12 +37,6 @@ export default {
return this.getNoteableData.create_issue_to_resolve_discussions_path;
},
},
- created() {
- this.resolveSvg = resolveSvg;
- this.resolvedSvg = resolvedSvg;
- this.mrIssueSvg = mrIssueSvg;
- this.nextDiscussionSvg = nextDiscussionSvg;
- },
methods: {
...mapActions(['expandDiscussion']),
jumpToFirstUnresolvedDiscussion() {
@@ -66,15 +60,9 @@ export default {
<span
:class="{ 'is-active': allResolved }"
class="line-resolve-btn is-disabled"
- type="button">
- <span
- v-if="allResolved"
- v-html="resolvedSvg"
- ></span>
- <span
- v-else
- v-html="resolveSvg"
- ></span>
+ type="button"
+ >
+ <icon name="check-circle" />
</span>
<span class="line-resolve-text">
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved
@@ -90,7 +78,7 @@ export default {
:title="s__('Resolve all discussions in new issue')"
data-container="body"
class="new-issue-for-discussion btn btn-default discussion-create-issue-btn">
- <span v-html="mrIssueSvg"></span>
+ <icon name="issue-new" />
</a>
</div>
<div
@@ -103,7 +91,7 @@ export default {
data-container="body"
class="btn btn-default discussion-next-btn"
@click="jumpToFirstUnresolvedDiscussion">
- <span v-html="nextDiscussionSvg"></span>
+ <icon name="comment-next" />
</button>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 01cbe40f444..f7a61fbfcd4 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -1,12 +1,5 @@
<script>
import { mapGetters } from 'vuex';
-import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
-import emojiSmile from 'icons/_emoji_smile.svg';
-import emojiSmiley from 'icons/_emoji_smiley.svg';
-import editSvg from 'icons/_icon_pencil.svg';
-import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg';
-import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg';
-import ellipsisSvg from 'icons/_ellipsis_v.svg';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { GlLoadingIcon } from '@gitlab-org/gitlab-ui';
@@ -110,15 +103,6 @@ export default {
return title;
},
},
- created() {
- this.emojiSmiling = emojiSmiling;
- this.emojiSmile = emojiSmile;
- this.emojiSmiley = emojiSmiley;
- this.editSvg = editSvg;
- this.ellipsisSvg = ellipsisSvg;
- this.resolveDiscussionSvg = resolveDiscussionSvg;
- this.resolvedDiscussionSvg = resolvedDiscussionSvg;
- },
methods: {
onEdit() {
this.$emit('handleEdit');
@@ -152,12 +136,7 @@ export default {
class="line-resolve-btn note-action-button"
@click="onResolve">
<template v-if="!isResolving">
- <div
- v-if="isResolved"
- v-html="resolvedDiscussionSvg"></div>
- <div
- v-else
- v-html="resolveDiscussionSvg"></div>
+ <icon name="check-circle" />
</template>
<gl-loading-icon
v-else
@@ -179,18 +158,18 @@ export default {
title="Add reaction"
>
<gl-loading-icon inline/>
- <span
- class="link-highlight award-control-icon-neutral"
- v-html="emojiSmiling">
- </span>
- <span
- class="link-highlight award-control-icon-positive"
- v-html="emojiSmiley">
- </span>
- <span
- class="link-highlight award-control-icon-super-positive"
- v-html="emojiSmile">
- </span>
+ <icon
+ css-classes="link-highlight award-control-icon-neutral"
+ name="emoji_slightly_smiling_face"
+ />
+ <icon
+ css-classes="link-highlight award-control-icon-positive"
+ name="emoji_smiley"
+ />
+ <icon
+ css-classes="link-highlight award-control-icon-super-positive"
+ name="emoji_smiley"
+ />
</a>
</div>
<div
@@ -204,10 +183,10 @@ export default {
data-container="body"
data-placement="bottom"
@click="onEdit">
- <span
- class="link-highlight"
- v-html="editSvg">
- </span>
+ <icon
+ name="pencil"
+ css-classes="link-highlight"
+ />
</button>
</div>
<div
@@ -240,10 +219,10 @@ export default {
data-toggle="dropdown"
data-container="body"
data-placement="bottom">
- <span
- class="icon"
- v-html="ellipsisSvg">
- </span>
+ <icon
+ css-classes="icon"
+ name="ellipsis_v"
+ />
</button>
<ul class="dropdown-menu more-actions-dropdown dropdown-open-left">
<li v-if="canReportAsAbuse">
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index df7ab4502a6..401bcfabbe4 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -1,13 +1,14 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import emojiSmiling from 'icons/_emoji_slightly_smiling_face.svg';
-import emojiSmile from 'icons/_emoji_smile.svg';
-import emojiSmiley from 'icons/_emoji_smiley.svg';
+import Icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash';
import { glEmojiTag } from '../../emoji';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
+ components: {
+ Icon,
+ },
directives: {
tooltip,
},
@@ -72,11 +73,6 @@ export default {
return this.noteAuthorId === this.getUserData.id;
},
},
- created() {
- this.emojiSmiling = emojiSmiling;
- this.emojiSmile = emojiSmile;
- this.emojiSmiley = emojiSmiley;
- },
methods: {
...mapActions(['toggleAwardRequest']),
getAwardHTML(name) {
@@ -196,17 +192,14 @@ export default {
data-boundary="viewport"
data-placement="bottom"
type="button">
- <span
- class="award-control-icon award-control-icon-neutral"
- v-html="emojiSmiling">
+ <span class="award-control-icon award-control-icon-neutral">
+ <icon name="emoji_slightly_smiling_face" />
</span>
- <span
- class="award-control-icon award-control-icon-positive"
- v-html="emojiSmiley">
+ <span class="award-control-icon award-control-icon-positive">
+ <icon name="emoji_smiley" />
</span>
- <span
- class="award-control-icon award-control-icon-super-positive"
- v-html="emojiSmile">
+ <span class="award-control-icon award-control-icon-super-positive">
+ <icon name="emoji_smiley" />
</span>
<i
aria-hidden="true"
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 7b6e7b72caf..dd7313d7b10 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -45,6 +45,9 @@ export default {
noteTimestampLink() {
return `#note_${this.noteId}`;
},
+ hasAuthor() {
+ return this.author && Object.keys(this.author).length;
+ },
},
methods: {
...mapActions(['setTargetNoteHash']),
@@ -76,7 +79,7 @@ export default {
</button>
</div>
<a
- v-if="Object.keys(author).length"
+ v-if="hasAuthor"
:href="author.path"
>
<span class="note-header-author-name">{{ author.name }}</span>
@@ -92,9 +95,6 @@ export default {
</span>
<span class="note-headline-light">
<span class="note-headline-meta">
- <template v-if="actionText">
- {{ actionText }}
- </template>
<span class="system-note-message">
<slot></slot>
</span>
@@ -102,7 +102,9 @@ export default {
v-if="createdAt"
>
<span class="system-note-separator">
- &middot;
+ <template v-if="actionText">
+ {{ actionText }}
+ </template>
</span>
<a
:href="noteTimestampLink"
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 07115ca07c4..a153edd0476 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,16 +1,16 @@
<script>
import { mapActions, mapGetters } from 'vuex';
-import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
-import nextDiscussionsSvg from 'icons/_next_discussion.svg';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
-import systemNote from '~/vue_shared/components/notes/system_note.vue';
import { s__ } from '~/locale';
+import systemNote from '~/vue_shared/components/notes/system_note.vue';
+import icon from '~/vue_shared/components/icon.vue';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import noteableNote from './noteable_note.vue';
import noteHeader from './note_header.vue';
+import toggleRepliesWidget from './toggle_replies_widget.vue';
import noteSignedOutWidget from './note_signed_out_widget.vue';
import noteEditedText from './note_edited_text.vue';
import noteForm from './note_form.vue';
@@ -26,6 +26,7 @@ import tooltip from '../../vue_shared/directives/tooltip';
export default {
name: 'NoteableDiscussion',
components: {
+ icon,
noteableNote,
diffWithNote,
userAvatarLink,
@@ -33,6 +34,7 @@ export default {
noteSignedOutWidget,
noteEditedText,
noteForm,
+ toggleRepliesWidget,
placeholderNote,
placeholderSystemNote,
systemNote,
@@ -46,11 +48,6 @@ export default {
type: Object,
required: true,
},
- renderHeader: {
- type: Boolean,
- required: false,
- default: true,
- },
renderDiffFile: {
type: Boolean,
required: false,
@@ -72,6 +69,7 @@ export default {
isReplying: false,
isResolving: false,
resolveAsThread: true,
+ isRepliesCollapsed: (!this.discussion.diff_discussion && this.discussion.resolved) || false,
};
},
computed: {
@@ -112,6 +110,15 @@ export default {
newNotePath() {
return this.getNoteableData.create_note_path;
},
+ hasReplies() {
+ return this.discussion.notes.length > 1;
+ },
+ initialDiscussion() {
+ return this.discussion.notes.slice(0, 1)[0];
+ },
+ replies() {
+ return this.discussion.notes.slice(1);
+ },
lastUpdatedBy() {
const { notes } = this.discussion;
@@ -147,6 +154,12 @@ export default {
return diffDiscussion && diffFile && this.renderDiffFile;
},
+ shouldGroupReplies() {
+ return !this.shouldRenderDiffs && !this.transformedDiscussion.diffDiscussion;
+ },
+ shouldRenderHeader() {
+ return this.shouldRenderDiffs;
+ },
wrapperComponent() {
return this.shouldRenderDiffs ? diffWithNote : 'div';
},
@@ -160,6 +173,22 @@ export default {
wrapperClass() {
return this.isDiffDiscussion ? '' : 'card discussion-wrapper';
},
+ componentClassName() {
+ if (this.shouldRenderDiffs) {
+ if (!this.lastUpdatedAt && !this.discussion.resolved) {
+ return 'unresolved';
+ }
+ }
+
+ return '';
+ },
+ shouldShowDiscussions() {
+ const isExpanded = this.discussion.expanded;
+ const { diffDiscussion, resolved } = this.transformedDiscussion;
+ const isResolvedNonDiffDiscussion = !diffDiscussion && resolved;
+
+ return isExpanded || this.alwaysExpanded || isResolvedNonDiffDiscussion;
+ },
},
watch: {
isReplying() {
@@ -173,10 +202,6 @@ export default {
}
},
},
- created() {
- this.resolveDiscussionsSvg = resolveDiscussionsSvg;
- this.nextDiscussionsSvg = nextDiscussionsSvg;
- },
methods: {
...mapActions([
'saveNote',
@@ -207,6 +232,9 @@ export default {
toggleDiscussionHandler() {
this.toggleDiscussion({ discussionId: this.discussion.id });
},
+ toggleReplies() {
+ this.isRepliesCollapsed = !this.isRepliesCollapsed;
+ },
showReplyForm() {
this.isReplying = true;
},
@@ -274,26 +302,29 @@ Please check your network connection and try again.`;
</script>
<template>
- <li class="note note-discussion timeline-entry">
+ <li
+ class="note note-discussion timeline-entry"
+ :class="componentClassName"
+ >
<div class="timeline-entry-inner">
- <div class="timeline-icon">
- <user-avatar-link
- v-if="author"
- :link-href="author.path"
- :img-src="author.avatar_url"
- :img-alt="author.name"
- :img-size="40"
- />
- </div>
<div class="timeline-content">
<div
:data-discussion-id="transformedDiscussion.discussion_id"
class="discussion js-discussion-container"
>
<div
- v-if="renderHeader"
- class="discussion-header"
+ v-if="shouldRenderHeader"
+ class="discussion-header note-wrapper"
>
+ <div class="timeline-icon">
+ <user-avatar-link
+ v-if="author"
+ :link-href="author.path"
+ :img-src="author.avatar_url"
+ :img-alt="author.name"
+ :img-size="40"
+ />
+ </div>
<note-header
:author="author"
:created-at="transformedDiscussion.created_at"
@@ -339,7 +370,7 @@ Please check your network connection and try again.`;
/>
</div>
<div
- v-if="discussion.expanded || alwaysExpanded"
+ v-if="shouldShowDiscussions"
class="discussion-body">
<component
:is="wrapperComponent"
@@ -348,45 +379,70 @@ Please check your network connection and try again.`;
>
<div class="discussion-notes">
<ul class="notes">
- <component
- :is="componentName(note)"
- v-for="(note, index) in discussion.notes"
- :key="note.id"
- :note="componentData(note)"
- @handleDeleteNote="deleteNoteHandler"
- >
- <slot
- v-if="index === 0"
- slot="avatar-badge"
- name="avatar-badge"
+ <template v-if="shouldGroupReplies">
+ <component
+ :is="componentName(initialDiscussion)"
+ :note="componentData(initialDiscussion)"
+ @handleDeleteNote="deleteNoteHandler"
>
- </slot>
- </component>
+ <slot
+ slot="avatar-badge"
+ name="avatar-badge"
+ >
+ </slot>
+ </component>
+ <toggle-replies-widget
+ v-if="hasReplies"
+ :collapsed="isRepliesCollapsed"
+ :replies="replies"
+ @toggle="toggleReplies"
+ />
+ <template v-if="!isRepliesCollapsed">
+ <component
+ :is="componentName(note)"
+ v-for="note in replies"
+ :key="note.id"
+ :note="componentData(note)"
+ @handleDeleteNote="deleteNoteHandler"
+ />
+ </template>
+ </template>
+ <template v-else>
+ <component
+ :is="componentName(note)"
+ v-for="(note, index) in discussion.notes"
+ :key="note.id"
+ :note="componentData(note)"
+ @handleDeleteNote="deleteNoteHandler"
+ >
+ <slot
+ v-if="index === 0"
+ slot="avatar-badge"
+ name="avatar-badge"
+ >
+ </slot>
+ </component>
+ </template>
</ul>
<div
+ v-if="!isRepliesCollapsed"
:class="{ 'is-replying': isReplying }"
class="discussion-reply-holder"
>
<template v-if="!isReplying && canReply">
- <div
- class="btn-group d-flex discussion-with-resolve-btn"
- role="group">
- <div
- class="btn-group w-100"
- role="group">
- <button
- type="button"
- class="js-vue-discussion-reply btn btn-text-field mr-2 qa-discussion-reply"
- title="Add a reply"
- @click="showReplyForm">Reply...</button>
- </div>
- <div
- v-if="discussion.resolvable"
- class="btn-group"
- role="group">
+ <div class="discussion-with-resolve-btn">
+ <button
+ type="button"
+ class="js-vue-discussion-reply btn btn-text-field mr-2 qa-discussion-reply"
+ title="Add a reply"
+ @click="showReplyForm"
+ >
+ Reply...
+ </button>
+ <div v-if="discussion.resolvable">
<button
type="button"
- class="btn btn-default"
+ class="btn btn-default mx-sm-2"
@click="resolveHandler()"
>
<i
@@ -414,7 +470,7 @@ Please check your network connection and try again.`;
btn-default discussion-create-issue-btn"
data-container="body"
>
- <span v-html="resolveDiscussionsSvg"></span>
+ <icon name="issue-new" />
</a>
</div>
<div
@@ -428,7 +484,7 @@ Please check your network connection and try again.`;
data-container="body"
@click="jumpToNextDiscussion"
>
- <span v-html="nextDiscussionsSvg"></span>
+ <icon name="comment-next" />
</button>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 40222ac4a80..e302a89ee95 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -173,7 +173,7 @@ export default {
:class="classNameBindings"
:data-award-url="note.toggle_award_path"
:data-note-id="note.id"
- class="note timeline-entry"
+ class="note timeline-entry note-wrapper"
>
<div class="timeline-entry-inner">
<div class="timeline-icon">
@@ -196,6 +196,7 @@ export default {
:author="author"
:created-at="note.created_at"
:note-id="note.id"
+ action-text="commented"
/>
<note-actions
:author-id="author.id"
diff --git a/app/assets/javascripts/notes/components/toggle_replies_widget.vue b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
new file mode 100644
index 00000000000..78ecbbb9247
--- /dev/null
+++ b/app/assets/javascripts/notes/components/toggle_replies_widget.vue
@@ -0,0 +1,94 @@
+<script>
+import _ from 'underscore';
+import Icon from '~/vue_shared/components/icon.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+
+export default {
+ components: {
+ Icon,
+ UserAvatarLink,
+ TimeAgoTooltip,
+ },
+ props: {
+ collapsed: {
+ type: Boolean,
+ required: true,
+ },
+ replies: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ lastReply() {
+ return this.replies[this.replies.length - 1];
+ },
+ uniqueAuthors() {
+ const authors = this.replies.map(reply => reply.author || {});
+
+ return _.uniq(authors, author => author.username);
+ },
+ className() {
+ return this.collapsed ? 'collapsed' : 'expanded';
+ },
+ },
+ methods: {
+ toggle() {
+ this.$emit('toggle');
+ },
+ },
+};
+</script>
+
+<template>
+ <li
+ :class="className"
+ class="replies-toggle"
+ >
+ <template v-if="collapsed">
+ <icon
+ name="chevron-right"
+ @click.native="toggle"
+ />
+ <div>
+ <user-avatar-link
+ v-for="author in uniqueAuthors"
+ :key="author.username"
+ :link-href="author.path"
+ :img-alt="author.name"
+ :img-src="author.avatar_url"
+ :img-size="26"
+ :tooltip-text="author.name"
+ tooltip-placement="bottom"
+ />
+ </div>
+ <button
+ class="btn btn-link js-replies-text"
+ type="button"
+ @click="toggle"
+ >
+ {{ replies.length }} {{ n__('reply', 'replies', replies.length) }}
+ </button>
+ {{ __('Last reply by') }}
+ <a
+ :href="lastReply.author.path"
+ class="btn btn-link author-link"
+ >
+ {{ lastReply.author.name }}
+ </a>
+ <time-ago-tooltip
+ :time="lastReply.created_at"
+ tooltip-placement="bottom"
+ />
+ </template>
+ <span
+ v-else
+ class="collapse-replies-btn js-collapse-replies"
+ @click="toggle"
+ >
+ <icon name="chevron-down" />
+ {{ s__('Notes|Collapse replies') }}
+ </span>
+ </li>
+</template>
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 4b090212d83..ce051582299 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -1,4 +1,4 @@
-/* eslint-disable func-names, one-var, no-var, prefer-rest-params, vars-on-top, prefer-arrow-callback, consistent-return, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, yoda, prefer-spread, no-void, camelcase, no-param-reassign */
+/* eslint-disable func-names, one-var, no-var, prefer-rest-params, vars-on-top, prefer-arrow-callback, consistent-return, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, yoda, prefer-spread, camelcase, no-param-reassign */
/* global Issuable */
/* global emitSidebarEvent */
@@ -696,17 +696,21 @@ UsersSelect.prototype.formatResult = function(user) {
} else {
avatar = gon.default_avatar_url;
}
- return (
- "<div class='user-result " +
- (!user.username ? 'no-username' : void 0) +
- "'> <div class='user-image'><img class='avatar avatar-inline s32' src='" +
- avatar +
- "'></div> <div class='user-name dropdown-menu-user-full-name'>" +
- _.escape(user.name) +
- "</div> <div class='user-username dropdown-menu-user-username'>" +
- (!user.invite ? '@' + _.escape(user.username) : '') +
- '</div> </div>'
- );
+ return `
+ <div class='user-result'>
+ <div class='user-image'>
+ <img class='avatar avatar-inline s32' src='${avatar}'>
+ </div>
+ <div class='user-info'>
+ <div class='user-name dropdown-menu-user-full-name'>
+ ${_.escape(user.name)}
+ </div>
+ <div class='user-username dropdown-menu-user-username text-secondary'>
+ ${!user.invite ? '@' + _.escape(user.username) : ''}
+ </div>
+ </div>
+ </div>
+ `;
};
UsersSelect.prototype.formatSelection = function(user) {
diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
index f56414c3c63..8cb72afcdc0 100644
--- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -10,7 +10,7 @@ export default {
</script>
<template>
- <li class="timeline-entry note">
+ <li class="timeline-entry note note-wrapper">
<div class="timeline-entry-inner">
<div class="timeline-icon">
</div>
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index de3c7a80365..6a44e6a29ed 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -76,7 +76,7 @@ export default {
<li
:id="noteAnchorId"
:class="{ target: isTargetNote }"
- class="note system-note timeline-entry">
+ class="note system-note timeline-entry note-wrapper">
<div class="timeline-entry-inner">
<div
class="timeline-icon"
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
index 7737b9f2697..4cfb1ded0a9 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -15,14 +15,14 @@
*/
+import { GlTooltip } from '@gitlab-org/gitlab-ui';
import defaultAvatarUrl from 'images/no_avatar.png';
import { placeholderImage } from '../../../lazy_loader';
-import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarImage',
- directives: {
- tooltip,
+ components: {
+ GlTooltip,
},
props: {
lazy: {
@@ -73,9 +73,6 @@ export default {
resultantSrcAttribute() {
return this.lazy ? placeholderImage : this.sanitizedSource;
},
- tooltipContainer() {
- return this.tooltipText ? 'body' : null;
- },
avatarSizeClass() {
return `s${this.size}`;
},
@@ -84,22 +81,30 @@ export default {
</script>
<template>
- <img
- v-tooltip
- :class="{
- lazy: lazy,
- [avatarSizeClass]: true,
- [cssClasses]: true
- }"
- :src="resultantSrcAttribute"
- :width="size"
- :height="size"
- :alt="imgAlt"
- :data-src="sanitizedSource"
- :data-container="tooltipContainer"
- :data-placement="tooltipPlacement"
- :title="tooltipText"
- class="avatar"
- data-boundary="window"
- />
+ <span>
+ <img
+ ref="userAvatarImage"
+ :class="{
+ lazy: lazy,
+ [avatarSizeClass]: true,
+ [cssClasses]: true
+ }"
+ :src="resultantSrcAttribute"
+ :width="size"
+ :height="size"
+ :alt="imgAlt"
+ :data-src="sanitizedSource"
+ class="avatar"
+ />
+ <gl-tooltip
+ :target="() => $refs.userAvatarImage"
+ :placement="tooltipPlacement"
+ boundary="window"
+ class="js-user-avatar-image-toolip"
+ >
+ <slot>
+ {{ tooltipText }}
+ </slot>
+ </gl-tooltip>
+ </span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
index dd6f96e2609..351a639c6e8 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue
@@ -17,9 +17,8 @@
*/
-import { GlLink } from '@gitlab-org/gitlab-ui';
+import { GlLink, GlTooltipDirective } from '@gitlab-org/gitlab-ui';
import userAvatarImage from './user_avatar_image.vue';
-import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarLink',
@@ -28,7 +27,7 @@ export default {
userAvatarImage,
},
directives: {
- tooltip,
+ GlTooltip: GlTooltipDirective,
},
props: {
linkHref: {
@@ -94,11 +93,14 @@ export default {
:size="imgSize"
:tooltip-text="avatarTooltipText"
:tooltip-placement="tooltipPlacement"
- /><span
+ >
+ <slot></slot>
+ </user-avatar-image><span
v-if="shouldShowUsername"
- v-tooltip
+ v-gl-tooltip
:title="tooltipText"
:tooltip-placement="tooltipPlacement"
+ class="js-user-avatar-link-username"
>{{ username }}</span><slot name="avatar-badge"></slot>
</gl-link>
</template>
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 702276780e9..7a95db5976d 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -148,10 +148,7 @@
.award-control-icon svg {
background: $award-emoji-positive-add-bg;
-
- path {
- fill: $award-emoji-positive-add-lines;
- }
+ fill: $award-emoji-positive-add-lines;
}
.award-control-icon-neutral {
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index c4296c7a88a..219fd99b097 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -207,6 +207,10 @@
@include btn-with-margin;
}
+ &.btn-icon {
+ color: $gl-gray-700;
+ }
+
.fa-caret-down,
.fa-chevron-down {
margin-left: 5px;
@@ -218,6 +222,25 @@
}
}
+ &.btn-text-field {
+ width: 100%;
+ text-align: left;
+ padding: 6px 16px;
+ border-color: $border-color;
+ color: $gray-darkest;
+ background-color: $gray-light;
+
+ &:hover,
+ &:active,
+ &:focus {
+ cursor: text;
+ box-shadow: none;
+ border-color: lighten($blue-300, 20%);
+ color: $gray-darkest;
+ background-color: $gray-light;
+ }
+ }
+
&.dot-highlight::after {
content: '';
background-color: $blue-500;
@@ -335,25 +358,6 @@
}
}
-.btn-text-field {
- width: 100%;
- text-align: left;
- padding: 6px 16px;
- border-color: $border-color;
- color: $gray-darkest;
- background-color: $gray-light;
-
- &:hover,
- &:active,
- &:focus {
- cursor: text;
- box-shadow: none;
- border-color: lighten($blue-300, 20%);
- color: $gray-darkest;
- background-color: $gray-light;
- }
-}
-
.btn-build {
margin-left: 10px;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index fa753b13e5f..626c8f92d1d 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -33,6 +33,11 @@
color: $brand-danger;
}
+.text-danger-muted,
+.text-danger-muted:hover {
+ color: $red-300;
+}
+
.text-warning,
.text-warning:hover {
color: $brand-warning;
@@ -345,6 +350,7 @@ img.emoji {
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-2 { margin-top: 2px; }
+.prepend-top-4 { margin-top: $gl-padding-4; }
.prepend-top-5 { margin-top: 5px; }
.prepend-top-8 { margin-top: $grid-size; }
.prepend-top-10 { margin-top: 10px; }
@@ -365,6 +371,7 @@ img.emoji {
.append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right: 20px; }
.append-bottom-0 { margin-bottom: 0; }
+.append-bottom-4 { margin-bottom: $gl-padding-4; }
.append-bottom-5 { margin-bottom: 5px; }
.append-bottom-8 { margin-bottom: $grid-size; }
.append-bottom-10 { margin-bottom: 10px; }
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 53f198b47c6..6bdcb20210b 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -36,7 +36,6 @@
text-align: left;
padding: 10px $gl-padding;
word-wrap: break-word;
- border-radius: $border-radius-default $border-radius-default 0 0;
&.file-title-clear {
padding-left: 0;
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 381c0290d32..7f0edd88dfb 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -243,6 +243,8 @@
.user-result {
min-height: 24px;
+ display: flex;
+ align-items: center;
.user-image {
float: left;
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index dfb145debe7..4a311da1675 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -1,7 +1,7 @@
.timeline {
- @include basic-list;
margin: 0;
padding: 0;
+ list-style: none;
&::before {
@include notes-media('max', map-get($grid-breakpoints, sm)) {
@@ -26,10 +26,8 @@
}
.timeline-entry {
- border-color: $white-normal;
color: $gl-text-color;
- border-bottom: 1px solid $border-white-light;
- background: $white-light;
+ background-color: $white-light;
.timeline-entry-inner {
position: relative;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index bfcac3f1c3f..f4540146a25 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -195,6 +195,7 @@ $well-light-text-color: #5b6169;
* Text
*/
$gl-font-size: 14px;
+$gl-font-size-xs: 11px;
$gl-font-size-small: 12px;
$gl-font-weight-normal: 400;
$gl-font-weight-bold: 600;
@@ -440,7 +441,7 @@ $ci-skipped-color: #888;
* Boards
*/
$issue-boards-font-size: 14px;
-$issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
+$issue-boards-card-shadow: rgba(0, 0, 0, 0.1);
/*
The following heights are used in boards.scss and are used for calculation of the board height.
They probably should be derived in a smarter way.
@@ -640,3 +641,8 @@ Modals
$modal-body-height: 134px;
$priority-label-empty-state-width: 114px;
+
+/*
+Issues Analytics
+*/
+$issues-analytics-popover-boarder-color: rgba(0, 0, 0, 0.15);
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 54fbd40cece..c6074eb9df4 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -90,20 +90,14 @@
}
.with-performance-bar & {
- height: calc(
- 100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height}
- );
+ height: calc(100vh - #{$issue-board-list-difference-xs} - #{$performance-bar-height});
@include media-breakpoint-only(sm) {
- height: calc(
- 100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height}
- );
+ height: calc(100vh - #{$issue-board-list-difference-sm} - #{$performance-bar-height});
}
@include media-breakpoint-up(md) {
- height: calc(
- 100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height}
- );
+ height: calc(100vh - #{$issue-board-list-difference-md} - #{$performance-bar-height});
}
}
}
@@ -271,7 +265,7 @@
height: 100%;
width: 100%;
margin-bottom: 0;
- padding: 5px;
+ padding: $gl-padding-4;
list-style: none;
overflow-y: auto;
overflow-x: hidden;
@@ -284,14 +278,16 @@
.board-card {
position: relative;
- padding: 11px 10px 11px $gl-padding;
+ padding: $gl-padding;
background: $white-light;
border-radius: $border-radius-default;
+ border: 1px solid $theme-gray-200;
box-shadow: 0 1px 2px $issue-boards-card-shadow;
list-style: none;
+ line-height: $gl-padding;
&:not(:last-child) {
- margin-bottom: 5px;
+ margin-bottom: $gl-padding-8;
}
&.is-active,
@@ -302,113 +298,120 @@
.badge {
border: 0;
outline: 0;
+
+ &:hover {
+ text-decoration: underline;
+ }
+
+ @include media-breakpoint-down(lg) {
+ font-size: $gl-font-size-xs;
+ padding-left: $gl-padding-4;
+ padding-right: $gl-padding-4;
+ font-weight: $gl-font-weight-bold;
+ }
+ }
+
+ svg {
+ vertical-align: top;
}
.confidential-icon {
- vertical-align: text-top;
- margin-right: 5px;
+ color: $orange-600;
+ cursor: help;
+ }
+
+ @include media-breakpoint-down(md) {
+ padding: $gl-padding-8;
}
}
.board-card-title {
@include overflow-break-word();
- margin: 0 30px 0 0;
font-size: 1em;
- line-height: inherit;
a {
color: $gl-text-color;
- margin-right: 2px;
+ }
+
+ @include media-breakpoint-down(md) {
+ font-size: $label-font-size;
}
}
.board-card-header {
display: flex;
- min-height: 20px;
-
- .board-card-assignee {
- display: flex;
- justify-content: flex-end;
- position: absolute;
- right: 15px;
- height: 20px;
- width: 20px;
+}
- .avatar-counter {
- display: none;
- vertical-align: middle;
- min-width: 20px;
- line-height: 19px;
- height: 20px;
- padding-left: 2px;
- padding-right: 2px;
- border-radius: 2em;
- }
+.board-card-assignee {
+ display: flex;
+ margin-top: -$gl-padding-4;
+ margin-bottom: -$gl-padding-4;
+
+ .avatar-counter {
+ vertical-align: middle;
+ line-height: $gl-padding-24;
+ min-width: $gl-padding-24;
+ height: $gl-padding-24;
+ border-radius: $gl-padding-24;
+ background-color: $gl-text-color-tertiary;
+ font-size: $gl-font-size-xs;
+ cursor: help;
+ font-weight: $gl-font-weight-bold;
+ margin-left: -$gl-padding-4;
+ border: 0;
+ padding: 0 $gl-padding-4;
- img {
- vertical-align: top;
+ @include media-breakpoint-down(md) {
+ min-width: auto;
+ height: $gl-padding;
+ border-radius: $gl-padding;
+ line-height: $gl-padding;
}
+ }
- a {
- position: relative;
- margin-left: -15px;
- }
+ img {
+ vertical-align: top;
+ }
- a:nth-child(1) {
- z-index: 3;
- }
+ .user-avatar-link:not(:only-child) {
+ margin-left: -$gl-padding-4;
- a:nth-child(2) {
+ &:nth-of-type(1) {
z-index: 2;
}
- a:nth-child(3) {
+ &:nth-of-type(2) {
z-index: 1;
}
+ }
- a:nth-child(4) {
- display: none;
- }
-
- &:hover {
- .avatar-counter {
- display: inline-block;
- }
-
- a {
- position: static;
- background-color: $white-light;
- transition: background-color 0s;
- margin-left: auto;
-
- &:nth-child(4) {
- display: block;
- }
+ .avatar {
+ margin: 0;
- &:first-child:not(:only-child) {
- box-shadow: -10px 0 10px 1px $white-light;
- }
- }
+ @include media-breakpoint-down(md) {
+ width: $gl-padding;
+ height: $gl-padding;
}
}
- .avatar {
- margin: 0;
+ @include media-breakpoint-down(md) {
+ margin-top: 0;
+ margin-bottom: 0;
}
}
-.board-card-footer {
- margin: 0 0 5px;
+.board-card-number {
+ font-size: $gl-font-size-xs;
+ color: $gl-text-color-secondary;
+ overflow: hidden;
- .badge {
- margin-top: 5px;
- margin-right: 6px;
+ @include media-breakpoint-up(md) {
+ font-size: $label-font-size;
}
}
-.board-card-number {
- font-size: 12px;
- color: $gl-text-color-secondary;
+.board-card-number-container {
+ overflow: hidden;
}
.issue-boards-search {
@@ -474,8 +477,7 @@
.right-sidebar.right-sidebar-expanded {
&.boards-sidebar-slide-enter-active,
&.boards-sidebar-slide-leave-active {
- transition: width $sidebar-transition-duration,
- padding $sidebar-transition-duration;
+ transition: width $sidebar-transition-duration, padding $sidebar-transition-duration;
}
&.boards-sidebar-slide-enter,
@@ -650,3 +652,36 @@
}
}
}
+
+.board-card-info {
+ color: $gl-text-color-secondary;
+ white-space: nowrap;
+ margin-right: $gl-padding-8;
+
+ &:not(.board-card-weight) {
+ cursor: help;
+ }
+
+ &.board-card-weight {
+ color: $gl-text-color;
+ cursor: pointer;
+
+ &:hover {
+ color: initial;
+ text-decoration: underline;
+ }
+ }
+
+ .board-card-info-icon {
+ color: $theme-gray-600;
+ margin-right: $gl-padding-4;
+ }
+
+ @include media-breakpoint-down(md) {
+ font-size: $label-font-size;
+ }
+}
+
+.board-issue-path.js-show-tooltip {
+ cursor: help;
+}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 19bc4262e21..6d998fa1e07 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -59,6 +59,7 @@
margin: 0;
padding: 0;
table-layout: fixed;
+ border-radius: 0 0 $border-radius-default $border-radius-default;
.diff-line-num {
width: 50px;
@@ -859,7 +860,7 @@
}
.diff-file .note-container > .new-note,
-.note-container .discussion-notes {
+.note-container .discussion-notes.diff-discussions {
margin-left: 100px;
border-left: 1px solid $white-normal;
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index c60bb360a03..855d73a9939 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -239,6 +239,7 @@
.discussion-reply-holder {
background-color: $white-light;
padding: 10px 16px;
+ border-radius: 0 0 $border-radius-default $border-radius-default;
&.is-replying {
padding-bottom: $gl-padding;
@@ -247,10 +248,15 @@
}
.discussion-with-resolve-btn {
+ @include media-breakpoint-up(sm) {
+ display: flex;
+ }
+
+
.discussion-actions {
display: table;
- .btn-default path {
+ svg {
fill: $gray-darkest;
}
@@ -270,6 +276,12 @@
.btn {
width: 100%;
}
+
+ .btn-text-field {
+ @include media-breakpoint-down(xs) {
+ margin-bottom: $gl-padding-8;
+ }
+ }
}
.discussion-notes-count {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index be535ade0a6..dcb1275d182 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -1,26 +1,135 @@
-/**
- * Notes
- */
+$system-note-icon-size: 32px;
+$system-note-svg-size: 16px;
+$note-form-margin-left: 70px;
-@-webkit-keyframes targe3-note {
- from {
- background: $note-targe3-outside;
+@mixin vertical-line($left) {
+ &::before {
+ content: '';
+ border-left: 2px solid $theme-gray-100;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: $left;
}
+}
- 50% {
- background: $note-targe3-inside;
- }
+.note-wrapper {
+ padding: $gl-padding;
+}
+
+.issuable-discussion {
+ .notes.timeline > .timeline-entry {
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+ margin: $gl-padding 0;
+
+ &.system-note,
+ &.note-form {
+ border: 0;
+ }
+
+ &.note-form {
+ margin-left: 0;
- to {
- background: $note-targe3-outside;
+ @include notes-media('min', map-get($grid-breakpoints, md)) {
+ margin-left: $note-form-margin-left;
+ }
+
+ .timeline-icon {
+ @include notes-media('min', map-get($grid-breakpoints, sm)) {
+ margin-left: -$note-icon-gutter-width;
+ }
+ }
+
+ .timeline-content {
+ margin-left: 0;
+ }
+ }
+
+ .notes_content {
+ border: 0;
+ border-top: 1px solid $border-color;
+ }
}
}
-ul.notes {
+.main-notes-list {
+ @include vertical-line(39px);
+}
+
+.notes {
display: block;
list-style: none;
margin: 0;
padding: 0;
+ position: relative;
+
+ > .note-discussion {
+ .card {
+ border: 0;
+ }
+
+ li.note {
+ border-bottom: 1px solid $border-color;
+
+ &:first-child {
+ border-radius: $border-radius-default $border-radius-default 0 0;
+ }
+ }
+ }
+
+ .replies-toggle {
+ background-color: $gray-light;
+ padding: $gl-padding-8 $gl-padding;
+
+ .collapse-replies-btn:hover {
+ color: $blue-600;
+ }
+
+ &.expanded {
+ border-bottom: 1px solid $border-color;
+
+ span {
+ cursor: pointer;
+ }
+
+ svg {
+ position: relative;
+ top: 3px;
+ }
+ }
+
+ &.collapsed {
+ color: $gl-text-color-secondary;
+
+ svg {
+ float: left;
+ position: relative;
+ top: $gl-padding-4;
+ margin-right: $gl-padding-8;
+ cursor: pointer;
+ }
+
+ img {
+ margin: -2px 4px 0 0;
+ }
+
+ .author-link {
+ color: $gl-text-color;
+ }
+ }
+
+ .user-avatar-link {
+ &:last-child img {
+ margin-right: $gl-padding-8;
+ }
+ }
+
+ .btn-link {
+ border: 0;
+ vertical-align: baseline;
+ }
+ }
.note-created-ago,
.note-updated-at {
@@ -28,8 +137,6 @@ ul.notes {
}
.discussion-body {
- padding-top: 8px;
-
.card {
margin-bottom: 0;
}
@@ -46,21 +153,10 @@ ul.notes {
}
> li {
- // .timeline-entry
- padding: 0;
display: block;
position: relative;
border-bottom: 0;
- @include notes-media('min', map-get($grid-breakpoints, sm)) {
- padding-left: $note-icon-gutter-width;
- }
-
- .timeline-entry-inner {
- padding: $gl-padding $gl-btn-padding;
- border-bottom: 1px solid $white-normal;
- }
-
&:target,
&.target {
border-bottom: 1px solid $white-normal;
@@ -75,23 +171,10 @@ ul.notes {
}
}
- .timeline-icon {
- @include notes-media('min', map-get($grid-breakpoints, sm)) {
- margin-left: -$note-icon-gutter-width;
- }
- }
-
- .timeline-content {
- margin-left: $note-icon-gutter-width;
-
- @include notes-media('min', map-get($grid-breakpoints, sm)) {
- margin-left: 0;
- }
- }
-
&.being-posted {
pointer-events: none;
opacity: 0.5;
+ padding: $gl-padding;
.dummy-avatar {
background-color: $gl-gray-200;
@@ -104,12 +187,6 @@ ul.notes {
}
}
- &.note-discussion {
- .timeline-entry-inner {
- padding: $gl-padding 10px;
- }
- }
-
.editing-spinner {
display: none;
}
@@ -191,8 +268,9 @@ ul.notes {
}
.system-note {
- font-size: 14px;
- clear: both;
+ padding: 6px $gl-padding-24;
+ margin: $gl-padding-24 0;
+ background-color: transparent;
.note-header-info {
padding-bottom: 0;
@@ -225,17 +303,21 @@ ul.notes {
.timeline-icon {
float: left;
-
- @include notes-media('min', map-get($grid-breakpoints, sm)) {
- margin-left: 0;
- width: auto;
- }
+ display: flex;
+ align-items: center;
+ background-color: $white-light;
+ width: $system-note-icon-size;
+ height: $system-note-icon-size;
+ border: 1px solid $border-color;
+ border-radius: $system-note-icon-size;
+ margin: -6px $gl-padding 0 0;
svg {
- width: 16px;
- height: 16px;
+ width: $system-note-svg-size;
+ height: $system-note-svg-size;
fill: $gray-darkest;
- margin-top: 2px;
+ display: block;
+ margin: 0 auto;
}
}
@@ -302,10 +384,17 @@ ul.notes {
.discussion-body .diff-file {
.file-title {
cursor: default;
+ line-height: 42px;
+ padding: 0 $gl-padding;
+ border-top: 1px solid $border-color;
&:hover {
background-color: $gray-light;
}
+
+ .btn-clipboard {
+ top: 10px;
+ }
}
.line_content {
@@ -320,6 +409,23 @@ ul.notes {
}
}
+ .discussion-notes {
+ &:not(:first-child) {
+ border-top: 1px solid $white-normal;
+ margin-top: 20px;
+ }
+
+ &:not(:last-child) {
+ border-bottom: 1px solid $white-normal;
+ margin-bottom: 20px;
+ }
+
+ .system-note {
+ margin: 0;
+ padding: $gl-padding;
+ }
+ }
+
// Merge request notes in diffs
// Diff is inline
.notes_content .note-header .note-headline-light {
@@ -335,7 +441,6 @@ ul.notes {
border-left: 0;
&.notes_content {
- background-color: $gray-light;
border-width: 1px 0;
padding: 0;
vertical-align: top;
@@ -349,18 +454,6 @@ ul.notes {
}
}
- .discussion-notes {
- &:not(:first-child) {
- border-top: 1px solid $white-normal;
- margin-top: 20px;
- }
-
- &:not(:last-child) {
- border-bottom: 1px solid $white-normal;
- margin-bottom: 20px;
- }
- }
-
.notes {
background-color: $white-light;
}
@@ -374,6 +467,30 @@ ul.notes {
}
}
+.diffs {
+ .discussion-notes {
+ margin-left: 0;
+ border-left: 0;
+
+ .notes {
+ position: relative;
+ @include vertical-line(52px);
+ }
+ }
+
+ .note-wrapper {
+ margin: $gl-padding;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+ }
+
+ .discussion-reply-holder {
+ border-radius: 0 0 $border-radius-default $border-radius-default;
+ border-top: 1px solid $border-color;
+ position: relative;
+ }
+}
+
.discussion-header,
.note-header-info {
a {
@@ -399,7 +516,17 @@ ul.notes {
}
.discussion-header {
- font-size: 14px;
+ min-height: 72px;
+
+ .note-header-info {
+ padding-bottom: 0;
+ }
+}
+
+.unresolved {
+ .note-header-info {
+ margin-top: $gl-padding-8;
+ }
}
.note-header {
@@ -409,7 +536,7 @@ ul.notes {
.note-header-info {
min-width: 0;
- padding-bottom: 8px;
+ padding-bottom: $gl-padding-8;
&.discussion {
padding-bottom: 0;
@@ -471,9 +598,18 @@ ul.notes {
margin-left: 10px;
color: $gray-darkest;
+ @include media-breakpoint-down(xs) {
+ width: 100%;
+ margin: $gl-padding-8 0;
+ }
+
.btn-group > .discussion-next-btn {
margin-left: -1px;
}
+
+ svg {
+ height: 15px;
+ }
}
.note-actions {
@@ -585,19 +721,6 @@ ul.notes {
z-index: 10;
}
-.discussion-body,
-.diff-file {
- .notes .note {
- border-bottom: 1px solid $white-normal;
-
- .timeline-entry-inner {
- padding-left: $gl-padding;
- padding-right: $gl-padding;
- border-bottom: 0;
- }
- }
-}
-
.disabled-comment {
background-color: $gray-light;
border-radius: $border-radius-base;
@@ -634,7 +757,7 @@ ul.notes {
}
.btn {
- svg path {
+ svg {
fill: $gray-darkest;
}
@@ -659,7 +782,7 @@ ul.notes {
.line-resolve-all {
vertical-align: middle;
display: inline-block;
- padding: 5px 10px 6px;
+ padding: 6px 10px;
background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
diff --git a/app/controllers/chaos_controller.rb b/app/controllers/chaos_controller.rb
new file mode 100644
index 00000000000..b4f46cddbe9
--- /dev/null
+++ b/app/controllers/chaos_controller.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+class ChaosController < ActionController::Base
+ before_action :validate_request
+
+ def leakmem
+ memory_mb = (params[:memory_mb]&.to_i || 100)
+ duration_s = (params[:duration_s]&.to_i || 30).seconds
+
+ start = Time.now
+ retainer = []
+ # Add `n` 1mb chunks of memory to the retainer array
+ memory_mb.times { retainer << "x" * 1.megabyte }
+
+ duration_taken = (Time.now - start).seconds
+ Kernel.sleep duration_s - duration_taken if duration_s > duration_taken
+
+ render text: "OK", content_type: 'text/plain'
+ end
+
+ def cpuspin
+ duration_s = (params[:duration_s]&.to_i || 30).seconds
+ end_time = Time.now + duration_s.seconds
+
+ rand while Time.now < end_time
+
+ render text: "OK", content_type: 'text/plain'
+ end
+
+ def sleep
+ duration_s = (params[:duration_s]&.to_i || 30).seconds
+ Kernel.sleep duration_s
+
+ render text: "OK", content_type: 'text/plain'
+ end
+
+ def kill
+ Process.kill("KILL", Process.pid)
+ end
+
+ private
+
+ def validate_request
+ secret = ENV['GITLAB_CHAOS_SECRET']
+ # GITLAB_CHAOS_SECRET is required unless you're running in Development mode
+ if !secret && !Rails.env.development?
+ render text: "chaos misconfigured: please configure GITLAB_CHAOS_SECRET when using GITLAB_ENABLE_CHAOS_ENDPOINTS outside of a development environment", content_type: 'text/plain', status: 500
+ end
+
+ return unless secret
+
+ unless request.headers["HTTP_X_CHAOS_SECRET"] == secret
+ render text: "To experience chaos, please set X-Chaos-Secret header", content_type: 'text/plain', status: 401
+ end
+ end
+end
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index f644702cbdb..b3777fd2b0f 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -86,10 +86,10 @@ module CreatesCommit
def new_merge_request_path
project_new_merge_request_path(
@project_to_commit_into,
- merge_request_source_branch: @branch_name,
merge_request: {
source_project_id: @project_to_commit_into.id,
target_project_id: @project.id,
+ source_branch: @branch_name,
target_branch: @start_branch
}
)
diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb
index fdd1078cdf7..575c40d5f6f 100644
--- a/app/controllers/import/bitbucket_server_controller.rb
+++ b/app/controllers/import/bitbucket_server_controller.rb
@@ -54,14 +54,14 @@ class Import::BitbucketServerController < Import::BaseController
# rubocop: disable CodeReuse/ActiveRecord
def status
- repos = bitbucket_client.repos
+ @collection = bitbucket_client.repos(page_offset: page_offset, limit: limit_per_page)
+ @repos, @incompatible_repos = @collection.partition { |repo| repo.valid? }
- @repos, @incompatible_repos = repos.partition { |repo| repo.valid? }
-
- @already_added_projects = find_already_added_projects('bitbucket_server')
+ # Use the import URL to filter beyond what BaseService#find_already_added_projects
+ @already_added_projects = filter_added_projects('bitbucket_server', @repos.map(&:browse_url))
already_added_projects_names = @already_added_projects.pluck(:import_source)
- @repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.browse_url) }
+ @repos.reject! { |repo| already_added_projects_names.include?(repo.browse_url) }
rescue BitbucketServer::Connection::ConnectionError, BitbucketServer::Client::ServerError => e
flash[:alert] = "Unable to connect to server: #{e}"
clear_session_data
@@ -75,6 +75,12 @@ class Import::BitbucketServerController < Import::BaseController
private
+ # rubocop: disable CodeReuse/ActiveRecord
+ def filter_added_projects(import_type, import_sources)
+ current_user.created_projects.where(import_type: import_type, import_source: import_sources).includes(:import_state)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
def bitbucket_client
@bitbucket_client ||= BitbucketServer::Client.new(credentials)
end
@@ -130,4 +136,12 @@ class Import::BitbucketServerController < Import::BaseController
password: session[personal_access_token_key]
}
end
+
+ def page_offset
+ [0, params[:page].to_i].max
+ end
+
+ def limit_per_page
+ BitbucketServer::Paginator::PAGE_LENGTH
+ end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 2a6fe3b9c97..c02ec407262 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -122,7 +122,7 @@ class Projects::BlobController < Projects::ApplicationController
@lines.map! do |line|
# These are marked as context lines but are loaded from blobs.
# We also have context lines loaded from diffs in other places.
- diff_line = Gitlab::Diff::Line.new(line, nil, nil, nil, nil)
+ diff_line = Gitlab::Diff::Line.new(line, 'context', nil, nil, nil)
diff_line.rich_text = line
diff_line
end
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index bbf662a63c8..5639402a1e9 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -89,8 +89,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
def build_merge_request
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
- params[:merge_request][:source_branch] ||= params[:merge_request_source_branch].presence
-
@merge_request = ::MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute
end
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 740f41c0642..5307cd0720a 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -22,12 +22,6 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
def render_diffs
@environment = @merge_request.environments_for(current_user).last
- notes_grouped_by_path = @notes.group_by { |note| note.position.file_path }
-
- @diffs.diff_files.each do |diff_file|
- notes = notes_grouped_by_path.fetch(diff_file.file_path, [])
- notes.each { |note| diff_file.unfold_diff_lines(note.position) }
- end
@diffs.write_cache
diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb
index 57e397f6ca0..9ece8b0bc5b 100644
--- a/app/helpers/compare_helper.rb
+++ b/app/helpers/compare_helper.rb
@@ -13,8 +13,8 @@ module CompareHelper
def create_mr_path(from = params[:from], to = params[:to], project = @project)
project_new_merge_request_path(
project,
- merge_request_source_branch: to,
merge_request: {
+ source_branch: to,
target_branch: from
}
)
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 8f549bfce73..23d7aa427bb 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -11,10 +11,10 @@ module MergeRequestsHelper
def new_mr_from_push_event(event, target_project)
{
- merge_request_source_branch: event.branch_name,
merge_request: {
source_project_id: event.project.id,
target_project_id: target_project.id,
+ source_branch: event.branch_name,
target_branch: target_project.repository.root_ref
}
}
@@ -51,10 +51,10 @@ module MergeRequestsHelper
def mr_change_branches_path(merge_request)
project_new_merge_request_path(
@project,
- merge_request_source_branch: merge_request.source_branch,
merge_request: {
source_project_id: merge_request.source_project_id,
target_project_id: merge_request.target_project_id,
+ source_branch: merge_request.source_branch,
target_branch: merge_request.target_branch
},
change_branches: true
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index d9713f9c9b0..0a7f930110a 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -540,4 +540,13 @@ module ProjectsHelper
network
]
end
+
+ def sidebar_operations_paths
+ %w[
+ environments
+ clusters
+ user
+ gcp
+ ]
+ end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 5c1168e8ef5..889f8ce27a6 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -221,9 +221,7 @@ module Ci
build.deployment&.drop
- next if build.retries_max.zero?
-
- if build.retries_count < build.retries_max
+ if build.retry_failure?
begin
Ci::Build.retry(build, build.user)
rescue Gitlab::Access::AccessDeniedError => ex
@@ -321,7 +319,17 @@ module Ci
end
def retries_max
- self.options.to_h.fetch(:retry, 0).to_i
+ normalized_retry.fetch(:max, 0)
+ end
+
+ def retry_when
+ normalized_retry.fetch(:when, ['always'])
+ end
+
+ def retry_failure?
+ return false if retries_max.zero? || retries_count >= retries_max
+
+ retry_when.include?('always') || retry_when.include?(failure_reason.to_s)
end
def latest?
@@ -886,6 +894,16 @@ module Ci
options&.dig(:environment, :url) || persisted_environment&.external_url
end
+ # The format of the retry option changed in GitLab 11.5: Before it was
+ # integer only, after it is a hash. New builds are created with the new
+ # format, but builds created before GitLab 11.5 and saved in database still
+ # have the old integer only format. This method returns the retry option
+ # normalized as a hash in 11.5+ format.
+ def normalized_retry
+ value = options&.dig(:retry)
+ value.is_a?(Integer) ? { max: value } : value.to_h
+ end
+
def build_attributes_from_config
return {} unless pipeline.config_processor
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
new file mode 100644
index 00000000000..8adb99fcb04
--- /dev/null
+++ b/app/models/clusters/applications/knative.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class Knative < ActiveRecord::Base
+ VERSION = '0.1.3'.freeze
+ REPOSITORY = 'https://storage.googleapis.com/triggermesh-charts'.freeze
+
+ # This is required for helm version <= 2.10.x in order to support
+ # Setting up CRDs
+ ISTIO_CRDS = 'https://storage.googleapis.com/triggermesh-charts/istio-crds.yaml'.freeze
+
+ self.table_name = 'clusters_applications_knative'
+
+ include ::Clusters::Concerns::ApplicationCore
+ include ::Clusters::Concerns::ApplicationStatus
+ include ::Clusters::Concerns::ApplicationVersion
+ include ::Clusters::Concerns::ApplicationData
+
+ default_value_for :version, VERSION
+
+ validates :hostname, presence: true, hostname: true
+
+ def chart
+ 'knative/knative'
+ end
+
+ def values
+ { "domain" => hostname }.to_yaml
+ end
+
+ def install_command
+ Gitlab::Kubernetes::Helm::InstallCommand.new(
+ name: name,
+ version: VERSION,
+ rbac: cluster.platform_kubernetes_rbac?,
+ chart: chart,
+ files: files,
+ repository: REPOSITORY,
+ preinstall: install_script
+ )
+ end
+
+ private
+
+ def install_script
+ ["/usr/bin/kubectl apply -f #{ISTIO_CRDS} >/dev/null"]
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index e80d35d0f3c..48d6c0daa0f 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -12,7 +12,8 @@ module Clusters
Applications::Ingress.application_name => Applications::Ingress,
Applications::Prometheus.application_name => Applications::Prometheus,
Applications::Runner.application_name => Applications::Runner,
- Applications::Jupyter.application_name => Applications::Jupyter
+ Applications::Jupyter.application_name => Applications::Jupyter,
+ Applications::Knative.application_name => Applications::Knative
}.freeze
DEFAULT_ENVIRONMENT = '*'.freeze
@@ -35,6 +36,7 @@ module Clusters
has_one :application_prometheus, class_name: 'Clusters::Applications::Prometheus'
has_one :application_runner, class_name: 'Clusters::Applications::Runner'
has_one :application_jupyter, class_name: 'Clusters::Applications::Jupyter'
+ has_one :application_knative, class_name: 'Clusters::Applications::Knative'
has_many :kubernetes_namespaces
has_one :kubernetes_namespace, -> { order(id: :desc) }, class_name: 'Clusters::KubernetesNamespace'
@@ -100,7 +102,8 @@ module Clusters
application_ingress || build_application_ingress,
application_prometheus || build_application_prometheus,
application_runner || build_application_runner,
- application_jupyter || build_application_jupyter
+ application_jupyter || build_application_jupyter,
+ application_knative || build_application_knative
]
end
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index 008e08d9914..d69038be532 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -26,6 +26,7 @@ module Clusters
algorithm: 'aes-256-cbc'
before_validation :enforce_namespace_to_lower_case
+ before_validation :enforce_ca_whitespace_trimming
validates :namespace,
allow_blank: true,
@@ -201,6 +202,11 @@ module Clusters
self.namespace = self.namespace&.downcase
end
+ def enforce_ca_whitespace_trimming
+ self.ca_pem = self.ca_pem&.strip
+ self.token = self.token&.strip
+ end
+
def prevent_modification
return unless managed?
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index c32008aa9c7..5f59e4832db 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -66,10 +66,6 @@ class DiffNote < Note
self.original_position.diff_refs == diff_refs
end
- def discussion_first_note?
- self == discussion.first_note
- end
-
private
def enqueue_diff_file_creation_job
@@ -82,33 +78,26 @@ class DiffNote < Note
end
def should_create_diff_file?
- on_text? && note_diff_file.nil? && discussion_first_note?
+ on_text? && note_diff_file.nil? && self == discussion.first_note
end
def fetch_diff_file
- file =
- if note_diff_file
- diff = Gitlab::Git::Diff.new(note_diff_file.to_hash)
- Gitlab::Diff::File.new(diff,
- repository: project.repository,
- diff_refs: original_position.diff_refs)
- elsif created_at_diff?(noteable.diff_refs)
- # We're able to use the already persisted diffs (Postgres) if we're
- # presenting a "current version" of the MR discussion diff.
- # So no need to make an extra Gitaly diff request for it.
- # As an extra benefit, the returned `diff_file` already
- # has `highlighted_diff_lines` data set from Redis on
- # `Diff::FileCollection::MergeRequestDiff`.
- noteable.diffs(original_position.diff_options).diff_files.first
- else
- original_position.diff_file(self.project.repository)
- end
-
- # Since persisted diff files already have its content "unfolded"
- # there's no need to make it pass through the unfolding process.
- file&.unfold_diff_lines(position) unless note_diff_file
-
- file
+ if note_diff_file
+ diff = Gitlab::Git::Diff.new(note_diff_file.to_hash)
+ Gitlab::Diff::File.new(diff,
+ repository: project.repository,
+ diff_refs: original_position.diff_refs)
+ elsif created_at_diff?(noteable.diff_refs)
+ # We're able to use the already persisted diffs (Postgres) if we're
+ # presenting a "current version" of the MR discussion diff.
+ # So no need to make an extra Gitaly diff request for it.
+ # As an extra benefit, the returned `diff_file` already
+ # has `highlighted_diff_lines` data set from Redis on
+ # `Diff::FileCollection::MergeRequestDiff`.
+ noteable.diffs(original_position.diff_options).diff_files.first
+ else
+ original_position.diff_file(self.project.repository)
+ end
end
def supported?
diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb
index d59cb43dea4..7126bb66d80 100644
--- a/app/models/project_import_state.rb
+++ b/app/models/project_import_state.rb
@@ -56,4 +56,17 @@ class ProjectImportState < ActiveRecord::Base
end
end
end
+
+ def mark_as_failed(error_message)
+ original_errors = errors.dup
+ sanitized_message = Gitlab::UrlSanitizer.sanitize(error_message)
+
+ fail_op
+
+ update_column(:last_error, sanitized_message)
+ rescue ActiveRecord::ActiveRecordError => e
+ Rails.logger.error("Error setting import status to failed: #{e.message}. Original error: #{sanitized_message}")
+ ensure
+ @errors = original_errors
+ end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index ee5579329a8..6e179f61a7b 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1014,6 +1014,18 @@ class Repository
message: merge_request.title)
end
+ def update_submodule(user, submodule, commit_sha, message:, branch:)
+ with_cache_hooks do
+ raw.update_submodule(
+ user: user,
+ submodule: submodule,
+ commit_sha: commit_sha,
+ branch: branch,
+ message: message
+ )
+ end
+ end
+
def blob_data_at(sha, path)
blob = blob_at(sha, path)
return unless blob
diff --git a/app/serializers/issue_board_entity.rb b/app/serializers/issue_board_entity.rb
index 6a9e9638e70..4e3d03b236b 100644
--- a/app/serializers/issue_board_entity.rb
+++ b/app/serializers/issue_board_entity.rb
@@ -12,6 +12,7 @@ class IssueBoardEntity < Grape::Entity
expose :project_id
expose :relative_position
expose :weight, if: -> (*) { respond_to?(:weight) }
+ expose :time_estimate
expose :project do |issue|
API::Entities::Project.represent issue.project, only: [:id, :path]
diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb
index 5017fa093f3..19dc0478591 100644
--- a/app/services/clusters/applications/check_installation_progress_service.rb
+++ b/app/services/clusters/applications/check_installation_progress_service.rb
@@ -14,7 +14,8 @@ module Clusters
else
check_timeout
end
- rescue Kubeclient::HttpError
+ rescue Kubeclient::HttpError => e
+ Rails.logger.error "Kubernetes error: #{e.class.name} #{e.message}"
app.make_errored!("Kubernetes error") unless app.errored?
end
@@ -51,7 +52,8 @@ module Clusters
def remove_installation_pod
helm_api.delete_pod!(install_command.pod_name)
- rescue
+ rescue => e
+ Rails.logger.error "Kubernetes error: #{e.class.name} #{e.message}"
# no-op
end
diff --git a/app/services/clusters/applications/create_service.rb b/app/services/clusters/applications/create_service.rb
index 55f917798de..c348cad4803 100644
--- a/app/services/clusters/applications/create_service.rb
+++ b/app/services/clusters/applications/create_service.rb
@@ -45,7 +45,8 @@ module Clusters
"ingress" => -> (cluster) { cluster.application_ingress || cluster.build_application_ingress },
"prometheus" => -> (cluster) { cluster.application_prometheus || cluster.build_application_prometheus },
"runner" => -> (cluster) { cluster.application_runner || cluster.build_application_runner },
- "jupyter" => -> (cluster) { cluster.application_jupyter || cluster.build_application_jupyter }
+ "jupyter" => -> (cluster) { cluster.application_jupyter || cluster.build_application_jupyter },
+ "knative" => -> (cluster) { cluster.application_knative || cluster.build_application_knative }
}
end
diff --git a/app/services/clusters/applications/install_service.rb b/app/services/clusters/applications/install_service.rb
index dd8d2ed5eb6..5a24d78e712 100644
--- a/app/services/clusters/applications/install_service.rb
+++ b/app/services/clusters/applications/install_service.rb
@@ -12,9 +12,11 @@ module Clusters
ClusterWaitForAppInstallationWorker.perform_in(
ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
- rescue Kubeclient::HttpError
+ rescue Kubeclient::HttpError => e
+ Rails.logger.error "Kubernetes error: #{e.class.name} #{e.message}"
app.make_errored!("Kubernetes error.")
- rescue StandardError
+ rescue StandardError => e
+ Rails.logger.error "Can't start installation process: #{e.class.name} #{e.message}"
app.make_errored!("Can't start installation process.")
end
end
diff --git a/app/services/commits/commit_patch_service.rb b/app/services/commits/commit_patch_service.rb
new file mode 100644
index 00000000000..9253cfaac20
--- /dev/null
+++ b/app/services/commits/commit_patch_service.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Commits
+ class CommitPatchService < CreateService
+ # Requires:
+ # - project: `Project` to be committed into
+ # - user: `User` that will be the committer
+ # - params:
+ # - branch_name: `String` the branch that will be committed into
+ # - start_branch: `String` the branch that will will started from
+ # - patches: `Gitlab::Git::Patches::Collection` that contains the patches
+ def initialize(*args)
+ super
+
+ @patches = Gitlab::Git::Patches::Collection.new(Array(params[:patches]))
+ end
+
+ private
+
+ def new_branch?
+ !repository.branch_exists?(@branch_name)
+ end
+
+ def create_commit!
+ if @start_branch && new_branch?
+ prepare_branch!
+ end
+
+ Gitlab::Git::Patches::CommitPatches
+ .new(current_user, project.repository, @branch_name, @patches)
+ .commit
+ end
+
+ def prepare_branch!
+ branch_result = CreateBranchService.new(project, current_user)
+ .execute(@branch_name, @start_branch)
+
+ if branch_result[:status] != :success
+ raise ChangeError, branch_result[:message]
+ end
+ end
+
+ # Overridden from the Commits::CreateService, to skip some validations we
+ # don't need:
+ # - validate_on_branch!
+ # Not needed, the patches are applied on top of HEAD if the branch did not
+ # exist
+ # - validate_branch_existence!
+ # Not needed because we continue applying patches on the branch if it
+ # already existed, and create it if it did not exist.
+ def validate!
+ validate_patches!
+ validate_new_branch_name! if new_branch?
+ validate_permissions!
+ end
+
+ def validate_patches!
+ raise_error("Patches are too big") unless @patches.valid_size?
+ end
+ end
+end
diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb
index 3ce9acc833c..34593e12bd5 100644
--- a/app/services/commits/create_service.rb
+++ b/app/services/commits/create_service.rb
@@ -19,7 +19,12 @@ module Commits
new_commit = create_commit!
success(result: new_commit)
- rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Gitlab::Git::CommitError, Gitlab::Git::PreReceiveError => ex
+ rescue ValidationError,
+ ChangeError,
+ Gitlab::Git::Index::IndexError,
+ Gitlab::Git::CommitError,
+ Gitlab::Git::PreReceiveError,
+ Gitlab::Git::CommandError => ex
error(ex.message)
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index c388913ae65..e32e262ac31 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -126,12 +126,12 @@ class IssuableBaseService < BaseService
merge_quick_actions_into_params!(issuable)
end
- def merge_quick_actions_into_params!(issuable)
+ def merge_quick_actions_into_params!(issuable, only: nil)
original_description = params.fetch(:description, issuable.description)
description, command_params =
QuickActions::InterpretService.new(project, current_user)
- .execute(original_description, issuable)
+ .execute(original_description, issuable, only: only)
# Avoid a description already set on an issuable to be overwritten by a nil
params[:description] = description if description
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 0e76d2cc3ab..6c69452e2ab 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -6,8 +6,12 @@ module MergeRequests
def execute
@params_issue_iid = params.delete(:issue_iid)
+ self.merge_request = MergeRequest.new
+ # TODO: this should handle all quick actions that don't have side effects
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/53658
+ merge_quick_actions_into_params!(merge_request, only: [:target_branch])
+ merge_request.assign_attributes(params)
- self.merge_request = MergeRequest.new(params)
merge_request.author = current_user
merge_request.compare_commits = []
merge_request.source_project = find_source_project
diff --git a/app/services/merge_requests/get_urls_service.rb b/app/services/merge_requests/get_urls_service.rb
index 35a22449e34..7c88c9abb41 100644
--- a/app/services/merge_requests/get_urls_service.rb
+++ b/app/services/merge_requests/get_urls_service.rb
@@ -50,8 +50,8 @@ module MergeRequests
end
def url_for_new_merge_request(branch_name)
- url = Gitlab::Routing.url_helpers.project_new_merge_request_url(project, branch_name)
-
+ merge_request_params = { source_branch: branch_name }
+ url = Gitlab::Routing.url_helpers.project_new_merge_request_url(project, merge_request: merge_request_params)
{ branch_name: branch_name, url: url, new_merge_request: true }
end
diff --git a/app/services/merge_requests/reload_diffs_service.rb b/app/services/merge_requests/reload_diffs_service.rb
index c64b2e99b52..b47d8f3f63a 100644
--- a/app/services/merge_requests/reload_diffs_service.rb
+++ b/app/services/merge_requests/reload_diffs_service.rb
@@ -29,6 +29,10 @@ module MergeRequests
# rubocop: disable CodeReuse/ActiveRecord
def clear_cache(new_diff)
+ # Executing the iteration we cache highlighted diffs for each diff file of
+ # MergeRequestDiff.
+ cacheable_collection(new_diff).write_cache
+
# Remove cache for all diffs on this MR. Do not use the association on the
# model, as that will interfere with other actions happening when
# reloading the diff.
diff --git a/app/services/notes/base_service.rb b/app/services/notes/base_service.rb
deleted file mode 100644
index 431ff6c11c4..00000000000
--- a/app/services/notes/base_service.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-module Notes
- class BaseService < ::BaseService
- def clear_noteable_diffs_cache(note)
- noteable = note.noteable
-
- if note.is_a?(DiffNote) &&
- note.discussion_first_note? &&
- note.position.unfolded_diff?(project.repository)
- noteable.diffs.clear_cache
- end
- end
- end
-end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index e03789e3ca9..049e6c5a871 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module Notes
- class CreateService < ::Notes::BaseService
+ class CreateService < ::BaseService
def execute
merge_request_diff_head_sha = params.delete(:merge_request_diff_head_sha)
@@ -35,7 +35,6 @@ module Notes
if !only_commands && note.save
todo_service.new_note(note, current_user)
- clear_noteable_diffs_cache(note)
end
if command_params.present?
diff --git a/app/services/notes/destroy_service.rb b/app/services/notes/destroy_service.rb
index fa0c2c5c86b..64e9accd97f 100644
--- a/app/services/notes/destroy_service.rb
+++ b/app/services/notes/destroy_service.rb
@@ -1,13 +1,11 @@
# frozen_string_literal: true
module Notes
- class DestroyService < ::Notes::BaseService
+ class DestroyService < BaseService
def execute(note)
TodoService.new.destroy_target(note) do |note|
note.destroy
end
-
- clear_noteable_diffs_cache(note)
end
end
end
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index eb431c36807..9c81de7e90e 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -23,13 +23,13 @@ module QuickActions
# Takes a text and interprets the commands that are extracted from it.
# Returns the content without commands, and hash of changes to be applied to a record.
- def execute(content, issuable)
+ def execute(content, issuable, only: nil)
return [content, {}] unless current_user.can?(:use_quick_actions)
@issuable = issuable
@updates = {}
- content, commands = extractor.extract_commands(content)
+ content, commands = extractor.extract_commands(content, only: only)
extract_updates(commands)
[content, @updates]
diff --git a/app/services/submodules/update_service.rb b/app/services/submodules/update_service.rb
new file mode 100644
index 00000000000..a6011a920bd
--- /dev/null
+++ b/app/services/submodules/update_service.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Submodules
+ class UpdateService < Commits::CreateService
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(*args)
+ super
+
+ @start_branch = @branch_name
+ @commit_sha = params[:commit_sha].presence
+ @submodule = params[:submodule].presence
+ @commit_message = params[:commit_message].presence || "Update submodule #{@submodule} with oid #{@commit_sha}"
+ end
+
+ def validate!
+ super
+
+ raise ValidationError, 'The repository is empty' if repository.empty?
+ end
+
+ def execute
+ super
+ rescue StandardError => e
+ error(e.message)
+ end
+
+ def create_commit!
+ repository.update_submodule(current_user,
+ @submodule,
+ @commit_sha,
+ message: @commit_message,
+ branch: @branch_name)
+ rescue ArgumentError, TypeError
+ raise ValidationError, 'Invalid parameters'
+ end
+ end
+end
diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index 1e1157c34bd..7ea85fe43d6 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -13,6 +13,7 @@
install_prometheus_path: clusterable.install_applications_cluster_path(@cluster, :prometheus),
install_runner_path: clusterable.install_applications_cluster_path(@cluster, :runner),
install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter),
+ install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative),
toggle_status: @cluster.enabled? ? 'true': 'false',
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason,
diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml
index ae09e0dfa18..56d4f2ba881 100644
--- a/app/views/import/bitbucket_server/status.html.haml
+++ b/app/views/import/bitbucket_server/status.html.haml
@@ -84,4 +84,6 @@
= link_to 'import flow', status_import_bitbucket_server_path
again.
+= paginate_without_count(@collection)
+
.js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_server_path}", import_path: "#{import_bitbucket_server_path}" } }
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index b7d69539eb7..474ef25cef7 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -71,7 +71,7 @@
= link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: _('Stop impersonation'), aria: { label: _('Stop impersonation') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
= icon('user-secret')
- if header_link?(:sign_in)
- %li.nav-item
+ %li.nav-item.m-auto
%div
- sign_in_text = allow_signup? ? _('Sign in / Register') : _('Sign in')
= link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in'
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 163556f4509..b390c396a09 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -74,6 +74,8 @@
%span
= boards_link_text
+ = render_if_exists 'layouts/nav/issues_analytics_link'
+
- if group_sidebar_link?(:labels)
= nav_link(path: 'labels#index') do
= link_to group_labels_path(@group), title: _('Labels') do
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 174033f3d49..ab15889a465 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -195,7 +195,7 @@
= _('Charts')
- if project_nav_tab? :operations
- = nav_link(controller: [:environments, :clusters, :user, :gcp]) do
+ = nav_link(controller: sidebar_operations_paths) do
= link_to metrics_project_environments_path(@project), class: 'shortcuts-operations' do
.nav-icon-container
= sprite_icon('cloud-gear')
@@ -203,7 +203,7 @@
= _('Operations')
%ul.sidebar-sub-level-items
- = nav_link(controller: [:environments, :clusters, :user, :gcp], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: sidebar_operations_paths, html_options: { class: "fly-out-top-item" } ) do
= link_to metrics_project_environments_path(@project) do
%strong.fly-out-top-item-name
= _('Operations')
@@ -215,6 +215,8 @@
%span
= _('Metrics')
+ = render_if_exists "layouts/nav/sidebar/tracing_link"
+
= nav_link(controller: :environments, action: [:index, :folder, :show, :new, :edit, :create, :update, :stop, :terminal]) do
= link_to project_environments_path(@project), title: _('Environments'), class: 'shortcuts-environments qa-operations-environments-link' do
%span
@@ -326,6 +328,7 @@
= link_to project_settings_ci_cd_path(@project), title: _('CI / CD') do
%span
= _('CI / CD')
+ = render_if_exists 'projects/sidebar/settings_operations'
- if @project.pages_available?
= nav_link(controller: :pages) do
= link_to project_pages_path(@project), title: _('Pages') do
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index 0d2f6bb77d6..f0d1dd162df 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -10,7 +10,7 @@
-# haml-lint:disable InlineJavaScript
%script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board"
- %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
+ %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal, show_sorting_dropdown: false
%script#js-board-promotion{ type: "text/x-template" }= render_if_exists "shared/promotions/promote_issue_board"
#board-app.boards-app{ "v-cloak" => true, data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" }
diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml
index 9133ce8ed22..bee26cd8312 100644
--- a/app/views/shared/empty_states/_labels.html.haml
+++ b/app/views/shared/empty_states/_labels.html.haml
@@ -8,7 +8,7 @@
%p= _("You can also star a label to make it a priority label.")
.text-center
- if can?(current_user, :admin_label, @project)
- = link_to _('New label'), new_project_label_path(@project), class: 'btn btn-success', title: _('New label'), id: 'new_label_link'
+ = link_to _('New label'), new_project_label_path(@project), class: 'btn btn-success qa-label-create-new', title: _('New label'), id: 'new_label_link'
= link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn btn-success btn-inverted', title: _('Generate a default set of labels'), id: 'generate_labels_link'
- if can?(current_user, :admin_label, @group)
= link_to _('New label'), new_group_label_path(@group), class: 'btn btn-success', title: _('New label'), id: 'new_label_link'
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index d27f79dc404..95f32bd0180 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -3,6 +3,7 @@
- block_css_class = type != :boards_modal ? 'row-content-block second-block' : ''
- full_path = @project.present? ? @project.full_path : @group.full_path
- user_can_admin_list = board && can?(current_user, :admin_list, board.parent)
+- show_sorting_dropdown = local_assigns.fetch(:show_sorting_dropdown, true)
.issues-filters
.issues-details-filters.filtered-search-block{ class: block_css_class, "v-pre" => type == :boards_modal }
@@ -140,5 +141,5 @@
- if @project
#js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @project) } }
#js-toggle-focus-btn
- - elsif type != :boards_modal
+ - elsif show_sorting_dropdown
= render 'shared/sort_dropdown'
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index 9dd1c24fdfa..ec1e10bb0c1 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -7,8 +7,8 @@
= render 'shared/notes/edit_form', project: @project
- if can_create_note?
- %ul.notes.notes-form.timeline
- %li.timeline-entry
+ .notes.notes-form.timeline
+ .timeline-entry
.timeline-entry-inner
.flash-container.timeline-content
diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index 12706613ac2..bf637f82df2 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -40,6 +40,8 @@ class EmailReceiverWorker
"You are not allowed to perform this action. If you believe this is in error, contact a staff member."
when Gitlab::Email::NoteableNotFoundError
"The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member."
+ when Gitlab::Email::InvalidAttachment
+ error.message
when Gitlab::Email::InvalidRecordError
can_retry = true
error.message
diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb
index de92f3eca6a..667a4121131 100644
--- a/app/workers/stuck_import_jobs_worker.rb
+++ b/app/workers/stuck_import_jobs_worker.rb
@@ -7,60 +7,58 @@ class StuckImportJobsWorker
IMPORT_JOBS_EXPIRATION = 15.hours.to_i
def perform
- projects_without_jid_count = mark_projects_without_jid_as_failed!
- projects_with_jid_count = mark_projects_with_jid_as_failed!
+ import_state_without_jid_count = mark_import_states_without_jid_as_failed!
+ import_state_with_jid_count = mark_import_states_with_jid_as_failed!
Gitlab::Metrics.add_event(:stuck_import_jobs,
- projects_without_jid_count: projects_without_jid_count,
- projects_with_jid_count: projects_with_jid_count)
+ projects_without_jid_count: import_state_without_jid_count,
+ projects_with_jid_count: import_state_with_jid_count)
end
private
- def mark_projects_without_jid_as_failed!
- enqueued_projects_without_jid.each do |project|
- project.mark_import_as_failed(error_message)
+ def mark_import_states_without_jid_as_failed!
+ enqueued_import_states_without_jid.each do |import_state|
+ import_state.mark_as_failed(error_message)
end.count
end
# rubocop: disable CodeReuse/ActiveRecord
- def mark_projects_with_jid_as_failed!
- # TODO: Rollback this change to use SQL through #pluck
- jids_and_ids = enqueued_projects_with_jid.map { |project| [project.import_jid, project.id] }.to_h
+ def mark_import_states_with_jid_as_failed!
+ jids_and_ids = enqueued_import_states_with_jid.pluck(:jid, :id).to_h
# Find the jobs that aren't currently running or that exceeded the threshold.
completed_jids = Gitlab::SidekiqStatus.completed_jids(jids_and_ids.keys)
return unless completed_jids.any?
- completed_project_ids = jids_and_ids.values_at(*completed_jids)
+ completed_import_state_ids = jids_and_ids.values_at(*completed_jids)
- # We select the projects again, because they may have transitioned from
+ # We select the import states again, because they may have transitioned from
# scheduled/started to finished/failed while we were looking up their Sidekiq status.
- completed_projects = enqueued_projects_with_jid.where(id: completed_project_ids)
+ completed_import_states = enqueued_import_states_with_jid.where(id: completed_import_state_ids)
- Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_projects.map(&:import_jid).join(', ')}")
+ completed_import_state_jids = completed_import_states.map { |import_state| import_state.jid }.join(', ')
+ Rails.logger.info("Marked stuck import jobs as failed. JIDs: #{completed_import_state_jids}")
- completed_projects.each do |project|
- project.mark_import_as_failed(error_message)
+ completed_import_states.each do |import_state|
+ import_state.mark_as_failed(error_message)
end.count
end
# rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
- def enqueued_projects
- Project.joins_import_state.where("(import_state.status = 'scheduled' OR import_state.status = 'started') OR (projects.import_status = 'scheduled' OR projects.import_status = 'started')")
+ def enqueued_import_states
+ ProjectImportState.with_status([:scheduled, :started])
end
- # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
- def enqueued_projects_with_jid
- enqueued_projects.where.not("import_state.jid IS NULL AND projects.import_jid IS NULL")
+ def enqueued_import_states_with_jid
+ enqueued_import_states.where.not(jid: nil)
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
- def enqueued_projects_without_jid
- enqueued_projects.where("import_state.jid IS NULL AND projects.import_jid IS NULL")
+ def enqueued_import_states_without_jid
+ enqueued_import_states.where(jid: nil)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/changelogs/unreleased/47008-issue-board-card-design.yml b/changelogs/unreleased/47008-issue-board-card-design.yml
new file mode 100644
index 00000000000..39238687943
--- /dev/null
+++ b/changelogs/unreleased/47008-issue-board-card-design.yml
@@ -0,0 +1,5 @@
+---
+title: Issue board card design
+merge_request: 21229
+author:
+type: changed
diff --git a/changelogs/unreleased/52767-more-chaos-for-gitlab.yml b/changelogs/unreleased/52767-more-chaos-for-gitlab.yml
new file mode 100644
index 00000000000..067777cb7fa
--- /dev/null
+++ b/changelogs/unreleased/52767-more-chaos-for-gitlab.yml
@@ -0,0 +1,5 @@
+---
+title: Add endpoints for simulating certain failure modes in the application
+merge_request: 22746
+author:
+type: other
diff --git a/changelogs/unreleased/6500-fix-misaligned-approvers-dropdown.yml b/changelogs/unreleased/6500-fix-misaligned-approvers-dropdown.yml
new file mode 100644
index 00000000000..3e87c5875c6
--- /dev/null
+++ b/changelogs/unreleased/6500-fix-misaligned-approvers-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: Fix misaligned approvers dropdown
+merge_request: 22832
+author:
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml b/changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml
deleted file mode 100644
index b89ba754952..00000000000
--- a/changelogs/unreleased/blackst0ne-update-push-new-merge-request-url.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Make new merge request URL more friendly when pushing code
-merge_request: 22526
-author: "@blackst0ne"
-type: changed
diff --git a/changelogs/unreleased/bvl-patches-via-mail.yml b/changelogs/unreleased/bvl-patches-via-mail.yml
new file mode 100644
index 00000000000..6fd9e6a956c
--- /dev/null
+++ b/changelogs/unreleased/bvl-patches-via-mail.yml
@@ -0,0 +1,5 @@
+---
+title: Allow adding patches when creating a merge request via email
+merge_request: 22723
+author: Serdar Dogruyol
+type: added
diff --git a/changelogs/unreleased/fix-stuck-import-jobs-query-performance-issue.yml b/changelogs/unreleased/fix-stuck-import-jobs-query-performance-issue.yml
new file mode 100644
index 00000000000..d8455a8509f
--- /dev/null
+++ b/changelogs/unreleased/fix-stuck-import-jobs-query-performance-issue.yml
@@ -0,0 +1,5 @@
+---
+title: Improves performance of stuck import jobs detection
+merge_request: 22879
+author:
+type: performance
diff --git a/changelogs/unreleased/fj-41213-api-update-submodule-commit.yml b/changelogs/unreleased/fj-41213-api-update-submodule-commit.yml
new file mode 100644
index 00000000000..c06b02b05e8
--- /dev/null
+++ b/changelogs/unreleased/fj-41213-api-update-submodule-commit.yml
@@ -0,0 +1,5 @@
+---
+title: Add endpoint to update a git submodule reference
+merge_request: 20949
+author:
+type: added
diff --git a/changelogs/unreleased/gt-align-sign-in-button.yml b/changelogs/unreleased/gt-align-sign-in-button.yml
new file mode 100644
index 00000000000..a51fa319231
--- /dev/null
+++ b/changelogs/unreleased/gt-align-sign-in-button.yml
@@ -0,0 +1,5 @@
+---
+title: Align sign in button
+merge_request: 22888
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/introduce-knative-support.yml b/changelogs/unreleased/introduce-knative-support.yml
new file mode 100644
index 00000000000..53290d71977
--- /dev/null
+++ b/changelogs/unreleased/introduce-knative-support.yml
@@ -0,0 +1,5 @@
+---
+title: Introduce Knative support
+author: Chris Baumbauer
+merge_request: 43959
+type: added
diff --git a/changelogs/unreleased/max_retries_when.yml b/changelogs/unreleased/max_retries_when.yml
new file mode 100644
index 00000000000..dad3cd8a123
--- /dev/null
+++ b/changelogs/unreleased/max_retries_when.yml
@@ -0,0 +1,5 @@
+---
+title: Allow to configure when to retry failed CI jobs
+merge_request: 21758
+author: Markus Doits
+type: added
diff --git a/changelogs/unreleased/osw-comment-on-any-line-on-diffs.yml b/changelogs/unreleased/osw-comment-on-any-line-on-diffs.yml
deleted file mode 100644
index 7b48a94a993..00000000000
--- a/changelogs/unreleased/osw-comment-on-any-line-on-diffs.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow commenting on any diff line in Merge Requests
-merge_request: 22398
-author:
-type: added
diff --git a/changelogs/unreleased/sh-paginate-bitbucket-server-imports.yml b/changelogs/unreleased/sh-paginate-bitbucket-server-imports.yml
new file mode 100644
index 00000000000..b5743e71cf9
--- /dev/null
+++ b/changelogs/unreleased/sh-paginate-bitbucket-server-imports.yml
@@ -0,0 +1,5 @@
+---
+title: Paginate Bitbucket Server importer projects
+merge_request: 22825
+author:
+type: changed
diff --git a/config/initializers/fill_shards.rb b/config/initializers/fill_shards.rb
index 0f45cf44621..18e067c8854 100644
--- a/config/initializers/fill_shards.rb
+++ b/config/initializers/fill_shards.rb
@@ -1,4 +1,3 @@
-return unless Shard.connected?
-return if Gitlab::Database.read_only?
-
-Shard.populate!
+if Shard.connected? && !Gitlab::Database.read_only?
+ Shard.populate!
+end
diff --git a/config/routes.rb b/config/routes.rb
index d2d91647d0b..484e05114be 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -82,6 +82,13 @@ Rails.application.routes.draw do
draw :operations
draw :instance_statistics
+
+ if ENV['GITLAB_ENABLE_CHAOS_ENDPOINTS']
+ get '/chaos/leakmem' => 'chaos#leakmem'
+ get '/chaos/cpuspin' => 'chaos#cpuspin'
+ get '/chaos/sleep' => 'chaos#sleep'
+ get '/chaos/kill' => 'chaos#kill'
+ end
end
concern :clusterable do
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 387d2363552..3f1ad90dfca 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -149,7 +149,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
scope path: 'merge_requests', controller: 'merge_requests/creations' do
post '', action: :create, as: nil
- scope path: 'new/(:merge_request_source_branch)', as: :new_merge_request do
+ scope path: 'new', as: :new_merge_request do
+ get '', action: :new
+
scope constraints: { format: nil }, action: :new do
get :diffs, defaults: { tab: 'diffs' }
get :pipelines, defaults: { tab: 'pipelines' }
@@ -163,7 +165,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :diff_for_path
get :branch_from
get :branch_to
- get '', action: :new
end
end
diff --git a/danger/metadata/Dangerfile b/danger/metadata/Dangerfile
index 51fc9e6bfca..1adca152736 100644
--- a/danger/metadata/Dangerfile
+++ b/danger/metadata/Dangerfile
@@ -23,3 +23,10 @@ has_pick_into_stable_label = gitlab.mr_labels.find { |label| label.start_with?('
if gitlab.branch_for_base != "master" && !has_pick_into_stable_label
warn "Most of the time, merge requests should target `master`. Otherwise, please set the relevant `Pick into X.Y` label."
end
+
+if gitlab.mr_json['title'].length > 72
+ warn 'The title of this merge request is longer than 72 characters and ' \
+ 'would violate our commit message rules when using the Squash on Merge ' \
+ 'feature. Please consider adjusting the title, or rebase the ' \
+ "commits manually and don't use Squash on Merge."
+end
diff --git a/db/fixtures/development/09_issues.rb b/db/fixtures/development/09_issues.rb
index 0b32a461d56..16243b72f9a 100644
--- a/db/fixtures/development/09_issues.rb
+++ b/db/fixtures/development/09_issues.rb
@@ -8,7 +8,8 @@ Gitlab::Seeder.quiet do
description: FFaker::Lorem.sentence,
state: ['opened', 'closed'].sample,
milestone: project.milestones.sample,
- assignees: [project.team.users.sample]
+ assignees: [project.team.users.sample],
+ created_at: rand(12).months.ago
}
Issues::CreateService.new(project, project.team.users.sample, issue_params).execute
diff --git a/db/migrate/20180912111628_add_knative_application.rb b/db/migrate/20180912111628_add_knative_application.rb
new file mode 100644
index 00000000000..bfda6a945a7
--- /dev/null
+++ b/db/migrate/20180912111628_add_knative_application.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddKnativeApplication < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ create_table "clusters_applications_knative" do |t|
+ t.references :cluster, null: false, unique: true, foreign_key: { on_delete: :cascade }
+
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.integer "status", null: false
+ t.string "version", null: false
+ t.string "hostname"
+ t.text "status_reason"
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 2c8300839d8..56137caf1d7 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -705,6 +705,16 @@ ActiveRecord::Schema.define(version: 20181107054254) do
t.text "status_reason"
end
+ create_table "clusters_applications_knative", force: :cascade do |t|
+ t.integer "cluster_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.integer "status", null: false
+ t.string "version", null: false
+ t.string "hostname"
+ t.text "status_reason"
+ end
+
create_table "clusters_applications_prometheus", force: :cascade do |t|
t.integer "cluster_id", null: false
t.integer "status", null: false
@@ -2422,6 +2432,7 @@ ActiveRecord::Schema.define(version: 20181107054254) do
add_foreign_key "clusters_applications_ingress", "clusters", name: "fk_753a7b41c1", on_delete: :cascade
add_foreign_key "clusters_applications_jupyter", "clusters", on_delete: :cascade
add_foreign_key "clusters_applications_jupyter", "oauth_applications", on_delete: :nullify
+ add_foreign_key "clusters_applications_knative", "clusters", on_delete: :cascade
add_foreign_key "clusters_applications_prometheus", "clusters", name: "fk_557e773639", on_delete: :cascade
add_foreign_key "clusters_applications_runners", "ci_runners", column: "runner_id", name: "fk_02de2ded36", on_delete: :nullify
add_foreign_key "clusters_applications_runners", "clusters", on_delete: :cascade
diff --git a/doc/administration/auth/okta.md b/doc/administration/auth/okta.md
index 664657650d4..ae38094391b 100644
--- a/doc/administration/auth/okta.md
+++ b/doc/administration/auth/okta.md
@@ -11,7 +11,7 @@ The following documentation enables Okta as a SAML provider.
1. When the app screen comes up you see another button to **Create an App** and
choose SAML 2.0 on the next screen.
1. Now, very important, add a logo
- (you can choose it from https://about.gitlab.com/press/). You'll have to
+ (you can choose it from <https://about.gitlab.com/press/>). You'll have to
crop and resize it.
1. Next, you'll need the to fill in the SAML general config. Here's an example
image.
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index 890b780fe80..cfe7b0e05e3 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -71,7 +71,7 @@ A Registry init file is not shipped with GitLab if you install it from source.
Hence, [restarting GitLab][restart gitlab] will not restart the Registry should
you modify its settings. Read the upstream documentation on how to achieve that.
-At the absolute minimum, make sure your [Registry configuration][registry-auth]
+At the **absolute** minimum, make sure your [Registry configuration][registry-auth]
has `container_registry` as the service and `https://gitlab.example.com/jwt/auth`
as the realm:
@@ -84,6 +84,9 @@ auth:
rootcertbundle: /root/certs/certbundle
```
+CAUTION: **Caution:**
+If `auth` is not set up, users will be able to pull docker images without authentication.
+
## Container Registry domain configuration
There are two ways you can configure the Registry's external domain.
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 5700f640e4c..c9a2778b3a4 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -45,7 +45,7 @@ The following metrics are available:
| redis_ping_success | Gauge | 9.4 | Whether or not the last redis ping succeeded |
| redis_ping_latency_seconds | Gauge | 9.4 | Round trip time of the redis ping |
| user_session_logins_total | Counter | 9.4 | Counter of how many users have logged in |
-| upload_file_does_not_exist | Counter | 10.7 | Number of times an upload record could not find its file |
+| upload_file_does_not_exist | Counter | 10.7 in EE, 11.5 in CE | Number of times an upload record could not find its file |
| failed_login_captcha_total | Gauge | 11.0 | Counter of failed CAPTCHA attempts during login |
| successful_login_captcha_total | Gauge | 11.0 | Counter of successful CAPTCHA attempts during login |
diff --git a/doc/api/README.md b/doc/api/README.md
index a620a13a3b3..19abbdc7a1e 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -61,6 +61,7 @@ following locations:
- [Protected Tags](protected_tags.md)
- [Repositories](repositories.md)
- [Repository Files](repository_files.md)
+- [Repository Submodules](repository_submodules.md)
- [Runners](runners.md)
- [Search](search.md)
- [Services](services.md)
@@ -234,7 +235,7 @@ provided you are authenticated as an administrator with an OAuth or Personal Acc
You need to pass the `sudo` parameter either via query string or a header with an ID/username of
the user you want to perform the operation as. If passed as a header, the
-header name must be `Sudo`.
+header name must be `Sudo`.
NOTE: **Note:**
Usernames are case insensitive.
diff --git a/doc/api/repository_submodules.md b/doc/api/repository_submodules.md
new file mode 100644
index 00000000000..2e6797f18f4
--- /dev/null
+++ b/doc/api/repository_submodules.md
@@ -0,0 +1,49 @@
+# Repository submodules API
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/41213) in GitLab 11.5
+
+## Update existing submodule reference in repository
+
+In some workflows, especially automated ones, it can be useful to update a
+submodule's reference to keep up to date other projects that use it.
+This endpoint allows you to update a [Git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) reference in a
+specific branch.
+
+```
+PUT /projects/:id/repository/submodules/:submodule
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `submodule` | string | yes | URL encoded full path to the submodule. For example, `lib%2Fclass%2Erb` |
+| `branch` | string | yes | Name of the branch to commit into |
+| `commit_sha` | string | yes | Full commit SHA to update the submodule to |
+| `commit_message` | string | no | Commit message. If no message is provided, a default one will be set |
+
+```sh
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repositories/submodules/lib%2Fmodules%2Fexample"
+--data "branch=master&commit_sha=3ddec28ea23acc5caa5d8331a6ecb2a65fc03e88&commit_message=Update submodule reference"
+```
+
+Example response:
+
+```json
+{
+ "id": "ed899a2f4b50b4370feeea94676502b42383c746",
+ "short_id": "ed899a2f4b5",
+ "title": "Updated submodule example_submodule with oid 3ddec28ea23acc5caa5d8331a6ecb2a65fc03e88",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dzaporozhets@sphereconsultinginc.com",
+ "committer_name": "Dmitriy Zaporozhets",
+ "committer_email": "dzaporozhets@sphereconsultinginc.com",
+ "created_at": "2018-09-20T09:26:24.000-07:00",
+ "message": "Updated submodule example_submodule with oid 3ddec28ea23acc5caa5d8331a6ecb2a65fc03e88",
+ "parent_ids": [
+ "ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba"
+ ],
+ "committed_date": "2018-09-20T09:26:24.000-07:00",
+ "authored_date": "2018-09-20T09:26:24.000-07:00",
+ "status": null
+}
+```
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index b3a55e48f4e..c827faace33 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -74,7 +74,7 @@ A job is defined by a list of parameters that define the job behavior.
| after_script | no | Override a set of commands that are executed after job |
| environment | no | Defines a name of environment to which deployment is done by this job |
| coverage | no | Define code coverage settings for a given job |
-| retry | no | Define how many times a job can be auto-retried in case of a failure |
+| retry | no | Define when and how many times a job can be auto-retried in case of a failure |
| parallel | no | Defines how many instances of a job should be run in parallel |
### `extends`
@@ -1433,18 +1433,20 @@ job1:
## `retry`
> [Introduced][ce-12909] in GitLab 9.5.
+> [Behaviour expanded](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21758)
+> in GitLab 11.5 to control on which failures to retry.
`retry` allows you to configure how many times a job is going to be retried in
case of a failure.
-When a job fails, and has `retry` configured it is going to be processed again
+When a job fails and has `retry` configured, it is going to be processed again
up to the amount of times specified by the `retry` keyword.
If `retry` is set to 2, and a job succeeds in a second run (first retry), it won't be retried
again. `retry` value has to be a positive integer, equal or larger than 0, but
lower or equal to 2 (two retries maximum, three runs in total).
-A simple example:
+A simple example to retry in all failure cases:
```yaml
test:
@@ -1452,6 +1454,57 @@ test:
retry: 2
```
+By default, a job will be retried on all failure cases. To have a better control
+on which failures to retry, `retry` can be a hash with with the following keys:
+
+- `max`: The maximum number of retries.
+- `when`: The failure cases to retry.
+
+To retry only runner system failures at maximum two times:
+
+```yaml
+test:
+ script: rspec
+ retry:
+ max: 2
+ when: runner_system_failure
+```
+
+If there is another failure, other than a runner system failure, the job will
+not be retried.
+
+To retry on multiple failure cases, `when` can also be an array of failures:
+
+```yaml
+test:
+ script: rspec
+ retry:
+ max: 2
+ when:
+ - runner_system_failure
+ - stuck_or_timeout_failure
+```
+
+Possible values for `when` are:
+
+<!--
+ Please make sure to update `RETRY_WHEN_IN_DOCUMENTATION` array in
+ `spec/lib/gitlab/ci/config/entry/retry_spec.rb` if you change any of
+ the documented values below. The test there makes sure that all documented
+ values are really valid as a config option and therefore should always
+ stay in sync with this documentation.
+ -->
+
+- `always`: Retry on any failure (default).
+- `unknown_failure`: Retry when the failure reason is unknown.
+- `script_failure`: Retry when the script failed.
+- `api_failure`: Retry on API failure.
+- `stuck_or_timeout_failure`: Retry when the job got stuck or timed out.
+- `runner_system_failure`: Retry if there was a runner system failure (e.g. setting up the job failed).
+- `missing_dependency_failure`: Retry if a dependency was missing.
+- `runner_unsupported`: Retry if the runner was unsupported.
+
+
## `parallel`
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22631) in GitLab 11.5.
diff --git a/doc/development/chaos_endpoints.md b/doc/development/chaos_endpoints.md
new file mode 100644
index 00000000000..403a5b21827
--- /dev/null
+++ b/doc/development/chaos_endpoints.md
@@ -0,0 +1,117 @@
+# Generating chaos in a test GitLab instance
+
+As [Werner Vogels](https://twitter.com/Werner), the CTO at Amazon Web Services, famously put it, **Everything fails, all the time**.
+
+As a developer, it's as important to consider the failure modes in which your software will operate as much as normal operation. Doing so can mean the difference between a minor hiccup leading to a scattering of `500` errors experienced by a tiny fraction of users and a full site outage that affects all users for an extended period.
+
+To paraphrase [Tolstoy](https://en.wikipedia.org/wiki/Anna_Karenina_principle), _all happy servers are alike, but all failing servers are failing in their own way_. Luckily, there are ways we can attempt to simulate these failure modes, and the chaos endpoints are tools for assisting in this process.
+
+Currently, there are four endpoints for simulating the following conditions:
+
+- Slow requests.
+- CPU-bound requests.
+- Memory leaks.
+- Unexpected process crashes.
+
+## Enabling chaos endpoints
+
+For obvious reasons, these endpoints are not enabled by default. They can be enabled by setting the `GITLAB_ENABLE_CHAOS_ENDPOINTS` environment variable to `1`.
+
+For example, if you're using the [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit) this can be done with the following command:
+
+```bash
+GITLAB_ENABLE_CHAOS_ENDPOINTS=1 gdk run
+```
+
+## Securing the chaos endpoints
+
+DANGER: **Danger:**
+It is highly recommended that you secure access to the chaos endpoints using a secret token. This is recommended when enabling these endpoints locally and essential when running in a staging or other shared environment. You should not enable them in production unless you absolutely know what you're doing.
+
+A secret token can be set through the `GITLAB_CHAOS_SECRET` environment variable. For example, when using the [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit) this can be done with the following command:
+
+```bash
+GITLAB_ENABLE_CHAOS_ENDPOINTS=1 GITLAB_CHAOS_SECRET=secret gdk run
+```
+
+Replace `secret` with your own secret token.
+
+## Invoking chaos
+
+Once you have enabled the chaos endpoints and restarted the application, you can start testing using the endpoints.
+
+## Memory leaks
+
+To simulate a memory leak in your application, use the `/-/chaos/leakmem` endpoint.
+
+NOTE: **Note:**
+The memory is not retained after the request finishes. Once the request has completed, the Ruby garbage collector will attempt to recover the memory.
+
+```
+GET /-/chaos/leakmem
+GET /-/chaos/leakmem?memory_mb=1024
+GET /-/chaos/leakmem?memory_mb=1024&duration_s=50
+```
+
+| Attribute | Type | Required | Description |
+| ------------ | ------- | -------- | ---------------------------------------------------------------------------------- |
+| `memory_mb` | integer | no | How much memory, in MB, should be leaked. Defaults to 100MB. |
+| `duration_s` | integer | no | Minimum duration, in seconds, that the memory should be retained. Defaults to 30s. |
+
+```bash
+curl http://localhost:3000/-/chaos/leakmem?memory_mb=1024&duration_s=10 --header 'X-Chaos-Secret: secret'
+```
+
+## CPU spin
+
+This endpoint attempts to fully utilise a single core, at 100%, for the given period.
+
+Depending on your rack server setup, your request may timeout after a predermined period (normally 60 seconds).
+If you're using Unicorn, this is done by killing the worker process.
+
+```
+GET /-/chaos/cpuspin
+GET /-/chaos/cpuspin?duration_s=50
+```
+
+| Attribute | Type | Required | Description |
+| ------------ | ------- | -------- | --------------------------------------------------------------------- |
+| `duration_s` | integer | no | Duration, in seconds, that the core will be utilised. Defaults to 30s |
+
+```bash
+curl http://localhost:3000/-/chaos/cpuspin?duration_s=60 --header 'X-Chaos-Secret: secret'
+```
+
+## Sleep
+
+This endpoint is similar to the CPU Spin endpoint but simulates off-processor activity, such as network calls to backend services. It will sleep for a given duration.
+
+As with the CPU Spin endpoint, this may lead to your request timing out if duration exceeds the configured limit.
+
+```
+GET /-/chaos/sleep
+GET /-/chaos/sleep?duration_s=50
+```
+
+| Attribute | Type | Required | Description |
+| ------------ | ------- | -------- | ---------------------------------------------------------------------- |
+| `duration_s` | integer | no | Duration, in seconds, that the request will sleep for. Defaults to 30s |
+
+```bash
+curl http://localhost:3000/-/chaos/sleep?duration_s=60 --header 'X-Chaos-Secret: secret'
+```
+
+## Kill
+
+This endpoint will simulate the unexpected death of a worker process using a `kill` signal.
+
+NOTE: **Note:**
+Since this endpoint uses the `KILL` signal, the worker is not given a chance to cleanup or shutdown.
+
+```
+GET /-/chaos/kill
+```
+
+```bash
+curl http://localhost:3000/-/chaos/kill --header 'X-Chaos-Secret: secret'
+```
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index 51f5ddfc1e0..154ede087cc 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -4,9 +4,9 @@ description: Learn how to contribute to GitLab Documentation.
# GitLab Documentation guidelines
- - **General Documentation**: written by the [developers responsible by creating features](#contributing-to-docs). Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers.
- - **[Technical Articles](#technical-articles)**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/).
- - **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs in the same merge request containing code. They gather all resources for that topic in a single page (user and admin documentation, articles, and third-party docs).
+- **General Documentation**: written by the [developers responsible by creating features](#contributing-to-docs). Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers.
+- **[Technical Articles](#technical-articles)**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/).
+- **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs in the same merge request containing code. They gather all resources for that topic in a single page (user and admin documentation, articles, and third-party docs).
## Contributing to docs
@@ -41,7 +41,7 @@ how to structure GitLab docs.
## Markdown and styles
-Currently GitLab docs use Redcarpet as [markdown](../../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future.
+Currently GitLab docs use [Kramdown](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) as the [markdown](../../user/markdown.md) engine.
All the docs follow the [documentation style guidelines](styleguide.md). See [Linting](#linting) for help to follow the guidelines.
@@ -61,11 +61,11 @@ in small iterations. Please don't create new docs in these folders.
### Documentation files
- When you create a new directory, always start with an `index.md` file.
-Do not use another file name and **do not** create `README.md` files
+ Do not use another file name and **do not** create `README.md` files.
- **Do not** use special chars and spaces, or capital letters in file names,
-directory names, branch names, and anything that generates a path.
-- Max screenshot size: 100KB
-- We do not support videos (yet)
+ directory names, branch names, and anything that generates a path.
+- Max screenshot size: 100KB.
+- We do not support videos (yet).
### Location and naming documents
@@ -83,16 +83,16 @@ and cross-link between any related content.
The table below shows what kind of documentation goes where.
-| Directory | What belongs here |
-| --------- | -------------- |
-| `doc/user/` | User related documentation. Anything that can be done within the GitLab UI goes here including `/admin`. |
-| `doc/administration/` | Documentation that requires the user to have access to the server where GitLab is installed. The admin settings that can be accessed via GitLab's interface go under `doc/user/admin_area/`. |
-| `doc/api/` | API related documentation. |
-| `doc/development/` | Documentation related to the development of GitLab. Any styleguides should go here. |
-| `doc/legal/` | Legal documents about contributing to GitLab. |
-| `doc/install/`| Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). |
-| `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. |
-| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`): all resources for that topic (user and admin documentation, articles, and third-party docs) |
+| Directory | What belongs here |
+|:----------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `doc/user/` | User related documentation. Anything that can be done within the GitLab UI goes here including `/admin`. |
+| `doc/administration/` | Documentation that requires the user to have access to the server where GitLab is installed. The admin settings that can be accessed via GitLab's interface go under `doc/user/admin_area/`. |
+| `doc/api/` | API related documentation. |
+| `doc/development/` | Documentation related to the development of GitLab. Any styleguides should go here. |
+| `doc/legal/` | Legal documents about contributing to GitLab. |
+| `doc/install/` | Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). |
+| `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. |
+| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`): all resources for that topic (user and admin documentation, articles, and third-party docs) |
---
@@ -134,13 +134,13 @@ merge request.
Changing a document's location is not to be taken lightly. Remember that the
documentation is available to all installations under `help/` and not only to
-GitLab.com or http://docs.gitlab.com. Make sure this is discussed with the
+GitLab.com or <http://docs.gitlab.com>. Make sure this is discussed with the
Documentation team beforehand.
If you indeed need to change a document's location, do NOT remove the old
document, but rather replace all of its contents with a new line:
-```
+```md
This document was moved to [another location](path/to/new_doc.md).
```
@@ -154,7 +154,7 @@ For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to
1. Copy `doc/workflow/lfs/lfs_administration.md` to `doc/administration/lfs.md`
1. Replace the contents of `doc/workflow/lfs/lfs_administration.md` with:
- ```
+ ```md
This document was moved to [another location](../../administration/lfs.md).
```
@@ -162,7 +162,7 @@ For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to
A quick way to find them is to use `git grep`. First go to the root directory
where you cloned the `gitlab-ce` repository and then do:
- ```
+ ```sh
git grep -n "workflow/lfs/lfs_administration"
git grep -n "lfs/lfs_administration"
```
@@ -226,17 +226,16 @@ even if it's `index.html` or `README.html`.
## Linting
To help adhere to the [documentation style guidelines](styleguide.md), and to improve the content
- added to documentation, consider locally installing and running documentation linters. This will
- help you catch common issues before raising merge requests for review of documentation.
+added to documentation, consider locally installing and running documentation linters. This will
+help you catch common issues before raising merge requests for review of documentation.
The following are some suggested linters you can install locally and sample configuration:
-- `proselint`
-- `markdownlint`
+- [`proselint`](#proselint)
+- [`markdownlint`](#markdownlint)
NOTE: **Note:**
-This list does not limit what other linters you can add to your local documentation writing
- toolchain.
+This list does not limit what other linters you can add to your local documentation writing toolchain.
### `proselint`
@@ -262,19 +261,20 @@ proselint **/*.md
#### Sample `proselint` configuration
-All of the checks are good to use. However, excluding the `typography.symbols` checks might reduce
- noise. The following sample `proselint` configuration disables the `typography.symbols` checks:
+All of the checks are good to use. However, excluding the `typography.symbols` and `misc.phrasal_adjectives` checks will reduce
+noise. The following sample `proselint` configuration disables these checks:
```json
{
"checks": {
- "typography.symbols": false
+ "typography.symbols": false,
+ "misc.phrasal_adjectives": false
}
}
```
A file with `proselint` configuration must be placed in a
- [valid location](https://github.com/amperser/proselint#checks). For example, `~/.config/proselint/config`.
+[valid location](https://github.com/amperser/proselint#checks). For example, `~/.config/proselint/config`.
### `markdownlint`
@@ -306,6 +306,7 @@ The following sample `markdownlint` configuration modifies the available default
- Adhere to the [style guidelines](styleguide.md).
- Apply conventions found in the GitLab documentation.
+- Allow the flexibility of using some inline HTML.
```json
{
@@ -316,14 +317,31 @@ The following sample `markdownlint` configuration modifies the available default
"no-trailing-punctuation": false,
"ol-prefix": { "style": "one" },
"blanks-around-fences": false,
+ "no-inline-html": {
+ "allowed_elements": [
+ "table",
+ "tbody",
+ "tr",
+ "td",
+ "ul",
+ "ol",
+ "li",
+ "br",
+ "img",
+ "a",
+ "strong",
+ "i",
+ "div"
+ ]
+ },
"hr-style": { "style": "---" },
"fenced-code-language": false
}
```
For [`markdownlint`](https://github.com/DavidAnson/markdownlint/), this configuration must be
- placed in a [valid location](https://github.com/igorshubovych/markdownlint-cli#configuration). For
- example, `~/.markdownlintrc`.
+placed in a [valid location](https://github.com/igorshubovych/markdownlint-cli#configuration). For
+example, `~/.markdownlintrc`.
## Testing
@@ -350,8 +368,8 @@ If your contribution contains **only** documentation changes, you can speed up
the CI process by following some branch naming conventions. You have three
choices:
-| Branch name | Valid example |
-| ----------- | ------------- |
+| Branch name | Valid example |
+|:----------------------|:-----------------------------|
| Starting with `docs/` | `docs/update-api-issues` |
| Starting with `docs-` | `docs-update-api-issues` |
| Ending in `-docs` | `123-update-api-issues-docs` |
@@ -400,15 +418,15 @@ Follow this [method for cherry-picking from CE to EE](../automatic_ce_ee_merge.m
- Create the EE-equivalent branch ending with `-ee`, e.g.,
`git checkout -b docs-example-ee`
- Once all the jobs are passing in CE and EE, and you've addressed the
-feedback from your own team, assign the CE MR to a technical writer for review
+ feedback from your own team, assign the CE MR to a technical writer for review
- When both MRs are ready, the EE merge request will be merged first, and the
-CE-equivalent will be merged next.
+ CE-equivalent will be merged next.
- Note that the review will occur only in the CE MR, as the EE MR
-contains the same commits as the CE MR.
+ contains the same commits as the CE MR.
- If you have a few more changes that apply to the EE-version only, you can submit
-a couple more commits to the EE branch, but ask the reviewer to review the EE merge request
-additionally to the CE MR. If there are many EE-only changes though, start a new MR
-to EE only.
+ a couple more commits to the EE branch, but ask the reviewer to review the EE merge request
+ additionally to the CE MR. If there are many EE-only changes though, start a new MR
+ to EE only.
## Previewing the changes live
@@ -418,9 +436,9 @@ To preview your changes to documentation locally, follow this
The live preview is currently enabled for the following projects:
-- https://gitlab.com/gitlab-org/gitlab-ce
-- https://gitlab.com/gitlab-org/gitlab-ee
-- https://gitlab.com/gitlab-org/gitlab-runner
+- <https://gitlab.com/gitlab-org/gitlab-ce>
+- <https://gitlab.com/gitlab-org/gitlab-ee>
+- <https://gitlab.com/gitlab-org/gitlab-runner>
If your branch contains only documentation changes, you can use
[special branch names](#branch-naming) to avoid long running pipelines.
@@ -516,8 +534,8 @@ Every GitLab instance includes the documentation, which is available from `/help
The documentation available online on docs.gitlab.com is continuously
deployed every hour from the `master` branch of CE, EE, Omnibus, and Runner. Therefore,
-once a merge request gets merged, it will be available online on the same day,
-but they will be shipped (and available on `/help`) within the milestone assigned
+once a merge request gets merged, it will be available online on the same day.
+However, they will be shipped (and available on `/help`) within the milestone assigned
to the MR.
For instance, let's say your merge request has a milestone set to 11.3, which
@@ -614,7 +632,7 @@ They should be placed in a new directory named `/article-title/index.md` under a
- **User guides**: technical content to guide regular users from point A to point B
- **Admin guides**: technical content to guide administrators of GitLab instances from point A to point B
- **Technical Overviews**: technical content describing features, solutions, and third-party integrations
-- **Tutorials**: technical content provided step-by-step on how to do things, or how to reach very specific objectives
+- **Tutorials**: technical content provided step-by-step on how to do things, or how to reach specific objectives
#### Understanding guides, tutorials, and technical overviews
@@ -646,7 +664,6 @@ with the following information:
For example:
-
```yaml
---
author: John Doe
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index c43f91278de..8c3ab7643ba 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -15,10 +15,10 @@ For help adhering to the guidelines, see [Linting](index.md#linting).
## Files
- [Directory structure](index.md#location-and-naming-documents): place the docs
-in the correct location.
+ in the correct location.
- [Documentation files](index.md#documentation-files): name the files accordingly.
- [Markdown](../../user/markdown.md): use the GitLab Flavored Markdown in the
-documentation.
+ documentation.
NOTE: **Note:**
**Do not** use capital letters, spaces, or special chars in file names,
@@ -45,10 +45,10 @@ a test that will fail if it spots a new `README.md` file.
- Capitalize "G" and "L" in GitLab.
- Use sentence case for titles, headings, labels, menu items, and buttons.
- Use title case when referring to [features](https://about.gitlab.com/features/) or
-[products](https://about.gitlab.com/pricing/) (e.g., GitLab Runner, Geo,
-Issue Boards, GitLab Core, Git, Prometheus, Kubernetes, etc), and methods or methodologies
-(e.g., Continuous Integration, Continuous Deployment, Scrum, Agile, etc). Note that
-some features are also objects (e.g. "Merge Requests" and "merge requests").
+ [products](https://about.gitlab.com/pricing/) (e.g., GitLab Runner, Geo,
+ Issue Boards, GitLab Core, Git, Prometheus, Kubernetes, etc), and methods or methodologies
+ (e.g., Continuous Integration, Continuous Deployment, Scrum, Agile, etc). Note that
+ some features are also objects (e.g. "Merge Requests" and "merge requests").
## Formatting
@@ -130,9 +130,9 @@ To indicate the steps of navigation through the UI:
- Use the exact word as shown in the UI, including any capital letters as-is.
- Use bold text for navigation items and the char `>` as separator
-(e.g., `Navigate to your project's **Settings > CI/CD**` ).
+ (e.g., `Navigate to your project's **Settings > CI/CD**` ).
- If there are any expandable menus, make sure to mention that the user
-needs to expand the tab to find the settings you're referring to.
+ needs to expand the tab to find the settings you're referring to.
## Images
@@ -147,7 +147,7 @@ needs to expand the tab to find the settings you're referring to.
- Compress all images with <https://tinypng.com/> or similar tool.
- Compress gifs with <https://ezgif.com/optimize> or similar tool.
- Images should be used (only when necessary) to _illustrate_ the description
-of a process, not to _replace_ it.
+ of a process, not to _replace_ it.
- Max image size: 100KB (gifs included).
- The GitLab docs do not support videos yet.
@@ -296,7 +296,7 @@ keyword "only":
- For GitLab Core: `**[CORE ONLY]**`.
The tier should be ideally added to headers, so that the full badge will be displayed.
-But it can be also mentioned from paragraphs, list items, and table cells. For these cases,
+However, it can be also mentioned from paragraphs, list items, and table cells. For these cases,
the tier mention will be represented by an orange question mark.
E.g., `**[STARTER]**` renders **[STARTER]**, `**[STARTER ONLY]**` renders **[STARTER ONLY]**.
@@ -317,7 +317,7 @@ avoid duplication, link to the special document that can be found in
[`doc/administration/restart_gitlab.md`][doc-restart]. Usually the text will
read like:
-```
+```md
Save the file and [reconfigure GitLab](../../administration/restart_gitlab.md)
for the changes to take effect.
```
@@ -403,19 +403,19 @@ low.
You can use the following fake tokens as examples.
-| **Token type** | **Token value** |
-| --------------------- | --------------------------------- |
-| Private user token | `9koXpg98eAheJpvBs5tK` |
-| Personal access token | `n671WNGecHugsdEDPsyo` |
+| **Token type** | **Token value** |
+|:----------------------|:-------------------------------------------------------------------|
+| Private user token | `9koXpg98eAheJpvBs5tK` |
+| Personal access token | `n671WNGecHugsdEDPsyo` |
| Application ID | `2fcb195768c39e9a94cec2c2e32c59c0aad7a3365c10892e8116b5d83d4096b6` |
| Application secret | `04f294d1eaca42b8692017b426d53bbc8fe75f827734f0260710b83a556082df` |
-| Secret CI variable | `Li8j-mLUVA3eZYjPfd_H` |
-| Specific Runner token | `yrnZW46BrtBFqM7xDzE7dddd` |
-| Shared Runner token | `6Vk7ZsosqQyfreAxXTZr` |
-| Trigger token | `be20d8dcc028677c931e04f3871a9b` |
-| Webhook secret token | `6XhDroRcYPM5by_h-HLY` |
-| Health check token | `Tu7BgjR9qeZTEyRzGG2P` |
-| Request profile token | `7VgpS4Ax5utVD2esNstz` |
+| Secret CI variable | `Li8j-mLUVA3eZYjPfd_H` |
+| Specific Runner token | `yrnZW46BrtBFqM7xDzE7dddd` |
+| Shared Runner token | `6Vk7ZsosqQyfreAxXTZr` |
+| Trigger token | `be20d8dcc028677c931e04f3871a9b` |
+| Webhook secret token | `6XhDroRcYPM5by_h-HLY` |
+| Health check token | `Tu7BgjR9qeZTEyRzGG2P` |
+| Request profile token | `7VgpS4Ax5utVD2esNstz` |
### API
@@ -438,16 +438,16 @@ on this document. Further explanation is given below.
Use the following table headers to describe the methods. Attributes should
always be in code blocks using backticks (``` ` ```).
-```
+```md
| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
+|:----------|:-----|:---------|:------------|
```
Rendered example:
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `user` | string | yes | The GitLab username |
+| Attribute | Type | Required | Description |
+|:----------|:-------|:---------|:--------------------|
+| `user` | string | yes | The GitLab username |
#### cURL commands
@@ -459,12 +459,12 @@ Rendered example:
- Prefer to use examples using the personal access token and don't pass data of
username and password.
-| Methods | Description |
-| ------- | ----------- |
+| Methods | Description |
+|:-------------------------------------------|:------------------------------------------------------|
| `-H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"` | Use this method as is, whenever authentication needed |
-| `-X POST` | Use this method when creating new objects |
-| `-X PUT` | Use this method when updating existing objects |
-| `-X DELETE` | Use this method when removing existing objects |
+| `-X POST` | Use this method when creating new objects |
+| `-X PUT` | Use this method when updating existing objects |
+| `-X DELETE` | Use this method when removing existing objects |
#### cURL Examples
diff --git a/doc/development/documentation/workflow.md b/doc/development/documentation/workflow.md
index 52bc059a925..75ce8640e87 100644
--- a/doc/development/documentation/workflow.md
+++ b/doc/development/documentation/workflow.md
@@ -7,16 +7,16 @@ description: Learn the process of shipping documentation for GitLab.
At GitLab, developers contribute new or updated documentation along with their code, but product managers and technical writers also have essential roles in the process.
- Product Managers (PMs): in the issue for all new and updated features,
-PMs include specific documentation requirements that the developer who is
-writing or updating the docs must meet, along with feature descriptions
-and use cases. They call out any specific areas where collaborating with
-a technical writer is recommended, and usually act as the first reviewer
-of the docs.
+ PMs include specific documentation requirements that the developer who is
+ writing or updating the docs must meet, along with feature descriptions
+ and use cases. They call out any specific areas where collaborating with
+ a technical writer is recommended, and usually act as the first reviewer
+ of the docs.
- Developers: author documentation and merge it on time (up to a week after
-the feature freeze).
+ the feature freeze).
- Technical Writers: review each issue to ensure PM's requirements are complete,
-help developers with any questions throughout the process, and act as the final
-reviewer of all new and updated docs content before it's merged.
+ help developers with any questions throughout the process, and act as the final
+ reviewer of all new and updated docs content before it's merged.
## Requirements
@@ -112,17 +112,17 @@ and the missed-deliverable due date (the 14th of each month) are both respected.
The developer should add to the feature MR the documentation containing:
- The [documentation blurb](structure.md#documentation-blurb): copy the
-feature name, overview/description, and use cases from the feature issue
+ feature name, overview/description, and use cases from the feature issue
- Instructions: write how to use the feature, step by step, with no gaps.
- [Crosslink for discoverability](structure.md#discoverability): link with
-internal docs and external resources (if applicable)
+ internal docs and external resources (if applicable)
- Index: link the new doc or the new heading from the higher-level index
-for [discoverability](#discoverability)
+ for [discoverability](#discoverability)
- [Screenshots](styleguide.md#images): when necessary, add screenshots for:
- Illustrating a step of the process
- Indicating the location of a navigation menu
- Label the MR with `Documentation`, `Deliverable`, `docs-P1`, and assign
-the correct milestone
+ the correct milestone
- Assign the PM for review
- When done, mention the `@gl\-docsteam` in the MR asking for review
- **Due date**: feature freeze date and time
@@ -133,10 +133,10 @@ If the docs aren't being shipped within the feature MR:
- Create a new issue mentioning "docs" or "documentation" in the title (use the Documentation issue description template)
- Label the issue with: `Documentation`, `Deliverable`, `docs-P1`, `<product-label>`
-(product label == CI/CD, Pages, Prometheus, etc)
+ (product label == CI/CD, Pages, Prometheus, etc)
- Add the correct milestone
- Create a new MR for shipping the docs changes and follow the same
-process [described above](#documentation-shipped-in-the-feature-mr)
+ process [described above](#documentation-shipped-in-the-feature-mr)
- Use the MR description template called "Documentation"
- Add the same labels and milestone as you did for the issue
- Assign the PM for review
@@ -162,9 +162,9 @@ The **due date** for **merging** `missed-deliverable` MRs is on the
- **Planning**
- Once an issue contains a Documentation label and the current milestone, a
-technical writer reviews the Product Manager's documentation requirements
+ technical writer reviews the Product Manager's documentation requirements.
- Once the documentation requirements are approved, the technical writer can
-work with the developer to discuss any documentation questions and plans/outlines, as needed.
+ work with the developer to discuss any documentation questions and plans/outlines, as needed.
- **Review** - A technical writer must review the documentation for:
- Clarity
@@ -183,4 +183,3 @@ work with the developer to discuss any documentation questions and plans/outline
- Describe the difference between new features and feature updates
- Creating a new doc vs updating an existing doc
-->
-
diff --git a/doc/development/performance.md b/doc/development/performance.md
index c7b10dfd5ce..e738f2b4b66 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -34,13 +34,14 @@ graphs/dashboards.
## Tooling
-GitLab provides built-in tools to aid the process of improving performance:
+GitLab provides built-in tools to help improve performance and availability:
* [Profiling](profiling.md)
* [Sherlock](profiling.md#sherlock)
* [GitLab Performance Monitoring](../administration/monitoring/performance/index.md)
* [Request Profiling](../administration/monitoring/performance/request_profiling.md)
* [QueryRecoder](query_recorder.md) for preventing `N+1` regressions
+* [Chaos endpoints](chaos_endpoints.md) for testing failure scenarios. Intended mainly for testing availability.
GitLab employees can use GitLab.com's performance monitoring systems located at
<https://dashboards.gitlab.net>, this requires you to log in using your
diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md
index acaed53e052..4cb8ca4f3e7 100644
--- a/doc/install/database_mysql.md
+++ b/doc/install/database_mysql.md
@@ -1,15 +1,20 @@
# Database MySQL
-> **Note:**
-> - We do not recommend using MySQL due to various issues. For example, case
- [(in)sensitivity](https://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html)
- and [problems](https://bugs.mysql.com/bug.php?id=65830) that
- [suggested](https://bugs.mysql.com/bug.php?id=50909)
- [fixes](https://bugs.mysql.com/bug.php?id=65830) [have](https://bugs.mysql.com/bug.php?id=63164).
+NOTE: **Note:**
+We do not recommend using MySQL due to various issues.
+For example, there have been bugs with case
+[(in)sensitivity](https://dev.mysql.com/doc/refman/5.7/en/case-sensitivity.html).
+
+Bugs relating to case sensitivity:
+
+- <https://bugs.mysql.com/bug.php?id=65830>
+- <https://bugs.mysql.com/bug.php?id=50909>
+- <https://bugs.mysql.com/bug.php?id=65830>
+- <https://bugs.mysql.com/bug.php?id=63164>
## Initial database setup
-```
+```sh
# Install the database packages
sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev
@@ -84,8 +89,9 @@ GitLab 8.14 has introduced [a feature](https://gitlab.com/gitlab-org/gitlab-ce/m
Follow the below instructions to ensure you use the most up to date requirements for your GitLab MySQL Database.
**We are about to do the following:**
+
- Ensure you can enable `utf8mb4` encoding and `utf8mb4_general_ci` collation for your GitLab DB, tables and data.
-- Convert your GitLab tables and data from `utf8`/`utf8_general_ci` to `utf8mb4`/`utf8mb4_general_ci`
+- Convert your GitLab tables and data from `utf8`/`utf8_general_ci` to `utf8mb4`/`utf8mb4_general_ci`.
### Check for utf8mb4 support
@@ -130,7 +136,8 @@ We need to check, enable and maybe convert your existing GitLab DB tables to the
Whatever the results of your checks above, we now need to check if your GitLab database has been created using [InnoDB File-Per-Table Tablespaces](http://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) (i.e. `innodb_file_per_table` was set to **1** at initial setup time).
-> Note: This setting is [enabled by default](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_file_per_table) since MySQL 5.6.6.
+NOTE: **Note:**
+This setting is [enabled by default](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_file_per_table) since MySQL 5.6.6.
# Run this command with root privileges, replace the data dir if different:
sudo ls -lh /var/lib/mysql/gitlabhq_production/*.ibd | wc -l
@@ -138,20 +145,19 @@ Whatever the results of your checks above, we now need to check if your GitLab d
# Run this command with root privileges, replace the data dir if different:
sudo ls -lh /var/lib/mysql/gitlabhq_production/*.frm | wc -l
-
- **Case 1: a result > 0 for both commands**
-Congrats, your GitLab database uses the right InnoDB tablespace format.
+Congratulations, your GitLab database uses the right InnoDB tablespace format.
However, you must still ensure that any **future tables** created by GitLab will still use the right format:
- If `SELECT @@innodb_file_per_table` returned **1** previously, your server is running correctly.
- > It's however a requirement to check *now* that this setting is indeed persisted in your [my.cnf](https://dev.mysql.com/doc/refman/5.7/en/tablespace-enabling.html) file!
+ > It's however a requirement to check *now* that this setting is indeed persisted in your [`my.cnf`](https://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) file!
- If `SELECT @@innodb_file_per_table` returned **0** previously, your server is not running correctly.
- > [Enable innodb_file_per_table](https://dev.mysql.com/doc/refman/5.7/en/tablespace-enabling.html) by running in a MySQL session as root the command `SET GLOBAL innodb_file_per_table=1, innodb_file_format=Barracuda;` and persist the two settings in your [my.cnf](https://dev.mysql.com/doc/refman/5.7/en/tablespace-enabling.html) file
+ > [Enable innodb_file_per_table](https://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) by running in a MySQL session as root the command `SET GLOBAL innodb_file_per_table=1, innodb_file_format=Barracuda;` and persist the two settings in your [`my.cnf`](https://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) file.
Now, if you have a **different result** returned by the 2 commands above, it means you have a **mix of tables format** uses in your GitLab database. This can happen if your MySQL server had different values for `innodb_file_per_table` in its life and you updated GitLab at different moments with those inconsistent values. So keep reading.
@@ -172,7 +178,7 @@ Let's enable what we need on the running server:
# You can now quit the database session
mysql> \q
-> Now, **persist** [innodb_file_per_table](https://dev.mysql.com/doc/refman/5.6/en/tablespace-enabling.html) and [innodb_file_format](https://dev.mysql.com/doc/refman/5.6/en/innodb-file-format-enabling.html) in your `my.cnf` file.
+> Now, **persist** [innodb_file_per_table](https://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) and [innodb_file_format](https://dev.mysql.com/doc/refman/5.7/en/innodb-file-format-enabling.html) in your `my.cnf` file.
Ensure at this stage that your GitLab instance is indeed **stopped**.
@@ -184,7 +190,7 @@ Now, let's convert all the GitLab database tables to the new tablespace format:
# Type the MySQL root password
mysql > use gitlabhq_production;
- # Safety check: you should still have those values set as follow:
+ # Safety check: you should still have those values set as follows:
mysql> SELECT @@innodb_file_per_table, @@innodb_file_format;
+-------------------------+----------------------+
| @@innodb_file_per_table | @@innodb_file_format |
@@ -203,7 +209,7 @@ Now, let's convert all the GitLab database tables to the new tablespace format:
#### Check for proper InnoDB File Format, Row Format, Large Prefix and tables conversion
-We need to check, enable and probably convert your existing GitLab DB tables to use the [Barracuda InnoDB file format](https://dev.mysql.com/doc/refman/5.6/en/innodb-file-format.html), the [DYNAMIC row format](https://dev.mysql.com/doc/refman/5.6/en/glossary.html#glos_dynamic_row_format) and [innodb_large_prefix](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_large_prefix) as a second prerequisite for supporting **utfb8mb4 with long indexes** used by recent GitLab databases.
+We need to check, enable and probably convert your existing GitLab DB tables to use the [Barracuda InnoDB file format](https://dev.mysql.com/doc/refman/5.7/en/innodb-file-format.html), the [DYNAMIC row format](https://dev.mysql.com/doc/refman/5.7/en/glossary.html#glos_dynamic_row_format) and [innodb_large_prefix](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_large_prefix) as a second prerequisite for supporting **utfb8mb4 with long indexes** used by recent GitLab databases.
# Login to MySQL
mysql -u root -p
@@ -229,7 +235,7 @@ We need to check, enable and probably convert your existing GitLab DB tables to
| utf8 | utf8_general_ci |
+--------------------------+----------------------+
-> Now, ensure that [innodb_file_format](https://dev.mysql.com/doc/refman/5.6/en/tablespace-enabling.html) and [innodb_large_prefix](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_large_prefix) are **persisted** in your `my.cnf` file.
+> Now, ensure that [innodb_file_format](https://dev.mysql.com/doc/refman/5.7/en/innodb-multiple-tablespaces.html) and [innodb_large_prefix](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_large_prefix) are **persisted** in your `my.cnf` file.
#### Tables and data conversion to utf8mb4
@@ -257,7 +263,7 @@ Now that you have a persistent MySQL setup, you can safely upgrade tables after
Ensure your GitLab database configuration file uses a proper connection encoding and collation:
-```sudo -u git -H editor config/database.yml```
+`sudo -u git -H editor config/database.yml`
production:
adapter: mysql2
@@ -266,19 +272,19 @@ Ensure your GitLab database configuration file uses a proper connection encoding
[Restart your GitLab instance](../administration/restart_gitlab.md).
-
## MySQL strings limits
After installation or upgrade, remember to run the `add_limits_mysql` Rake task:
**Omnibus GitLab installations**
-```
+
+```sh
sudo gitlab-rake add_limits_mysql
```
**Installations from source**
-```
+```sh
bundle exec rake add_limits_mysql RAILS_ENV=production
```
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 37c826ce9e0..316411d1047 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -12,7 +12,7 @@ Since installations from source don't have Runit, Sidekiq can't be terminated an
## Select Version to Install
-Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the branch (version) of GitLab you would like to install (e.g., `11-4-stable`).
+Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the branch (version) of GitLab you would like to install (e.g., `11-5-stable`).
You can select the branch in the version dropdown in the top left corner of GitLab (below the menu bar).
If the highest number stable branch is unclear please check the [GitLab Blog](https://about.gitlab.com/blog/) for installation guide links by version.
@@ -300,9 +300,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-4-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-5-stable gitlab
-**Note:** You can change `11-4-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `11-5-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
diff --git a/doc/raketasks/import.md b/doc/raketasks/import.md
index 97e9b36d1a6..bb316df5b9a 100644
--- a/doc/raketasks/import.md
+++ b/doc/raketasks/import.md
@@ -6,6 +6,7 @@
- The groups will be created as needed, including subgroups
- The owner of the group will be the first admin
- Existing projects will be skipped
+- Projects in hashed storage may be skipped (see [Importing bare repositories from hashed storage](#importing-bare-repositories-from-hashed-storage))
- The existing Git repos will be moved from disk (removed from the original path)
## How to use
@@ -26,7 +27,6 @@ sudo -u git mkdir /var/opt/gitlab/git-data/repository-import-<date>/new_group
If we copy the repos to `/var/opt/gitlab/git-data/repository-import-<date>`, and repo A needs to be under the groups G1 and G2, it will
have to be created under those folders: `/var/opt/gitlab/git-data/repository-import-<date>/G1/G2/A.git`.
-
```
sudo cp -r /old/git/foo.git /var/opt/gitlab/git-data/repository-import-<date>/new_group/
@@ -70,3 +70,73 @@ Processing /var/opt/gitlab/git-data/repository-import-1/group/xyz.git
* Skipping repo /var/opt/gitlab/git-data/repository-import-1/@shared/a/b/abcd.git
[...]
```
+
+## Importing bare repositories from hashed storage
+
+### Background
+
+Projects in legacy storage have a directory structure that mirrors their full
+project path in GitLab, including their namespace structure. This information is
+leveraged by the bare repository importer to import projects into their proper
+locations. Each project and its parent namespaces are meaningfully named.
+
+However, the directory structure of projects in hashed storage do not contain
+this information. This is beneficial for a variety of reasons, especially
+improved performance and data integrity. See
+[Repository Storage Types](../administration/repository_storage_types.md) for
+more details.
+
+### Which repositories are importable?
+
+#### GitLab 10.3 or earlier
+
+Importing bare repositories from hashed storage is unsupported.
+
+#### GitLab 10.4 and later
+
+To support importing bare repositories from hashed storage, GitLab 10.4 and
+later stores the full project path with each repository, in a special section of
+the git repository's config file. This section is formatted as follows:
+
+```
+[gitlab]
+ fullpath = gitlab-org/gitlab-ce
+```
+
+However, existing repositories were not migrated to include this path.
+
+Bare repositories are importable if the following events occurred to the
+repository in GitLab 10.4 and later:
+
+- Created
+- Migrated to hashed storage
+- Renamed
+- Transferred to another namespace
+- Ancestor renamed
+- Ancestor transferred to another namespace
+
+Bare repositories are **not** importable by GitLab 10.4 and later when all the following are true about the repository:
+
+- It was created in GitLab 10.3 or earlier.
+- It was not renamed, transferred, or migrated to hashed storage in GitLab 10.4 and later.
+- Its ancestor namespaces were not renamed or transferred in GitLab 10.4 and later.
+
+There is an [open issue to add a migration to make all bare repositories
+importable](https://gitlab.com/gitlab-org/gitlab-ce/issues/41776).
+
+Until then, you may wish to manually migrate repositories yourself. You can use
+[Rails console](https://docs.gitlab.com/omnibus/maintenance/#starting-a-rails-console-session)
+to do so. In a Rails console session, run the following to migrate a project:
+
+```
+project = Project.find_by_full_path('gitlab-org/gitlab-ce')
+project.write_repository_config
+```
+
+In a Rails console session, run the following to migrate all of a namespace's
+projects (this may take a while if there are 1000s of projects in a namespace):
+
+```
+namespace = Namespace.find_by_full_path('gitlab-org')
+namespace.send(:write_projects_repository_config)
+```
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index 73301394e9f..394f3ea60b7 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -36,7 +36,7 @@ This page gathers all the resources for the topic **Authentication** within GitL
## API
- [OAuth 2 Tokens](../../api/README.md#oauth-2-tokens)
-- [Private Tokens](../../api/README.md#private-tokens)
+- [Personal access tokens](../../api/README.md#personal-access-tokens)
- [Impersonation tokens](../../api/README.md#impersonation-tokens)
- [GitLab as an OAuth2 provider](../../api/oauth2.md#gitlab-as-an-oauth2-provider)
diff --git a/doc/university/training/topics/additional_resources.md b/doc/university/training/topics/additional_resources.md
index d01634df744..4871372d105 100644
--- a/doc/university/training/topics/additional_resources.md
+++ b/doc/university/training/topics/additional_resources.md
@@ -4,9 +4,9 @@ comments: false
# Additional Resources
-1. GitLab Documentation [http://docs.gitlab.com](http://docs.gitlab.com/)
-2. GUI Clients [http://git-scm.com/downloads/guis](http://git-scm.com/downloads/guis)
-3. Pro git book [http://git-scm.com/book](http://git-scm.com/book)
-4. Platzi Course [https://courses.platzi.com/courses/git-gitlab/](https://courses.platzi.com/courses/git-gitlab/)
-5. Code School tutorial [http://try.github.io/](http://try.github.io/)
-6. Contact Us at `subscribers@gitlab.com`
+1. GitLab Documentation: <http://docs.gitlab.com>.
+1. GUI Clients: <http://git-scm.com/downloads/guis>.
+1. Pro Git book: <http://git-scm.com/book>.
+1. Platzi Course: <https://courses.platzi.com/courses/git-gitlab/>.
+1. Code School tutorial: <http://try.github.io/>.
+1. Contact us at `subscribers@gitlab.com`.
diff --git a/doc/university/training/topics/tags.md b/doc/university/training/topics/tags.md
index 6333ceedbd7..9526bcbfb82 100644
--- a/doc/university/training/topics/tags.md
+++ b/doc/university/training/topics/tags.md
@@ -22,7 +22,7 @@ comments: false
**Additional resources**
-[http://git-scm.com/book/en/Git-Basics-Tagging](http://git-scm.com/book/en/Git-Basics-Tagging)
+<http://git-scm.com/book/en/Git-Basics-Tagging>
----------
diff --git a/doc/university/training/user_training.md b/doc/university/training/user_training.md
index f3a4d766522..ca3f777f403 100644
--- a/doc/university/training/user_training.md
+++ b/doc/university/training/user_training.md
@@ -385,7 +385,7 @@ Thank you for your hard work!
- GitLab Documentation: <http://docs.gitlab.com/>.
- GUI Clients: <http://git-scm.com/downloads/guis>.
-- Pro git book: <http://git-scm.com/book>.
+- Pro Git book: <http://git-scm.com/book>.
- Platzi Course: <https://courses.platzi.com/courses/git-gitlab/>.
- Code School tutorial: <http://try.github.io/>.
- Contact us at `subscribers@gitlab.com`.
diff --git a/doc/update/11.4-to-11.5.md b/doc/update/11.4-to-11.5.md
index e64ab2acae2..44105348d14 100644
--- a/doc/update/11.4-to-11.5.md
+++ b/doc/update/11.4-to-11.5.md
@@ -91,11 +91,11 @@ Download and install Go:
# Remove former Go installation folder
sudo rm -rf /usr/local/go
-curl --remote-name --progress https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz
-echo 'fa1b0e45d3b647c252f51f5e1204aba049cde4af177ef9f2181f43004f901035 go1.10.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
- sudo tar -C /usr/local -xzf go1.10.3.linux-amd64.tar.gz
+curl --remote-name --progress https://dl.google.com/go/go1.10.5.linux-amd64.tar.gz
+echo 'a035d9beda8341b645d3f45a1b620cf2d8fb0c5eb409be36b389c0fd384ecc3a go1.10.5.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.10.5.linux-amd64.tar.gz
sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
-rm go1.10.3.linux-amd64.tar.gz
+rm go1.10.5.linux-amd64.tar.gz
```
### 6. Get latest code
@@ -153,20 +153,6 @@ sudo -u git -H make
### 9. Update Gitaly
-#### New Gitaly configuration options required
-
-In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`.
-
-```shell
-echo '
-[gitaly-ruby]
-dir = "/home/git/gitaly/ruby"
-
-[gitlab-shell]
-dir = "/home/git/gitlab-shell"
-' | sudo -u git tee -a /home/git/gitaly/config.toml
-```
-
#### Check Gitaly configuration
Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
@@ -272,10 +258,10 @@ Ensure you're still up-to-date with the latest NGINX configuration changes:
cd /home/git/gitlab
# For HTTPS configurations
-git diff origin/11-1-stable:lib/support/nginx/gitlab-ssl origin/11-5-stable:lib/support/nginx/gitlab-ssl
+git diff origin/11-4-stable:lib/support/nginx/gitlab-ssl origin/11-5-stable:lib/support/nginx/gitlab-ssl
# For HTTP configurations
-git diff origin/11-1-stable:lib/support/nginx/gitlab origin/11-5-stable:lib/support/nginx/gitlab
+git diff origin/11-4-stable:lib/support/nginx/gitlab origin/11-5-stable:lib/support/nginx/gitlab
```
If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
@@ -309,7 +295,7 @@ There might be new configuration options available for [`gitlab.default.example`
```sh
cd /home/git/gitlab
-git diff origin/11-1-stable:lib/support/init.d/gitlab.default.example origin/11-5-stable:lib/support/init.d/gitlab.default.example
+git diff origin/11-4-stable:lib/support/init.d/gitlab.default.example origin/11-5-stable:lib/support/init.d/gitlab.default.example
```
Ensure you're still up-to-date with the latest init script changes:
diff --git a/doc/user/project/clusters/eks_and_gitlab/index.md b/doc/user/project/clusters/eks_and_gitlab/index.md
index 10f0cdb333e..ef19b05fb9e 100644
--- a/doc/user/project/clusters/eks_and_gitlab/index.md
+++ b/doc/user/project/clusters/eks_and_gitlab/index.md
@@ -13,11 +13,13 @@ date: 2018-06-05
In this tutorial, we will show how easy it is to integrate an [Amazon EKS](https://aws.amazon.com/eks/) cluster with GitLab, and begin deploying applications.
For an end-to-end walkthrough we will:
+
1. Start with a new project based on the sample Ruby on Rails template
1. Integrate an EKS cluster
1. Utilize [Auto DevOps](../../../../topics/autodevops/) to build, test, and deploy our application
You will need:
+
1. An account on GitLab, like [GitLab.com](https://gitlab.com)
1. An Amazon EKS cluster
1. `kubectl` [installed and configured for access to the EKS cluster](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html#get-started-kubectl)
@@ -49,11 +51,12 @@ A few details from the EKS cluster will be required to connect it to GitLab.
1. The API server endpoint is also required, so GitLab can connect to the cluster. This is displayed on the AWS EKS console, when viewing the EKS cluster details.
You now have all the information needed to connect the EKS cluster:
+
* Kubernetes cluster name: Provide a name for the cluster to identify it within GitLab.
* Environment scope: Leave this as `*` for now, since we are only connecting a single cluster.
* API URL: Paste in the API server endpoint retrieved above.
* CA Certificate: Paste the certificate data from the earlier step, as-is.
-* Paste the token value. Note on some versions of Kubernetes a trailing `%` is output, do not include it.
+* Paste the token value.
* Project namespace: This can be left blank to accept the default namespace, based on the project name.
![Add Cluster](img/add_cluster.png)
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index 762d254d6cc..233ed205790 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -132,59 +132,62 @@ functionalities needed to successfully build and deploy a containerized
application. Bare in mind that the same credentials are used for all the
applications running on the cluster.
-When GitLab creates the cluster, it enables and uses the legacy
-[Attribute-based access control (ABAC)](https://kubernetes.io/docs/admin/authorization/abac/).
-The newer [RBAC](https://kubernetes.io/docs/admin/authorization/rbac/)
-authorization is [experimental](#role-based-access-control-rbac).
-
-### Role-based access control (RBAC) **[CORE ONLY]**
-
-> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21401) in GitLab 11.4.
-
-CAUTION: **Warning:**
-The RBAC authorization is experimental.
-
-Once RBAC is enabled for a cluster, GitLab will create the necessary service accounts
-and privileges in order to install and run [GitLab managed applications](#installing-applications).
-
-If you are creating a [new GKE cluster via
-GitLab](#adding-and-creating-a-new-gke-cluster-via-gitlab), you will be
-asked if you would like to create an RBAC-enabled cluster. Enabling this
-setting will create a `gitlab` service account which will be used by
-GitLab to manage the newly created cluster. To enable this, this service
-account will have the `cluster-admin` privilege.
-
-If you are [adding an existing Kubernetes
-cluster](#adding-an-existing-kubernetes-cluster), you will be asked if
-the cluster you are adding is a RBAC-enabled cluster. Ensure the
-token of the account has administrator privileges for the cluster.
-
-In both cases above, when you install Helm Tiller into your cluster, an
-RBAC-enabled cluster will create a `tiller` service account, with `cluster-admin`
-privileges in the `gitlab-managed-apps` namespace. This service account will be
-added to the installed Helm Tiller and will be used by Helm to install and run
-[GitLab managed applications](#installing-applications).
-
-The table below summarizes which resources will be created in a
-RBAC-enabled cluster :
-
-| Name | Kind | Details | Created when |
-| --- | --- | --- | --- |
-| `gitlab` | `ServiceAccount` | `default` namespace | Creating a new GKE Cluster |
-| `gitlab-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Creating a new GKE Cluster |
-| `gitlab-token` | `Secret` | Token for `gitlab` ServiceAccount | Creating a new GKE Cluster |
-| `tiller` | `ServiceAccount` | `gitlab-managed-apps` namespace | Installing Helm Tiller |
-| `tiller-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Installing Helm Tiller |
-
-
-Helm Tiller will also create additional service accounts and other RBAC
-resources for each installed application. Consult the documentation for the
-Helm charts for each application for details.
-
-NOTE: **Note:**
-Auto DevOps will not successfully complete in a cluster that only has RBAC
-authorization enabled. RBAC support for Auto DevOps is planned in a
-[future release](https://gitlab.com/gitlab-org/gitlab-ce/issues/44597).
+## Access controls
+
+When creating a cluster in GitLab, you will be asked if you would like to create an
+[Attribute-based access control (ABAC)](https://kubernetes.io/docs/admin/authorization/abac/) cluster, or
+a [Role-based access control (RBAC)](https://kubernetes.io/docs/admin/authorization/rbac/) one.
+
+Whether ABAC or RBAC is enabled, GitLab will create the necessary
+service accounts and privileges in order to install and run
+[GitLab managed applications](#installing-applications):
+
+- A `gitlab` service account with `cluster-admin` privileges will be created in the
+ `default` namespace, which will be used by GitLab to manage the newly created cluster.
+
+- A project service account with `edit` privileges will be created in
+ the project namespace (also created by GitLab), which will be used in
+ [deployment jobs](#deployment-variables).
+
+ NOTE: **Note:**
+ Restricted service account for deployment was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/51716) in GitLab 11.5.
+
+- When you install Helm Tiller into your cluster, the `tiller` service account
+ will be created with `cluster-admin` privileges in the `gitlab-managed-apps`
+ namespace. This service account will be added to the installed Helm Tiller and will
+ be used by Helm to install and run [GitLab managed applications](#installing-applications).
+ Helm Tiller will also create additional service accounts and other resources for each
+ installed application. Consult the documentation of the Helm charts for each application
+ for details.
+
+If you are [adding an existing Kubernetes cluster](#adding-an-existing-kubernetes-cluster),
+ensure the token of the account has administrator privileges for the cluster.
+
+The following sections summarize which resources will be created on ABAC/RBAC clusters.
+
+### Attribute-based access control (ABAC)
+
+| Name | Kind | Details | Created when |
+| --- | --- | --- | --- |
+| `gitlab` | `ServiceAccount` | `default` namespace | Creating a new GKE Cluster |
+| `gitlab-token` | `Secret` | Token for `gitlab` ServiceAccount | Creating a new GKE Cluster |
+| `tiller` | `ServiceAccount` | `gitlab-managed-apps` namespace | Installing Helm Tiller |
+| `tiller-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Installing Helm Tiller |
+| Project namespace | `ServiceAccount` | Uses namespace of Project | Creating/Adding a new GKE Cluster |
+| Project namespace | `Secret` | Token for project ServiceAccount | Creating/Adding a new GKE Cluster |
+
+### Role-based access control (RBAC)
+
+| Name | Kind | Details | Created when |
+| --- | --- | --- | --- |
+| `gitlab` | `ServiceAccount` | `default` namespace | Creating a new GKE Cluster |
+| `gitlab-admin` | `ClusterRoleBinding` | [`cluster-admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) roleRef | Creating a new GKE Cluster |
+| `gitlab-token` | `Secret` | Token for `gitlab` ServiceAccount | Creating a new GKE Cluster |
+| `tiller` | `ServiceAccount` | `gitlab-managed-apps` namespace | Installing Helm Tiller |
+| `tiller-admin` | `ClusterRoleBinding` | `cluster-admin` roleRef | Installing Helm Tiller |
+| Project namespace | `ServiceAccount` | Uses namespace of Project | Creating/Adding a new GKE Cluster |
+| Project namespace | `Secret` | Token for project ServiceAccount | Creating/Adding a new GKE Cluster |
+| Project namespace | `RoleBinding` | [`edit`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) roleRef | Creating/Adding a new GKE Cluster |
### Security of GitLab Runners
@@ -211,7 +214,7 @@ added directly to your configured cluster. Those applications are needed for
[Review Apps](../../../ci/review_apps/index.md) and [deployments](../../../ci/environments.md).
NOTE: **Note:**
-The applications will be installed in a dedicated namespace called
+With the exception of Knative, the applications will be installed in a dedicated namespace called
`gitlab-managed-apps`. In case you have added an existing Kubernetes cluster
with Tiller already installed, you should be careful as GitLab cannot
detect it. By installing it via the applications will result into having it
@@ -224,6 +227,7 @@ twice, which can lead to confusion during deployments.
| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications. | [stable/prometheus](https://github.com/helm/charts/tree/master/stable/prometheus) |
| [GitLab Runner](https://docs.gitlab.com/runner/) | 10.6+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/), the open-source continuous integration service included with GitLab that coordinates the jobs. When installing the GitLab Runner via the applications, it will run in **privileged mode** by default. Make sure you read the [security implications](#security-implications) before doing so. | [runner/gitlab-runner](https://gitlab.com/charts/gitlab-runner) |
| [JupyterHub](http://jupyter.org/) | 11.0+ | [JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a multi-user service for managing notebooks across a team. [Jupyter Notebooks](https://jupyter-notebook.readthedocs.io/en/latest/) provide a web-based interactive programming environment used for data analysis, visualization, and machine learning. We use [this](https://gitlab.com/gitlab-org/jupyterhub-user-image/blob/master/Dockerfile) custom Jupyter image that installs additional useful packages on top of the base Jupyter. You will also see ready-to-use DevOps Runbooks built with Nurtch's [Rubix library](https://github.com/amit1rrr/rubix). More information on creating executable runbooks can be found at [Nurtch Documentation](http://docs.nurtch.com/en/latest). **Note**: Authentication will be enabled for any user of the GitLab server via OAuth2. HTTPS will be supported in a future release. | [jupyter/jupyterhub](https://jupyterhub.github.io/helm-chart/) |
+| [Knative](https://cloud.google.com/knative) | 0.1.2 | Knative provides a platform to create, deploy, and manage serverless workloads from a Kubernetes cluster. It is used in conjunction with, and includes [Istio](https://istio.io) to provide an external IP address for all programs hosted by Knative. You will be prompted to enter a wildcard domain where your applications will be exposed. Configure your DNS server to use the external IP address for that domain. For any application created and installed, they will be accessible as <program_name>.<kubernetes_namespace>.<domain_name>. **Note**: This will require your kubernetes cluster to have RBAC enabled. | [knative/knative](https://storage.googleapis.com/triggermesh-charts)
## Getting the external IP address
@@ -232,6 +236,10 @@ You need a load balancer installed in your cluster in order to obtain the
external IP address with the following procedure. It can be deployed using the
[**Ingress** application](#installing-applications).
+NOTE: **Note:**
+Knative will include its own load balancer in the form of [Istio](https://istio.io).
+At this time, to determine the external IP address, you will need to follow the manual approach.
+
In order to publish your web application, you first need to find the external IP
address associated to your load balancer.
@@ -262,6 +270,12 @@ run the following command:
kubectl get svc --namespace=gitlab-managed-apps ingress-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
```
+NOTE: **Note:**
+For Istio/Knative, the command will be different:
+```bash
+kubectl get svc --namespace=istio-system knative-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip} '
+```
+
Otherwise, you can list the IP addresses of all load balancers:
```bash
@@ -376,12 +390,16 @@ GitLab CI/CD build environment.
| Variable | Description |
| -------- | ----------- |
| `KUBE_URL` | Equal to the API URL. |
-| `KUBE_TOKEN` | The Kubernetes token. |
+| `KUBE_TOKEN` | The Kubernetes token of the [project service account](#access-controls). |
| `KUBE_NAMESPACE` | The Kubernetes namespace is auto-generated if not specified. The default value is `<project_name>-<project_id>`. You can overwrite it to use different one if needed, otherwise the `KUBE_NAMESPACE` variable will receive the default value. |
-| `KUBE_CA_PEM_FILE` | Only present if a custom CA bundle was specified. Path to a file containing PEM data. |
-| `KUBE_CA_PEM` | (**deprecated**) Only if a custom CA bundle was specified. Raw PEM data. |
+| `KUBE_CA_PEM_FILE` | Path to a file containing PEM data. Only present if a custom CA bundle was specified. |
+| `KUBE_CA_PEM` | (**deprecated**) Raw PEM data. Only if a custom CA bundle was specified. |
| `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. |
+NOTE: **NOTE:**
+Prior to GitLab 11.5, `KUBE_TOKEN` was the Kubernetes token of the main
+service account of the cluster integration.
+
## Enabling or disabling the Kubernetes cluster integration
After you have successfully added your cluster information, you can enable the
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 0a7f7d37384..6de2ab07fc4 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -166,6 +166,23 @@ administrator to do so.
![Create new merge requests by email](img/create_from_email.png)
+### Adding patches when creating a merge request via e-mail
+
+> **Note**: This feature was [implemented in GitLab 11.5](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22723)
+
+You can add commits to the merge request being created by adding
+patches as attachments to the email, all attachments with a filename
+ending in `.patch` will be considered patches. The patches will be processed
+ordered by name.
+
+The combined size of the patches can be 2MB.
+
+If the source branch from the subject does not exist, it will be
+created from the repository's HEAD or the specified target branch to
+apply the patches. The target branch can be specified using the
+[`/target_branch` quick action](../quick_actions.md). If the source
+branch already exists, the patches will be applied on top of it.
+
## Find the merge request that introduced a change
> **Note**: this feature was [implemented in GitLab 10.5](https://gitlab.com/gitlab-org/gitlab-ce/issues/2383).
diff --git a/lib/api/api.rb b/lib/api/api.rb
index c49c52213bf..8e259961828 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -143,6 +143,7 @@ module API
mount ::API::Settings
mount ::API::SidekiqMetrics
mount ::API::Snippets
+ mount ::API::Submodules
mount ::API::Subscriptions
mount ::API::SystemHooks
mount ::API::Tags
diff --git a/lib/api/submodules.rb b/lib/api/submodules.rb
new file mode 100644
index 00000000000..72d7d994102
--- /dev/null
+++ b/lib/api/submodules.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module API
+ class Submodules < Grape::API
+ before { authenticate! }
+
+ helpers do
+ def commit_params(attrs)
+ {
+ submodule: attrs[:submodule],
+ commit_sha: attrs[:commit_sha],
+ branch_name: attrs[:branch],
+ commit_message: attrs[:commit_message]
+ }
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The project ID'
+ end
+ resource :projects, requirements: Files::FILE_ENDPOINT_REQUIREMENTS do
+ desc 'Update existing submodule reference in repository' do
+ success Entities::Commit
+ end
+ params do
+ requires :submodule, type: String, desc: 'Url encoded full path to submodule.'
+ requires :commit_sha, type: String, desc: 'Commit sha to update the submodule to.'
+ requires :branch, type: String, desc: 'Name of the branch to commit into.'
+ optional :commit_message, type: String, desc: 'Commit message. If no message is provided a default one will be set.'
+ end
+ put ":id/repository/submodules/:submodule", requirements: Files::FILE_ENDPOINT_REQUIREMENTS do
+ authorize! :push_code, user_project
+
+ submodule_params = declared_params(include_missing: false)
+
+ result = ::Submodules::UpdateService.new(user_project, current_user, commit_params(submodule_params)).execute
+
+ if result[:status] == :success
+ commit_detail = user_project.repository.commit(result[:result])
+ present commit_detail, with: Entities::CommitDetail
+ else
+ render_api_error!(result[:message], result[:http_status] || 400)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bitbucket_server/client.rb b/lib/bitbucket_server/client.rb
index 15e59f93141..83e8808db07 100644
--- a/lib/bitbucket_server/client.rb
+++ b/lib/bitbucket_server/client.rb
@@ -35,9 +35,9 @@ module BitbucketServer
BitbucketServer::Representation::Repo.new(parsed_response)
end
- def repos
+ def repos(page_offset: 0, limit: nil)
path = "/repos"
- get_collection(path, :repo)
+ get_collection(path, :repo, page_offset: page_offset, limit: limit)
end
def create_branch(project_key, repo, branch_name, sha)
@@ -61,8 +61,8 @@ module BitbucketServer
private
- def get_collection(path, type)
- paginator = BitbucketServer::Paginator.new(connection, Addressable::URI.escape(path), type)
+ def get_collection(path, type, page_offset: 0, limit: nil)
+ paginator = BitbucketServer::Paginator.new(connection, Addressable::URI.escape(path), type, page_offset: page_offset, limit: limit)
BitbucketServer::Collection.new(paginator)
rescue *SERVER_ERRORS => e
raise ServerError, e
diff --git a/lib/bitbucket_server/collection.rb b/lib/bitbucket_server/collection.rb
index b50c5dde352..7e4b2277bbe 100644
--- a/lib/bitbucket_server/collection.rb
+++ b/lib/bitbucket_server/collection.rb
@@ -2,7 +2,13 @@
module BitbucketServer
class Collection < Enumerator
+ attr_reader :paginator
+
+ delegate :page_offset, :has_next_page?, to: :paginator
+
def initialize(paginator)
+ @paginator = paginator
+
super() do |yielder|
loop do
paginator.items.each { |item| yielder << item }
@@ -12,6 +18,24 @@ module BitbucketServer
lazy
end
+ def current_page
+ return 1 if page_offset <= 1
+
+ [1, page_offset].max
+ end
+
+ def prev_page
+ return nil unless current_page > 1
+
+ current_page - 1
+ end
+
+ def next_page
+ return nil unless has_next_page?
+
+ current_page + 1
+ end
+
def method_missing(method, *args)
return super unless self.respond_to?(method)
diff --git a/lib/bitbucket_server/paginator.rb b/lib/bitbucket_server/paginator.rb
index c351fb2f11f..aa5f84f44b3 100644
--- a/lib/bitbucket_server/paginator.rb
+++ b/lib/bitbucket_server/paginator.rb
@@ -4,34 +4,49 @@ module BitbucketServer
class Paginator
PAGE_LENGTH = 25
- def initialize(connection, url, type)
+ attr_reader :page_offset
+
+ def initialize(connection, url, type, page_offset: 0, limit: nil)
@connection = connection
@type = type
@url = url
@page = nil
+ @page_offset = page_offset
+ @limit = limit || PAGE_LENGTH
+ @total = 0
end
def items
raise StopIteration unless has_next_page?
+ raise StopIteration if over_limit?
@page = fetch_next_page
+ @total += @page.items.count
@page.items
end
+ def has_next_page?
+ page.nil? || page.next?
+ end
+
private
- attr_reader :connection, :page, :url, :type
+ attr_reader :connection, :page, :url, :type, :limit
- def has_next_page?
- page.nil? || page.next?
+ def over_limit?
+ @limit.positive? && @total >= @limit
end
def next_offset
- page.nil? ? 0 : page.next
+ page.nil? ? starting_offset : page.next
+ end
+
+ def starting_offset
+ [0, page_offset - 1].max * limit
end
def fetch_next_page
- parsed_response = connection.get(@url, start: next_offset, limit: PAGE_LENGTH)
+ parsed_response = connection.get(@url, start: next_offset, limit: @limit)
Page.new(parsed_response, type)
end
end
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 362014b1a09..8e8c979f973 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -26,16 +26,12 @@ module Gitlab
with_options allow_nil: true do
validates :tags, array_of_strings: true
validates :allow_failure, boolean: true
- validates :retry, numericality: { only_integer: true,
- greater_than_or_equal_to: 0,
- less_than_or_equal_to: 2 }
validates :parallel, numericality: { only_integer: true,
greater_than_or_equal_to: 2 }
validates :when,
inclusion: { in: %w[on_success on_failure always manual delayed],
message: 'should be on_success, on_failure, ' \
'always, manual or delayed' }
-
validates :dependencies, array_of_strings: true
validates :extends, type: String
end
@@ -86,6 +82,9 @@ module Gitlab
entry :coverage, Entry::Coverage,
description: 'Coverage configuration for this job.'
+ entry :retry, Entry::Retry,
+ description: 'Retry configuration for this job.'
+
helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables,
:artifacts, :commands, :environment, :coverage, :retry,
@@ -160,7 +159,7 @@ module Gitlab
environment: environment_defined? ? environment_value : nil,
environment_name: environment_defined? ? environment_value[:name] : nil,
coverage: coverage_defined? ? coverage_value : nil,
- retry: retry_defined? ? retry_value.to_i : nil,
+ retry: retry_defined? ? retry_value : nil,
parallel: parallel_defined? ? parallel_value.to_i : nil,
artifacts: artifacts_value,
after_script: after_script_value,
diff --git a/lib/gitlab/ci/config/entry/retry.rb b/lib/gitlab/ci/config/entry/retry.rb
new file mode 100644
index 00000000000..e39cc5de229
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/retry.rb
@@ -0,0 +1,90 @@
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ ##
+ # Entry that represents a retry config for a job.
+ #
+ class Retry < Simplifiable
+ strategy :SimpleRetry, if: -> (config) { config.is_a?(Integer) }
+ strategy :FullRetry, if: -> (config) { config.is_a?(Hash) }
+
+ class SimpleRetry < Entry::Node
+ include Entry::Validatable
+
+ validations do
+ validates :config, numericality: { only_integer: true,
+ greater_than_or_equal_to: 0,
+ less_than_or_equal_to: 2 }
+ end
+
+ def value
+ {
+ max: config
+ }
+ end
+
+ def location
+ 'retry'
+ end
+ end
+
+ class FullRetry < Entry::Node
+ include Entry::Validatable
+ include Entry::Attributable
+
+ ALLOWED_KEYS = %i[max when].freeze
+ attributes :max, :when
+
+ validations do
+ validates :config, allowed_keys: ALLOWED_KEYS
+
+ with_options allow_nil: true do
+ validates :max, numericality: { only_integer: true,
+ greater_than_or_equal_to: 0,
+ less_than_or_equal_to: 2 }
+
+ validates :when, array_of_strings_or_string: true
+ validates :when,
+ allowed_array_values: { in: FullRetry.possible_retry_when_values },
+ if: -> (config) { config.when.is_a?(Array) }
+ validates :when,
+ inclusion: { in: FullRetry.possible_retry_when_values },
+ if: -> (config) { config.when.is_a?(String) }
+ end
+ end
+
+ def self.possible_retry_when_values
+ @possible_retry_when_values ||= ::Ci::Build.failure_reasons.keys.map(&:to_s) + ['always']
+ end
+
+ def value
+ super.tap do |config|
+ # make sure that `when` is an array, because we allow it to
+ # be passed as a String in config for simplicity
+ config[:when] = Array.wrap(config[:when]) if config[:when]
+ end
+ end
+
+ def location
+ 'retry'
+ end
+ end
+
+ class UnknownStrategy < Entry::Node
+ def errors
+ ["#{location} has to be either an integer or a hash"]
+ end
+
+ def location
+ 'retry config'
+ end
+ end
+
+ def self.default
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb
index 805d26ca8d8..a1d552fb2e5 100644
--- a/lib/gitlab/ci/config/entry/validators.rb
+++ b/lib/gitlab/ci/config/entry/validators.rb
@@ -7,11 +7,11 @@ module Gitlab
module Validators
class AllowedKeysValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
- unknown_keys = record.config.try(:keys).to_a - options[:in]
+ unknown_keys = value.try(:keys).to_a - options[:in]
if unknown_keys.any?
- record.errors.add(:config, 'contains unknown keys: ' +
- unknown_keys.join(', '))
+ record.errors.add(attribute, "contains unknown keys: " +
+ unknown_keys.join(', '))
end
end
end
@@ -24,6 +24,16 @@ module Gitlab
end
end
+ class AllowedArrayValuesValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unkown_values = value - options[:in]
+ unless unkown_values.empty?
+ record.errors.add(attribute, "contains unknown values: " +
+ unkown_values.join(', '))
+ end
+ end
+ end
+
class ArrayOfStringsValidator < ActiveModel::EachValidator
include LegacyValidationHelpers
@@ -68,6 +78,14 @@ module Gitlab
end
end
+ class HashOrIntegerValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless value.is_a?(Hash) || value.is_a?(Integer)
+ record.errors.add(attribute, 'should be a hash or an integer')
+ end
+ end
+ end
+
class KeyValidator < ActiveModel::EachValidator
include LegacyValidationHelpers
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 84595f8afd7..fb117baca9e 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -26,7 +26,6 @@ module Gitlab
@repository = repository
@diff_refs = diff_refs
@fallback_diff_refs = fallback_diff_refs
- @unfolded = false
# Ensure items are collected in the the batch
new_blob_lazy
@@ -136,24 +135,6 @@ module Gitlab
Gitlab::Diff::Parser.new.parse(raw_diff.each_line, diff_file: self).to_a
end
- # Changes diff_lines according to the given position. That is,
- # it checks whether the position requires blob lines into the diff
- # in order to be presented.
- def unfold_diff_lines(position)
- return unless position
-
- unfolder = Gitlab::Diff::LinesUnfolder.new(self, position)
-
- if unfolder.unfold_required?
- @diff_lines = unfolder.unfolded_diff_lines
- @unfolded = true
- end
- end
-
- def unfolded?
- @unfolded
- end
-
def highlighted_diff_lines
@highlighted_diff_lines ||=
Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index 70063071ee7..5b67cd46c48 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -3,9 +3,9 @@ module Gitlab
class Line
SERIALIZE_KEYS = %i(line_code rich_text text type index old_pos new_pos).freeze
- attr_reader :line_code, :type, :old_pos, :new_pos
+ attr_reader :line_code, :type, :index, :old_pos, :new_pos
attr_writer :rich_text
- attr_accessor :text, :index
+ attr_accessor :text
def initialize(text, type, index, old_pos, new_pos, parent_file: nil, line_code: nil, rich_text: nil)
@text, @type, @index = text, type, index
@@ -19,14 +19,7 @@ module Gitlab
end
def self.init_from_hash(hash)
- new(hash[:text],
- hash[:type],
- hash[:index],
- hash[:old_pos],
- hash[:new_pos],
- parent_file: hash[:parent_file],
- line_code: hash[:line_code],
- rich_text: hash[:rich_text])
+ new(hash[:text], hash[:type], hash[:index], hash[:old_pos], hash[:new_pos], line_code: hash[:line_code], rich_text: hash[:rich_text])
end
def to_hash
diff --git a/lib/gitlab/diff/lines_unfolder.rb b/lib/gitlab/diff/lines_unfolder.rb
deleted file mode 100644
index 9306b7e16a2..00000000000
--- a/lib/gitlab/diff/lines_unfolder.rb
+++ /dev/null
@@ -1,235 +0,0 @@
-# frozen_string_literal: true
-
-# Given a position, calculates which Blob lines should be extracted, treated and
-# injected in the current diff file lines in order to present a "unfolded" diff.
-module Gitlab
- module Diff
- class LinesUnfolder
- include Gitlab::Utils::StrongMemoize
-
- UNFOLD_CONTEXT_SIZE = 3
-
- def initialize(diff_file, position)
- @diff_file = diff_file
- @blob = diff_file.old_blob
- @position = position
- @generate_top_match_line = true
- @generate_bottom_match_line = true
-
- # These methods update `@generate_top_match_line` and
- # `@generate_bottom_match_line`.
- @from_blob_line = calculate_from_blob_line!
- @to_blob_line = calculate_to_blob_line!
- end
-
- # Returns merged diff lines with required blob lines with correct
- # positions.
- def unfolded_diff_lines
- strong_memoize(:unfolded_diff_lines) do
- next unless unfold_required?
-
- merged_diff_with_blob_lines
- end
- end
-
- # Returns the extracted lines from the old blob which should be merged
- # with the current diff lines.
- def blob_lines
- strong_memoize(:blob_lines) do
- # Blob lines, unlike diffs, doesn't start with an empty space for
- # unchanged line, so the parsing and highlighting step can get fuzzy
- # without the following change.
- line_prefix = ' '
- blob_as_diff_lines = @blob.data.each_line.map { |line| "#{line_prefix}#{line}" }
-
- lines = Gitlab::Diff::Parser.new.parse(blob_as_diff_lines, diff_file: @diff_file).to_a
-
- from = from_blob_line - 1
- to = to_blob_line - 1
-
- lines[from..to]
- end
- end
-
- def unfold_required?
- strong_memoize(:unfold_required) do
- next false unless @diff_file.text?
- next false unless @position.unchanged?
- next false if @diff_file.new_file? || @diff_file.deleted_file?
- next false unless @position.old_line
- # Invalid position (MR import scenario)
- next false if @position.old_line > @blob.lines.size
- next false if @diff_file.diff_lines.empty?
- next false if @diff_file.line_for_position(@position)
- next false unless unfold_line
-
- true
- end
- end
-
- private
-
- attr_reader :from_blob_line, :to_blob_line
-
- def merged_diff_with_blob_lines
- lines = @diff_file.diff_lines
- match_line = unfold_line
- insert_index = bottom? ? -1 : match_line.index
-
- lines -= [match_line] unless bottom?
-
- lines.insert(insert_index, *blob_lines_with_matches)
-
- # The inserted blob lines have invalid indexes, so we need
- # to reindex them.
- reindex(lines)
-
- lines
- end
-
- # Returns 'unchanged' blob lines with recalculated `old_pos` and
- # `new_pos` and the recalculated new match line (needed if we for instance
- # we unfolded once, but there are still folded lines).
- def blob_lines_with_matches
- old_pos = from_blob_line
- new_pos = from_blob_line + offset
-
- new_blob_lines = []
-
- new_blob_lines.push(top_blob_match_line) if top_blob_match_line
-
- blob_lines.each do |line|
- new_blob_lines << Gitlab::Diff::Line.new(line.text, line.type, nil, old_pos, new_pos,
- parent_file: @diff_file)
-
- old_pos += 1
- new_pos += 1
- end
-
- new_blob_lines.push(bottom_blob_match_line) if bottom_blob_match_line
-
- new_blob_lines
- end
-
- def reindex(lines)
- lines.each_with_index { |line, i| line.index = i }
- end
-
- def top_blob_match_line
- strong_memoize(:top_blob_match_line) do
- next unless @generate_top_match_line
-
- old_pos = from_blob_line
- new_pos = from_blob_line + offset
-
- build_match_line(old_pos, new_pos)
- end
- end
-
- def bottom_blob_match_line
- strong_memoize(:bottom_blob_match_line) do
- # The bottom line match addition is already handled on
- # Diff::File#diff_lines_for_serializer
- next if bottom?
- next unless @generate_bottom_match_line
-
- position = line_after_unfold_position.old_pos
-
- old_pos = position
- new_pos = position + offset
-
- build_match_line(old_pos, new_pos)
- end
- end
-
- def build_match_line(old_pos, new_pos)
- blob_lines_length = blob_lines.length
- old_line_ref = [old_pos, blob_lines_length].join(',')
- new_line_ref = [new_pos, blob_lines_length].join(',')
- new_match_line_str = "@@ -#{old_line_ref}+#{new_line_ref} @@"
-
- Gitlab::Diff::Line.new(new_match_line_str, 'match', nil, old_pos, new_pos)
- end
-
- # Returns the first line position that should be extracted
- # from `blob_lines`.
- def calculate_from_blob_line!
- return unless unfold_required?
-
- from = comment_position - UNFOLD_CONTEXT_SIZE
-
- # There's no line before the match if it's in the top-most
- # position.
- prev_line_number = line_before_unfold_position&.old_pos || 0
-
- if from <= prev_line_number + 1
- @generate_top_match_line = false
- from = prev_line_number + 1
- end
-
- from
- end
-
- # Returns the last line position that should be extracted
- # from `blob_lines`.
- def calculate_to_blob_line!
- return unless unfold_required?
-
- to = comment_position + UNFOLD_CONTEXT_SIZE
-
- return to if bottom?
-
- next_line_number = line_after_unfold_position.old_pos
-
- if to >= next_line_number - 1
- @generate_bottom_match_line = false
- to = next_line_number - 1
- end
-
- to
- end
-
- def offset
- unfold_line.new_pos - unfold_line.old_pos
- end
-
- def line_before_unfold_position
- return unless index = unfold_line&.index
-
- @diff_file.diff_lines[index - 1] if index > 0
- end
-
- def line_after_unfold_position
- return unless index = unfold_line&.index
-
- @diff_file.diff_lines[index + 1] if index >= 0
- end
-
- def bottom?
- strong_memoize(:bottom) do
- @position.old_line > last_line.old_pos
- end
- end
-
- # Returns the line which needed to be expanded in order to send a comment
- # in `@position`.
- def unfold_line
- strong_memoize(:unfold_line) do
- next last_line if bottom?
-
- @diff_file.diff_lines.find do |line|
- line.old_pos > comment_position && line.type == 'match'
- end
- end
- end
-
- def comment_position
- @position.old_line
- end
-
- def last_line
- @diff_file.diff_lines.last
- end
- end
- end
-end
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index 7bfab2d808f..f967494199e 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -101,10 +101,6 @@ module Gitlab
@diff_refs ||= DiffRefs.new(base_sha: base_sha, start_sha: start_sha, head_sha: head_sha)
end
- def unfolded_diff?(repository)
- diff_file(repository)&.unfolded?
- end
-
def diff_file(repository)
return @diff_file if defined?(@diff_file)
@@ -138,13 +134,7 @@ module Gitlab
return unless diff_refs.complete?
return unless comparison = diff_refs.compare_in(repository.project)
- file = comparison.diffs(diff_options).diff_files.first
-
- # We need to unfold diff lines according to the position in order
- # to correctly calculate the line code and trace position changes.
- file&.unfold_diff_lines(self)
-
- file
+ comparison.diffs(diff_options).diff_files.first
end
def get_formatter_class(type)
diff --git a/lib/gitlab/email/handler/create_merge_request_handler.rb b/lib/gitlab/email/handler/create_merge_request_handler.rb
index e68ae60ff98..5772727e855 100644
--- a/lib/gitlab/email/handler/create_merge_request_handler.rb
+++ b/lib/gitlab/email/handler/create_merge_request_handler.rb
@@ -44,10 +44,26 @@ module Gitlab
@project ||= Project.find_by_full_path(project_path)
end
+ def metrics_params
+ super.merge(includes_patches: patch_attachments.any?)
+ end
+
private
+ def build_merge_request
+ MergeRequests::BuildService.new(project, author, merge_request_params).execute
+ end
+
def create_merge_request
- merge_request = MergeRequests::BuildService.new(project, author, merge_request_params).execute
+ merge_request = build_merge_request
+
+ if patch_attachments.any?
+ apply_patches_to_source_branch(start_branch: merge_request.target_branch)
+ remove_patch_attachments
+ # Rebuild the merge request as the source branch might just have
+ # been created, so we should re-validate.
+ merge_request = build_merge_request
+ end
if merge_request.errors.any?
merge_request
@@ -59,12 +75,42 @@ module Gitlab
def merge_request_params
params = {
source_project_id: project.id,
- source_branch: mail.subject,
+ source_branch: source_branch,
target_project_id: project.id
}
params[:description] = message if message.present?
params
end
+
+ def apply_patches_to_source_branch(start_branch:)
+ patches = patch_attachments.map { |patch| patch.body.decoded }
+
+ result = Commits::CommitPatchService
+ .new(project, author, branch_name: source_branch, patches: patches, start_branch: start_branch)
+ .execute
+
+ if result[:status] != :success
+ message = "Could not apply patches to #{source_branch}:\n#{result[:message]}"
+ raise InvalidAttachment, message
+ end
+ end
+
+ def remove_patch_attachments
+ patch_attachments.each { |patch| mail.parts.delete(patch) }
+ # reset the message, so it needs to be reporocessed when the attachments
+ # have been modified
+ @message = nil
+ end
+
+ def patch_attachments
+ @patches ||= mail.attachments
+ .select { |attachment| attachment.filename.ends_with?('.patch') }
+ .sort_by(&:filename)
+ end
+
+ def source_branch
+ @source_branch ||= mail.subject
+ end
end
end
end
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index d8c594ad0e7..3a689967a64 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -18,6 +18,7 @@ module Gitlab
InvalidIssueError = Class.new(InvalidRecordError)
InvalidMergeRequestError = Class.new(InvalidRecordError)
UnknownIncomingEmail = Class.new(ProcessingError)
+ InvalidAttachment = Class.new(ProcessingError)
class Receiver
def initialize(raw)
diff --git a/lib/gitlab/git/patches/collection.rb b/lib/gitlab/git/patches/collection.rb
new file mode 100644
index 00000000000..ad6b5d32abc
--- /dev/null
+++ b/lib/gitlab/git/patches/collection.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Git
+ module Patches
+ class Collection
+ MAX_PATCH_SIZE = 2.megabytes
+
+ def initialize(one_or_more_patches)
+ @patches = Array(one_or_more_patches).map do |patch_content|
+ Gitlab::Git::Patches::Patch.new(patch_content)
+ end
+ end
+
+ def content
+ @patches.map(&:content).join("\n")
+ end
+
+ def valid_size?
+ size < MAX_PATCH_SIZE
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ # `@patches` is not an `ActiveRecord` relation, but an `Enumerable`
+ # We're using sum from `ActiveSupport`
+ def size
+ @size ||= @patches.sum(&:size)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/patches/commit_patches.rb b/lib/gitlab/git/patches/commit_patches.rb
new file mode 100644
index 00000000000..c62994432d3
--- /dev/null
+++ b/lib/gitlab/git/patches/commit_patches.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Git
+ module Patches
+ class CommitPatches
+ include Gitlab::Git::WrapsGitalyErrors
+
+ def initialize(user, repository, branch, patch_collection)
+ @user, @repository, @branch, @patches = user, repository, branch, patch_collection
+ end
+
+ def commit
+ repository.with_cache_hooks do
+ wrapped_gitaly_errors do
+ operation_service.user_commit_patches(user, branch, patches.content)
+ end
+ end
+ end
+
+ private
+
+ attr_reader :user, :repository, :branch, :patches
+
+ def operation_service
+ repository.raw.gitaly_operation_client
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/patches/patch.rb b/lib/gitlab/git/patches/patch.rb
new file mode 100644
index 00000000000..fe6ae1b5b00
--- /dev/null
+++ b/lib/gitlab/git/patches/patch.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Git
+ module Patches
+ class Patch
+ attr_reader :content
+
+ def initialize(content)
+ @content = content
+ end
+
+ def size
+ content.bytesize
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 20cd257bb98..1642c4c5687 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -571,6 +571,20 @@ module Gitlab
end
end
+ def update_submodule(user:, submodule:, commit_sha:, message:, branch:)
+ args = {
+ user: user,
+ submodule: submodule,
+ commit_sha: commit_sha,
+ branch: branch,
+ message: message
+ }
+
+ wrapped_gitaly_errors do
+ gitaly_operation_client.user_update_submodule(args)
+ end
+ end
+
# Delete the specified branch from the repository
def delete_branch(branch_name)
wrapped_gitaly_errors do
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index 0f148614b20..4c78b790ce5 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -230,6 +230,32 @@ module Gitlab
response.squash_sha
end
+ def user_update_submodule(user:, submodule:, commit_sha:, branch:, message:)
+ request = Gitaly::UserUpdateSubmoduleRequest.new(
+ repository: @gitaly_repo,
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
+ commit_sha: commit_sha,
+ branch: encode_binary(branch),
+ submodule: encode_binary(submodule),
+ commit_message: encode_binary(message)
+ )
+
+ response = GitalyClient.call(
+ @repository.storage,
+ :operation_service,
+ :user_update_submodule,
+ request
+ )
+
+ if response.pre_receive_error.present?
+ raise Gitlab::Git::PreReceiveError, response.pre_receive_error
+ elsif response.commit_error.present?
+ raise Gitlab::Git::CommitError, response.commit_error
+ else
+ Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update)
+ end
+ end
+
def user_commit_files(
user, branch_name, commit_message, actions, author_email, author_name,
start_branch_name, start_repository)
@@ -273,6 +299,29 @@ module Gitlab
Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update)
end
+ def user_commit_patches(user, branch_name, patches)
+ header = Gitaly::UserApplyPatchRequest::Header.new(
+ repository: @gitaly_repo,
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
+ target_branch: encode_binary(branch_name)
+ )
+ reader = binary_stringio(patches)
+
+ chunks = Enumerator.new do |chunk|
+ chunk.yield Gitaly::UserApplyPatchRequest.new(header: header)
+
+ until reader.eof?
+ patch_chunk = reader.read(MAX_MSG_SIZE)
+
+ chunk.yield(Gitaly::UserApplyPatchRequest.new(patches: patch_chunk))
+ end
+ end
+
+ response = GitalyClient.call(@repository.storage, :operation_service, :user_apply_patch, chunks)
+
+ Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update)
+ end
+
private
def call_cherry_pick_or_revert(rpc, user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
diff --git a/lib/gitlab/kubernetes/helm/install_command.rb b/lib/gitlab/kubernetes/helm/install_command.rb
index 1be7924d6ac..55add06bdb4 100644
--- a/lib/gitlab/kubernetes/helm/install_command.rb
+++ b/lib/gitlab/kubernetes/helm/install_command.rb
@@ -4,22 +4,27 @@ module Gitlab
class InstallCommand
include BaseCommand
- attr_reader :name, :files, :chart, :version, :repository
+ attr_reader :name, :files, :chart, :version, :repository, :preinstall, :postinstall
- def initialize(name:, chart:, files:, rbac:, version: nil, repository: nil)
+ def initialize(name:, chart:, files:, rbac:, version: nil, repository: nil, preinstall: nil, postinstall: nil)
@name = name
@chart = chart
@version = version
@rbac = rbac
@files = files
@repository = repository
+ @preinstall = preinstall
+ @postinstall = postinstall
end
def generate_script
super + [
init_command,
repository_command,
- script_command
+ repository_update_command,
+ preinstall_command,
+ install_command,
+ postinstall_command
].compact.join("\n")
end
@@ -37,12 +42,24 @@ module Gitlab
['helm', 'repo', 'add', name, repository].shelljoin if repository
end
- def script_command
+ def repository_update_command
+ 'helm repo update >/dev/null' if repository
+ end
+
+ def install_command
command = ['helm', 'install', chart] + install_command_flags
command.shelljoin + " >/dev/null\n"
end
+ def preinstall_command
+ preinstall.join("\n") if preinstall
+ end
+
+ def postinstall_command
+ postinstall.join("\n") if postinstall
+ end
+
def install_command_flags
name_flag = ['--name', name]
namespace_flag = ['--namespace', Gitlab::Kubernetes::Helm::NAMESPACE]
diff --git a/lib/gitlab/quick_actions/extractor.rb b/lib/gitlab/quick_actions/extractor.rb
index 30c6806b68e..59f8dd889aa 100644
--- a/lib/gitlab/quick_actions/extractor.rb
+++ b/lib/gitlab/quick_actions/extractor.rb
@@ -29,7 +29,7 @@ module Gitlab
# commands = extractor.extract_commands(msg) #=> [['labels', '~foo ~"bar baz"']]
# msg #=> "hello\nworld"
# ```
- def extract_commands(content)
+ def extract_commands(content, only: nil)
return [content, []] unless content
content = content.dup
@@ -37,7 +37,7 @@ module Gitlab
commands = []
content.delete!("\r")
- content.gsub!(commands_regex) do
+ content.gsub!(commands_regex(only: only)) do
if $~[:cmd]
commands << [$~[:cmd].downcase, $~[:arg]].reject(&:blank?)
''
@@ -60,8 +60,8 @@ module Gitlab
# It looks something like:
#
# /^\/(?<cmd>close|reopen|...)(?:( |$))(?<arg>[^\/\n]*)(?:\n|$)/
- def commands_regex
- names = command_names.map(&:to_s)
+ def commands_regex(only:)
+ names = command_names(limit_to_commands: only).map(&:to_s)
@commands_regex ||= %r{
(?<code>
@@ -133,10 +133,14 @@ module Gitlab
[content, commands]
end
- def command_names
+ def command_names(limit_to_commands:)
command_definitions.flat_map do |command|
next if command.noop?
+ if limit_to_commands && (command.all_names & limit_to_commands).empty?
+ next
+ end
+
command.all_names
end.compact
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index cc0817bdcd2..069cd1f802a 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -62,6 +62,7 @@ module Gitlab
clusters_applications_ingress: count(::Clusters::Applications::Ingress.installed),
clusters_applications_prometheus: count(::Clusters::Applications::Prometheus.installed),
clusters_applications_runner: count(::Clusters::Applications::Runner.installed),
+ clusters_applications_knative: count(::Clusters::Applications::Knative.installed),
in_review_folder: count(::Environment.in_review_folder),
groups: count(Group),
issues: count(Issue),
@@ -126,7 +127,6 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def services_usage
types = {
- JiraService: :projects_jira_active,
SlackService: :projects_slack_notifications_active,
SlackSlashCommandsService: :projects_slack_slash_active,
PrometheusService: :projects_prometheus_active
@@ -134,6 +134,23 @@ module Gitlab
results = count(Service.unscoped.where(type: types.keys, active: true).group(:type), fallback: Hash.new(-1))
types.each_with_object({}) { |(klass, key), response| response[key] = results[klass.to_s] || 0 }
+ .merge(jira_usage)
+ end
+
+ def jira_usage
+ # Jira Cloud does not support custom domains as per https://jira.atlassian.com/browse/CLOUD-6999
+ # so we can just check for subdomains of atlassian.net
+ services = count(
+ Service.unscoped.where(type: :JiraService, active: true)
+ .group("CASE WHEN properties LIKE '%.atlassian.net%' THEN 'cloud' ELSE 'server' END"),
+ fallback: Hash.new(-1)
+ )
+
+ {
+ projects_jira_server_active: services['server'] || 0,
+ projects_jira_cloud_active: services['cloud'] || 0,
+ projects_jira_active: services['server'] == -1 ? -1 : services.values.sum
+ }
end
def count(relation, fallback: -1)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 99f3138501b..3d66b092143 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -103,6 +103,9 @@ msgstr ""
msgid "%{counter_storage} (%{counter_repositories} repositories, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS)"
msgstr ""
+msgid "%{count} more assignees"
+msgstr ""
+
msgid "%{count} participant"
msgid_plural "%{count} participants"
msgstr[0] ""
@@ -1352,6 +1355,9 @@ msgstr ""
msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which may incur additional costs depending on the hosting provider your Kubernetes cluster is installed on. If you are using Google Kubernetes Engine, you can %{pricingLink}."
msgstr ""
+msgid "ClusterIntegration|A Knative build extends Kubernetes and utilizes existing Kubernetes primitives to provide you with the ability to run on-cluster container builds from source. For example, you can write a build that uses Kubernetes-native resources to obtain your source code from a repository, build it into container a image, and then run that image."
+msgstr ""
+
msgid "ClusterIntegration|API URL"
msgstr ""
@@ -1502,6 +1508,12 @@ msgstr ""
msgid "ClusterIntegration|JupyterHub, a multi-user Hub, spawns, manages, and proxies multiple instances of the single-user Jupyter notebook server. JupyterHub can be used to serve notebooks to a class of students, a corporate data science group, or a scientific research group."
msgstr ""
+msgid "ClusterIntegration|Knative"
+msgstr ""
+
+msgid "ClusterIntegration|Knative Domain Name:"
+msgstr ""
+
msgid "ClusterIntegration|Kubernetes cluster"
msgstr ""
@@ -3579,6 +3591,9 @@ msgstr ""
msgid "Last edited by %{name}"
msgstr ""
+msgid "Last reply by"
+msgstr ""
+
msgid "Last update"
msgstr ""
@@ -4180,6 +4195,9 @@ msgstr ""
msgid "Notes|Are you sure you want to cancel creating this comment?"
msgstr ""
+msgid "Notes|Collapse replies"
+msgstr ""
+
msgid "Notes|Show all activity"
msgstr ""
@@ -6362,6 +6380,9 @@ msgstr ""
msgid "Time between merge request creation and merge/close"
msgstr ""
+msgid "Time estimate"
+msgstr ""
+
msgid "Time remaining"
msgstr ""
@@ -6576,6 +6597,9 @@ msgstr ""
msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
msgstr ""
+msgid "Today"
+msgstr ""
+
msgid "Todo"
msgstr ""
@@ -6609,6 +6633,9 @@ msgstr ""
msgid "Token"
msgstr ""
+msgid "Tomorrow"
+msgstr ""
+
msgid "Too many changes to show."
msgstr ""
@@ -7077,6 +7104,9 @@ msgstr ""
msgid "Yes, let me map Google Code users to full names or GitLab users."
msgstr ""
+msgid "Yesterday"
+msgstr ""
+
msgid "You are an admin, which means granting access to <strong>%{client_name}</strong> will allow them to interact with GitLab as an admin as well. Proceed with caution."
msgstr ""
@@ -7544,6 +7574,11 @@ msgstr ""
msgid "remove due date"
msgstr ""
+msgid "reply"
+msgid_plural "replies"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "source"
msgstr ""
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index 27a88534258..7f959441dac 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -109,6 +109,28 @@ module QA
known_hosts_file.close(true)
end
+ def push_with_git_protocol(version, file_name, file_content, commit_message = 'Initial commit')
+ self.git_protocol = version
+ add_file(file_name, file_content)
+ commit(commit_message)
+ push_changes
+
+ fetch_supported_git_protocol
+ end
+
+ def git_protocol=(value)
+ raise ArgumentError, "Please specify the protocol you would like to use: 0, 1, or 2" unless %w[0 1 2].include?(value.to_s)
+
+ run("git config protocol.version #{value}")
+ end
+
+ def fetch_supported_git_protocol
+ # ls-remote is one command known to respond to Git protocol v2 so we use
+ # it to get output including the version reported via Git tracing
+ output = run("git ls-remote #{uri}", "GIT_TRACE_PACKET=1")
+ output[/git< version (\d+)/, 1] || 'unknown'
+ end
+
private
attr_reader :uri, :username, :password, :known_hosts_file, :private_key_file
@@ -117,8 +139,8 @@ module QA
!private_key_file.nil?
end
- def run(command_str)
- command = [env_vars, command_str, '2>&1'].compact.join(' ')
+ def run(command_str, *extra_env)
+ command = [env_vars, *extra_env, command_str, '2>&1'].compact.join(' ')
Runtime::Logger.debug "Git: command=[#{command}]"
output, _ = Open3.capture2(command)
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
index de18b9cefa6..23def93c7dd 100644
--- a/qa/qa/page/project/issue/show.rb
+++ b/qa/qa/page/project/issue/show.rb
@@ -37,6 +37,11 @@ module QA
def select_comments_only_filter
click_element :discussion_filter
+ all_elements(:filter_options)[1].click
+ end
+
+ def select_history_only_filter
+ click_element :discussion_filter
all_elements(:filter_options).last.click
end
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index c7052a9f300..c4500f9be90 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -7,6 +7,16 @@ module QA
attr_writer :personal_access_token
+ # The environment variables used to indicate if the environment under test
+ # supports the given feature
+ SUPPORTED_FEATURES = {
+ git_protocol_v2: 'QA_CAN_TEST_GIT_PROTOCOL_V2'
+ }.freeze
+
+ def supported_features
+ SUPPORTED_FEATURES
+ end
+
def debug?
enabled?(ENV['QA_DEBUG'], default: false)
end
@@ -104,6 +114,15 @@ module QA
raise ArgumentError, "Please provide GITHUB_ACCESS_TOKEN"
end
+ # Returns true if there is an environment variable that indicates that
+ # the feature is supported in the environment under test.
+ # All features are supported by default.
+ def can_test?(feature)
+ raise ArgumentError, %Q(Unknown feature "#{feature}") unless SUPPORTED_FEATURES.include? feature
+
+ enabled?(ENV[SUPPORTED_FEATURES[feature]], default: true)
+ end
+
private
def enabled?(value, default: true)
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
index f5002c8032f..7145b950b6c 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
@@ -31,6 +31,7 @@ module QA
create_issue
Page::Project::Issue::Show.perform do |show|
+ show.select_all_activities_filter
show.comment('See attached banana for scale', attachment: file_to_attach)
show.refresh
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
index 83603f1cda7..ac34f72bb8f 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb
@@ -27,6 +27,11 @@ module QA
expect(show_page).to have_content("made the issue confidential")
expect(show_page).to have_content("My own comment")
+
+ show_page.select_history_only_filter
+
+ expect(show_page).to have_content("made the issue confidential")
+ expect(show_page).not_to have_content("My own comment")
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb
new file mode 100644
index 00000000000..bc88e6450f5
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+module QA
+ context 'Create' do
+ describe 'Push over SSH using Git protocol version 2', :requires_git_protocol_v2 do
+ # Note: If you run this test against GDK make sure you've enabled sshd and
+ # enabled setting the Git protocol by adding `AcceptEnv GIT_PROTOCOL` to
+ # `sshd_config`
+ # See: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/run_qa_against_gdk.md
+
+ let(:key_title) { "key for ssh tests #{Time.now.to_f}" }
+ let(:ssh_key) do
+ Factory::Resource::SSHKey.fabricate! do |resource|
+ resource.title = key_title
+ end
+ end
+
+ def login
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+ end
+
+ around do |example|
+ # Create an SSH key to be used with Git
+ login
+ ssh_key
+
+ example.run
+
+ # Remove the SSH key
+ login
+ Page::Main::Menu.perform(&:go_to_profile_settings)
+ Page::Profile::Menu.perform(&:click_ssh_keys)
+ Page::Profile::SSHKeys.perform do |ssh_keys|
+ ssh_keys.remove_key(key_title)
+ end
+ end
+
+ it 'user pushes to the repository' do
+ # Create a project to push to
+ project = Factory::Resource::Project.fabricate! do |project|
+ project.name = 'git-protocol-project'
+ end
+
+ file_name = 'README.md'
+ file_content = 'Test Git protocol v2'
+ git_protocol = '2'
+ git_protocol_reported = nil
+
+ # Use Git to clone the project, push a file to it, and then check the
+ # supported Git protocol
+ Git::Repository.perform do |repository|
+ username = 'GitLab QA'
+ email = 'root@gitlab.com'
+
+ repository.uri = project.repository_ssh_location.uri
+
+ begin
+ repository.use_ssh_key(ssh_key)
+ repository.clone
+ repository.configure_identity(username, email)
+
+ git_protocol_reported = repository.push_with_git_protocol(
+ git_protocol,
+ file_name,
+ file_content)
+ ensure
+ repository.delete_ssh_key
+ end
+ end
+
+ project.visit!
+ Page::Project::Show.perform(&:wait_for_push)
+
+ # Check that the push worked
+ expect(page).to have_content(file_name)
+ expect(page).to have_content(file_content)
+
+ # And check that the correct Git protocol was used
+ expect(git_protocol_reported).to eq(git_protocol)
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
index ad397c13f0c..1bd8101c36d 100644
--- a/qa/qa/specs/runner.rb
+++ b/qa/qa/specs/runner.rb
@@ -25,6 +25,10 @@ module QA
args.push(%w[--tag ~skip_signup_disabled]) if QA::Runtime::Env.signup_disabled?
+ QA::Runtime::Env.supported_features.each_key do |key|
+ args.push(["--tag", "~requires_#{key}"]) unless QA::Runtime::Env.can_test? key
+ end
+
args.push(options)
args.push(DEFAULT_TEST_PATH_ARGS) unless options.any? { |opt| opt =~ %r{/features/} }
diff --git a/qa/spec/git/repository_spec.rb b/qa/spec/git/repository_spec.rb
index c629f802aa4..faa154c78da 100644
--- a/qa/spec/git/repository_spec.rb
+++ b/qa/spec/git/repository_spec.rb
@@ -26,6 +26,36 @@ describe QA::Git::Repository do
end
end
+ describe '#git_protocol=' do
+ [0, 1, 2].each do |version|
+ it "configures git to use protocol version #{version}" do
+ expect(repository).to receive(:run).with("git config protocol.version #{version}")
+ repository.git_protocol = version
+ end
+ end
+
+ it 'raises an error if the version is unsupported' do
+ expect { repository.git_protocol = 'foo' }.to raise_error(ArgumentError, "Please specify the protocol you would like to use: 0, 1, or 2")
+ end
+ end
+
+ describe '#fetch_supported_git_protocol' do
+ it "reports the detected version" do
+ expect(repository).to receive(:run).and_return("packet: git< version 2")
+ expect(repository.fetch_supported_git_protocol).to eq('2')
+ end
+
+ it 'reports unknown if version is unknown' do
+ expect(repository).to receive(:run).and_return("packet: git< version -1")
+ expect(repository.fetch_supported_git_protocol).to eq('unknown')
+ end
+
+ it 'reports unknown if content does not identify a version' do
+ expect(repository).to receive(:run).and_return("foo")
+ expect(repository.fetch_supported_git_protocol).to eq('unknown')
+ end
+ end
+
def cd_empty_temp_directory
tmp_dir = 'tmp/git-repository-spec/'
FileUtils.rm_rf(tmp_dir) if ::File.exist?(tmp_dir)
diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb
index c59c415c148..ded51d5bb7c 100644
--- a/qa/spec/runtime/env_spec.rb
+++ b/qa/spec/runtime/env_spec.rb
@@ -3,49 +3,62 @@
describe QA::Runtime::Env do
include Support::StubENV
- shared_examples 'boolean method' do |method, env_key, default|
+ shared_examples 'boolean method' do |**kwargs|
+ it_behaves_like 'boolean method with parameter', kwargs
+ end
+
+ shared_examples 'boolean method with parameter' do |method:, param: nil, env_key:, default:|
context 'when there is an env variable set' do
it 'returns false when falsey values specified' do
stub_env(env_key, 'false')
- expect(described_class.public_send(method)).to be_falsey
+ expect(described_class.public_send(method, *param)).to be_falsey
stub_env(env_key, 'no')
- expect(described_class.public_send(method)).to be_falsey
+ expect(described_class.public_send(method, *param)).to be_falsey
stub_env(env_key, '0')
- expect(described_class.public_send(method)).to be_falsey
+ expect(described_class.public_send(method, *param)).to be_falsey
end
it 'returns true when anything else specified' do
stub_env(env_key, 'true')
- expect(described_class.public_send(method)).to be_truthy
+ expect(described_class.public_send(method, *param)).to be_truthy
stub_env(env_key, '1')
- expect(described_class.public_send(method)).to be_truthy
+ expect(described_class.public_send(method, *param)).to be_truthy
stub_env(env_key, 'anything')
- expect(described_class.public_send(method)).to be_truthy
+ expect(described_class.public_send(method, *param)).to be_truthy
end
end
context 'when there is no env variable set' do
it "returns the default, #{default}" do
stub_env(env_key, nil)
- expect(described_class.public_send(method)).to be(default)
+ expect(described_class.public_send(method, *param)).to be(default)
end
end
end
describe '.signup_disabled?' do
- it_behaves_like 'boolean method', :signup_disabled?, 'SIGNUP_DISABLED', false
+ it_behaves_like 'boolean method',
+ method: :signup_disabled?,
+ env_key: 'SIGNUP_DISABLED',
+ default: false
end
describe '.debug?' do
- it_behaves_like 'boolean method', :debug?, 'QA_DEBUG', false
+ it_behaves_like 'boolean method',
+ method: :debug?,
+ env_key: 'QA_DEBUG',
+ default: false
end
describe '.chrome_headless?' do
- it_behaves_like 'boolean method', :chrome_headless?, 'CHROME_HEADLESS', true
+ it_behaves_like 'boolean method',
+ method: :chrome_headless?,
+ env_key: 'CHROME_HEADLESS',
+ default: true
end
describe '.running_in_ci?' do
@@ -182,4 +195,16 @@ describe QA::Runtime::Env do
expect(described_class.log_destination).to eq('path/to_file')
end
end
+
+ describe '.can_test?' do
+ it_behaves_like 'boolean method with parameter',
+ method: :can_test?,
+ param: :git_protocol_v2,
+ env_key: 'QA_CAN_TEST_GIT_PROTOCOL_V2',
+ default: true
+
+ it 'raises ArgumentError if feature is unknown' do
+ expect { described_class.can_test? :foo }.to raise_error(ArgumentError, 'Unknown feature "foo"')
+ end
+ end
end
diff --git a/qa/spec/specs/runner_spec.rb b/qa/spec/specs/runner_spec.rb
index 9ddaf7ab1b3..741821ddf8c 100644
--- a/qa/spec/specs/runner_spec.rb
+++ b/qa/spec/specs/runner_spec.rb
@@ -76,6 +76,20 @@ describe QA::Specs::Runner do
end
end
+ context 'when git protocol v2 is not supported' do
+ before do
+ allow(QA::Runtime::Env).to receive(:can_test?).with(:git_protocol_v2).and_return(false)
+ end
+
+ subject { described_class.new }
+
+ it 'it includes default args and excludes the requires_git_protocol_v2 tag' do
+ expect_rspec_runner_arguments(['--tag', '~orchestrated', '--tag', '~requires_git_protocol_v2', *described_class::DEFAULT_TEST_PATH_ARGS])
+
+ subject.perform
+ end
+ end
+
def expect_rspec_runner_arguments(arguments)
expect(RSpec::Core::Runner).to receive(:run)
.with(arguments, $stderr, $stdout)
diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb
index 5024ef71771..77060fdc3be 100644
--- a/spec/controllers/import/bitbucket_server_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_server_controller_spec.rb
@@ -121,12 +121,19 @@ describe Import::BitbucketServerController do
@repo = double(slug: 'vim', project_key: 'asd', full_name: 'asd/vim', "valid?" => true, project_name: 'asd', browse_url: 'http://test', name: 'vim')
@invalid_repo = double(slug: 'invalid', project_key: 'foobar', full_name: 'asd/foobar', "valid?" => false, browse_url: 'http://bad-repo')
+ @created_repo = double(slug: 'created', project_key: 'existing', full_name: 'group/created', "valid?" => true, browse_url: 'http://existing')
assign_session_tokens
end
it 'assigns repository categories' do
- created_project = create(:project, import_type: 'bitbucket_server', creator_id: user.id, import_source: 'foo/bar', import_status: 'finished')
- expect(client).to receive(:repos).and_return([@repo, @invalid_repo])
+ created_project = create(:project, import_type: 'bitbucket_server', creator_id: user.id, import_status: 'finished', import_source: @created_repo.browse_url)
+ repos = instance_double(BitbucketServer::Collection)
+
+ expect(repos).to receive(:partition).and_return([[@repo, @created_repo], [@invalid_repo]])
+ expect(repos).to receive(:current_page).and_return(1)
+ expect(repos).to receive(:next_page).and_return(2)
+ expect(repos).to receive(:prev_page).and_return(nil)
+ expect(client).to receive(:repos).and_return(repos)
get :status
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index f58aa25cbdd..28f7e4634a5 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -157,7 +157,7 @@ describe Projects::BlobController do
match_line = JSON.parse(response.body).first
- expect(match_line['type']).to be_nil
+ expect(match_line['type']).to eq('context')
end
it 'adds bottom match line when "t"o is less than blob size' do
@@ -177,7 +177,7 @@ describe Projects::BlobController do
match_line = JSON.parse(response.body).last
- expect(match_line['type']).to be_nil
+ expect(match_line['type']).to eq('context')
end
end
end
@@ -331,10 +331,10 @@ describe Projects::BlobController do
expect(response).to redirect_to(
project_new_merge_request_path(
forked_project,
- merge_request_source_branch: "fork-test-1",
merge_request: {
source_project_id: forked_project.id,
target_project_id: project.id,
+ source_branch: "fork-test-1",
target_branch: "master"
}
)
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
index 3c9ca22a051..ff65c76cf26 100644
--- a/spec/factories/clusters/applications/helm.rb
+++ b/spec/factories/clusters/applications/helm.rb
@@ -57,6 +57,11 @@ FactoryBot.define do
cluster factory: %i(cluster with_installed_helm provided_by_gcp)
end
+ factory :clusters_applications_knative, class: Clusters::Applications::Knative do
+ hostname 'example.com'
+ cluster factory: %i(cluster with_installed_helm provided_by_gcp)
+ end
+
factory :clusters_applications_jupyter, class: Clusters::Applications::Jupyter do
oauth_application factory: :oauth_application
cluster factory: %i(cluster with_installed_helm provided_by_gcp project)
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index 0d4fd49bf3a..5be56a49903 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -46,6 +46,17 @@ FactoryBot.define do
)
end
+ factory :jira_cloud_service, class: JiraService do
+ project
+ active true
+ properties(
+ url: 'https://mysite.atlassian.net',
+ username: 'jira_user',
+ password: 'my-secret-password',
+ project_key: 'jira-key'
+ )
+ end
+
factory :hipchat_service do
project
type 'HipchatService'
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb
index eebc987499d..030993462b5 100644
--- a/spec/features/boards/add_issues_modal_spec.rb
+++ b/spec/features/boards/add_issues_modal_spec.rb
@@ -160,7 +160,7 @@ describe 'Issue Boards add issue modal', :js do
it 'changes button text with plural' do
page.within('.add-issues-modal') do
- all('.board-card .board-card-number').each do |el|
+ all('.board-card .js-board-card-number-container').each do |el|
el.click
end
diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb
index ec0ca21450a..21779336559 100644
--- a/spec/features/boards/issue_ordering_spec.rb
+++ b/spec/features/boards/issue_ordering_spec.rb
@@ -78,7 +78,7 @@ describe 'Issue Boards', :js do
end
it 'moves from bottom to top' do
- drag(from_index: 2, to_index: 0)
+ drag(from_index: 2, to_index: 0, duration: 1020)
wait_for_requests
@@ -130,7 +130,7 @@ describe 'Issue Boards', :js do
end
it 'moves to bottom of another list' do
- drag(list_from_index: 1, list_to_index: 2, to_index: 2)
+ drag(list_from_index: 1, list_to_index: 2, to_index: 2, duration: 1020)
wait_for_requests
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 0a24c5e906a..975b7944741 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -147,12 +147,10 @@ describe 'Dashboard Projects' do
end
context 'last push widget', :use_clean_rails_memory_store_caching do
- let(:ref) { "feature" }
-
before do
event = create(:push_event, project: project, author: user)
- create(:push_event_payload, event: event, ref: ref, action: :created)
+ create(:push_event_payload, event: event, ref: 'feature', action: :created)
Users::LastPushEventService.new(user).cache_last_push_event(event)
@@ -167,9 +165,9 @@ describe 'Dashboard Projects' do
end
expect(page).to have_selector('.merge-request-form')
- expect(current_path).to eq project_new_merge_request_path(project, merge_request_source_branch: ref)
+ expect(current_path).to eq project_new_merge_request_path(project)
expect(find('#merge_request_target_project_id', visible: false).value).to eq project.id.to_s
- expect(find('input#merge_request_source_branch', visible: false).value).to eq ref
+ expect(find('input#merge_request_source_branch', visible: false).value).to eq 'feature'
expect(find('input#merge_request_target_branch', visible: false).value).to eq 'master'
end
end
diff --git a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
index a124c99ecc8..0ccab5b2fac 100644
--- a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
+++ b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
@@ -9,10 +9,10 @@ describe 'create a merge request, allowing commits from members who can merge to
def visit_new_merge_request
visit project_new_merge_request_path(
source_project,
- merge_request_source_branch: 'fix',
merge_request: {
source_project_id: source_project.id,
target_project_id: target_project.id,
+ source_branch: 'fix',
target_branch: 'master'
})
end
diff --git a/spec/features/merge_request/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb
index 51b78d3e7d1..fa148715855 100644
--- a/spec/features/merge_request/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb
@@ -85,13 +85,12 @@ describe 'Merge request > User posts diff notes', :js do
# `.line_holder` will be an unfolded line.
let(:line_holder) { first('#a5cc2925ca8258af241be7e5b0381edf30266302 .line_holder') }
- it 'allows commenting on the left side' do
- should_allow_commenting(line_holder, 'left')
+ it 'does not allow commenting on the left side' do
+ should_not_allow_commenting(line_holder, 'left')
end
- it 'allows commenting on the right side' do
- # Automatically shifts comment box to left side.
- should_allow_commenting(line_holder, 'right')
+ it 'does not allow commenting on the right side' do
+ should_not_allow_commenting(line_holder, 'right')
end
end
end
@@ -148,8 +147,8 @@ describe 'Merge request > User posts diff notes', :js do
# `.line_holder` will be an unfolded line.
let(:line_holder) { first('.line_holder[id="a5cc2925ca8258af241be7e5b0381edf30266302_1_1"]') }
- it 'allows commenting' do
- should_allow_commenting line_holder
+ it 'does not allow commenting' do
+ should_not_allow_commenting line_holder
end
end
diff --git a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
index d3da8cc6752..b58c433bbfe 100644
--- a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb
@@ -89,16 +89,17 @@ describe 'Merge request > User sees avatars on diff notes', :js do
page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').send_keys(:return)
- expect(page).to have_selector('img.js-diff-comment-avatar', count: 1)
+ expect(page).to have_selector('.js-diff-comment-avatar img', count: 1)
end
end
it 'shows comment on note avatar' do
page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').send_keys(:return)
-
- expect(first('img.js-diff-comment-avatar')["data-original-title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}")
+ first('.js-diff-comment-avatar img').hover
end
+
+ expect(page).to have_content "#{note.author.name}: #{note.note.truncate(17)}"
end
it 'toggles comments when clicking avatar' do
@@ -109,7 +110,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
expect(page).not_to have_selector('.notes_holder')
page.within find_line(position.line_code(project.repository)) do
- first('img.js-diff-comment-avatar').click
+ first('.js-diff-comment-avatar img').click
end
expect(page).to have_selector('.notes_holder')
@@ -125,7 +126,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
wait_for_requests
page.within find_line(position.line_code(project.repository)) do
- expect(page).not_to have_selector('img.js-diff-comment-avatar')
+ expect(page).not_to have_selector('.js-diff-comment-avatar img')
end
end
@@ -143,7 +144,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').send_keys(:return)
- expect(page).to have_selector('img.js-diff-comment-avatar', count: 2)
+ expect(page).to have_selector('.js-diff-comment-avatar img', count: 2)
end
end
@@ -162,7 +163,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do
page.within find_line(position.line_code(project.repository)) do
find('.diff-notes-collapse').send_keys(:return)
- expect(page).to have_selector('img.js-diff-comment-avatar', count: 3)
+ expect(page).to have_selector('.js-diff-comment-avatar img', count: 3)
expect(find('.diff-comments-more-count')).to have_content '+1'
end
end
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index d907ed4198c..582be101399 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -20,10 +20,10 @@ describe 'Merge request > User sees merge widget', :js do
before do
visit project_new_merge_request_path(
project,
- merge_request_source_branch: 'feature',
merge_request: {
source_project_id: project.id,
target_project_id: project.id,
+ source_branch: 'feature',
target_branch: 'master'
})
end
diff --git a/spec/features/merge_request/user_sees_wip_help_message_spec.rb b/spec/features/merge_request/user_sees_wip_help_message_spec.rb
index 6dfc819fe8a..92cc73ddf1f 100644
--- a/spec/features/merge_request/user_sees_wip_help_message_spec.rb
+++ b/spec/features/merge_request/user_sees_wip_help_message_spec.rb
@@ -13,10 +13,10 @@ describe 'Merge request > User sees WIP help message' do
it 'shows a specific WIP hint' do
visit project_new_merge_request_path(
project,
- merge_request_source_branch: 'wip',
merge_request: {
source_project_id: project.id,
target_project_id: project.id,
+ source_branch: 'wip',
target_branch: 'master'
})
@@ -32,10 +32,10 @@ describe 'Merge request > User sees WIP help message' do
it 'shows the regular WIP message' do
visit project_new_merge_request_path(
project,
- merge_request_source_branch: 'fix',
merge_request: {
source_project_id: project.id,
target_project_id: project.id,
+ source_branch: 'fix',
target_branch: 'master'
})
diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
index 147544740dc..ae41cf90576 100644
--- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
+++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
@@ -109,13 +109,13 @@ describe 'Merge request > User selects branches for new MR', :js do
end
it 'populates source branch button' do
- visit project_new_merge_request_path(project, change_branches: true, merge_request_source_branch: 'fix', merge_request: { target_branch: 'master' })
+ visit project_new_merge_request_path(project, change_branches: true, merge_request: { target_branch: 'master', source_branch: 'fix' })
expect(find('.js-source-branch')).to have_content('fix')
end
it 'allows to change the diff view' do
- visit project_new_merge_request_path(project, merge_request_source_branch: 'fix', merge_request: { target_branch: 'master' })
+ visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: 'fix' })
click_link 'Changes'
@@ -131,7 +131,7 @@ describe 'Merge request > User selects branches for new MR', :js do
end
it 'does not allow non-existing branches' do
- visit project_new_merge_request_path(project, merge_request_source_branch: 'non-exist-source', merge_request: { target_branch: 'non-exist-target' })
+ visit project_new_merge_request_path(project, merge_request: { target_branch: 'non-exist-target', source_branch: 'non-exist-source' })
expect(page).to have_content('The form contains the following errors')
expect(page).to have_content('Source branch "non-exist-source" does not exist')
@@ -140,7 +140,7 @@ describe 'Merge request > User selects branches for new MR', :js do
context 'when a branch contains commits that both delete and add the same image' do
it 'renders the diff successfully' do
- visit project_new_merge_request_path(project, merge_request_source_branch: 'deleted-image-test', merge_request: { target_branch: 'master' })
+ visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: 'deleted-image-test' })
click_link "Changes"
@@ -165,8 +165,7 @@ describe 'Merge request > User selects branches for new MR', :js do
it 'shows pipelines for a new merge request' do
visit project_new_merge_request_path(
project,
- merge_request_source_branch: 'fix',
- merge_request: { target_branch: 'master' })
+ merge_request: { target_branch: 'master', source_branch: 'fix' })
page.within('.merge-request') do
click_link 'Pipelines'
diff --git a/spec/features/merge_request/user_uses_quick_actions_spec.rb b/spec/features/merge_request/user_uses_quick_actions_spec.rb
index 6e681185e1f..b81478a481f 100644
--- a/spec/features/merge_request/user_uses_quick_actions_spec.rb
+++ b/spec/features/merge_request/user_uses_quick_actions_spec.rb
@@ -144,7 +144,7 @@ describe 'Merge request > User uses quick actions', :js do
describe '/target_branch command in merge request' do
let(:another_project) { create(:project, :public, :repository) }
- let(:new_url_opts) { { merge_request_source_branch: 'feature' } }
+ let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
before do
another_project.add_maintainer(user)
diff --git a/spec/features/merge_requests/user_squashes_merge_request_spec.rb b/spec/features/merge_requests/user_squashes_merge_request_spec.rb
index 8ecdec491b8..ec1153b7f7f 100644
--- a/spec/features/merge_requests/user_squashes_merge_request_spec.rb
+++ b/spec/features/merge_requests/user_squashes_merge_request_spec.rb
@@ -65,7 +65,7 @@ describe 'User squashes a merge request', :js do
context 'when squash is enabled on merge request creation' do
before do
- visit project_new_merge_request_path(project, merge_request_source_branch: source_branch, merge_request: { target_branch: 'master' })
+ visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: source_branch })
check 'merge_request[squash]'
click_on 'Submit merge request'
wait_for_requests
@@ -95,7 +95,7 @@ describe 'User squashes a merge request', :js do
context 'when squash is not enabled on merge request creation' do
before do
- visit project_new_merge_request_path(project, merge_request_source_branch: source_branch, merge_request: { target_branch: 'master' })
+ visit project_new_merge_request_path(project, merge_request: { target_branch: 'master', source_branch: source_branch })
click_on 'Submit merge request'
wait_for_requests
end
diff --git a/spec/features/projects/files/user_creates_directory_spec.rb b/spec/features/projects/files/user_creates_directory_spec.rb
index 6f620dff82b..847b5f0860f 100644
--- a/spec/features/projects/files/user_creates_directory_spec.rb
+++ b/spec/features/projects/files/user_creates_directory_spec.rb
@@ -57,7 +57,7 @@ describe 'Projects > Files > User creates a directory', :js do
expect(page).to have_content('From new-feature into master')
expect(page).to have_content('Add new directory')
- expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new-feature"))
+ expect(current_path).to eq(project_new_merge_request_path(project))
end
end
@@ -80,7 +80,8 @@ describe 'Projects > Files > User creates a directory', :js do
click_button('Create directory')
fork = user.fork_of(project2.reload)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1"))
+
+ expect(current_path).to eq(project_new_merge_request_path(fork))
end
end
end
diff --git a/spec/features/projects/files/user_creates_files_spec.rb b/spec/features/projects/files/user_creates_files_spec.rb
index 14b5bd58bd1..a4f94b7a76d 100644
--- a/spec/features/projects/files/user_creates_files_spec.rb
+++ b/spec/features/projects/files/user_creates_files_spec.rb
@@ -144,7 +144,7 @@ describe 'Projects > Files > User creates files' do
fill_in(:branch_name, with: 'new_branch_name', visible: true)
click_button('Commit changes')
- expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new_branch_name"))
+ expect(current_path).to eq(project_new_merge_request_path(project))
click_link('Changes')
@@ -182,7 +182,7 @@ describe 'Projects > Files > User creates files' do
fork = user.fork_of(project2.reload)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1"))
+ expect(current_path).to eq(project_new_merge_request_path(fork))
expect(page).to have_content('New commit message')
end
end
diff --git a/spec/features/projects/files/user_deletes_files_spec.rb b/spec/features/projects/files/user_deletes_files_spec.rb
index faf11ee9dd8..614b11fa5c8 100644
--- a/spec/features/projects/files/user_deletes_files_spec.rb
+++ b/spec/features/projects/files/user_deletes_files_spec.rb
@@ -63,7 +63,7 @@ describe 'Projects > Files > User deletes files', :js do
fork = user.fork_of(project2.reload)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1"))
+ expect(current_path).to eq(project_new_merge_request_path(fork))
expect(page).to have_content('New commit message')
end
end
diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb
index c6b2aaea906..9eb65ec159c 100644
--- a/spec/features/projects/files/user_edits_files_spec.rb
+++ b/spec/features/projects/files/user_edits_files_spec.rb
@@ -86,7 +86,7 @@ describe 'Projects > Files > User edits files', :js do
fill_in(:branch_name, with: 'new_branch_name', visible: true)
click_button('Commit changes')
- expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new_branch_name"))
+ expect(current_path).to eq(project_new_merge_request_path(project))
click_link('Changes')
@@ -155,7 +155,7 @@ describe 'Projects > Files > User edits files', :js do
fork = user.fork_of(project2.reload)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1"))
+ expect(current_path).to eq(project_new_merge_request_path(fork))
wait_for_requests
@@ -183,7 +183,7 @@ describe 'Projects > Files > User edits files', :js do
fork = user.fork_of(project2)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "patch-1"))
+ expect(current_path).to eq(project_new_merge_request_path(fork))
wait_for_requests
diff --git a/spec/features/projects/files/user_replaces_files_spec.rb b/spec/features/projects/files/user_replaces_files_spec.rb
index 09feb315465..e3da28d73c3 100644
--- a/spec/features/projects/files/user_replaces_files_spec.rb
+++ b/spec/features/projects/files/user_replaces_files_spec.rb
@@ -78,7 +78,7 @@ describe 'Projects > Files > User replaces files', :js do
fork = user.fork_of(project2.reload)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "undefined"))
+ expect(current_path).to eq(project_new_merge_request_path(fork))
click_link('Changes')
diff --git a/spec/features/projects/files/user_uploads_files_spec.rb b/spec/features/projects/files/user_uploads_files_spec.rb
index 92df8303f46..af3fc528a20 100644
--- a/spec/features/projects/files/user_uploads_files_spec.rb
+++ b/spec/features/projects/files/user_uploads_files_spec.rb
@@ -36,7 +36,7 @@ describe 'Projects > Files > User uploads files' do
click_button('Upload file')
expect(page).to have_content('New commit message')
- expect(current_path).to eq(project_new_merge_request_path(project, merge_request_source_branch: "new_branch_name"))
+ expect(current_path).to eq(project_new_merge_request_path(project))
click_link('Changes')
find("a[data-action='diffs']", text: 'Changes').click
@@ -92,7 +92,7 @@ describe 'Projects > Files > User uploads files' do
fork = user.fork_of(project2.reload)
- expect(current_path).to eq(project_new_merge_request_path(fork, merge_request_source_branch: "undefined"))
+ expect(current_path).to eq(project_new_merge_request_path(fork))
find("a[data-action='diffs']", text: 'Changes').click
diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb
index 4be31511ceb..69561b4d733 100644
--- a/spec/features/projects/merge_request_button_spec.rb
+++ b/spec/features/projects/merge_request_button_spec.rb
@@ -22,8 +22,8 @@ describe 'Merge Request button' do
it 'shows Create merge request button' do
href = project_new_merge_request_path(project,
- merge_request_source_branch: 'feature',
- merge_request: { target_branch: 'master' })
+ merge_request: { source_branch: 'feature',
+ target_branch: 'master' })
visit url
@@ -77,8 +77,8 @@ describe 'Merge Request button' do
it 'shows Create merge request button' do
href = project_new_merge_request_path(forked_project,
- merge_request_source_branch: 'feature',
- merge_request: { target_branch: 'master' })
+ merge_request: { source_branch: 'feature',
+ target_branch: 'master' })
visit fork_url
diff --git a/spec/fixtures/api/schemas/entities/issue_board.json b/spec/fixtures/api/schemas/entities/issue_board.json
index 8d821ebb843..3e252ddd13c 100644
--- a/spec/fixtures/api/schemas/entities/issue_board.json
+++ b/spec/fixtures/api/schemas/entities/issue_board.json
@@ -8,6 +8,7 @@
"due_date": { "type": "date" },
"project_id": { "type": "integer" },
"relative_position": { "type": ["integer", "null"] },
+ "time_estimate": { "type": "integer" },
"weight": { "type": "integer" },
"project": {
"type": "object",
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index 4878df43d28..a83ec55cede 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -13,6 +13,7 @@
"confidential": { "type": "boolean" },
"due_date": { "type": ["date", "null"] },
"relative_position": { "type": "integer" },
+ "time_estimate": { "type": "integer" },
"issue_sidebar_endpoint": { "type": "string" },
"toggle_subscription_endpoint": { "type": "string" },
"assignable_labels_endpoint": { "type": "string" },
diff --git a/spec/fixtures/emails/merge_request_multiple_patches.eml b/spec/fixtures/emails/merge_request_multiple_patches.eml
new file mode 100644
index 00000000000..311b99a525d
--- /dev/null
+++ b/spec/fixtures/emails/merge_request_multiple_patches.eml
@@ -0,0 +1,181 @@
+From: "Jake the Dog" <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+merge-request+auth_token@appmail.adventuretime.ooo
+Subject: new-branch-with-a-patch
+Date: Wed, 31 Oct 2018 17:27:52 +0100
+X-Mailer: MailMate (1.12r5523)
+Message-ID: <7BE7C2E6-F0D9-4C85-9F55-B2A4BA01BFEC@gitlab.com>
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_="
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+
+This applies nicely to a branch freshly created from the root-ref
+
+The other attachments in this email are ignored
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment;
+ filename=0002-This-does-not-apply-to-the-feature-branch.patch
+Content-Transfer-Encoding: quoted-printable
+
+=46rom 00c68c2b4f954370ce82a1162bc29c13f524897e Mon Sep 17 00:00:00 2001
+From: Patch user <patchuser@gitlab.org>
+Date: Mon, 22 Oct 2018 11:05:48 +0200
+Subject: [PATCH] This does not apply to the `feature` branch
+
+---
+ files/ruby/feature.rb | 5 +++++
+ 1 file changed, 5 insertions(+)
+ create mode 100644 files/ruby/feature.rb
+
+diff --git a/files/ruby/feature.rb b/files/ruby/feature.rb
+new file mode 100644
+index 0000000..fef26e4
+--- /dev/null
++++ b/files/ruby/feature.rb
+@@ -0,0 +1,5 @@
++class Feature
++ def bar
++ puts 'foo'
++ end
++end
+-- =
+
+2.19.1
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment; filename=0001-A-commit-from-a-patch.patch
+Content-Transfer-Encoding: quoted-printable
+
+=46rom 3fee0042e610fb3563e4379e316704cb1210f3de Mon Sep 17 00:00:00 2001
+From: Patch user <patchuser@gitlab.org>
+Date: Thu, 18 Oct 2018 13:40:35 +0200
+Subject: [PATCH] A commit from a patch
+
+---
+ README | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/README b/README
+index 3742e48..e40a3b9 100644
+--- a/README
++++ b/README
+@@ -1 +1,3 @@
+ Sample repo for testing gitlab features
++
++This was applied in a patch!
+-- =
+
+2.19.1
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment; filename=really-not-a-patch.png
+Content-Type: image/png
+Content-Transfer-Encoding: base64
+
+iVBORw0KGgoAAAANSUhEUgAAAjMAAAAfCAYAAAASo0ymAAABfGlDQ1BJQ0MgUHJvZmlsZQAAKJFj
+YGAqSSwoyGFhYGDIzSspCnJ3UoiIjFJgv8PAzcDDIMRgxSCemFxc4BgQ4MOAE3y7xsAIoi/rgsxK
+8/x506a1fP4WNq+ZclYlOrj1gQF3SmpxMgMDIweQnZxSnJwLZOcA2TrJBUUlQPYMIFu3vKQAxD4B
+ZIsUAR0IZN8BsdMh7A8gdhKYzcQCVhMS5AxkSwDZAkkQtgaInQ5hW4DYyRmJKUC2B8guiBvAgNPD
+RcHcwFLXkYC7SQa5OaUwO0ChxZOaFxoMcgcQyzB4MLgwKDCYMxgwWDLoMjiWpFaUgBQ65xdUFmWm
+Z5QoOAJDNlXBOT+3oLQktUhHwTMvWU9HwcjA0ACkDhRnEKM/B4FNZxQ7jxDLX8jAYKnMwMDcgxBL
+msbAsH0PA4PEKYSYyjwGBn5rBoZt5woSixLhDmf8xkKIX5xmbARh8zgxMLDe+///sxoDA/skBoa/
+E////73o//+/i4H2A+PsQA4AJHdp4IxrEg8AAAGcaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8
+eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA1LjQu
+MCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1y
+ZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAg
+ICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAg
+ICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjU2MzwvZXhpZjpQaXhlbFhEaW1lbnNpb24+CiAgICAg
+ICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4zMTwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAg
+IDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgpZ5vHZAAAS+ElE
+QVR4Ae2cC1QUV5rH/xnA1uZli02MiEHQqBMdTGB8ZIniA5xgfCTRycb1rBmdTHJGJ+toPBtzzGg2
+Rs3LdVTOYhKMcDgmDNHVNXKiGKJINCoqiXpUEFdB0CDKCtrSYp/Zr6q7muqmu+pWd7XgnFtH6Xrc
++h6/+92qr+69VQ9dqrvxd/CFE+AEOAFOgBPgBDiBB5TALx5Qu7nZnAAnwAlwApwAJ8AJiAR4MsMD
+gRPgBDgBToAT4AQeaAI8mXmgq48bzwlwApwAJ8AJcAI8meExwAlwApwAJ8AJcAIPNAGezDzQ1ceN
+5wQ4AU6AE+AEOIFgTwiamls87eb7OAFOgBPgBDgBToAT0I1ARHhXXWTxnhldMHIhnAAnwAlwApwA
+J9BRBIL+vOjfl7srt969577L63Zj1VGU/liHHn1j0I0xNbI1X8TRo5V4yNQLEQbGk7xawH6go/Sy
+W8hLBoqAL3EaKFtY5doaz6PkwCk8FN0H3f1oJx0d93r5wcpN13K2RhzbX4KKn4FH+vSAx65sXRX+
+YwgTYq5k72FUXqzChaoq1DQGI7Z3dwT6ai/oPbj/OJoNDyM6IsRvmL74odTe9LbPbwc7gQCDQZ9W
+5VFK0co52FKp7OW0N9di6sAI1B//G7YU3kHvEb+Gyah8jnTUWncYWdm7kf7GWrz0eIS0O+C/HaVX
+P8dsqPphL07dicFvxg6BQT/BnVSSfv76EqcdDcVafww5X1A7iRmGR/1oJ+pxrx9nT8z08sOT7IDv
+s9YiPzcf10InImlkfy9tLrD8Au5jABRY6w8j56vdbZIFfslxCG/bE5A1Idazqc2EpUVj/cwhfuvw
+xQ+l9qa3fX47eN8EWFC4cj4KKgdjadZiJATg5uUxUQ6JiEGYOQpm8X9bhiLtC4MRIcH2PCg4RBjv
+Mml7YgkOFRFGhnrMpQKHt6P06ubRbZzIy8f2radxVzeZnVmQfv76FKcdjSa4i2hBiL/NRDXu9ePs
+EZlefngUHuCdQcF0daOlRxcEeVUVYH5e9XbeA8aEF/HZZ5vw2Zo/wSyYqchPRz8csdbD70Zjt8kn
+P5Tam8726Ugu4KJaxam4TWixBUaVx8tk6vx3kSrps57HqtdWosI8Ge9/8BylMV4WR0u32WwICvLe
+7IWzjQnP4vPPn/UiKHC7O0qvfh4ZYOxB0lpCYb/N6Se5c0oKgL8a4rRjmNhATUhsQwblZsRsnnrc
+B4Az9PeD2eH7XjAQ/O67E7orFG8Dxm5iMnhNd+n3T6BWP9Tb2/2zvTNpkgb9PCYdOhjKINcxf4Y6
+YJQSquoTxdiVl4eTtwWrYjDzzYVIGyg+0zjNrC3NQ27JFUREGGC1Av/04qsY0ad9f1PD6d3IzdmJ
+k9cs4rlh5niMmTIL01PinLK0rHSE3trSTfikBMhIj8P3m5W5NJwrxqfr81AhsgP6Jk3GnDnP4VEp
+c7ReRM7H2agjpytqBM+/wZqPz0CA2NJiQMYfX8eIXu05KjFqvnwCuwv34shPZ3DNofexpIl4/l+m
+Y6BJ+13U7u9djEg0YR91L4sXL/MwzF/4KpJktjHp1eyvFee+244tW3ejWvTFiMeSnsZvZ09HQrir
+LyxxqsRNOsbkBxXWEgdV1D7WZhfjlqAkNgWzUyRtvv2qxr1mzmx2sPqhGvcOdZarJ1CwaQv2VV4X
+94TFDsfLf/ydS1wJB1iuG1rqw6He+0+A+Kn5a71cgjXritB7yh8oRmJl9jWh6JN1KL07DAvmP2vv
+UaKjavIEAbpykVnEssoaB6zl3HU2nP4auTsO42ZECt6YPzFgQ12q7c3dMMe2N/tY6s2LSI+7Wfhp
+iQO97fNotIadHoeZvJ3veluQSgk30Vps2ZiHq/1SkJ4UY99enYlz9lxEKmj/peLW+gqcPFWOmtuU
+0bgttqvFWPxRPiUyQHLaRExKG47elgvYlV2KZreymjbvs96WhjpUV5YiK1OZS/PpfCxebU9kho4Z
+h6eGRKH62E4sn7cOl5x4gtDFEEFJYATCRKe7wWAwiNuRkeHahvgc0Kr3rseuQ5QQ9R6MVOKcOiQG
+Fcd2Y/XCNahy6mUnbPf3CAookemWNA6TxsQD18qxYckK/CirODa9Wvy1onTDIqzOFRKZKKRmTEb6
+mEdFX0ovOLI00Q2NcariOpsf1InGGAe1peuwQkxkYpA+dSKG3iil+TKlKlYwHFaMey2cGXRREVY/
+2OKebsRVuzFvyXoxkXlsFF0PxgzGrZojFFeLcLih7fGK9brBWh/tvPX4tQr9+bH4a+gRjZZrtdj3
+5X40ygy1XS7DlkMXUA26Vjj2s8gTivrMRabfl1XWOGAt525D7Q+b6H6yDScrDXj+n1MDlsg49Sq2
+N2cp54o3+1jrzSlIZYWVH2sc6G2fivlMhxl6ZpjkIPmltzAvvb9Y+Fdb3sZHRRdwsqYJA2mSsLTE
+pMzCEnratF38Gr9/Z5u02+XXevOmuJ3+xgc0OdjRNTHzFTQ3W30OxI7SKziizMWC4vzdor8zaEJ1
+hshqFp7c/DY27C/H1oM1WDiWnrwMsXhp0WIqZ0PhX15BQcsYzKMnL+H27OvSf+p7WPVCNHo5ey5e
+xIQ9H2LpF2dw4mITEmT1pkVH34w/4Z0ZT4in/Lr3Oiz/ohyFJeeROMkeG0x6NfhrOVeI7GOUNZvH
+YcWqWYhxZNy/faEGV++1J6RcH+yeMvkhE6es9woKs8updBTmf/guknrS6uSn8Om/LcNBeT4mk8ey
+qhr3Gjiz6ANY/WCMe+rf2/Gf+aQ6CnNXrUZKL3vlPjehGAvezkPWl98jef5ocS6L1uuGcn3IvDUM
+wpLPN8l2yFZ158for3EQpowyYsOhYpRdnoE0R+/22ZLvROOmpSU75vcwypO5xMxFdo7vq6xxwFqu
+zZKudOuoKs2iB4QjQOhwLP/gtbae7rZiuq6ptjeZNmX7tNebTLSHVe38lOPAP/vY35X24IrCLt2S
+mcRf9XOq6ZtIs8iLahHipc/Aamt1lm2/Yp/auicnE5HPP4PEuBiYzCaEh0tjLu3PYN3TEXoVuViq
+cbyGrDdPxNOy5CHp2QyE7f8UNxvc72RW2Mm1ihOA29+qWUlQfmR6BIaLJ1D4zQn878+30KVLF9xt
+qGcX0K6k0J1jRMYEeyIjHH40JR19KZmpOHwSFkpmhBrUplfd35qfjguqkD57ijOREbaDwmNpsLP9
+olgf7Yt73aPND0BRr+UmLguaaHhxmJDICEtQLMZNiMfBHRfs2378VY57QbA6Zyb1rH6wxr3lOs6K
+TeA6DnyTgzN37deGLtRHKw7FVV+B0Plrf0NG23VDsT6YnJUX0osfu79Dxv8GOLQNRSWVSBPe2rHV
+YB9dc4HhGDHQca3UxM/uj75c5Iw8rDPHgdbrJA3Hf/UBVogqB2P5XymR8Tys4MEo/3eptzcV+3yo
+N0WrWTnLhCjGgT/2hfbDw/7fymWWtq3qkMwIN7EYmGXzLIK62qentkJ7Dmbsn46Zo05Rd+kZFGyk
+/w5bU//1Lcwa21/hjYI2p3xZ018vAxdqYOK3D4mXS1sLN6M3OVFRXUe3mUEuPTDSJCqX8j44fLqA
+es8KhYsf5VKx8TCRIS3/d8cHSfJTTOguD1RbM3V5uy5a9ar5GxxiT+mMXdVCmaE+XE1V3GL3g0Gv
+ozLND0e7xMG9VuHc+7OocWaygtUPKscU91KQmyktratDg8yIoQPiYe1pdvJib78M9SHTw7qqJz8w
++GtIGIHU0G2UwBSjlpIZ0/mDKCNj+05NRS/JaA38hIRWz+u4ZILir4Y4YIoXj8rOYN/RK5g98hGP
+Rzt+pwf7NNUbgwesnEVRDHHgo33ig7jR5P0lIgZXlIqo3QGUznU91jZ87bpf61aQCWl/eBfjZjXh
+Wn0dzh7dg62F5diXuxHJIz6ENPKkVaxq+UDpVeJCx8ShePfx+LvN4li4uW9vl0SmzYe7NODkx0Jv
+qP2PkMiEplD36xxn96u16r/x2oqdfghuQYvwgCx1GXUJx2O0WUH+ifb6rNe7v9INv5U1b/YLnAON
+L34o6XUcu3NbNrmIVAUbJZAOvQH/8c6ZSTWrH1SOKe4lZt3HYslb45RN0Np+JdnKUjUe1YcfWPyl
+F55TnomnyfblKKtqRJ/9B8hWI9JGDmizWfKRSZ7jNOmcNin+rZE898ubU6B0zL2A+/WPtZxTMBA2
+6hW8P6Mr3l9I8602voeEuL86hyllxdhXJRvYz1AsqWgf6RIXLfWmpE2yXY2zXIZkg3yftC4d02Sf
+EVP/YxOmSjIC8KtpArBe+g1B9ueYkGAPF2ubVbzxBRkj0CtuEFJnvI7X0qJItZib+2VCR+n1arQx
+CoNC6ei1gzgpe+y8VLJXfBvIZHTPNe/Zh5munUKdkED7utju2S8w/eKciQxNtcS3+d/6KpHOE+ry
+OkrLapwyGs+WoYK2zMMG2IcCNOtV9ze6n314c9fXB8QhB6dyetK0WKVW17ZXlzXNfqhoNXRFNBW5
+tX+fbPJ1E04d9n+ISdCsGPeiaeqcxWJqf1j9YI17Q6jIBZXf4Uf5TFfRDrf6DeB1Q81tUA+0+NTp
+b7vU4i8ZlTBivPhCwPaNK7H5EA24xY7Hk455RaLNGuWp++lDCWNvDDXTeTdui30/LhJY44C1nEx4
+j2jqATA9gQULhCTYguwlmaj153Kg5IdMr7Cq3t7osztK9uldbz7wc3PJddMn+yy4dO4sTp87j0Z/
+7l2ulrhsud8tXQ66bFBW508s2JrP40BJFVqNIWi9Qm/R0HL820JEVhthae2G4eNHoyd1X53bthSr
+C7siffpY/DLOhObKE8gvEl7JHIwwD7mPKEjhT0fpVTBJdsiM0S8Mw57ccmQtXoHrr05ExNVjyN4h
+8IlCxuj+srLCagT6D6LErqYWGz7OwoRkupHTNWzI+AntXkF2O9F10xGM1afykEnjeKm/DMdPO3Kw
+p1KYgeDfUpa7DDmWlzHIeAVbc4tFYRmjHU+LmvWq+2tKfg6TzKXYdSof81bWYm76UIS0/Izvs7ch
+ir4wPduPL+d6JaHZD6+S7AeC4jBhagzKqN5XvJeFuVOScP2Hv2F7W16oIqD9Yda4t5+pzrm9Bg97
+mP1gjHuaN/Tiqyko21iKtQsXY9JLk9GPPlF/4+oZFO0oxZ0xC7H+ZZovQove1w0P3ins0osfu7+i
+MT0T8UwsUFBzXZxDlJw+0rULXwM/Bef8PBSKfv1p7JkmK/9l5S2MGRBGHUiPY/KkJ+jxhzEOmMvJ
+TBWzS/qca+IsLEg7h7VF5Vi6thhZi8Y5O45lpRlWlfygeyPj/c2pSMk+3euNlbPTOuUVX+yjN5Iz
+V68BvaQcsC//sycz3UOc49OungoZhudUSz4B2Fp/kj5v7TqEUX1oJ3IOCdKiaLIoJTMU85GxifS0
+UYw9X+Vhj6QoNB6z//x7nyZxdZRee0+FOpeYsfPwhiUTH1F3ccHG/3J4HI+5y15Hon1mo0RB/H38
+hdcx7cYn2H7sCLZXHrEfG/o0JTPyySoup7TfoGD83bKXUf/OZpQV5tF/oUg80tMM2FN0hiZu+76Y
+B0RR1/dm7HOImDTvPaRK3xLyQa+6vyZMX/U+IrPX0TyrUmTT6/D2JQZzI+XZL1ucOk5W/tHkB5ve
+gdMWY279h8g+dATZmUK9RuGpUTQBmF61lb62rWyU61HWuJfOUucslVT+ZfWDNe57jpyDVcEmrMnc
+iV1fbG5THhqDaU887Nxmv26w1YdTMOOKXvxY/bWbZcTwKSkoyBRiPh4TnnyknbXs8gLDhWayI2nu
+Msy4uw4FdM3aVUkmmkORQcmMsLDGAWu5IMfVq6usVztx5kJMOruIHnjy8GXZUMxOFrqKtC7KfrC2
+N1b72OuNzQ9Wfqz3Lc32Ob6kLSQzkarzG9l8ci/10KW6G39339nU7D645l4i0Ns2WCy36UuoNljv
+BcFkivCSSOltR0fppcze0kTdb9T3Rf/Ce5p8fHrQysOKxkZ7b4zR5J/Oqu1vY8UOA5Z+thRx5EvT
+PRvN+aC30IRrZLtFP71y0VZLI+gNfhho+NJIyR119AV40d8PS2Oj+GhgECbKeWQXYJd0Es/qB3vc
+E+uGJqpcA4KDDAj3CKfj2q9O2GRiWPyVFVdd1VueqkJNBVjjgLWcJuWdurC+9aY/P//tiwj3fwqJ
+UIWdNJnp1NHFjfNAwJ7MAG9mvgvpzVAPxfguToAT4AQ4AU7ASUCvZIZ9mMmpmq9wAu0J3Gu1z9Bk
+famovQS+hxPgBDgBToAT8I0A75nxjRs/y42ApaEGV24Go08CfYzP7Rjf5AQ4AU6AE+AEPBHQq2fG
+YzLjSSHfxwlwApwAJ8AJcAKcQGck8IvOaBS3iRPgBDgBToAT4AQ4AVYCPJlhJcXLcQKcACfACXAC
+nECnJMCTmU5ZLdwoToAT4AQ4AU6AE2AlwJMZVlK8HCfACXACnAAnwAl0SgI8memU1cKN4gQ4AU6A
+E+AEOAFWAv8PxvdR0yK8jugAAAAASUVORK5CYII=
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=--
diff --git a/spec/fixtures/emails/merge_request_with_conflicting_patch.eml b/spec/fixtures/emails/merge_request_with_conflicting_patch.eml
new file mode 100644
index 00000000000..ddfdfe9e24a
--- /dev/null
+++ b/spec/fixtures/emails/merge_request_with_conflicting_patch.eml
@@ -0,0 +1,45 @@
+From: "Jake the Dog" <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+merge-request+auth_token@appmail.adventuretime.ooo
+Subject: feature
+Date: Wed, 31 Oct 2018 17:27:52 +0100
+X-Mailer: MailMate (1.12r5523)
+Message-ID: <7BE7C2E6-F0D9-4C85-9F55-B2A4BA01BFEC@gitlab.com>
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_="
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+
+This does not apply
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment;
+ filename=0002-This-does-not-apply-to-the-feature-branch.patch
+Content-Transfer-Encoding: quoted-printable
+
+=46rom 00c68c2b4f954370ce82a1162bc29c13f524897e Mon Sep 17 00:00:00 2001
+From: Patch user <patchuser@gitlab.org>
+Date: Mon, 22 Oct 2018 11:05:48 +0200
+Subject: [PATCH] This does not apply to the `feature` branch
+
+---
+ files/ruby/feature.rb | 5 +++++
+ 1 file changed, 5 insertions(+)
+ create mode 100644 files/ruby/feature.rb
+
+diff --git a/files/ruby/feature.rb b/files/ruby/feature.rb
+new file mode 100644
+index 0000000..fef26e4
+--- /dev/null
++++ b/files/ruby/feature.rb
+@@ -0,0 +1,5 @@
++class Feature
++ def bar
++ puts 'foo'
++ end
++end
+-- =
+
+2.19.1
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=--
diff --git a/spec/fixtures/emails/merge_request_with_patch_and_target_branch.eml b/spec/fixtures/emails/merge_request_with_patch_and_target_branch.eml
new file mode 100644
index 00000000000..965658721cd
--- /dev/null
+++ b/spec/fixtures/emails/merge_request_with_patch_and_target_branch.eml
@@ -0,0 +1,44 @@
+From: "Jake the Dog" <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+merge-request+auth_token@appmail.adventuretime.ooo
+Subject: new-branch-with-a-patch
+Date: Wed, 24 Oct 2018 16:39:49 +0200
+X-Mailer: MailMate (1.12r5523)
+Message-ID: <F1F36291-728D-4E8F-AFEB-C398B8D9BB4E@gitlab.com>
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_="
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+
+This applies nicely to a branch freshly created from the root-ref
+
+The other attachments in this email are ignored
+
+/target_branch with-codeowners
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment; filename=0001-A-commit-from-a-patch.patch
+Content-Transfer-Encoding: quoted-printable
+
+=46rom 3fee0042e610fb3563e4379e316704cb1210f3de Mon Sep 17 00:00:00 2001
+From: Patch user <patchuser@gitlab.org>
+Date: Thu, 18 Oct 2018 13:40:35 +0200
+Subject: [PATCH] A commit from a patch
+
+---
+ README | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/README b/README
+index 3742e48..e40a3b9 100644
+--- a/README
++++ b/README
+@@ -1 +1,3 @@
+ Sample repo for testing gitlab features
++
++This was applied in a patch!
+-- =
+
+2.19.1
diff --git a/spec/fixtures/emails/valid_merge_request_with_patch.eml b/spec/fixtures/emails/valid_merge_request_with_patch.eml
new file mode 100644
index 00000000000..143fa77d1fa
--- /dev/null
+++ b/spec/fixtures/emails/valid_merge_request_with_patch.eml
@@ -0,0 +1,151 @@
+From: "Jake the Dog" <jake@adventuretime.ooo>
+To: incoming+gitlabhq/gitlabhq+merge-request+auth_token@appmail.adventuretime.ooo
+Subject: new-branch-with-a-patch
+Date: Wed, 24 Oct 2018 16:39:49 +0200
+X-Mailer: MailMate (1.12r5523)
+Message-ID: <F1F36291-728D-4E8F-AFEB-C398B8D9BB4E@gitlab.com>
+MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_="
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+
+This applies nicely to a branch freshly created from the root-ref
+
+The other attachments in this email are ignored
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment; filename=0001-A-commit-from-a-patch.patch
+Content-Transfer-Encoding: quoted-printable
+
+=46rom 3fee0042e610fb3563e4379e316704cb1210f3de Mon Sep 17 00:00:00 2001
+From: Patch user <patchuser@gitlab.org>
+Date: Thu, 18 Oct 2018 13:40:35 +0200
+Subject: [PATCH] A commit from a patch
+
+---
+ README | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/README b/README
+index 3742e48..e40a3b9 100644
+--- a/README
++++ b/README
+@@ -1 +1,3 @@
+ Sample repo for testing gitlab features
++
++This was applied in a patch!
+-- =
+
+2.19.1
+
+
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=
+Content-Disposition: attachment; filename=really-not-a-patch.png
+Content-Type: image/png
+Content-Transfer-Encoding: base64
+
+iVBORw0KGgoAAAANSUhEUgAAAjMAAAAfCAYAAAASo0ymAAABfGlDQ1BJQ0MgUHJvZmlsZQAAKJFj
+YGAqSSwoyGFhYGDIzSspCnJ3UoiIjFJgv8PAzcDDIMRgxSCemFxc4BgQ4MOAE3y7xsAIoi/rgsxK
+8/x506a1fP4WNq+ZclYlOrj1gQF3SmpxMgMDIweQnZxSnJwLZOcA2TrJBUUlQPYMIFu3vKQAxD4B
+ZIsUAR0IZN8BsdMh7A8gdhKYzcQCVhMS5AxkSwDZAkkQtgaInQ5hW4DYyRmJKUC2B8guiBvAgNPD
+RcHcwFLXkYC7SQa5OaUwO0ChxZOaFxoMcgcQyzB4MLgwKDCYMxgwWDLoMjiWpFaUgBQ65xdUFmWm
+Z5QoOAJDNlXBOT+3oLQktUhHwTMvWU9HwcjA0ACkDhRnEKM/B4FNZxQ7jxDLX8jAYKnMwMDcgxBL
+msbAsH0PA4PEKYSYyjwGBn5rBoZt5woSixLhDmf8xkKIX5xmbARh8zgxMLDe+///sxoDA/skBoa/
+E////73o//+/i4H2A+PsQA4AJHdp4IxrEg8AAAGcaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8
+eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA1LjQu
+MCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1y
+ZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAg
+ICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAg
+ICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjU2MzwvZXhpZjpQaXhlbFhEaW1lbnNpb24+CiAgICAg
+ICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4zMTwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAg
+IDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgpZ5vHZAAAS+ElE
+QVR4Ae2cC1QUV5rH/xnA1uZli02MiEHQqBMdTGB8ZIniA5xgfCTRycb1rBmdTHJGJ+toPBtzzGg2
+Rs3LdVTOYhKMcDgmDNHVNXKiGKJINCoqiXpUEFdB0CDKCtrSYp/Zr6q7muqmu+pWd7XgnFtH6Xrc
++h6/+92qr+69VQ9dqrvxd/CFE+AEOAFOgBPgBDiBB5TALx5Qu7nZnAAnwAlwApwAJ8AJiAR4MsMD
+gRPgBDgBToAT4AQeaAI8mXmgq48bzwlwApwAJ8AJcAI8meExwAlwApwAJ8AJcAIPNAGezDzQ1ceN
+5wQ4AU6AE+AEOIFgTwiamls87eb7OAFOgBPgBDgBToAT0I1ARHhXXWTxnhldMHIhnAAnwAlwApwA
+J9BRBIL+vOjfl7srt969577L63Zj1VGU/liHHn1j0I0xNbI1X8TRo5V4yNQLEQbGk7xawH6go/Sy
+W8hLBoqAL3EaKFtY5doaz6PkwCk8FN0H3f1oJx0d93r5wcpN13K2RhzbX4KKn4FH+vSAx65sXRX+
+YwgTYq5k72FUXqzChaoq1DQGI7Z3dwT6ai/oPbj/OJoNDyM6IsRvmL74odTe9LbPbwc7gQCDQZ9W
+5VFK0co52FKp7OW0N9di6sAI1B//G7YU3kHvEb+Gyah8jnTUWncYWdm7kf7GWrz0eIS0O+C/HaVX
+P8dsqPphL07dicFvxg6BQT/BnVSSfv76EqcdDcVafww5X1A7iRmGR/1oJ+pxrx9nT8z08sOT7IDv
+s9YiPzcf10InImlkfy9tLrD8Au5jABRY6w8j56vdbZIFfslxCG/bE5A1Idazqc2EpUVj/cwhfuvw
+xQ+l9qa3fX47eN8EWFC4cj4KKgdjadZiJATg5uUxUQ6JiEGYOQpm8X9bhiLtC4MRIcH2PCg4RBjv
+Mml7YgkOFRFGhnrMpQKHt6P06ubRbZzIy8f2radxVzeZnVmQfv76FKcdjSa4i2hBiL/NRDXu9ePs
+EZlefngUHuCdQcF0daOlRxcEeVUVYH5e9XbeA8aEF/HZZ5vw2Zo/wSyYqchPRz8csdbD70Zjt8kn
+P5Tam8726Ugu4KJaxam4TWixBUaVx8tk6vx3kSrps57HqtdWosI8Ge9/8BylMV4WR0u32WwICvLe
+7IWzjQnP4vPPn/UiKHC7O0qvfh4ZYOxB0lpCYb/N6Se5c0oKgL8a4rRjmNhATUhsQwblZsRsnnrc
+B4Az9PeD2eH7XjAQ/O67E7orFG8Dxm5iMnhNd+n3T6BWP9Tb2/2zvTNpkgb9PCYdOhjKINcxf4Y6
+YJQSquoTxdiVl4eTtwWrYjDzzYVIGyg+0zjNrC3NQ27JFUREGGC1Av/04qsY0ad9f1PD6d3IzdmJ
+k9cs4rlh5niMmTIL01PinLK0rHSE3trSTfikBMhIj8P3m5W5NJwrxqfr81AhsgP6Jk3GnDnP4VEp
+c7ReRM7H2agjpytqBM+/wZqPz0CA2NJiQMYfX8eIXu05KjFqvnwCuwv34shPZ3DNofexpIl4/l+m
+Y6BJ+13U7u9djEg0YR91L4sXL/MwzF/4KpJktjHp1eyvFee+244tW3ejWvTFiMeSnsZvZ09HQrir
+LyxxqsRNOsbkBxXWEgdV1D7WZhfjlqAkNgWzUyRtvv2qxr1mzmx2sPqhGvcOdZarJ1CwaQv2VV4X
+94TFDsfLf/ydS1wJB1iuG1rqw6He+0+A+Kn5a71cgjXritB7yh8oRmJl9jWh6JN1KL07DAvmP2vv
+UaKjavIEAbpykVnEssoaB6zl3HU2nP4auTsO42ZECt6YPzFgQ12q7c3dMMe2N/tY6s2LSI+7Wfhp
+iQO97fNotIadHoeZvJ3veluQSgk30Vps2ZiHq/1SkJ4UY99enYlz9lxEKmj/peLW+gqcPFWOmtuU
+0bgttqvFWPxRPiUyQHLaRExKG47elgvYlV2KZreymjbvs96WhjpUV5YiK1OZS/PpfCxebU9kho4Z
+h6eGRKH62E4sn7cOl5x4gtDFEEFJYATCRKe7wWAwiNuRkeHahvgc0Kr3rseuQ5QQ9R6MVOKcOiQG
+Fcd2Y/XCNahy6mUnbPf3CAookemWNA6TxsQD18qxYckK/CirODa9Wvy1onTDIqzOFRKZKKRmTEb6
+mEdFX0ovOLI00Q2NcariOpsf1InGGAe1peuwQkxkYpA+dSKG3iil+TKlKlYwHFaMey2cGXRREVY/
+2OKebsRVuzFvyXoxkXlsFF0PxgzGrZojFFeLcLih7fGK9brBWh/tvPX4tQr9+bH4a+gRjZZrtdj3
+5X40ygy1XS7DlkMXUA26Vjj2s8gTivrMRabfl1XWOGAt525D7Q+b6H6yDScrDXj+n1MDlsg49Sq2
+N2cp54o3+1jrzSlIZYWVH2sc6G2fivlMhxl6ZpjkIPmltzAvvb9Y+Fdb3sZHRRdwsqYJA2mSsLTE
+pMzCEnratF38Gr9/Z5u02+XXevOmuJ3+xgc0OdjRNTHzFTQ3W30OxI7SKziizMWC4vzdor8zaEJ1
+hshqFp7c/DY27C/H1oM1WDiWnrwMsXhp0WIqZ0PhX15BQcsYzKMnL+H27OvSf+p7WPVCNHo5ey5e
+xIQ9H2LpF2dw4mITEmT1pkVH34w/4Z0ZT4in/Lr3Oiz/ohyFJeeROMkeG0x6NfhrOVeI7GOUNZvH
+YcWqWYhxZNy/faEGV++1J6RcH+yeMvkhE6es9woKs8updBTmf/guknrS6uSn8Om/LcNBeT4mk8ey
+qhr3Gjiz6ANY/WCMe+rf2/Gf+aQ6CnNXrUZKL3vlPjehGAvezkPWl98jef5ocS6L1uuGcn3IvDUM
+wpLPN8l2yFZ158for3EQpowyYsOhYpRdnoE0R+/22ZLvROOmpSU75vcwypO5xMxFdo7vq6xxwFqu
+zZKudOuoKs2iB4QjQOhwLP/gtbae7rZiuq6ptjeZNmX7tNebTLSHVe38lOPAP/vY35X24IrCLt2S
+mcRf9XOq6ZtIs8iLahHipc/Aamt1lm2/Yp/auicnE5HPP4PEuBiYzCaEh0tjLu3PYN3TEXoVuViq
+cbyGrDdPxNOy5CHp2QyE7f8UNxvc72RW2Mm1ihOA29+qWUlQfmR6BIaLJ1D4zQn878+30KVLF9xt
+qGcX0K6k0J1jRMYEeyIjHH40JR19KZmpOHwSFkpmhBrUplfd35qfjguqkD57ijOREbaDwmNpsLP9
+olgf7Yt73aPND0BRr+UmLguaaHhxmJDICEtQLMZNiMfBHRfs2378VY57QbA6Zyb1rH6wxr3lOs6K
+TeA6DnyTgzN37deGLtRHKw7FVV+B0Plrf0NG23VDsT6YnJUX0osfu79Dxv8GOLQNRSWVSBPe2rHV
+YB9dc4HhGDHQca3UxM/uj75c5Iw8rDPHgdbrJA3Hf/UBVogqB2P5XymR8Tys4MEo/3eptzcV+3yo
+N0WrWTnLhCjGgT/2hfbDw/7fymWWtq3qkMwIN7EYmGXzLIK62qentkJ7Dmbsn46Zo05Rd+kZFGyk
+/w5bU//1Lcwa21/hjYI2p3xZ018vAxdqYOK3D4mXS1sLN6M3OVFRXUe3mUEuPTDSJCqX8j44fLqA
+es8KhYsf5VKx8TCRIS3/d8cHSfJTTOguD1RbM3V5uy5a9ar5GxxiT+mMXdVCmaE+XE1V3GL3g0Gv
+ozLND0e7xMG9VuHc+7OocWaygtUPKscU91KQmyktratDg8yIoQPiYe1pdvJib78M9SHTw7qqJz8w
++GtIGIHU0G2UwBSjlpIZ0/mDKCNj+05NRS/JaA38hIRWz+u4ZILir4Y4YIoXj8rOYN/RK5g98hGP
+Rzt+pwf7NNUbgwesnEVRDHHgo33ig7jR5P0lIgZXlIqo3QGUznU91jZ87bpf61aQCWl/eBfjZjXh
+Wn0dzh7dg62F5diXuxHJIz6ENPKkVaxq+UDpVeJCx8ShePfx+LvN4li4uW9vl0SmzYe7NODkx0Jv
+qP2PkMiEplD36xxn96u16r/x2oqdfghuQYvwgCx1GXUJx2O0WUH+ifb6rNe7v9INv5U1b/YLnAON
+L34o6XUcu3NbNrmIVAUbJZAOvQH/8c6ZSTWrH1SOKe4lZt3HYslb45RN0Np+JdnKUjUe1YcfWPyl
+F55TnomnyfblKKtqRJ/9B8hWI9JGDmizWfKRSZ7jNOmcNin+rZE898ubU6B0zL2A+/WPtZxTMBA2
+6hW8P6Mr3l9I8602voeEuL86hyllxdhXJRvYz1AsqWgf6RIXLfWmpE2yXY2zXIZkg3yftC4d02Sf
+EVP/YxOmSjIC8KtpArBe+g1B9ueYkGAPF2ubVbzxBRkj0CtuEFJnvI7X0qJItZib+2VCR+n1arQx
+CoNC6ei1gzgpe+y8VLJXfBvIZHTPNe/Zh5munUKdkED7utju2S8w/eKciQxNtcS3+d/6KpHOE+ry
+OkrLapwyGs+WoYK2zMMG2IcCNOtV9ze6n314c9fXB8QhB6dyetK0WKVW17ZXlzXNfqhoNXRFNBW5
+tX+fbPJ1E04d9n+ISdCsGPeiaeqcxWJqf1j9YI17Q6jIBZXf4Uf5TFfRDrf6DeB1Q81tUA+0+NTp
+b7vU4i8ZlTBivPhCwPaNK7H5EA24xY7Hk455RaLNGuWp++lDCWNvDDXTeTdui30/LhJY44C1nEx4
+j2jqATA9gQULhCTYguwlmaj153Kg5IdMr7Cq3t7osztK9uldbz7wc3PJddMn+yy4dO4sTp87j0Z/
+7l2ulrhsud8tXQ66bFBW508s2JrP40BJFVqNIWi9Qm/R0HL820JEVhthae2G4eNHoyd1X53bthSr
+C7siffpY/DLOhObKE8gvEl7JHIwwD7mPKEjhT0fpVTBJdsiM0S8Mw57ccmQtXoHrr05ExNVjyN4h
+8IlCxuj+srLCagT6D6LErqYWGz7OwoRkupHTNWzI+AntXkF2O9F10xGM1afykEnjeKm/DMdPO3Kw
+p1KYgeDfUpa7DDmWlzHIeAVbc4tFYRmjHU+LmvWq+2tKfg6TzKXYdSof81bWYm76UIS0/Izvs7ch
+ir4wPduPL+d6JaHZD6+S7AeC4jBhagzKqN5XvJeFuVOScP2Hv2F7W16oIqD9Yda4t5+pzrm9Bg97
+mP1gjHuaN/Tiqyko21iKtQsXY9JLk9GPPlF/4+oZFO0oxZ0xC7H+ZZovQove1w0P3ins0osfu7+i
+MT0T8UwsUFBzXZxDlJw+0rULXwM/Bef8PBSKfv1p7JkmK/9l5S2MGRBGHUiPY/KkJ+jxhzEOmMvJ
+TBWzS/qca+IsLEg7h7VF5Vi6thhZi8Y5O45lpRlWlfygeyPj/c2pSMk+3euNlbPTOuUVX+yjN5Iz
+V68BvaQcsC//sycz3UOc49OungoZhudUSz4B2Fp/kj5v7TqEUX1oJ3IOCdKiaLIoJTMU85GxifS0
+UYw9X+Vhj6QoNB6z//x7nyZxdZRee0+FOpeYsfPwhiUTH1F3ccHG/3J4HI+5y15Hon1mo0RB/H38
+hdcx7cYn2H7sCLZXHrEfG/o0JTPyySoup7TfoGD83bKXUf/OZpQV5tF/oUg80tMM2FN0hiZu+76Y
+B0RR1/dm7HOImDTvPaRK3xLyQa+6vyZMX/U+IrPX0TyrUmTT6/D2JQZzI+XZL1ucOk5W/tHkB5ve
+gdMWY279h8g+dATZmUK9RuGpUTQBmF61lb62rWyU61HWuJfOUucslVT+ZfWDNe57jpyDVcEmrMnc
+iV1fbG5THhqDaU887Nxmv26w1YdTMOOKXvxY/bWbZcTwKSkoyBRiPh4TnnyknbXs8gLDhWayI2nu
+Msy4uw4FdM3aVUkmmkORQcmMsLDGAWu5IMfVq6usVztx5kJMOruIHnjy8GXZUMxOFrqKtC7KfrC2
+N1b72OuNzQ9Wfqz3Lc32Ob6kLSQzkarzG9l8ci/10KW6G39339nU7D645l4i0Ns2WCy36UuoNljv
+BcFkivCSSOltR0fppcze0kTdb9T3Rf/Ce5p8fHrQysOKxkZ7b4zR5J/Oqu1vY8UOA5Z+thRx5EvT
+PRvN+aC30IRrZLtFP71y0VZLI+gNfhho+NJIyR119AV40d8PS2Oj+GhgECbKeWQXYJd0Es/qB3vc
+E+uGJqpcA4KDDAj3CKfj2q9O2GRiWPyVFVdd1VueqkJNBVjjgLWcJuWdurC+9aY/P//tiwj3fwqJ
+UIWdNJnp1NHFjfNAwJ7MAG9mvgvpzVAPxfguToAT4AQ4AU7ASUCvZIZ9mMmpmq9wAu0J3Gu1z9Bk
+famovQS+hxPgBDgBToAT8I0A75nxjRs/y42ApaEGV24Go08CfYzP7Rjf5AQ4AU6AE+AEPBHQq2fG
+YzLjSSHfxwlwApwAJ8AJcAKcQGck8IvOaBS3iRPgBDgBToAT4AQ4AVYCPJlhJcXLcQKcACfACXAC
+nECnJMCTmU5ZLdwoToAT4AQ4AU6AE2AlwJMZVlK8HCfACXACnAAnwAl0SgI8memU1cKN4gQ4AU6A
+E+AEOAFWAv8PxvdR0yK8jugAAAAASUVORK5CYII=
+--=_MailMate_D2C4B06A-4F8D-4AAB-B247-C507E35AAED6_=--
diff --git a/spec/fixtures/patchfiles/0001-A-commit-from-a-patch.patch b/spec/fixtures/patchfiles/0001-A-commit-from-a-patch.patch
new file mode 100644
index 00000000000..cc38682a0ab
--- /dev/null
+++ b/spec/fixtures/patchfiles/0001-A-commit-from-a-patch.patch
@@ -0,0 +1,19 @@
+From 3fee0042e610fb3563e4379e316704cb1210f3de Mon Sep 17 00:00:00 2001
+From: Patch User <patchuser@gitlab.org>
+Date: Thu, 18 Oct 2018 13:40:35 +0200
+Subject: [PATCH] A commit from a patch
+
+---
+ README | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/README b/README
+index 3742e48..e40a3b9 100644
+--- a/README
++++ b/README
+@@ -1 +1,3 @@
+ Sample repo for testing gitlab features
++
++This was applied in a patch!
+--
+2.19.1
diff --git a/spec/fixtures/patchfiles/0001-This-does-not-apply-to-the-feature-branch.patch b/spec/fixtures/patchfiles/0001-This-does-not-apply-to-the-feature-branch.patch
new file mode 100644
index 00000000000..905002ae898
--- /dev/null
+++ b/spec/fixtures/patchfiles/0001-This-does-not-apply-to-the-feature-branch.patch
@@ -0,0 +1,23 @@
+From 00c68c2b4f954370ce82a1162bc29c13f524897e Mon Sep 17 00:00:00 2001
+From: Patch User <patchuser@gitlab.org>
+Date: Mon, 22 Oct 2018 11:05:48 +0200
+Subject: [PATCH] This does not apply to the `feature` branch
+
+---
+ files/ruby/feature.rb | 5 +++++
+ 1 file changed, 5 insertions(+)
+ create mode 100644 files/ruby/feature.rb
+
+diff --git a/files/ruby/feature.rb b/files/ruby/feature.rb
+new file mode 100644
+index 0000000..fef26e4
+--- /dev/null
++++ b/files/ruby/feature.rb
+@@ -0,0 +1,5 @@
++class Feature
++ def bar
++ puts 'foo'
++ end
++end
+--
+2.19.1
diff --git a/spec/javascripts/boards/components/issue_due_date_spec.js b/spec/javascripts/boards/components/issue_due_date_spec.js
new file mode 100644
index 00000000000..9e49330c052
--- /dev/null
+++ b/spec/javascripts/boards/components/issue_due_date_spec.js
@@ -0,0 +1,64 @@
+import Vue from 'vue';
+import dateFormat from 'dateformat';
+import IssueDueDate from '~/boards/components/issue_due_date.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Issue Due Date component', () => {
+ let vm;
+ let date;
+ const Component = Vue.extend(IssueDueDate);
+ const createComponent = (dueDate = new Date()) =>
+ mountComponent(Component, { date: dateFormat(dueDate, 'yyyy-mm-dd', true) });
+
+ beforeEach(() => {
+ date = new Date();
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render "Today" if the due date is today', () => {
+ const timeContainer = vm.$el.querySelector('time');
+
+ expect(timeContainer.textContent.trim()).toEqual('Today');
+ });
+
+ it('should render "Yesterday" if the due date is yesterday', () => {
+ date.setDate(date.getDate() - 1);
+ vm = createComponent(date);
+
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual('Yesterday');
+ });
+
+ it('should render "Tomorrow" if the due date is one day from now', () => {
+ date.setDate(date.getDate() + 1);
+ vm = createComponent(date);
+
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual('Tomorrow');
+ });
+
+ it('should render day of the week if due date is one week away', () => {
+ date.setDate(date.getDate() + 5);
+ vm = createComponent(date);
+
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual(dateFormat(date, 'dddd', true));
+ });
+
+ it('should render month and day for other dates', () => {
+ date.setDate(date.getDate() + 17);
+ vm = createComponent(date);
+
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual(
+ dateFormat(date, 'mmm d', true),
+ );
+ });
+
+ it('should contain the correct `.text-danger` css class for overdue issue', () => {
+ date.setDate(date.getDate() - 17);
+ vm = createComponent(date);
+
+ expect(vm.$el.querySelector('time').classList.contains('text-danger')).toEqual(true);
+ });
+});
diff --git a/spec/javascripts/boards/components/issue_time_estimate_spec.js b/spec/javascripts/boards/components/issue_time_estimate_spec.js
new file mode 100644
index 00000000000..ba65d3287da
--- /dev/null
+++ b/spec/javascripts/boards/components/issue_time_estimate_spec.js
@@ -0,0 +1,40 @@
+import Vue from 'vue';
+import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Issue Tine Estimate component', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(IssueTimeEstimate);
+ vm = mountComponent(Component, {
+ estimate: 374460,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders the correct time estimate', () => {
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual('2w 3d 1m');
+ });
+
+ it('renders expanded time estimate in tooltip', () => {
+ expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain(
+ '2 weeks 3 days 1 minute',
+ );
+ });
+
+ it('prevents tooltip xss', done => {
+ const alertSpy = spyOn(window, 'alert');
+ vm.estimate = 'Foo <script>alert("XSS")</script>';
+
+ vm.$nextTick(() => {
+ expect(alertSpy).not.toHaveBeenCalled();
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual('0m');
+ expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain('0m');
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index 58b7d45d913..6eda5047dd0 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -117,11 +117,9 @@ describe('Issue card component', () => {
});
it('sets title', () => {
- expect(
- component.$el
- .querySelector('.board-card-assignee img')
- .getAttribute('data-original-title'),
- ).toContain(`Assigned to ${user.name}`);
+ expect(component.$el.querySelector('.js-assignee-tooltip').textContent).toContain(
+ `${user.name}`,
+ );
});
it('sets users path', () => {
@@ -154,7 +152,7 @@ describe('Issue card component', () => {
it('displays defaults avatar if users avatar is null', () => {
expect(component.$el.querySelector('.board-card-assignee img')).not.toBeNull();
expect(component.$el.querySelector('.board-card-assignee img').getAttribute('src')).toBe(
- 'default_avatar?width=20',
+ 'default_avatar?width=24',
);
});
});
@@ -163,7 +161,6 @@ describe('Issue card component', () => {
describe('multiple assignees', () => {
beforeEach(done => {
component.issue.assignees = [
- user,
new ListAssignee({
id: 2,
name: 'user2',
@@ -187,11 +184,11 @@ describe('Issue card component', () => {
Vue.nextTick(() => done());
});
- it('renders all four assignees', () => {
- expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(4);
+ it('renders all three assignees', () => {
+ expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(3);
});
- describe('more than four assignees', () => {
+ describe('more than three assignees', () => {
beforeEach(done => {
component.issue.assignees.push(
new ListAssignee({
@@ -207,12 +204,12 @@ describe('Issue card component', () => {
it('renders more avatar counter', () => {
expect(
- component.$el.querySelector('.board-card-assignee .avatar-counter').innerText,
+ component.$el.querySelector('.board-card-assignee .avatar-counter').innerText.trim(),
).toEqual('+2');
});
- it('renders three assignees', () => {
- expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(3);
+ it('renders two assignees', () => {
+ expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(2);
});
it('renders 99+ avatar counter', done => {
@@ -228,7 +225,7 @@ describe('Issue card component', () => {
Vue.nextTick(() => {
expect(
- component.$el.querySelector('.board-card-assignee .avatar-counter').innerText,
+ component.$el.querySelector('.board-card-assignee .avatar-counter').innerText.trim(),
).toEqual('99+');
done();
});
diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js
index a70138c7eee..0e2cc13fa52 100644
--- a/spec/javascripts/clusters/components/applications_spec.js
+++ b/spec/javascripts/clusters/components/applications_spec.js
@@ -23,6 +23,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub' },
+ knative: { title: 'Knative' },
},
});
});
@@ -46,6 +47,10 @@ describe('Applications', () => {
it('renders a row for Jupyter', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBe(null);
});
+
+ it('renders a row for Knative', () => {
+ expect(vm.$el.querySelector('.js-cluster-application-row-knative')).not.toBe(null);
+ });
});
describe('Ingress application', () => {
@@ -63,6 +68,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '' },
+ knative: { title: 'Knative', hostname: '' },
},
});
@@ -86,6 +92,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '' },
+ knative: { title: 'Knative', hostname: '' },
},
});
@@ -105,6 +112,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '' },
+ knative: { title: 'Knative', hostname: '' },
},
});
@@ -123,6 +131,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
+ knative: { title: 'Knative', hostname: '', status: 'installable' },
},
});
@@ -139,6 +148,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
+ knative: { title: 'Knative', hostname: '', status: 'installable' },
},
});
@@ -155,6 +165,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' },
+ knative: { title: 'Knative', status: 'installed', hostname: '' },
},
});
@@ -171,6 +182,7 @@ describe('Applications', () => {
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', status: 'not_installable' },
+ knative: { title: 'Knative' },
},
});
});
diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js
index 4e6ad11cd92..73abf6504c0 100644
--- a/spec/javascripts/clusters/services/mock_data.js
+++ b/spec/javascripts/clusters/services/mock_data.js
@@ -33,6 +33,11 @@ const CLUSTERS_MOCK_DATA = {
status: APPLICATION_STATUS.INSTALLING,
status_reason: 'Cannot connect',
},
+ {
+ name: 'knative',
+ status: APPLICATION_STATUS.INSTALLING,
+ status_reason: 'Cannot connect',
+ },
],
},
},
@@ -67,6 +72,11 @@ const CLUSTERS_MOCK_DATA = {
status: APPLICATION_STATUS.INSTALLABLE,
status_reason: 'Cannot connect',
},
+ {
+ name: 'knative',
+ status: APPLICATION_STATUS.INSTALLABLE,
+ status_reason: 'Cannot connect',
+ },
],
},
},
@@ -77,6 +87,7 @@ const CLUSTERS_MOCK_DATA = {
'/gitlab-org/gitlab-shell/clusters/1/applications/runner': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': {},
'/gitlab-org/gitlab-shell/clusters/1/applications/jupyter': {},
+ '/gitlab-org/gitlab-shell/clusters/1/applications/knative': {},
},
};
diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js
index e0f55a12fca..34ed36afa5b 100644
--- a/spec/javascripts/clusters/stores/clusters_store_spec.js
+++ b/spec/javascripts/clusters/stores/clusters_store_spec.js
@@ -100,6 +100,14 @@ describe('Clusters Store', () => {
requestReason: null,
hostname: '',
},
+ knative: {
+ title: 'Knative',
+ status: mockResponseData.applications[5].status,
+ statusReason: mockResponseData.applications[5].status_reason,
+ requestStatus: null,
+ requestReason: null,
+ hostname: null,
+ },
},
});
});
diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js
index 8821cde76f4..fed04cbaed8 100644
--- a/spec/javascripts/diffs/store/mutations_spec.js
+++ b/spec/javascripts/diffs/store/mutations_spec.js
@@ -98,7 +98,7 @@ describe('DiffsStoreMutations', () => {
it('should call utils.addContextLines with proper params', () => {
const options = {
lineNumbers: { oldLineNumber: 1, newLineNumber: 2 },
- contextLines: [{ oldLine: 1, newLine: 1, lineCode: 'ff9200_1_1', discussions: [] }],
+ contextLines: [{ oldLine: 1 }],
fileHash: 'ff9200',
params: {
bottom: true,
@@ -110,7 +110,7 @@ describe('DiffsStoreMutations', () => {
parallelDiffLines: [],
};
const state = { diffFiles: [diffFile] };
- const lines = [{ oldLine: 1, newLine: 1 }];
+ const lines = [{ oldLine: 1 }];
const findDiffFileSpy = spyOnDependency(mutations, 'findDiffFile').and.returnValue(diffFile);
const removeMatchLineSpy = spyOnDependency(mutations, 'removeMatchLine');
diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js
index 98c995393b9..fcf3780f0ea 100644
--- a/spec/javascripts/jobs/components/job_app_spec.js
+++ b/spec/javascripts/jobs/components/job_app_spec.js
@@ -102,7 +102,7 @@ describe('Job App ', () => {
.querySelector('.header-main-content')
.textContent.replace(/\s+/g, ' ')
.trim(),
- ).toEqual('passed Job #4757 triggered 1 year ago by Root');
+ ).toContain('passed Job #4757 triggered 1 year ago by Root');
done();
}, 0);
});
@@ -128,7 +128,7 @@ describe('Job App ', () => {
.querySelector('.header-main-content')
.textContent.replace(/\s+/g, ' ')
.trim(),
- ).toEqual('passed Job #4757 created 3 weeks ago by Root');
+ ).toContain('passed Job #4757 created 3 weeks ago by Root');
done();
}, 0);
});
diff --git a/spec/javascripts/lib/utils/datetime_utility_spec.js b/spec/javascripts/lib/utils/datetime_utility_spec.js
index d699e66b8ca..bebe76f76c5 100644
--- a/spec/javascripts/lib/utils/datetime_utility_spec.js
+++ b/spec/javascripts/lib/utils/datetime_utility_spec.js
@@ -336,6 +336,12 @@ describe('prettyTime methods', () => {
expect(timeString).toBe('0m');
});
+
+ it('should return non-condensed representation of time object', () => {
+ const timeObject = { weeks: 1, days: 0, hours: 1, minutes: 0 };
+
+ expect(datetimeUtility.stringifyTime(timeObject, true)).toEqual('1 week 1 hour');
+ });
});
describe('abbreviateTime', () => {
diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index b447e79b0df..81cb3e1f74d 100644
--- a/spec/javascripts/notes/components/noteable_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -3,6 +3,7 @@ import createStore from '~/notes/stores';
import noteableDiscussion from '~/notes/components/noteable_discussion.vue';
import '~/behaviors/markdown/render_gfm';
import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
+import mockDiffFile from '../../diffs/mock_data/diff_file';
const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
@@ -33,9 +34,20 @@ describe('noteable_discussion component', () => {
expect(vm.$el.querySelector('.user-avatar-link')).not.toBeNull();
});
+ it('should not render discussion header for non diff discussions', () => {
+ expect(vm.$el.querySelector('.discussion-header')).toBeNull();
+ });
+
it('should render discussion header', () => {
- expect(vm.$el.querySelector('.discussion-header')).not.toBeNull();
- expect(vm.$el.querySelector('.notes').children.length).toEqual(discussionMock.notes.length);
+ const discussion = { ...discussionMock };
+ discussion.diff_file = mockDiffFile;
+ discussion.diff_discussion = true;
+ const diffDiscussionVm = new Component({
+ store,
+ propsData: { discussion },
+ }).$mount();
+
+ expect(diffDiscussionVm.$el.querySelector('.discussion-header')).not.toBeNull();
});
describe('actions', () => {
diff --git a/spec/javascripts/notes/components/toggle_replies_widget_spec.js b/spec/javascripts/notes/components/toggle_replies_widget_spec.js
new file mode 100644
index 00000000000..2ead8cc6e6a
--- /dev/null
+++ b/spec/javascripts/notes/components/toggle_replies_widget_spec.js
@@ -0,0 +1,78 @@
+import Vue from 'vue';
+import toggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { note } from '../mock_data';
+
+const deepCloneObject = obj => JSON.parse(JSON.stringify(obj));
+
+describe('toggle replies widget for notes', () => {
+ let vm;
+ let ToggleRepliesWidget;
+ const noteFromOtherUser = deepCloneObject(note);
+ noteFromOtherUser.author.username = 'fatihacet';
+
+ const noteFromAnotherUser = deepCloneObject(note);
+ noteFromAnotherUser.author.username = 'mgreiling';
+ noteFromAnotherUser.author.name = 'Mike Greiling';
+
+ const replies = [note, note, note, noteFromOtherUser, noteFromAnotherUser];
+
+ beforeEach(() => {
+ ToggleRepliesWidget = Vue.extend(toggleRepliesWidget);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('collapsed state', () => {
+ beforeEach(() => {
+ vm = mountComponent(ToggleRepliesWidget, {
+ replies,
+ collapsed: true,
+ });
+ });
+
+ it('should render the collapsed', () => {
+ const vmTextContent = vm.$el.textContent.replace(/\s\s+/g, ' ');
+
+ expect(vm.$el.classList.contains('collapsed')).toEqual(true);
+ expect(vm.$el.querySelectorAll('.user-avatar-link').length).toEqual(3);
+ expect(vm.$el.querySelector('time')).not.toBeNull();
+ expect(vmTextContent).toContain('5 replies');
+ expect(vmTextContent).toContain(`Last reply by ${noteFromAnotherUser.author.name}`);
+ });
+
+ it('should emit toggle event when the replies text clicked', () => {
+ const spy = spyOn(vm, '$emit');
+
+ vm.$el.querySelector('.js-replies-text').click();
+
+ expect(spy).toHaveBeenCalledWith('toggle');
+ });
+ });
+
+ describe('expanded state', () => {
+ beforeEach(() => {
+ vm = mountComponent(ToggleRepliesWidget, {
+ replies,
+ collapsed: false,
+ });
+ });
+
+ it('should render expanded state', () => {
+ const vmTextContent = vm.$el.textContent.replace(/\s\s+/g, ' ');
+
+ expect(vm.$el.querySelector('.collapse-replies-btn')).not.toBeNull();
+ expect(vmTextContent).toContain('Collapse replies');
+ });
+
+ it('should emit toggle event when the collapse replies text called', () => {
+ const spy = spyOn(vm, '$emit');
+
+ vm.$el.querySelector('.js-collapse-replies').click();
+
+ expect(spy).toHaveBeenCalledWith('toggle');
+ });
+ });
+});
diff --git a/spec/javascripts/pipelines/header_component_spec.js b/spec/javascripts/pipelines/header_component_spec.js
index 473a062fc40..556a0976b29 100644
--- a/spec/javascripts/pipelines/header_component_spec.js
+++ b/spec/javascripts/pipelines/header_component_spec.js
@@ -51,7 +51,7 @@ describe('Pipeline details header', () => {
.querySelector('.header-main-content')
.textContent.replace(/\s+/g, ' ')
.trim(),
- ).toEqual('failed Pipeline #123 triggered 3 weeks ago by Foo');
+ ).toContain('failed Pipeline #123 triggered 3 weeks ago by Foo');
});
describe('action buttons', () => {
diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js
index c9011b403b7..d6c44f4c976 100644
--- a/spec/javascripts/pipelines/pipeline_url_spec.js
+++ b/spec/javascripts/pipelines/pipeline_url_spec.js
@@ -63,12 +63,15 @@ describe('Pipeline Url Component', () => {
}).$mount();
const image = component.$el.querySelector('.js-pipeline-url-user img');
+ const tooltip = component.$el.querySelector(
+ '.js-pipeline-url-user .js-user-avatar-image-toolip',
+ );
expect(component.$el.querySelector('.js-pipeline-url-user').getAttribute('href')).toEqual(
mockData.pipeline.user.web_url,
);
- expect(image.getAttribute('data-original-title')).toEqual(mockData.pipeline.user.name);
+ expect(tooltip.textContent.trim()).toEqual(mockData.pipeline.user.name);
expect(image.getAttribute('src')).toEqual(`${mockData.pipeline.user.avatar_url}?width=20`);
});
diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js
index 506d01f5ec1..4c575536f0e 100644
--- a/spec/javascripts/pipelines/pipelines_table_row_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js
@@ -86,8 +86,8 @@ describe('Pipelines Table Row', () => {
expect(
component.$el
- .querySelector('.table-section:nth-child(2) img')
- .getAttribute('data-original-title'),
+ .querySelector('.table-section:nth-child(2) .js-user-avatar-image-toolip')
+ .textContent.trim(),
).toEqual(pipeline.user.name);
});
});
@@ -112,8 +112,8 @@ describe('Pipelines Table Row', () => {
const commitAuthorLink = commitAuthorElement.getAttribute('href');
const commitAuthorName = commitAuthorElement
- .querySelector('img.avatar')
- .getAttribute('data-original-title');
+ .querySelector('.js-user-avatar-image-toolip')
+ .textContent.trim();
return { commitAuthorElement, commitAuthorLink, commitAuthorName };
};
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js
index 97dacec1fce..18fcdf7ede1 100644
--- a/spec/javascripts/vue_shared/components/commit_spec.js
+++ b/spec/javascripts/vue_shared/components/commit_spec.js
@@ -98,8 +98,8 @@ describe('Commit component', () => {
it('Should render the author avatar with title and alt attributes', () => {
expect(
component.$el
- .querySelector('.commit-title .avatar-image-container img')
- .getAttribute('data-original-title'),
+ .querySelector('.commit-title .avatar-image-container .js-user-avatar-image-toolip')
+ .textContent.trim(),
).toContain(props.author.username);
expect(
diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
index 3bf497bc00b..7a741bdc067 100644
--- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js
+++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
@@ -73,7 +73,7 @@ describe('Header CI Component', () => {
});
it('should render user icon and name', () => {
- expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name);
+ expect(vm.$el.querySelector('.js-user-link').innerText.trim()).toContain(props.user.name);
});
it('should render provided actions', () => {
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
index dc7652c77f7..5c4aa7cf844 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { placeholderImage } from '~/lazy_loader';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent, { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper';
const DEFAULT_PROPS = {
size: 99,
@@ -32,18 +32,12 @@ describe('User Avatar Image Component', function() {
});
it('should have <img> as a child element', function() {
- expect(vm.$el.tagName).toBe('IMG');
- expect(vm.$el.getAttribute('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
- expect(vm.$el.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
- expect(vm.$el.getAttribute('alt')).toBe(DEFAULT_PROPS.imgAlt);
- });
-
- it('should properly compute tooltipContainer', function() {
- expect(vm.tooltipContainer).toBe('body');
- });
+ const imageElement = vm.$el.querySelector('img');
- it('should properly render tooltipContainer', function() {
- expect(vm.$el.getAttribute('data-container')).toBe('body');
+ expect(imageElement).not.toBe(null);
+ expect(imageElement.getAttribute('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
+ expect(imageElement.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
+ expect(imageElement.getAttribute('alt')).toBe(DEFAULT_PROPS.imgAlt);
});
it('should properly compute avatarSizeClass', function() {
@@ -51,7 +45,7 @@ describe('User Avatar Image Component', function() {
});
it('should properly render img css', function() {
- const { classList } = vm.$el;
+ const { classList } = vm.$el.querySelector('img');
const containsAvatar = classList.contains('avatar');
const containsSizeClass = classList.contains('s99');
const containsCustomClass = classList.contains(DEFAULT_PROPS.cssClasses);
@@ -73,12 +67,41 @@ describe('User Avatar Image Component', function() {
});
it('should add lazy attributes', function() {
- const { classList } = vm.$el;
- const lazyClass = classList.contains('lazy');
+ const imageElement = vm.$el.querySelector('img');
+ const lazyClass = imageElement.classList.contains('lazy');
expect(lazyClass).toBe(true);
- expect(vm.$el.getAttribute('src')).toBe(placeholderImage);
- expect(vm.$el.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
+ expect(imageElement.getAttribute('src')).toBe(placeholderImage);
+ expect(imageElement.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`);
+ });
+ });
+
+ describe('dynamic tooltip content', () => {
+ const props = DEFAULT_PROPS;
+ const slots = {
+ default: ['Action!'],
+ };
+
+ beforeEach(() => {
+ vm = mountComponentWithSlots(UserAvatarImage, { props, slots }).$mount();
+ });
+
+ it('renders the tooltip slot', () => {
+ expect(vm.$el.querySelector('.js-user-avatar-image-toolip')).not.toBe(null);
+ });
+
+ it('renders the tooltip content', () => {
+ expect(vm.$el.querySelector('.js-user-avatar-image-toolip').textContent).toContain(
+ slots.default[0],
+ );
+ });
+
+ it('does not render tooltip data attributes for on avatar image', () => {
+ const avatarImg = vm.$el.querySelector('img');
+
+ expect(avatarImg.dataset.originalTitle).not.toBeDefined();
+ expect(avatarImg.dataset.placement).not.toBeDefined();
+ expect(avatarImg.dataset.container).not.toBeDefined();
});
});
});
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
index e022245d3ea..0151ad23ba2 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -60,39 +60,43 @@ describe('User Avatar Link Component', function() {
it('should only render image tag in link', function() {
const childElements = this.userAvatarLink.$el.childNodes;
- expect(childElements[0].tagName).toBe('IMG');
+ expect(this.userAvatarLink.$el.querySelector('img')).not.toBe('null');
// Vue will render the hidden component as <!---->
expect(childElements[1].tagName).toBeUndefined();
});
it('should render avatar image tooltip', function() {
- expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual(
- this.propsData.tooltipText,
- );
+ expect(this.userAvatarLink.shouldShowUsername).toBe(false);
+ expect(this.userAvatarLink.avatarTooltipText).toEqual(this.propsData.tooltipText);
});
});
describe('username', function() {
it('should not render avatar image tooltip', function() {
- expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual('');
+ expect(
+ this.userAvatarLink.$el.querySelector('.js-user-avatar-image-toolip').innerText.trim(),
+ ).toEqual('');
});
it('should render username prop in <span>', function() {
- expect(this.userAvatarLink.$el.querySelector('span').innerText.trim()).toEqual(
- this.propsData.username,
- );
+ expect(
+ this.userAvatarLink.$el.querySelector('.js-user-avatar-link-username').innerText.trim(),
+ ).toEqual(this.propsData.username);
});
it('should render text tooltip for <span>', function() {
- expect(this.userAvatarLink.$el.querySelector('span').dataset.originalTitle).toEqual(
- this.propsData.tooltipText,
- );
+ expect(
+ this.userAvatarLink.$el.querySelector('.js-user-avatar-link-username').dataset
+ .originalTitle,
+ ).toEqual(this.propsData.tooltipText);
});
it('should render text tooltip placement for <span>', function() {
expect(
- this.userAvatarLink.$el.querySelector('span').getAttribute('tooltip-placement'),
+ this.userAvatarLink.$el
+ .querySelector('.js-user-avatar-link-username')
+ .getAttribute('tooltip-placement'),
).toEqual(this.propsData.tooltipPlacement);
});
});
diff --git a/spec/lib/bitbucket_server/client_spec.rb b/spec/lib/bitbucket_server/client_spec.rb
index f926ae963a4..5de0a9a65b5 100644
--- a/spec/lib/bitbucket_server/client_spec.rb
+++ b/spec/lib/bitbucket_server/client_spec.rb
@@ -13,7 +13,7 @@ describe BitbucketServer::Client do
let(:path) { "/projects/#{project}/repos/#{repo_slug}/pull-requests?state=ALL" }
it 'requests a collection' do
- expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :pull_request)
+ expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :pull_request, page_offset: 0, limit: nil)
subject.pull_requests(project, repo_slug)
end
@@ -29,7 +29,7 @@ describe BitbucketServer::Client do
let(:path) { "/projects/#{project}/repos/#{repo_slug}/pull-requests/1/activities" }
it 'requests a collection' do
- expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :activity)
+ expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :activity, page_offset: 0, limit: nil)
subject.activities(project, repo_slug, 1)
end
@@ -52,10 +52,16 @@ describe BitbucketServer::Client do
let(:path) { "/repos" }
it 'requests a collection' do
- expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :repo)
+ expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :repo, page_offset: 0, limit: nil)
subject.repos
end
+
+ it 'requests a collection with an offset and limit' do
+ expect(BitbucketServer::Paginator).to receive(:new).with(anything, path, :repo, page_offset: 10, limit: 25)
+
+ subject.repos(page_offset: 10, limit: 25)
+ end
end
describe '#create_branch' do
diff --git a/spec/lib/bitbucket_server/collection_spec.rb b/spec/lib/bitbucket_server/collection_spec.rb
new file mode 100644
index 00000000000..ddd02bac88a
--- /dev/null
+++ b/spec/lib/bitbucket_server/collection_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe BitbucketServer::Collection do
+ let(:connection) { instance_double(BitbucketServer::Connection) }
+ let(:page) { 1 }
+ let(:paginator) { BitbucketServer::Paginator.new(connection, 'http://more-data', :pull_request, page_offset: page) }
+
+ subject { described_class.new(paginator) }
+
+ describe '#current_page' do
+ it 'returns 1' do
+ expect(subject.current_page).to eq(1)
+ end
+ end
+
+ describe '#prev_page' do
+ it 'returns nil' do
+ expect(subject.prev_page).to be_nil
+ end
+ end
+
+ describe '#next_page' do
+ it 'returns 2' do
+ expect(subject.next_page).to eq(2)
+ end
+ end
+end
diff --git a/spec/lib/bitbucket_server/paginator_spec.rb b/spec/lib/bitbucket_server/paginator_spec.rb
index 2de50eba3c4..d268d4f23cf 100644
--- a/spec/lib/bitbucket_server/paginator_spec.rb
+++ b/spec/lib/bitbucket_server/paginator_spec.rb
@@ -20,6 +20,16 @@ describe BitbucketServer::Paginator do
expect { paginator.items }.to raise_error(StopIteration)
end
+ it 'obeys limits' do
+ limited = described_class.new(connection, 'http://more-data', :pull_request, page_offset: 0, limit: 1)
+ allow(limited).to receive(:fetch_next_page).and_return(first_page)
+
+ expect(limited.has_next_page?).to be_truthy
+ expect(limited.items).to match(['item_1'])
+ expect(limited.has_next_page?).to be_truthy
+ expect { limited.items }.to raise_error(StopIteration)
+ end
+
it 'calls the connection with different offsets' do
expect(connection).to receive(:get).with('http://more-data', start: 0, limit: BitbucketServer::Paginator::PAGE_LENGTH).and_return(page_attrs)
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index f1a2946acda..ac9b0c674a5 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -10,7 +10,7 @@ describe Gitlab::Ci::Config::Entry::Job do
let(:result) do
%i[before_script script stage type after_script cache
image services only except variables artifacts
- environment coverage]
+ environment coverage retry]
end
it { is_expected.to match_array result }
@@ -98,45 +98,6 @@ describe Gitlab::Ci::Config::Entry::Job do
end
end
- context 'when retry value is not correct' do
- context 'when it is not a numeric value' do
- let(:config) { { retry: true } }
-
- it 'returns error about invalid type' do
- expect(entry).not_to be_valid
- expect(entry.errors).to include 'job retry is not a number'
- end
- end
-
- context 'when it is lower than zero' do
- let(:config) { { retry: -1 } }
-
- it 'returns error about value too low' do
- expect(entry).not_to be_valid
- expect(entry.errors)
- .to include 'job retry must be greater than or equal to 0'
- end
- end
-
- context 'when it is not an integer' do
- let(:config) { { retry: 1.5 } }
-
- it 'returns error about wrong value' do
- expect(entry).not_to be_valid
- expect(entry.errors).to include 'job retry must be an integer'
- end
- end
-
- context 'when the value is too high' do
- let(:config) { { retry: 10 } }
-
- it 'returns error about value too high' do
- expect(entry).not_to be_valid
- expect(entry.errors).to include 'job retry must be less than or equal to 2'
- end
- end
- end
-
context 'when parallel value is not correct' do
context 'when it is not a numeric value' do
let(:config) { { parallel: true } }
diff --git a/spec/lib/gitlab/ci/config/entry/retry_spec.rb b/spec/lib/gitlab/ci/config/entry/retry_spec.rb
new file mode 100644
index 00000000000..164a9ed4c3d
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/retry_spec.rb
@@ -0,0 +1,236 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Retry do
+ let(:entry) { described_class.new(config) }
+
+ shared_context 'when retry value is a numeric', :numeric do
+ let(:config) { max }
+ let(:max) {}
+ end
+
+ shared_context 'when retry value is a hash', :hash do
+ let(:config) { { max: max, when: public_send(:when) }.compact }
+ let(:when) {}
+ let(:max) {}
+ end
+
+ describe '#value' do
+ subject(:value) { entry.value }
+
+ context 'when retry value is a numeric', :numeric do
+ let(:max) { 2 }
+
+ it 'is returned as a hash with max key' do
+ expect(value).to eq(max: 2)
+ end
+ end
+
+ context 'when retry value is a hash', :hash do
+ context 'and `when` is a string' do
+ let(:when) { 'unknown_failure' }
+
+ it 'returns when wrapped in an array' do
+ expect(value).to eq(when: ['unknown_failure'])
+ end
+ end
+
+ context 'and `when` is an array' do
+ let(:when) { %w[unknown_failure runner_system_failure] }
+
+ it 'returns when as it was passed' do
+ expect(value).to eq(when: %w[unknown_failure runner_system_failure])
+ end
+ end
+ end
+ end
+
+ describe 'validation' do
+ context 'when retry value is correct' do
+ context 'when it is a numeric', :numeric do
+ let(:max) { 2 }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'when it is a hash', :hash do
+ context 'with max' do
+ let(:max) { 2 }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'with string when' do
+ let(:when) { 'unknown_failure' }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'with string when always' do
+ let(:when) { 'always' }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'with array when' do
+ let(:when) { %w[unknown_failure runner_system_failure] }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ # Those values are documented at `doc/ci/yaml/README.md`. If any of
+ # those values gets invalid, documentation must be updated. To make
+ # sure this is catched, check explicitly that all of the documented
+ # values are valid. If they are not it means the documentation and this
+ # array must be updated.
+ RETRY_WHEN_IN_DOCUMENTATION = %w[
+ always
+ unknown_failure
+ script_failure
+ api_failure
+ stuck_or_timeout_failure
+ runner_system_failure
+ missing_dependency_failure
+ runner_unsupported
+ ].freeze
+
+ RETRY_WHEN_IN_DOCUMENTATION.each do |reason|
+ context "with when from documentation `#{reason}`" do
+ let(:when) { reason }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ ::Ci::Build.failure_reasons.each_key do |reason|
+ context "with when from CommitStatus.failure_reasons `#{reason}`" do
+ let(:when) { reason }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+ end
+ end
+
+ context 'when retry value is not correct' do
+ context 'when it is not a numeric nor an array' do
+ let(:config) { true }
+
+ it 'returns error about invalid type' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry config has to be either an integer or a hash'
+ end
+ end
+
+ context 'when it is a numeric', :numeric do
+ context 'when it is lower than zero' do
+ let(:max) { -1 }
+
+ it 'returns error about value too low' do
+ expect(entry).not_to be_valid
+ expect(entry.errors)
+ .to include 'retry config must be greater than or equal to 0'
+ end
+ end
+
+ context 'when it is not an integer' do
+ let(:max) { 1.5 }
+
+ it 'returns error about wrong value' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry config has to be either an integer or a hash'
+ end
+ end
+
+ context 'when the value is too high' do
+ let(:max) { 10 }
+
+ it 'returns error about value too high' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry config must be less than or equal to 2'
+ end
+ end
+ end
+
+ context 'when it is a hash', :hash do
+ context 'with unknown keys' do
+ let(:config) { { max: 2, unknown_key: :something, one_more: :key } }
+
+ it 'returns error about the unknown key' do
+ expect(entry).not_to be_valid
+ expect(entry.errors)
+ .to include 'retry config contains unknown keys: unknown_key, one_more'
+ end
+ end
+
+ context 'with max lower than zero' do
+ let(:max) { -1 }
+
+ it 'returns error about value too low' do
+ expect(entry).not_to be_valid
+ expect(entry.errors)
+ .to include 'retry max must be greater than or equal to 0'
+ end
+ end
+
+ context 'with max not an integer' do
+ let(:max) { 1.5 }
+
+ it 'returns error about wrong value' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry max must be an integer'
+ end
+ end
+
+ context 'iwth max too high' do
+ let(:max) { 10 }
+
+ it 'returns error about value too high' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry max must be less than or equal to 2'
+ end
+ end
+
+ context 'with when in wrong format' do
+ let(:when) { true }
+
+ it 'returns error about the wrong format' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry when should be an array of strings or a string'
+ end
+ end
+
+ context 'with an unknown when string' do
+ let(:when) { 'unknown_reason' }
+
+ it 'returns error about the wrong format' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry when is not included in the list'
+ end
+ end
+
+ context 'with an unknown failure reason in a when array' do
+ let(:when) { %w[unknown_reason runner_system_failure] }
+
+ it 'returns error about the wrong format' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry when contains unknown values: unknown_reason'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index dcfd54107a3..441e8214181 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -53,11 +53,11 @@ module Gitlab
describe 'retry entry' do
context 'when retry count is specified' do
let(:config) do
- YAML.dump(rspec: { script: 'rspec', retry: 1 })
+ YAML.dump(rspec: { script: 'rspec', retry: { max: 1 } })
end
it 'includes retry count in build options attribute' do
- expect(subject[:options]).to include(retry: 1)
+ expect(subject[:options]).to include(retry: { max: 1 })
end
end
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 3417896e259..2f51642b58e 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -41,52 +41,6 @@ describe Gitlab::Diff::File do
end
end
- describe '#unfold_diff_lines' do
- let(:unfolded_lines) { double('expanded-lines') }
- let(:unfolder) { instance_double(Gitlab::Diff::LinesUnfolder) }
- let(:position) { instance_double(Gitlab::Diff::Position, old_line: 10) }
-
- before do
- allow(Gitlab::Diff::LinesUnfolder).to receive(:new) { unfolder }
- end
-
- context 'when unfold required' do
- before do
- allow(unfolder).to receive(:unfold_required?) { true }
- allow(unfolder).to receive(:unfolded_diff_lines) { unfolded_lines }
- end
-
- it 'changes @unfolded to true' do
- diff_file.unfold_diff_lines(position)
-
- expect(diff_file).to be_unfolded
- end
-
- it 'updates @diff_lines' do
- diff_file.unfold_diff_lines(position)
-
- expect(diff_file.diff_lines).to eq(unfolded_lines)
- end
- end
-
- context 'when unfold not required' do
- before do
- allow(unfolder).to receive(:unfold_required?) { false }
- end
-
- it 'keeps @unfolded false' do
- diff_file.unfold_diff_lines(position)
-
- expect(diff_file).not_to be_unfolded
- end
-
- it 'does not update @diff_lines' do
- expect { diff_file.unfold_diff_lines(position) }
- .not_to change(diff_file, :diff_lines)
- end
- end
- end
-
describe '#mode_changed?' do
it { expect(diff_file.mode_changed?).to be_falsey }
end
diff --git a/spec/lib/gitlab/diff/lines_unfolder_spec.rb b/spec/lib/gitlab/diff/lines_unfolder_spec.rb
deleted file mode 100644
index 8e00c8e0e30..00000000000
--- a/spec/lib/gitlab/diff/lines_unfolder_spec.rb
+++ /dev/null
@@ -1,750 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::Diff::LinesUnfolder do
- let(:raw_diff) do
- <<-DIFF.strip_heredoc
- @@ -7,9 +7,6 @@
- "tags": ["devel", "development", "nightly"],
- "desktop-file-name-prefix": "(Development) ",
- "finish-args": [
- - "--share=ipc", "--socket=x11",
- - "--socket=wayland",
- - "--talk-name=org.gnome.OnlineAccounts",
- "--talk-name=org.freedesktop.Tracker1",
- "--filesystem=home",
- "--talk-name=org.gtk.vfs", "--talk-name=org.gtk.vfs.*",
- @@ -62,7 +59,7 @@
- },
- {
- "name": "gnome-desktop",
- - "config-opts": ["--disable-debug-tools", "--disable-udev"],
- + "config-opts": ["--disable-debug-tools", "--disable-"],
- "sources": [
- {
- "type": "git",
- @@ -83,11 +80,6 @@
- "buildsystem": "meson",
- "builddir": true,
- "name": "nautilus",
- - "config-opts": [
- - "-Denable-desktop=false",
- - "-Denable-selinux=false",
- - "--libdir=/app/lib"
- - ],
- "sources": [
- {
- "type": "git",
- DIFF
- end
-
- let(:raw_old_blob) do
- <<-BLOB.strip_heredoc
- {
- "app-id": "org.gnome.Nautilus",
- "runtime": "org.gnome.Platform",
- "runtime-version": "master",
- "sdk": "org.gnome.Sdk",
- "command": "nautilus",
- "tags": ["devel", "development", "nightly"],
- "desktop-file-name-prefix": "(Development) ",
- "finish-args": [
- "--share=ipc", "--socket=x11",
- "--socket=wayland",
- "--talk-name=org.gnome.OnlineAccounts",
- "--talk-name=org.freedesktop.Tracker1",
- "--filesystem=home",
- "--talk-name=org.gtk.vfs", "--talk-name=org.gtk.vfs.*",
- "--filesystem=xdg-run/dconf", "--filesystem=~/.config/dconf:ro",
- "--talk-name=ca.desrt.dconf", "--env=DCONF_USER_CONFIG_DIR=.config/dconf"
- ],
- "cleanup": [ "/include", "/share/bash-completion" ],
- "modules": [
- {
- "name": "exiv2",
- "sources": [
- {
- "type": "archive",
- "url": "http://exiv2.org/builds/exiv2-0.26-trunk.tar.gz",
- "sha256": "c75e3c4a0811bf700d92c82319373b7a825a2331c12b8b37d41eb58e4f18eafb"
- },
- {
- "type": "shell",
- "commands": [
- "cp -f /usr/share/gnu-config/config.sub ./config/",
- "cp -f /usr/share/gnu-config/config.guess ./config/"
- ]
- }
- ]
- },
- {
- "name": "gexiv2",
- "config-opts": [ "--disable-introspection" ],
- "sources": [
- {
- "type": "git",
- "url": "https://git.gnome.org/browse/gexiv2"
- }
- ]
- },
- {
- "name": "tracker",
- "cleanup": [ "/bin", "/etc", "/libexec" ],
- "config-opts": [ "--disable-miner-apps", "--disable-static",
- "--disable-tracker-extract", "--disable-tracker-needle",
- "--disable-tracker-preferences", "--disable-artwork",
- "--disable-tracker-writeback", "--disable-miner-user-guides",
- "--with-bash-completion-dir=no" ],
- "sources": [
- {
- "type": "git",
- "url": "https://git.gnome.org/browse/tracker"
- }
- ]
- },
- {
- "name": "gnome-desktop",
- "config-opts": ["--disable-debug-tools", "--disable-udev"],
- "sources": [
- {
- "type": "git",
- "url": "https://git.gnome.org/browse/gnome-desktop"
- }
- ]
- },
- {
- "name": "gnome-autoar",
- "sources": [
- {
- "type": "git",
- "url": "https://git.gnome.org/browse/gnome-autoar"
- }
- ]
- },
- {
- "buildsystem": "meson",
- "builddir": true,
- "name": "nautilus",
- "config-opts": [
- "-Denable-desktop=false",
- "-Denable-selinux=false",
- "--libdir=/app/lib"
- ],
- "sources": [
- {
- "type": "git",
- "url": "https://gitlab.gnome.org/GNOME/nautilus.git"
- }
- ]
- }
- ]
- },
- {
- "app-id": "foo",
- "runtime": "foo",
- "runtime-version": "foo",
- "sdk": "foo",
- "command": "foo",
- "tags": ["foo", "bar", "kux"],
- "desktop-file-name-prefix": "(Foo) ",
- {
- "buildsystem": "meson",
- "builddir": true,
- "name": "nautilus",
- "sources": [
- {
- "type": "git",
- "url": "https://gitlab.gnome.org/GNOME/nautilus.git"
- }
- ]
- }
- },
- {
- "app-id": "foo",
- "runtime": "foo",
- "runtime-version": "foo",
- "sdk": "foo",
- "command": "foo",
- "tags": ["foo", "bar", "kux"],
- "desktop-file-name-prefix": "(Foo) ",
- {
- "buildsystem": "meson",
- "builddir": true,
- "name": "nautilus",
- "sources": [
- {
- "type": "git",
- "url": "https://gitlab.gnome.org/GNOME/nautilus.git"
- }
- ]
- }
- }
- BLOB
- end
-
- let(:project) { create(:project) }
-
- let(:old_blob) { Gitlab::Git::Blob.new(data: raw_old_blob) }
-
- let(:diff) do
- Gitlab::Git::Diff.new(diff: raw_diff,
- new_path: "build-aux/flatpak/org.gnome.Nautilus.json",
- old_path: "build-aux/flatpak/org.gnome.Nautilus.json",
- a_mode: "100644",
- b_mode: "100644",
- new_file: false,
- renamed_file: false,
- deleted_file: false,
- too_large: false)
- end
-
- let(:diff_file) do
- Gitlab::Diff::File.new(diff, repository: project.repository)
- end
-
- before do
- allow(old_blob).to receive(:load_all_data!)
- allow(diff_file).to receive(:old_blob) { old_blob }
- end
-
- subject { described_class.new(diff_file, position) }
-
- context 'position requires a middle expansion and new match lines' do
- let(:position) do
- Gitlab::Diff::Position.new(base_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
- start_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
- head_sha: "1487062132228de836236c522fe52fed4980a46c",
- old_path: "build-aux/flatpak/org.gnome.Nautilus.json",
- new_path: "build-aux/flatpak/org.gnome.Nautilus.json",
- position_type: "text",
- old_line: 43,
- new_line: 40)
- end
-
- context 'blob lines' do
- let(:expected_blob_lines) do
- [[40, 40, " \"config-opts\": [ \"--disable-introspection\" ],"],
- [41, 41, " \"sources\": ["],
- [42, 42, " {"],
- [43, 43, " \"type\": \"git\","],
- [44, 44, " \"url\": \"https://git.gnome.org/browse/gexiv2\""],
- [45, 45, " }"],
- [46, 46, " ]"]]
- end
-
- it 'returns the extracted blob lines correctly' do
- extracted_lines = subject.blob_lines
-
- expect(extracted_lines.size).to eq(7)
-
- extracted_lines.each_with_index do |line, i|
- expect([line.old_line, line.new_line, line.text]).to eq(expected_blob_lines[i])
- end
- end
- end
-
- context 'diff lines' do
- let(:expected_diff_lines) do
- [[7, 7, "@@ -7,9 +7,6 @@"],
- [7, 7, " \"tags\": [\"devel\", \"development\", \"nightly\"],"],
- [8, 8, " \"desktop-file-name-prefix\": \"(Development) \","],
- [9, 9, " \"finish-args\": ["],
- [10, 10, "- \"--share=ipc\", \"--socket=x11\","],
- [11, 10, "- \"--socket=wayland\","],
- [12, 10, "- \"--talk-name=org.gnome.OnlineAccounts\","],
- [13, 10, " \"--talk-name=org.freedesktop.Tracker1\","],
- [14, 11, " \"--filesystem=home\","],
- [15, 12, " \"--talk-name=org.gtk.vfs\", \"--talk-name=org.gtk.vfs.*\","],
-
- # New match line
- [40, 37, "@@ -40,7+37,7 @@"],
-
- # Injected blob lines
- [40, 37, " \"config-opts\": [ \"--disable-introspection\" ],"],
- [41, 38, " \"sources\": ["],
- [42, 39, " {"],
- [43, 40, " \"type\": \"git\","], # comment
- [44, 41, " \"url\": \"https://git.gnome.org/browse/gexiv2\""],
- [45, 42, " }"],
- [46, 43, " ]"],
- # end
-
- # Second match line
- [62, 59, "@@ -62,7+59,7 @@"],
-
- [62, 59, " },"],
- [63, 60, " {"],
- [64, 61, " \"name\": \"gnome-desktop\","],
- [65, 62, "- \"config-opts\": [\"--disable-debug-tools\", \"--disable-udev\"],"],
- [66, 62, "+ \"config-opts\": [\"--disable-debug-tools\", \"--disable-\"],"],
- [66, 63, " \"sources\": ["],
- [67, 64, " {"],
- [68, 65, " \"type\": \"git\","],
- [83, 80, "@@ -83,11 +80,6 @@"],
- [83, 80, " \"buildsystem\": \"meson\","],
- [84, 81, " \"builddir\": true,"],
- [85, 82, " \"name\": \"nautilus\","],
- [86, 83, "- \"config-opts\": ["],
- [87, 83, "- \"-Denable-desktop=false\","],
- [88, 83, "- \"-Denable-selinux=false\","],
- [89, 83, "- \"--libdir=/app/lib\""],
- [90, 83, "- ],"],
- [91, 83, " \"sources\": ["],
- [92, 84, " {"],
- [93, 85, " \"type\": \"git\","]]
- end
-
- it 'return merge of blob lines with diff lines correctly' do
- new_diff_lines = subject.unfolded_diff_lines
-
- expected_diff_lines.each_with_index do |expected_line, i|
- line = new_diff_lines[i]
-
- expect([line.old_pos, line.new_pos, line.text])
- .to eq([expected_line[0], expected_line[1], expected_line[2]])
- end
- end
-
- it 'merged lines have correct line codes' do
- new_diff_lines = subject.unfolded_diff_lines
-
- new_diff_lines.each_with_index do |line, i|
- old_pos, new_pos = expected_diff_lines[i][0], expected_diff_lines[i][1]
-
- unless line.type == 'match'
- expect(line.line_code).to eq(Gitlab::Git.diff_line_code(diff_file.file_path, new_pos, old_pos))
- end
- end
- end
- end
- end
-
- context 'position requires a middle expansion and no top match line' do
- let(:position) do
- Gitlab::Diff::Position.new(base_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
- start_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
- head_sha: "1487062132228de836236c522fe52fed4980a46c",
- old_path: "build-aux/flatpak/org.gnome.Nautilus.json",
- new_path: "build-aux/flatpak/org.gnome.Nautilus.json",
- position_type: "text",
- old_line: 16,
- new_line: 17)
- end
-
- context 'blob lines' do
- let(:expected_blob_lines) do
- [[16, 16, " \"--filesystem=xdg-run/dconf\", \"--filesystem=~/.config/dconf:ro\","],
- [17, 17, " \"--talk-name=ca.desrt.dconf\", \"--env=DCONF_USER_CONFIG_DIR=.config/dconf\""],
- [18, 18, " ],"],
- [19, 19, " \"cleanup\": [ \"/include\", \"/share/bash-completion\" ],"]]
- end
-
- it 'returns the extracted blob lines correctly' do
- extracted_lines = subject.blob_lines
-
- expect(extracted_lines.size).to eq(4)
-
- extracted_lines.each_with_index do |line, i|
- expect([line.old_line, line.new_line, line.text]).to eq(expected_blob_lines[i])
- end
- end
- end
-
- context 'diff lines' do
- let(:expected_diff_lines) do
- [[7, 7, "@@ -7,9 +7,6 @@"],
- [7, 7, " \"tags\": [\"devel\", \"development\", \"nightly\"],"],
- [8, 8, " \"desktop-file-name-prefix\": \"(Development) \","],
- [9, 9, " \"finish-args\": ["],
- [10, 10, "- \"--share=ipc\", \"--socket=x11\","],
- [11, 10, "- \"--socket=wayland\","],
- [12, 10, "- \"--talk-name=org.gnome.OnlineAccounts\","],
- [13, 10, " \"--talk-name=org.freedesktop.Tracker1\","],
- [14, 11, " \"--filesystem=home\","],
- [15, 12, " \"--talk-name=org.gtk.vfs\", \"--talk-name=org.gtk.vfs.*\","],
- # No new match needed
-
- # Injected blob lines
- [16, 13, " \"--filesystem=xdg-run/dconf\", \"--filesystem=~/.config/dconf:ro\","],
- [17, 14, " \"--talk-name=ca.desrt.dconf\", \"--env=DCONF_USER_CONFIG_DIR=.config/dconf\""],
- [18, 15, " ],"],
- [19, 16, " \"cleanup\": [ \"/include\", \"/share/bash-completion\" ],"],
- # end
-
- # Second match line
- [62, 59, "@@ -62,4+59,4 @@"],
-
- [62, 59, " },"],
- [63, 60, " {"],
- [64, 61, " \"name\": \"gnome-desktop\","],
- [65, 62, "- \"config-opts\": [\"--disable-debug-tools\", \"--disable-udev\"],"],
- [66, 62, "+ \"config-opts\": [\"--disable-debug-tools\", \"--disable-\"],"],
- [66, 63, " \"sources\": ["],
- [67, 64, " {"],
- [68, 65, " \"type\": \"git\","],
- [83, 80, "@@ -83,11 +80,6 @@"],
- [83, 80, " \"buildsystem\": \"meson\","],
- [84, 81, " \"builddir\": true,"],
- [85, 82, " \"name\": \"nautilus\","],
- [86, 83, "- \"config-opts\": ["],
- [87, 83, "- \"-Denable-desktop=false\","],
- [88, 83, "- \"-Denable-selinux=false\","],
- [89, 83, "- \"--libdir=/app/lib\""],
- [90, 83, "- ],"],
- [91, 83, " \"sources\": ["],
- [92, 84, " {"],
- [93, 85, " \"type\": \"git\","]]
- end
-
- it 'return merge of blob lines with diff lines correctly' do
- new_diff_lines = subject.unfolded_diff_lines
-
- expected_diff_lines.each_with_index do |expected_line, i|
- line = new_diff_lines[i]
-
- expect([line.old_pos, line.new_pos, line.text])
- .to eq([expected_line[0], expected_line[1], expected_line[2]])
- end
- end
-
- it 'merged lines have correct line codes' do
- new_diff_lines = subject.unfolded_diff_lines
-
- new_diff_lines.each_with_index do |line, i|
- old_pos, new_pos = expected_diff_lines[i][0], expected_diff_lines[i][1]
-
- unless line.type == 'match'
- expect(line.line_code).to eq(Gitlab::Git.diff_line_code(diff_file.file_path, new_pos, old_pos))
- end
- end
- end
- end
- end
-
- context 'position requires a middle expansion and no bottom match line' do
- let(:position) do
- Gitlab::Diff::Position.new(base_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
- start_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
- head_sha: "1487062132228de836236c522fe52fed4980a46c",
- old_path: "build-aux/flatpak/org.gnome.Nautilus.json",
- new_path: "build-aux/flatpak/org.gnome.Nautilus.json",
- position_type: "text",
- old_line: 82,
- new_line: 79)
- end
-
- context 'blob lines' do
- let(:expected_blob_lines) do
- [[79, 79, " }"],
- [80, 80, " ]"],
- [81, 81, " },"],
- [82, 82, " {"]]
- end
-
- it 'returns the extracted blob lines correctly' do
- extracted_lines = subject.blob_lines
-
- expect(extracted_lines.size).to eq(4)
-
- extracted_lines.each_with_index do |line, i|
- expect([line.old_line, line.new_line, line.text]).to eq(expected_blob_lines[i])
- end
- end
- end
-
- context 'diff lines' do
- let(:expected_diff_lines) do
- [[7, 7, "@@ -7,9 +7,6 @@"],
- [7, 7, " \"tags\": [\"devel\", \"development\", \"nightly\"],"],
- [8, 8, " \"desktop-file-name-prefix\": \"(Development) \","],
- [9, 9, " \"finish-args\": ["],
- [10, 10, "- \"--share=ipc\", \"--socket=x11\","],
- [11, 10, "- \"--socket=wayland\","],
- [12, 10, "- \"--talk-name=org.gnome.OnlineAccounts\","],
- [13, 10, " \"--talk-name=org.freedesktop.Tracker1\","],
- [14, 11, " \"--filesystem=home\","],
- [15, 12, " \"--talk-name=org.gtk.vfs\", \"--talk-name=org.gtk.vfs.*\","],
- [62, 59, "@@ -62,7 +59,7 @@"],
- [62, 59, " },"],
- [63, 60, " {"],
- [64, 61, " \"name\": \"gnome-desktop\","],
- [65, 62, "- \"config-opts\": [\"--disable-debug-tools\", \"--disable-udev\"],"],
- [66, 62, "+ \"config-opts\": [\"--disable-debug-tools\", \"--disable-\"],"],
- [66, 63, " \"sources\": ["],
- [67, 64, " {"],
- [68, 65, " \"type\": \"git\","],
-
- # New top match line
- [79, 76, "@@ -79,4+76,4 @@"],
-
- # Injected blob lines
- [79, 76, " }"],
- [80, 77, " ]"],
- [81, 78, " },"],
- [82, 79, " {"],
- # end
-
- # No new second match line
- [83, 80, " \"buildsystem\": \"meson\","],
- [84, 81, " \"builddir\": true,"],
- [85, 82, " \"name\": \"nautilus\","],
- [86, 83, "- \"config-opts\": ["],
- [87, 83, "- \"-Denable-desktop=false\","],
- [88, 83, "- \"-Denable-selinux=false\","],
- [89, 83, "- \"--libdir=/app/lib\""],
- [90, 83, "- ],"],
- [91, 83, " \"sources\": ["],
- [92, 84, " {"],
- [93, 85, " \"type\": \"git\","]]
- end
-
- it 'return merge of blob lines with diff lines correctly' do
- new_diff_lines = subject.unfolded_diff_lines
-
- expected_diff_lines.each_with_index do |expected_line, i|
- line = new_diff_lines[i]
-
- expect([line.old_pos, line.new_pos, line.text])
- .to eq([expected_line[0], expected_line[1], expected_line[2]])
- end
- end
-
- it 'merged lines have correct line codes' do
- new_diff_lines = subject.unfolded_diff_lines
-
- new_diff_lines.each_with_index do |line, i|
- old_pos, new_pos = expected_diff_lines[i][0], expected_diff_lines[i][1]
-
- unless line.type == 'match'
- expect(line.line_code).to eq(Gitlab::Git.diff_line_code(diff_file.file_path, new_pos, old_pos))
- end
- end
- end
- end
- end
-
- context 'position requires a short top expansion' do
- let(:position) do
- Gitlab::Diff::Position.new(base_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
- start_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
- head_sha: "1487062132228de836236c522fe52fed4980a46c",
- old_path: "build-aux/flatpak/org.gnome.Nautilus.json",
- new_path: "build-aux/flatpak/org.gnome.Nautilus.json",
- position_type: "text",
- old_line: 6,
- new_line: 6)
- end
-
- context 'blob lines' do
- let(:expected_blob_lines) do
- [[3, 3, " \"runtime\": \"org.gnome.Platform\","],
- [4, 4, " \"runtime-version\": \"master\","],
- [5, 5, " \"sdk\": \"org.gnome.Sdk\","],
- [6, 6, " \"command\": \"nautilus\","]]
- end
-
- it 'returns the extracted blob lines correctly' do
- extracted_lines = subject.blob_lines
-
- expect(extracted_lines.size).to eq(4)
-
- extracted_lines.each_with_index do |line, i|
- expect([line.old_line, line.new_line, line.text]).to eq(expected_blob_lines[i])
- end
- end
- end
-
- context 'diff lines' do
- let(:expected_diff_lines) do
- # New match line
- [[3, 3, "@@ -3,4+3,4 @@"],
-
- # Injected blob lines
- [3, 3, " \"runtime\": \"org.gnome.Platform\","],
- [4, 4, " \"runtime-version\": \"master\","],
- [5, 5, " \"sdk\": \"org.gnome.Sdk\","],
- [6, 6, " \"command\": \"nautilus\","],
- # end
- [7, 7, " \"tags\": [\"devel\", \"development\", \"nightly\"],"],
- [8, 8, " \"desktop-file-name-prefix\": \"(Development) \","],
- [9, 9, " \"finish-args\": ["],
- [10, 10, "- \"--share=ipc\", \"--socket=x11\","],
- [11, 10, "- \"--socket=wayland\","],
- [12, 10, "- \"--talk-name=org.gnome.OnlineAccounts\","],
- [13, 10, " \"--talk-name=org.freedesktop.Tracker1\","],
- [14, 11, " \"--filesystem=home\","],
- [15, 12, " \"--talk-name=org.gtk.vfs\", \"--talk-name=org.gtk.vfs.*\","],
- [62, 59, "@@ -62,7 +59,7 @@"],
- [62, 59, " },"],
- [63, 60, " {"],
- [64, 61, " \"name\": \"gnome-desktop\","],
- [65, 62, "- \"config-opts\": [\"--disable-debug-tools\", \"--disable-udev\"],"],
- [66, 62, "+ \"config-opts\": [\"--disable-debug-tools\", \"--disable-\"],"],
- [66, 63, " \"sources\": ["],
- [67, 64, " {"],
- [68, 65, " \"type\": \"git\","],
- [83, 80, "@@ -83,11 +80,6 @@"],
- [83, 80, " \"buildsystem\": \"meson\","],
- [84, 81, " \"builddir\": true,"],
- [85, 82, " \"name\": \"nautilus\","],
- [86, 83, "- \"config-opts\": ["],
- [87, 83, "- \"-Denable-desktop=false\","],
- [88, 83, "- \"-Denable-selinux=false\","],
- [89, 83, "- \"--libdir=/app/lib\""],
- [90, 83, "- ],"],
- [91, 83, " \"sources\": ["],
- [92, 84, " {"],
- [93, 85, " \"type\": \"git\","]]
- end
-
- it 'return merge of blob lines with diff lines correctly' do
- new_diff_lines = subject.unfolded_diff_lines
-
- expected_diff_lines.each_with_index do |expected_line, i|
- line = new_diff_lines[i]
-
- expect([line.old_pos, line.new_pos, line.text])
- .to eq([expected_line[0], expected_line[1], expected_line[2]])
- end
- end
-
- it 'merged lines have correct line codes' do
- new_diff_lines = subject.unfolded_diff_lines
-
- new_diff_lines.each_with_index do |line, i|
- old_pos, new_pos = expected_diff_lines[i][0], expected_diff_lines[i][1]
-
- unless line.type == 'match'
- expect(line.line_code).to eq(Gitlab::Git.diff_line_code(diff_file.file_path, new_pos, old_pos))
- end
- end
- end
- end
- end
-
- context 'position sits between two match lines (no expasion needed)' do
- let(:position) do
- Gitlab::Diff::Position.new(base_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
- start_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
- head_sha: "1487062132228de836236c522fe52fed4980a46c",
- old_path: "build-aux/flatpak/org.gnome.Nautilus.json",
- new_path: "build-aux/flatpak/org.gnome.Nautilus.json",
- position_type: "text",
- old_line: 64,
- new_line: 61)
- end
-
- context 'diff lines' do
- it 'returns nil' do
- expect(subject.unfolded_diff_lines).to be_nil
- end
- end
- end
-
- context 'position requires bottom expansion and new match lines' do
- let(:position) do
- Gitlab::Diff::Position.new(base_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
- start_sha: "1c59dfa64afbea8c721bb09a06a9d326c952ea19",
- head_sha: "1487062132228de836236c522fe52fed4980a46c",
- old_path: "build-aux/flatpak/org.gnome.Nautilus.json",
- new_path: "build-aux/flatpak/org.gnome.Nautilus.json",
- position_type: "text",
- old_line: 107,
- new_line: 99)
- end
-
- context 'blob lines' do
- let(:expected_blob_lines) do
- [[104, 104, " \"sdk\": \"foo\","],
- [105, 105, " \"command\": \"foo\","],
- [106, 106, " \"tags\": [\"foo\", \"bar\", \"kux\"],"],
- [107, 107, " \"desktop-file-name-prefix\": \"(Foo) \","],
- [108, 108, " {"],
- [109, 109, " \"buildsystem\": \"meson\","],
- [110, 110, " \"builddir\": true,"]]
- end
-
- it 'returns the extracted blob lines correctly' do
- extracted_lines = subject.blob_lines
-
- expect(extracted_lines.size).to eq(7)
-
- extracted_lines.each_with_index do |line, i|
- expect([line.old_line, line.new_line, line.text]).to eq(expected_blob_lines[i])
- end
- end
- end
-
- context 'diff lines' do
- let(:expected_diff_lines) do
- [[7, 7, "@@ -7,9 +7,6 @@"],
- [7, 7, " \"tags\": [\"devel\", \"development\", \"nightly\"],"],
- [8, 8, " \"desktop-file-name-prefix\": \"(Development) \","],
- [9, 9, " \"finish-args\": ["],
- [10, 10, "- \"--share=ipc\", \"--socket=x11\","],
- [11, 10, "- \"--socket=wayland\","],
- [12, 10, "- \"--talk-name=org.gnome.OnlineAccounts\","],
- [13, 10, " \"--talk-name=org.freedesktop.Tracker1\","],
- [14, 11, " \"--filesystem=home\","],
- [15, 12, " \"--talk-name=org.gtk.vfs\", \"--talk-name=org.gtk.vfs.*\","],
- [62, 59, "@@ -62,7 +59,7 @@"],
- [62, 59, " },"],
- [63, 60, " {"],
- [64, 61, " \"name\": \"gnome-desktop\","],
- [65, 62, "- \"config-opts\": [\"--disable-debug-tools\", \"--disable-udev\"],"],
- [66, 62, "+ \"config-opts\": [\"--disable-debug-tools\", \"--disable-\"],"],
- [66, 63, " \"sources\": ["],
- [67, 64, " {"],
- [68, 65, " \"type\": \"git\","],
- [83, 80, "@@ -83,11 +80,6 @@"],
- [83, 80, " \"buildsystem\": \"meson\","],
- [84, 81, " \"builddir\": true,"],
- [85, 82, " \"name\": \"nautilus\","],
- [86, 83, "- \"config-opts\": ["],
- [87, 83, "- \"-Denable-desktop=false\","],
- [88, 83, "- \"-Denable-selinux=false\","],
- [89, 83, "- \"--libdir=/app/lib\""],
- [90, 83, "- ],"],
- [91, 83, " \"sources\": ["],
- [92, 84, " {"],
- [93, 85, " \"type\": \"git\","],
- # New match line
- [104, 96, "@@ -104,7+96,7 @@"],
-
- # Injected blob lines
- [104, 96, " \"sdk\": \"foo\","],
- [105, 97, " \"command\": \"foo\","],
- [106, 98, " \"tags\": [\"foo\", \"bar\", \"kux\"],"],
- [107, 99, " \"desktop-file-name-prefix\": \"(Foo) \","],
- [108, 100, " {"],
- [109, 101, " \"buildsystem\": \"meson\","],
- [110, 102, " \"builddir\": true,"]]
- # end
- end
-
- it 'return merge of blob lines with diff lines correctly' do
- new_diff_lines = subject.unfolded_diff_lines
-
- expected_diff_lines.each_with_index do |expected_line, i|
- line = new_diff_lines[i]
-
- expect([line.old_pos, line.new_pos, line.text])
- .to eq([expected_line[0], expected_line[1], expected_line[2]])
- end
- end
-
- it 'merged lines have correct line codes' do
- new_diff_lines = subject.unfolded_diff_lines
-
- new_diff_lines.each_with_index do |line, i|
- old_pos, new_pos = expected_diff_lines[i][0], expected_diff_lines[i][1]
-
- unless line.type == 'match'
- expect(line.line_code).to eq(Gitlab::Git.diff_line_code(diff_file.file_path, new_pos, old_pos))
- end
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
index ace3104f36f..f276f1a8ddf 100644
--- a/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb
@@ -93,5 +93,74 @@ describe Gitlab::Email::Handler::CreateMergeRequestHandler do
end
end
end
+
+ context 'when the email contains patch attachments' do
+ let(:email_raw) { fixture_file("emails/valid_merge_request_with_patch.eml") }
+
+ it 'creates the source branch and applies the patches' do
+ receiver.execute
+
+ branch = project.repository.find_branch('new-branch-with-a-patch')
+
+ expect(branch).not_to be_nil
+ expect(branch.dereferenced_target.message).to include('A commit from a patch')
+ end
+
+ it 'creates the merge request' do
+ expect { receiver.execute }
+ .to change { project.merge_requests.where(source_branch: 'new-branch-with-a-patch').size }.by(1)
+ end
+
+ it 'does not mention the patches in the created merge request' do
+ receiver.execute
+
+ merge_request = project.merge_requests.find_by!(source_branch: 'new-branch-with-a-patch')
+
+ expect(merge_request.description).not_to include('0001-A-commit-from-a-patch.patch')
+ end
+
+ context 'when the patch could not be applied' do
+ let(:email_raw) { fixture_file("emails/merge_request_with_conflicting_patch.eml") }
+
+ it 'raises an error' do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidAttachment)
+ end
+ end
+
+ context 'when specifying the target branch using quick actions' do
+ let(:email_raw) { fixture_file('emails/merge_request_with_patch_and_target_branch.eml') }
+
+ it 'creates the merge request with the correct target branch' do
+ receiver.execute
+
+ merge_request = project.merge_requests.find_by!(source_branch: 'new-branch-with-a-patch')
+
+ expect(merge_request.target_branch).to eq('with-codeowners')
+ end
+
+ it 'based the merge request of the target_branch' do
+ receiver.execute
+
+ merge_request = project.merge_requests.find_by!(source_branch: 'new-branch-with-a-patch')
+
+ expect(merge_request.diff_base_commit).to eq(project.repository.commit('with-codeowners'))
+ end
+ end
+ end
+ end
+
+ describe '#patch_attachments' do
+ let(:email_raw) { fixture_file('emails/merge_request_multiple_patches.eml') }
+ let(:mail) { Mail::Message.new(email_raw) }
+ subject(:handler) { described_class.new(mail, mail_key) }
+
+ it 'orders attachments ending in `.patch` by name' do
+ expected_filenames = ["0001-A-commit-from-a-patch.patch",
+ "0002-This-does-not-apply-to-the-feature-branch.patch"]
+
+ attachments = handler.__send__(:patch_attachments).map(&:filename)
+
+ expect(attachments).to eq(expected_filenames)
+ end
end
end
diff --git a/spec/lib/gitlab/git/patches/collection_spec.rb b/spec/lib/gitlab/git/patches/collection_spec.rb
new file mode 100644
index 00000000000..080be141c59
--- /dev/null
+++ b/spec/lib/gitlab/git/patches/collection_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::Git::Patches::Collection do
+ let(:patches_folder) { Rails.root.join('spec/fixtures/patchfiles') }
+ let(:patch_content1) do
+ File.read(File.join(patches_folder, "0001-This-does-not-apply-to-the-feature-branch.patch"))
+ end
+ let(:patch_content2) do
+ File.read(File.join(patches_folder, "0001-A-commit-from-a-patch.patch"))
+ end
+
+ subject(:collection) { described_class.new([patch_content1, patch_content2]) }
+
+ describe '#size' do
+ it 'combines the size of the patches' do
+ expect(collection.size).to eq(549.bytes + 424.bytes)
+ end
+ end
+
+ describe '#valid_size?' do
+ it 'is not valid if the total size is bigger than 2MB' do
+ expect(collection).to receive(:size).and_return(2500.kilobytes)
+
+ expect(collection).not_to be_valid_size
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/patches/commit_patches_spec.rb b/spec/lib/gitlab/git/patches/commit_patches_spec.rb
new file mode 100644
index 00000000000..760112155ce
--- /dev/null
+++ b/spec/lib/gitlab/git/patches/commit_patches_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::Git::Patches::CommitPatches do
+ describe '#commit' do
+ let(:patches) do
+ patches_folder = Rails.root.join('spec/fixtures/patchfiles')
+ content_1 = File.read(File.join(patches_folder, "0001-This-does-not-apply-to-the-feature-branch.patch"))
+ content_2 = File.read(File.join(patches_folder, "0001-A-commit-from-a-patch.patch"))
+
+ Gitlab::Git::Patches::Collection.new([content_1, content_2])
+ end
+ let(:user) { build(:user) }
+ let(:branch_name) { 'branch-with-patches' }
+ let(:repository) { create(:project, :repository).repository }
+
+ subject(:commit_patches) do
+ described_class.new(user, repository, branch_name, patches)
+ end
+
+ it 'applies the patches' do
+ new_rev = commit_patches.commit
+
+ expect(repository.commit(new_rev)).not_to be_nil
+ end
+
+ it 'updates the branch cache' do
+ expect(repository).to receive(:after_create_branch)
+
+ commit_patches.commit
+ end
+
+ context 'when the repository does not exist' do
+ let(:repository) { create(:project).repository }
+
+ it 'raises the correct error' do
+ expect { commit_patches.commit }.to raise_error(Gitlab::Git::Repository::NoRepository)
+ end
+ end
+
+ context 'when the patch does not apply' do
+ let(:branch_name) { 'feature' }
+
+ it 'raises the correct error' do
+ expect { commit_patches.commit }.to raise_error(Gitlab::Git::CommandError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/patches/patch_spec.rb b/spec/lib/gitlab/git/patches/patch_spec.rb
new file mode 100644
index 00000000000..7466e853b65
--- /dev/null
+++ b/spec/lib/gitlab/git/patches/patch_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::Git::Patches::Patch do
+ let(:patches_folder) { Rails.root.join('spec/fixtures/patchfiles') }
+ let(:patch_content) do
+ File.read(File.join(patches_folder, "0001-This-does-not-apply-to-the-feature-branch.patch"))
+ end
+ let(:patch) { described_class.new(patch_content) }
+
+ describe '#size' do
+ it 'is correct' do
+ expect(patch.size).to eq(549.bytes)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index eaf64e3c9b4..b37fe2686b6 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -335,4 +335,37 @@ describe Gitlab::GitalyClient::OperationService do
end
end
end
+
+ describe '#user_commit_patches' do
+ let(:patches_folder) { Rails.root.join('spec/fixtures/patchfiles') }
+ let(:patch_content) do
+ patch_names.map { |name| File.read(File.join(patches_folder, name)) }.join("\n")
+ end
+ let(:patch_names) { %w(0001-This-does-not-apply-to-the-feature-branch.patch) }
+ let(:branch_name) { 'branch-with-patches' }
+
+ subject(:commit_patches) do
+ client.user_commit_patches(user, branch_name, patch_content)
+ end
+
+ it 'applies the patch correctly' do
+ branch_update = commit_patches
+
+ expect(branch_update).to be_branch_created
+
+ commit = repository.commit(branch_update.newrev)
+ expect(commit.author_email).to eq('patchuser@gitlab.org')
+ expect(commit.committer_email).to eq(user.email)
+ expect(commit.message.chomp).to eq('This does not apply to the `feature` branch')
+ end
+
+ context 'when the patch could not be applied' do
+ let(:patch_names) { %w(0001-This-does-not-apply-to-the-feature-branch.patch) }
+ let(:branch_name) { 'feature' }
+
+ it 'raises the correct error' do
+ expect { commit_patches }.to raise_error(GRPC::FailedPrecondition)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
index 65f073b2df3..87ab81d8169 100644
--- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -23,15 +23,23 @@ describe 'Import/Export attribute configuration' do
let(:safe_attributes_file) { 'spec/lib/gitlab/import_export/safe_model_attributes.yml' }
let(:safe_model_attributes) { YAML.load_file(safe_attributes_file) }
+ let(:ee_safe_attributes_file) { 'ee/spec/lib/gitlab/import_export/safe_model_attributes.yml' }
+ let(:ee_safe_model_attributes) { File.exist?(ee_safe_attributes_file) ? YAML.load_file(ee_safe_attributes_file) : {} }
+
it 'has no new columns' do
relation_names.each do |relation_name|
relation_class = relation_class_for_name(relation_name)
relation_attributes = relation_class.new.attributes.keys
- expect(safe_model_attributes[relation_class.to_s]).not_to be_nil, "Expected exported class #{relation_class} to exist in safe_model_attributes"
-
current_attributes = parsed_attributes(relation_name, relation_attributes)
- safe_attributes = safe_model_attributes[relation_class.to_s]
+ safe_attributes = safe_model_attributes[relation_class.to_s].dup || []
+
+ ee_safe_model_attributes[relation_class.to_s].to_a.each do |attribute|
+ safe_attributes << attribute
+ end
+
+ expect(safe_attributes).not_to be_nil, "Expected exported class #{relation_class} to exist in safe_model_attributes"
+
new_attributes = current_attributes - safe_attributes
expect(new_attributes).to be_empty, failure_message(relation_class.to_s, new_attributes)
@@ -43,6 +51,7 @@ describe 'Import/Export attribute configuration' do
It looks like #{relation_class}, which is exported using the project Import/Export, has new attributes: #{new_attributes.join(',')}
Please add the attribute(s) to SAFE_MODEL_ATTRIBUTES if you consider this can be exported.
+ #{"If the model/associations are EE-specific, use `#{File.expand_path(ee_safe_attributes_file)}`.\n" if ee_safe_model_attributes.any?}
Otherwise, please blacklist the attribute(s) in IMPORT_EXPORT_CONFIG by adding it to its correspondent
model in the +excluded_attributes+ section.
diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
index f28941ce58f..ed879350004 100644
--- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
@@ -5,6 +5,8 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
let(:repository) { 'https://repository.example.com' }
let(:rbac) { false }
let(:version) { '1.2.3' }
+ let(:preinstall) { nil }
+ let(:postinstall) { nil }
let(:install_command) do
described_class.new(
@@ -13,7 +15,9 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
rbac: rbac,
files: files,
version: version,
- repository: repository
+ repository: repository,
+ preinstall: preinstall,
+ postinstall: postinstall
)
end
@@ -24,6 +28,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
<<~EOS
helm init --client-only >/dev/null
helm repo add app-name https://repository.example.com
+ helm repo update >/dev/null
#{helm_install_comand}
EOS
end
@@ -51,6 +56,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
<<~EOS
helm init --client-only >/dev/null
helm repo add app-name https://repository.example.com
+ helm repo update >/dev/null
#{helm_install_command}
EOS
end
@@ -99,6 +105,53 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
end
end
+ context 'when there is a pre-install script' do
+ let(:preinstall) { ['/bin/date', '/bin/true'] }
+
+ it_behaves_like 'helm commands' do
+ let(:commands) do
+ <<~EOS
+ helm init --client-only >/dev/null
+ helm repo add app-name https://repository.example.com
+ helm repo update >/dev/null
+ #{helm_install_command}
+ EOS
+ end
+
+ let(:helm_install_command) do
+ <<~EOS.strip
+ /bin/date
+ /bin/true
+ helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null
+ EOS
+ end
+ end
+ end
+
+ context 'when there is a post-install script' do
+ let(:postinstall) { ['/bin/date', "/bin/false\n"] }
+
+ it_behaves_like 'helm commands' do
+ let(:commands) do
+ <<~EOS
+ helm init --client-only >/dev/null
+ helm repo add app-name https://repository.example.com
+ helm repo update >/dev/null
+ #{helm_install_command}
+ EOS
+ end
+
+ let(:helm_install_command) do
+ <<~EOS.strip
+ helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null
+
+ /bin/date
+ /bin/false
+ EOS
+ end
+ end
+ end
+
context 'when there is no ca.pem file' do
let(:files) { { 'file.txt': 'some content' } }
@@ -107,6 +160,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
<<~EOS
helm init --client-only >/dev/null
helm repo add app-name https://repository.example.com
+ helm repo update >/dev/null
#{helm_install_command}
EOS
end
@@ -131,6 +185,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
<<~EOS
helm init --client-only >/dev/null
helm repo add app-name https://repository.example.com
+ helm repo update >/dev/null
#{helm_install_command}
EOS
end
diff --git a/spec/lib/gitlab/quick_actions/extractor_spec.rb b/spec/lib/gitlab/quick_actions/extractor_spec.rb
index 0166f6c2ee0..873bb359d6e 100644
--- a/spec/lib/gitlab/quick_actions/extractor_spec.rb
+++ b/spec/lib/gitlab/quick_actions/extractor_spec.rb
@@ -272,5 +272,24 @@ describe Gitlab::QuickActions::Extractor do
expect(commands).to be_empty
expect(msg).to eq expected
end
+
+ it 'limits to passed commands when they are passed' do
+ msg = <<~MSG.strip
+ Hello, we should only extract the commands passed
+ /reopen
+ /labels hello world
+ /power
+ MSG
+ expected_msg = <<~EXPECTED.strip
+ Hello, we should only extract the commands passed
+ /power
+ EXPECTED
+ expected_commands = [['reopen'], ['labels', 'hello world']]
+
+ msg, commands = extractor.extract_commands(msg, only: [:open, :labels])
+
+ expect(commands).to eq(expected_commands)
+ expect(msg).to eq expected_msg
+ end
end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 69ee5ff4bcd..b212d2b05f2 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -8,6 +8,7 @@ describe Gitlab::UsageData do
before do
create(:jira_service, project: projects[0])
create(:jira_service, project: projects[1])
+ create(:jira_cloud_service, project: projects[2])
create(:prometheus_service, project: projects[1])
create(:service, project: projects[0], type: 'SlackSlashCommandsService', active: true)
create(:service, project: projects[1], type: 'SlackService', active: true)
@@ -20,6 +21,7 @@ describe Gitlab::UsageData do
create(:clusters_applications_ingress, :installed, cluster: gcp_cluster)
create(:clusters_applications_prometheus, :installed, cluster: gcp_cluster)
create(:clusters_applications_runner, :installed, cluster: gcp_cluster)
+ create(:clusters_applications_knative, :installed, cluster: gcp_cluster)
end
subject { described_class.data }
@@ -81,6 +83,7 @@ describe Gitlab::UsageData do
clusters_applications_ingress
clusters_applications_prometheus
clusters_applications_runner
+ clusters_applications_knative
in_review_folder
groups
issues
@@ -95,6 +98,8 @@ describe Gitlab::UsageData do
projects
projects_imported_from_github
projects_jira_active
+ projects_jira_server_active
+ projects_jira_cloud_active
projects_slack_notifications_active
projects_slack_slash_active
projects_prometheus_active
@@ -114,7 +119,9 @@ describe Gitlab::UsageData do
expect(count_data[:projects]).to eq(3)
expect(count_data[:projects_prometheus_active]).to eq(1)
- expect(count_data[:projects_jira_active]).to eq(2)
+ expect(count_data[:projects_jira_active]).to eq(3)
+ expect(count_data[:projects_jira_server_active]).to eq(2)
+ expect(count_data[:projects_jira_cloud_active]).to eq(1)
expect(count_data[:projects_slack_notifications_active]).to eq(2)
expect(count_data[:projects_slack_slash_active]).to eq(1)
@@ -126,6 +133,7 @@ describe Gitlab::UsageData do
expect(count_data[:clusters_applications_ingress]).to eq(1)
expect(count_data[:clusters_applications_prometheus]).to eq(1)
expect(count_data[:clusters_applications_runner]).to eq(1)
+ expect(count_data[:clusters_applications_knative]).to eq(1)
end
it 'works when queries time out' do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 5bd2f096656..6849bc6db7a 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1472,15 +1472,15 @@ describe Ci::Build do
end
describe '#retries_max' do
- context 'when max retries value is defined' do
- subject { create(:ci_build, options: { retry: 1 }) }
+ context 'with retries max config option' do
+ subject { create(:ci_build, options: { retry: { max: 1 } }) }
- it 'returns a number of configured max retries' do
+ it 'returns the number of configured max retries' do
expect(subject.retries_max).to eq 1
end
end
- context 'when max retries value is not defined' do
+ context 'without retries max config option' do
subject { create(:ci_build) }
it 'returns zero' do
@@ -1495,6 +1495,104 @@ describe Ci::Build do
expect(subject.retries_max).to eq 0
end
end
+
+ context 'with integer only config option' do
+ subject { create(:ci_build, options: { retry: 1 }) }
+
+ it 'returns the number of configured max retries' do
+ expect(subject.retries_max).to eq 1
+ end
+ end
+ end
+
+ describe '#retry_when' do
+ context 'with retries when config option' do
+ subject { create(:ci_build, options: { retry: { when: ['some_reason'] } }) }
+
+ it 'returns the configured when' do
+ expect(subject.retry_when).to eq ['some_reason']
+ end
+ end
+
+ context 'without retries when config option' do
+ subject { create(:ci_build) }
+
+ it 'returns always array' do
+ expect(subject.retry_when).to eq ['always']
+ end
+ end
+
+ context 'with integer only config option' do
+ subject { create(:ci_build, options: { retry: 1 }) }
+
+ it 'returns always array' do
+ expect(subject.retry_when).to eq ['always']
+ end
+ end
+ end
+
+ describe '#retry_failure?' do
+ subject { create(:ci_build) }
+
+ context 'when retries max is zero' do
+ before do
+ expect(subject).to receive(:retries_max).at_least(:once).and_return(0)
+ end
+
+ it 'returns false' do
+ expect(subject.retry_failure?).to eq false
+ end
+ end
+
+ context 'when retries max equals retries count' do
+ before do
+ expect(subject).to receive(:retries_max).at_least(:once).and_return(1)
+ expect(subject).to receive(:retries_count).at_least(:once).and_return(1)
+ end
+
+ it 'returns false' do
+ expect(subject.retry_failure?).to eq false
+ end
+ end
+
+ context 'when retries max is higher than retries count' do
+ before do
+ expect(subject).to receive(:retries_max).at_least(:once).and_return(2)
+ expect(subject).to receive(:retries_count).at_least(:once).and_return(1)
+ end
+
+ context 'and retry when is always' do
+ before do
+ expect(subject).to receive(:retry_when).at_least(:once).and_return(['always'])
+ end
+
+ it 'returns true' do
+ expect(subject.retry_failure?).to eq true
+ end
+ end
+
+ context 'and retry when includes the failure_reason' do
+ before do
+ expect(subject).to receive(:failure_reason).at_least(:once).and_return('some_reason')
+ expect(subject).to receive(:retry_when).at_least(:once).and_return(['some_reason'])
+ end
+
+ it 'returns true' do
+ expect(subject.retry_failure?).to eq true
+ end
+ end
+
+ context 'and retry when does not include failure_reason' do
+ before do
+ expect(subject).to receive(:failure_reason).at_least(:once).and_return('some_reason')
+ expect(subject).to receive(:retry_when).at_least(:once).and_return(['some', 'other failure'])
+ end
+
+ it 'returns false' do
+ expect(subject.retry_failure?).to eq false
+ end
+ end
+ end
end
end
@@ -2887,7 +2985,7 @@ describe Ci::Build do
end
context 'when build is configured to be retried' do
- subject { create(:ci_build, :running, options: { retry: 3 }, project: project, user: user) }
+ subject { create(:ci_build, :running, options: { retry: { max: 3 } }, project: project, user: user) }
it 'retries build and assigns the same user to it' do
expect(described_class).to receive(:retry)
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index 1580ef36127..6b0b23eeab3 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -5,6 +5,7 @@ describe Clusters::Applications::Ingress do
include_examples 'cluster application core specs', :clusters_applications_ingress
include_examples 'cluster application status specs', :clusters_applications_ingress
+ include_examples 'cluster application helm specs', :clusters_applications_knative
before do
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
@@ -121,28 +122,5 @@ describe Clusters::Applications::Ingress do
expect(values).to include('stats')
expect(values).to include('podAnnotations')
end
-
- context 'when the helm application does not have a ca_cert' do
- before do
- application.cluster.application_helm.ca_cert = nil
- end
-
- it 'should not include cert files' do
- expect(subject[:'ca.pem']).not_to be_present
- expect(subject[:'cert.pem']).not_to be_present
- expect(subject[:'key.pem']).not_to be_present
- end
- end
-
- it 'should include cert files' do
- expect(subject[:'ca.pem']).to be_present
- expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
-
- expect(subject[:'cert.pem']).to be_present
- expect(subject[:'key.pem']).to be_present
-
- cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem'])
- expect(cert.not_after).to be < 60.minutes.from_now
- end
end
end
diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb
index 9c4396731eb..faaabafddb7 100644
--- a/spec/models/clusters/applications/jupyter_spec.rb
+++ b/spec/models/clusters/applications/jupyter_spec.rb
@@ -2,6 +2,7 @@ require 'rails_helper'
describe Clusters::Applications::Jupyter do
include_examples 'cluster application core specs', :clusters_applications_jupyter
+ include_examples 'cluster application helm specs', :clusters_applications_knative
it { is_expected.to belong_to(:oauth_application) }
@@ -79,29 +80,6 @@ describe Clusters::Applications::Jupyter do
subject { application.files }
- it 'should include cert files' do
- expect(subject[:'ca.pem']).to be_present
- expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
-
- expect(subject[:'cert.pem']).to be_present
- expect(subject[:'key.pem']).to be_present
-
- cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem'])
- expect(cert.not_after).to be < 60.minutes.from_now
- end
-
- context 'when the helm application does not have a ca_cert' do
- before do
- application.cluster.application_helm.ca_cert = nil
- end
-
- it 'should not include cert files' do
- expect(subject[:'ca.pem']).not_to be_present
- expect(subject[:'cert.pem']).not_to be_present
- expect(subject[:'key.pem']).not_to be_present
- end
- end
-
it 'should include valid values' do
expect(values).to include('ingress')
expect(values).to include('hub')
diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb
new file mode 100644
index 00000000000..be2a91d566b
--- /dev/null
+++ b/spec/models/clusters/applications/knative_spec.rb
@@ -0,0 +1,77 @@
+require 'rails_helper'
+
+describe Clusters::Applications::Knative do
+ let(:knative) { create(:clusters_applications_knative) }
+
+ include_examples 'cluster application core specs', :clusters_applications_knative
+ include_examples 'cluster application status specs', :clusters_applications_knative
+ include_examples 'cluster application helm specs', :clusters_applications_knative
+
+ describe '.installed' do
+ subject { described_class.installed }
+
+ let!(:cluster) { create(:clusters_applications_knative, :installed) }
+
+ before do
+ create(:clusters_applications_knative, :errored)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
+ describe '#make_installing!' do
+ before do
+ application.make_installing!
+ end
+
+ context 'application install previously errored with older version' do
+ let(:application) { create(:clusters_applications_knative, :scheduled, version: '0.1.3') }
+
+ it 'updates the application version' do
+ expect(application.reload.version).to eq('0.1.3')
+ end
+ end
+ end
+
+ describe '#make_installed' do
+ subject { described_class.installed }
+
+ let!(:cluster) { create(:clusters_applications_knative, :installed) }
+
+ before do
+ create(:clusters_applications_knative, :errored)
+ end
+
+ it { is_expected.to contain_exactly(cluster) }
+ end
+
+ describe '#install_command' do
+ subject { knative.install_command }
+
+ it 'should be an instance of Helm::InstallCommand' do
+ expect(subject).to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand)
+ end
+
+ it 'should be initialized with knative arguments' do
+ expect(subject.name).to eq('knative')
+ expect(subject.chart).to eq('knative/knative')
+ expect(subject.version).to eq('0.1.3')
+ expect(subject.files).to eq(knative.files)
+ end
+ end
+
+ describe '#files' do
+ let(:application) { knative }
+ let(:values) { subject[:'values.yaml'] }
+
+ subject { application.files }
+
+ it 'should include knative specific keys in the values.yaml file' do
+ expect(values).to include('domain')
+ end
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:hostname) }
+ end
+end
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index 48ba163b38c..86de9dc60f2 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -5,6 +5,7 @@ describe Clusters::Applications::Prometheus do
include_examples 'cluster application core specs', :clusters_applications_prometheus
include_examples 'cluster application status specs', :clusters_applications_prometheus
+ include_examples 'cluster application helm specs', :clusters_applications_knative
describe '.installed' do
subject { described_class.installed }
@@ -187,29 +188,6 @@ describe Clusters::Applications::Prometheus do
subject { application.files }
- it 'should include cert files' do
- expect(subject[:'ca.pem']).to be_present
- expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
-
- expect(subject[:'cert.pem']).to be_present
- expect(subject[:'key.pem']).to be_present
-
- cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem'])
- expect(cert.not_after).to be < 60.minutes.from_now
- end
-
- context 'when the helm application does not have a ca_cert' do
- before do
- application.cluster.application_helm.ca_cert = nil
- end
-
- it 'should not include cert files' do
- expect(subject[:'ca.pem']).not_to be_present
- expect(subject[:'cert.pem']).not_to be_present
- expect(subject[:'key.pem']).not_to be_present
- end
- end
-
it 'should include prometheus valid values' do
expect(values).to include('alertmanager')
expect(values).to include('kubeStateMetrics')
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index d5fb1a9d010..052cfdbc4b1 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -5,6 +5,7 @@ describe Clusters::Applications::Runner do
include_examples 'cluster application core specs', :clusters_applications_runner
include_examples 'cluster application status specs', :clusters_applications_runner
+ include_examples 'cluster application helm specs', :clusters_applications_knative
it { is_expected.to belong_to(:runner) }
@@ -74,29 +75,6 @@ describe Clusters::Applications::Runner do
subject { application.files }
- it 'should include cert files' do
- expect(subject[:'ca.pem']).to be_present
- expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
-
- expect(subject[:'cert.pem']).to be_present
- expect(subject[:'key.pem']).to be_present
-
- cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem'])
- expect(cert.not_after).to be < 60.minutes.from_now
- end
-
- context 'when the helm application does not have a ca_cert' do
- before do
- application.cluster.application_helm.ca_cert = nil
- end
-
- it 'should not include cert files' do
- expect(subject[:'ca.pem']).not_to be_present
- expect(subject[:'cert.pem']).not_to be_present
- expect(subject[:'key.pem']).not_to be_present
- end
- end
-
it 'should include runner valid values' do
expect(values).to include('concurrent')
expect(values).to include('checkInterval')
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 19b76ca8cfb..10b9ca1a778 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -314,9 +314,10 @@ describe Clusters::Cluster do
let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) }
let!(:runner) { create(:clusters_applications_runner, cluster: cluster) }
let!(:jupyter) { create(:clusters_applications_jupyter, cluster: cluster) }
+ let!(:knative) { create(:clusters_applications_knative, cluster: cluster) }
it 'returns a list of created applications' do
- is_expected.to contain_exactly(helm, ingress, prometheus, runner, jupyter)
+ is_expected.to contain_exactly(helm, ingress, prometheus, runner, jupyter, knative)
end
end
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 5ea869796b0..2ebcb787d06 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -683,7 +683,7 @@ describe API::Internal do
expect(json_response).to match [{
"branch_name" => "new_branch",
- "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/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
@@ -704,7 +704,7 @@ describe API::Internal do
expect(json_response).to match [{
"branch_name" => "new_branch",
- "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/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
@@ -837,7 +837,7 @@ describe API::Internal do
expect(json_response['merge_request_urls']).to match [{
"branch_name" => "new_branch",
- "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/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
diff --git a/spec/requests/api/submodules_spec.rb b/spec/requests/api/submodules_spec.rb
new file mode 100644
index 00000000000..fa447c028c2
--- /dev/null
+++ b/spec/requests/api/submodules_spec.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Submodules do
+ let(:user) { create(:user) }
+ let!(:project) { create(:project, :repository, namespace: user.namespace ) }
+ let(:guest) { create(:user) { |u| project.add_guest(u) } }
+ let(:submodule) { 'six' }
+ let(:commit_sha) { 'e25eda1fece24ac7a03624ed1320f82396f35bd8' }
+ let(:branch) { 'master' }
+ let(:commit_message) { 'whatever' }
+
+ let(:params) do
+ {
+ submodule: submodule,
+ commit_sha: commit_sha,
+ branch: branch,
+ commit_message: commit_message
+ }
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ def route(submodule = nil)
+ "/projects/#{project.id}/repository/submodules/#{submodule}"
+ end
+
+ describe "PUT /projects/:id/repository/submodule/:submodule" do
+ context 'when unauthenticated' do
+ it 'returns 401' do
+ put api(route(submodule)), params
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+ end
+
+ context 'when authenticated', 'as a guest' do
+ it 'returns 403' do
+ put api(route(submodule), guest), params
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
+ context 'when authenticated', 'as a developer' do
+ it 'returns 400 if params is missing' do
+ put api(route(submodule), user)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns 400 if branch is missing' do
+ put api(route(submodule), user), params.except(:branch)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns 400 if commit_sha is missing' do
+ put api(route(submodule), user), params.except(:commit_sha)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns the commmit' do
+ head_commit = project.repository.commit.id
+
+ put api(route(submodule), user), params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['message']).to eq commit_message
+ expect(json_response['author_name']).to eq user.name
+ expect(json_response['committer_name']).to eq user.name
+ expect(json_response['parent_ids'].first).to eq head_commit
+ end
+
+ context 'when the submodule name is urlencoded' do
+ let(:submodule) { 'test_inside_folder/another_folder/six' }
+ let(:branch) { 'submodule_inside_folder' }
+ let(:encoded_submodule) { CGI.escape(submodule) }
+
+ it 'returns the commmit' do
+ expect(Submodules::UpdateService)
+ .to receive(:new)
+ .with(any_args, hash_including(submodule: submodule))
+ .and_call_original
+
+ put api(route(encoded_submodule), user), params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['id']).to eq project.repository.commit(branch).id
+ expect(project.repository.blob_at(branch, submodule).id).to eq commit_sha
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 054b7b1561c..5c87ed5c3c6 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -435,16 +435,34 @@ describe Ci::CreatePipelineService do
end
context 'when builds with auto-retries are configured' do
- before do
- config = YAML.dump(rspec: { script: 'rspec', retry: 2 })
- stub_ci_pipeline_yaml_file(config)
+ context 'as an integer' do
+ before do
+ config = YAML.dump(rspec: { script: 'rspec', retry: 2 })
+ stub_ci_pipeline_yaml_file(config)
+ end
+
+ it 'correctly creates builds with auto-retry value configured' do
+ pipeline = execute_service
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.find_by(name: 'rspec').retries_max).to eq 2
+ expect(pipeline.builds.find_by(name: 'rspec').retry_when).to eq ['always']
+ end
end
- it 'correctly creates builds with auto-retry value configured' do
- pipeline = execute_service
+ context 'as hash' do
+ before do
+ config = YAML.dump(rspec: { script: 'rspec', retry: { max: 2, when: 'runner_system_failure' } })
+ stub_ci_pipeline_yaml_file(config)
+ end
- expect(pipeline).to be_persisted
- expect(pipeline.builds.find_by(name: 'rspec').retries_max).to eq 2
+ it 'correctly creates builds with auto-retry value configured' do
+ pipeline = execute_service
+
+ expect(pipeline).to be_persisted
+ expect(pipeline.builds.find_by(name: 'rspec').retries_max).to eq 2
+ expect(pipeline.builds.find_by(name: 'rspec').retry_when).to eq ['runner_system_failure']
+ end
end
end
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index 8c7258c42ad..538992b621e 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -671,9 +671,9 @@ describe Ci::ProcessPipelineService, '#execute' do
context 'when builds with auto-retries are configured' do
before do
- create_build('build:1', stage_idx: 0, user: user, options: { retry: 2 })
+ create_build('build:1', stage_idx: 0, user: user, options: { retry: { max: 2 } })
create_build('test:1', stage_idx: 1, user: user, when: :on_failure)
- create_build('test:2', stage_idx: 1, user: user, options: { retry: 1 })
+ create_build('test:2', stage_idx: 1, user: user, options: { retry: { max: 1 } })
end
it 'automatically retries builds in a valid order' do
diff --git a/spec/services/clusters/applications/create_service_spec.rb b/spec/services/clusters/applications/create_service_spec.rb
index 056db0c5486..a9985133b93 100644
--- a/spec/services/clusters/applications/create_service_spec.rb
+++ b/spec/services/clusters/applications/create_service_spec.rb
@@ -67,5 +67,38 @@ describe Clusters::Applications::CreateService do
expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError)
end
end
+
+ context 'knative application' do
+ let(:params) do
+ {
+ application: 'knative',
+ hostname: 'example.com'
+ }
+ end
+
+ before do
+ allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute)
+ end
+
+ it 'creates the application' do
+ expect do
+ subject
+
+ cluster.reload
+ end.to change(cluster, :application_knative)
+ end
+
+ it 'sets the hostname' do
+ expect(subject.hostname).to eq('example.com')
+ end
+ end
+
+ context 'invalid application' do
+ let(:params) { { application: 'non-existent' } }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Clusters::Applications::CreateService::InvalidApplicationError)
+ end
+ end
end
end
diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
index 7fbb6cf2cf5..efee158739d 100644
--- a/spec/services/clusters/gcp/finalize_creation_service_spec.rb
+++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb
@@ -33,7 +33,7 @@ describe Clusters::Gcp::FinalizeCreationService, '#execute' do
expect(provider.endpoint).to eq(endpoint)
expect(platform.api_url).to eq(api_url)
- expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert))
+ expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert).strip)
expect(platform.username).to eq(username)
expect(platform.password).to eq(password)
expect(platform.token).to eq(token)
diff --git a/spec/services/commits/commit_patch_service_spec.rb b/spec/services/commits/commit_patch_service_spec.rb
new file mode 100644
index 00000000000..f4fcec2fbc2
--- /dev/null
+++ b/spec/services/commits/commit_patch_service_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Commits::CommitPatchService do
+ describe '#execute' do
+ let(:patches) do
+ patches_folder = Rails.root.join('spec/fixtures/patchfiles')
+ content_1 = File.read(File.join(patches_folder, "0001-This-does-not-apply-to-the-feature-branch.patch"))
+ content_2 = File.read(File.join(patches_folder, "0001-A-commit-from-a-patch.patch"))
+
+ [content_1, content_2]
+ end
+ let(:user) { project.creator }
+ let(:branch_name) { 'branch-with-patches' }
+ let(:project) { create(:project, :repository) }
+ let(:start_branch) { nil }
+ let(:params) { { branch_name: branch_name, patches: patches, start_branch: start_branch } }
+
+ subject(:service) do
+ described_class.new(project, user, params)
+ end
+
+ it 'returns a successful result' do
+ result = service.execute
+
+ branch = project.repository.find_branch(branch_name)
+
+ expect(result[:status]).to eq(:success)
+ expect(result[:result]).to eq(branch.target)
+ end
+
+ it 'is based off HEAD when no start ref is passed' do
+ service.execute
+
+ merge_base = project.repository.merge_base(project.repository.root_ref, branch_name)
+
+ expect(merge_base).to eq(project.repository.commit('HEAD').sha)
+ end
+
+ context 'when specifying a different start branch' do
+ let(:start_branch) { 'with-codeowners' }
+
+ it 'is based of the correct branch' do
+ service.execute
+
+ merge_base = project.repository.merge_base(start_branch, branch_name)
+
+ expect(merge_base).to eq(project.repository.commit(start_branch).sha)
+ end
+ end
+
+ shared_examples 'an error response' do |expected_message|
+ it 'returns the correct error' do
+ result = service.execute
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to match(expected_message)
+ end
+ end
+
+ context 'when the user does not have access' do
+ let(:user) { create(:user) }
+
+ it_behaves_like 'an error response',
+ 'You are not allowed to push into this branch'
+ end
+
+ context 'when the patches are not valid' do
+ let(:patches) { "a" * 2.1.megabytes }
+
+ it_behaves_like 'an error response', 'Patches are too big'
+ end
+
+ context 'when the new branch name is invalid' do
+ let(:branch_name) { 'HEAD' }
+
+ it_behaves_like 'an error response', 'Branch name is invalid'
+ end
+
+ context 'when the patches do not apply' do
+ let(:branch_name) { 'feature' }
+
+ it_behaves_like 'an error response', 'Patch failed at'
+ end
+
+ context 'when specifying a non existent start branch' do
+ let(:start_branch) { 'does-not-exist' }
+
+ it_behaves_like 'an error response', 'Invalid reference name'
+ end
+ end
+end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 9f1da7d9419..c9a668994eb 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -392,5 +392,13 @@ describe MergeRequests::BuildService do
expect(merge_request.source_project).to eq(project)
end
end
+
+ context 'when specifying target branch in the description' do
+ let(:description) { "A merge request targeting another branch\n\n/target_branch with-codeowners" }
+
+ it 'sets the attribute from the quick actions' do
+ expect(merge_request.target_branch).to eq('with-codeowners')
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
index 3e33a165e55..274624aa8bb 100644
--- a/spec/services/merge_requests/get_urls_service_spec.rb
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -6,7 +6,7 @@ describe MergeRequests::GetUrlsService do
let(:project) { create(:project, :public, :repository) }
let(:service) { described_class.new(project) }
let(:source_branch) { "merge-test" }
- let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/#{source_branch}" }
+ let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" }
let(:show_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" }
let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" }
@@ -117,7 +117,7 @@ describe MergeRequests::GetUrlsService do
let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" }
let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/markdown" }
let(:changes) { "#{new_branch_changes}\n#{existing_branch_changes}" }
- let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new/new_branch" }
+ let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" }
it 'returns 2 urls for both creating new and showing merge request' do
result = service.execute(changes)
diff --git a/spec/services/merge_requests/reload_diffs_service_spec.rb b/spec/services/merge_requests/reload_diffs_service_spec.rb
index 5acd01828cb..546c9f277c5 100644
--- a/spec/services/merge_requests/reload_diffs_service_spec.rb
+++ b/spec/services/merge_requests/reload_diffs_service_spec.rb
@@ -31,11 +31,32 @@ describe MergeRequests::ReloadDiffsService, :use_clean_rails_memory_store_cachin
end
context 'cache clearing' do
+ before do
+ allow_any_instance_of(Gitlab::Diff::File).to receive(:text?).and_return(true)
+ allow_any_instance_of(Gitlab::Diff::File).to receive(:diffable?).and_return(true)
+ end
+
+ it 'retrieves the diff files to cache the highlighted result' do
+ new_diff = merge_request.create_merge_request_diff
+ cache_key = new_diff.diffs_collection.cache_key
+
+ expect(merge_request).to receive(:create_merge_request_diff).and_return(new_diff)
+ expect(Rails.cache).to receive(:read).with(cache_key).and_call_original
+ expect(Rails.cache).to receive(:write).with(cache_key, anything, anything).and_call_original
+
+ subject.execute
+ end
+
it 'clears the cache for older diffs on the merge request' do
old_diff = merge_request.merge_request_diff
old_cache_key = old_diff.diffs_collection.cache_key
+ new_diff = merge_request.create_merge_request_diff
+ new_cache_key = new_diff.diffs_collection.cache_key
+ expect(merge_request).to receive(:create_merge_request_diff).and_return(new_diff)
expect(Rails.cache).to receive(:delete).with(old_cache_key).and_call_original
+ expect(Rails.cache).to receive(:read).with(new_cache_key).and_call_original
+ expect(Rails.cache).to receive(:write).with(new_cache_key, anything, anything).and_call_original
subject.execute
end
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 80b015d4cd0..b1290fd0d47 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -57,57 +57,6 @@ describe Notes::CreateService do
end
end
- context 'noteable highlight cache clearing' do
- let(:project_with_repo) { create(:project, :repository) }
- let(:merge_request) do
- create(:merge_request, source_project: project_with_repo,
- target_project: project_with_repo)
- end
-
- let(:position) do
- Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb",
- new_path: "files/ruby/popen.rb",
- old_line: nil,
- new_line: 14,
- diff_refs: merge_request.diff_refs)
- end
-
- let(:new_opts) do
- opts.merge(in_reply_to_discussion_id: nil,
- type: 'DiffNote',
- noteable_type: 'MergeRequest',
- noteable_id: merge_request.id,
- position: position.to_h)
- end
-
- before do
- allow_any_instance_of(Gitlab::Diff::Position)
- .to receive(:unfolded_diff?) { true }
- end
-
- it 'clears noteable diff cache when it was unfolded for the note position' do
- expect_any_instance_of(Gitlab::Diff::HighlightCache).to receive(:clear)
-
- described_class.new(project_with_repo, user, new_opts).execute
- end
-
- it 'does not clear cache when note is not the first of the discussion' do
- prev_note =
- create(:diff_note_on_merge_request, noteable: merge_request,
- project: project_with_repo)
- reply_opts =
- opts.merge(in_reply_to_discussion_id: prev_note.discussion_id,
- type: 'DiffNote',
- noteable_type: 'MergeRequest',
- noteable_id: merge_request.id,
- position: position.to_h)
-
- expect(merge_request).not_to receive(:diffs)
-
- described_class.new(project_with_repo, user, reply_opts).execute
- end
- end
-
context 'note diff file' do
let(:project_with_repo) { create(:project, :repository) }
let(:merge_request) do
diff --git a/spec/services/notes/destroy_service_spec.rb b/spec/services/notes/destroy_service_spec.rb
index b1f4e87e8ea..64445be560e 100644
--- a/spec/services/notes/destroy_service_spec.rb
+++ b/spec/services/notes/destroy_service_spec.rb
@@ -21,38 +21,5 @@ describe Notes::DestroyService do
expect { described_class.new(project, user).execute(note) }
.to change { user.todos_pending_count }.from(1).to(0)
end
-
- context 'noteable highlight cache clearing' do
- let(:repo_project) { create(:project, :repository) }
- let(:merge_request) do
- create(:merge_request, source_project: repo_project,
- target_project: repo_project)
- end
-
- let(:note) do
- create(:diff_note_on_merge_request, project: repo_project,
- noteable: merge_request)
- end
-
- before do
- allow(note.position).to receive(:unfolded_diff?) { true }
- end
-
- it 'clears noteable diff cache when it was unfolded for the note position' do
- expect(merge_request).to receive_message_chain(:diffs, :clear_cache)
-
- described_class.new(repo_project, user).execute(note)
- end
-
- it 'does not clear cache when note is not the first of the discussion' do
- reply_note = create(:diff_note_on_merge_request, in_reply_to: note,
- project: repo_project,
- noteable: merge_request)
-
- expect(merge_request).not_to receive(:diffs)
-
- described_class.new(repo_project, user).execute(reply_note)
- end
- end
end
end
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index e513ee7ae44..5a7cafcb60f 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -1213,6 +1213,15 @@ describe QuickActions::InterpretService do
end
end
end
+
+ it 'limits to commands passed ' do
+ content = "/shrug\n/close"
+
+ text, commands = service.execute(content, issue, only: [:shrug])
+
+ expect(commands).to be_empty
+ expect(text).to eq("#{described_class::SHRUG}\n/close")
+ end
end
describe '#explain' do
diff --git a/spec/services/submodules/update_service_spec.rb b/spec/services/submodules/update_service_spec.rb
new file mode 100644
index 00000000000..cf92350c1b2
--- /dev/null
+++ b/spec/services/submodules/update_service_spec.rb
@@ -0,0 +1,212 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Submodules::UpdateService do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:user) { create(:user, :commit_email) }
+ let(:branch_name) { project.default_branch }
+ let(:submodule) { 'six' }
+ let(:commit_sha) { 'e25eda1fece24ac7a03624ed1320f82396f35bd8' }
+ let(:commit_message) { 'whatever' }
+ let(:current_sha) { repository.blob_at('HEAD', submodule).id }
+ let(:commit_params) do
+ {
+ submodule: submodule,
+ commit_message: commit_message,
+ commit_sha: commit_sha,
+ branch_name: branch_name
+ }
+ end
+
+ subject { described_class.new(project, user, commit_params) }
+
+ describe "#execute" do
+ shared_examples 'returns error result' do
+ it do
+ result = subject.execute
+
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq error_message
+ end
+ end
+
+ context 'when the user is not authorized' do
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'You are not allowed to push into this branch' }
+ end
+ end
+
+ context 'when the user is authorized' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'when the branch is protected' do
+ before do
+ create(:protected_branch, :no_one_can_push, project: project, name: branch_name)
+ end
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'You are not allowed to push into this branch' }
+ end
+ end
+
+ context 'validations' do
+ context 'when submodule' do
+ context 'is empty' do
+ let(:submodule) { '' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid parameters' }
+ end
+ end
+
+ context 'is not present' do
+ let(:submodule) { nil }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid parameters' }
+ end
+ end
+
+ context 'is invalid' do
+ let(:submodule) { 'VERSION' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid submodule path' }
+ end
+ end
+
+ context 'does not exist' do
+ let(:submodule) { 'non-existent-submodule' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid submodule path' }
+ end
+ end
+
+ context 'has traversal path' do
+ let(:submodule) { '../six' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid parameters' }
+ end
+ end
+ end
+
+ context 'commit_sha' do
+ context 'is empty' do
+ let(:commit_sha) { '' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid parameters' }
+ end
+ end
+
+ context 'is not present' do
+ let(:commit_sha) { nil }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid parameters' }
+ end
+ end
+
+ context 'is invalid' do
+ let(:commit_sha) { '1' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'Invalid parameters' }
+ end
+ end
+
+ context 'is the same as the current ref' do
+ let(:commit_sha) { current_sha }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { "The submodule #{submodule} is already at #{commit_sha}" }
+ end
+ end
+ end
+
+ context 'branch_name' do
+ context 'is empty' do
+ let(:branch_name) { '' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'You can only create or edit files when you are on a branch' }
+ end
+ end
+
+ context 'is not present' do
+ let(:branch_name) { nil }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'You can only create or edit files when you are on a branch' }
+ end
+ end
+
+ context 'does not exist' do
+ let(:branch_name) { 'non/existent-branch' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'You can only create or edit files when you are on a branch' }
+ end
+ end
+
+ context 'when commit message is empty' do
+ let(:commit_message) { '' }
+
+ it 'a default commit message is set' do
+ message = "Update submodule #{submodule} with oid #{commit_sha}"
+
+ expect(repository).to receive(:update_submodule).with(any_args, hash_including(message: message))
+
+ subject.execute
+ end
+ end
+ end
+ end
+
+ context 'when there is an unexpected error' do
+ before do
+ allow(repository).to receive(:update_submodule).and_raise(StandardError, 'error message')
+ end
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'error message' }
+ end
+ end
+
+ it 'updates the submodule reference' do
+ result = subject.execute
+
+ expect(result[:status]).to eq :success
+ expect(result[:result]).to eq repository.head_commit.id
+ expect(repository.blob_at('HEAD', submodule).id).to eq commit_sha
+ end
+
+ context 'when submodule is inside a directory' do
+ let(:submodule) { 'test_inside_folder/another_folder/six' }
+ let(:branch_name) { 'submodule_inside_folder' }
+
+ it 'updates the submodule reference' do
+ expect(repository.blob_at(branch_name, submodule).id).not_to eq commit_sha
+
+ subject.execute
+
+ expect(repository.blob_at(branch_name, submodule).id).to eq commit_sha
+ end
+ end
+
+ context 'when repository is empty' do
+ let(:project) { create(:project, :empty_repo) }
+ let(:branch_name) { 'master' }
+
+ it_behaves_like 'returns error result' do
+ let(:error_message) { 'The repository is empty' }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb
index 80604395adf..18cf08f0b9e 100644
--- a/spec/support/features/discussion_comments_shared_example.rb
+++ b/spec/support/features/discussion_comments_shared_example.rb
@@ -150,17 +150,25 @@ shared_examples 'discussion comments' do |resource_name|
end
if resource_name == 'merge request'
- let(:note_id) { find("#{comments_selector} .note", match: :first)['data-note-id'] }
+ let(:note_id) { find("#{comments_selector} .note:first-child", match: :first)['data-note-id'] }
+ let(:reply_id) { find("#{comments_selector} .note:last-child", match: :first)['data-note-id'] }
it 'shows resolved discussion when toggled' do
+ find("#{comments_selector} .js-vue-discussion-reply").click
+ find("#{comments_selector} .note-textarea").send_keys('a')
+
+ click_button "Comment"
+ wait_for_requests
+
click_button "Resolve discussion"
+ wait_for_requests
expect(page).to have_selector(".note-row-#{note_id}", visible: true)
refresh
- click_button "Toggle discussion"
+ click_button "1 reply"
- expect(page).to have_selector(".note-row-#{note_id}", visible: true)
+ expect(page).to have_selector(".note-row-#{reply_id}", visible: true)
end
end
end
diff --git a/spec/support/helpers/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb
index 0c35764ed9a..5887c3eab74 100644
--- a/spec/support/helpers/migrations_helpers.rb
+++ b/spec/support/helpers/migrations_helpers.rb
@@ -1,6 +1,10 @@
module MigrationsHelpers
+ def active_record_base
+ ActiveRecord::Base
+ end
+
def table(name)
- Class.new(ActiveRecord::Base) do
+ Class.new(active_record_base) do
self.table_name = name
self.inheritance_column = :_type_disabled
@@ -19,7 +23,7 @@ module MigrationsHelpers
end
def clear_schema_cache!
- ActiveRecord::Base.connection_pool.connections.each do |conn|
+ active_record_base.connection_pool.connections.each do |conn|
conn.schema_cache.clear!
end
end
@@ -40,11 +44,18 @@ module MigrationsHelpers
# Reset column information for the most offending classes **after** we
# migrated the schema up, otherwise, column information could be
# outdated. We have a separate method for this so we can override it in EE.
- ActiveRecord::Base.descendants.each(&method(:reset_column_information))
+ active_record_base.descendants.each(&method(:reset_column_information))
+ end
- # Without that, we get errors because of missing attributes, e.g.
+ def refresh_attribute_methods
+ # Without this, we get errors because of missing attributes, e.g.
# super: no superclass method `elasticsearch_indexing' for #<ApplicationSetting:0x00007f85628508d8>
- ApplicationSetting.define_attribute_methods
+ # attr_encrypted also expects ActiveRecord attribute methods to be
+ # defined, or it will override the accessors:
+ # https://gitlab.com/gitlab-org/gitlab-ee/issues/8234#note_113976421
+ [ApplicationSetting, SystemHook].each do |model|
+ model.define_attribute_methods
+ end
end
def reset_column_information(klass)
@@ -84,6 +95,7 @@ module MigrationsHelpers
end
reset_column_in_all_models
+ refresh_attribute_methods
end
def disable_migrations_output
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 80b96f20e3f..1f00cdf7e92 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -58,7 +58,8 @@ module TestEnv
'before-create-delete-modify-move' => '845009f',
'between-create-delete-modify-move' => '3f5f443',
'after-create-delete-modify-move' => 'ba3faa7',
- 'with-codeowners' => '219560e'
+ 'with-codeowners' => '219560e',
+ 'submodule_inside_folder' => 'b491b92'
}.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
@@ -161,8 +162,9 @@ module TestEnv
version: Gitlab::GitalyClient.expected_server_version,
task: "gitlab:gitaly:install[#{gitaly_dir},#{repos_path}]") do
- start_gitaly(gitaly_dir)
- end
+ Gitlab::SetupHelper.create_gitaly_configuration(gitaly_dir, { 'default' => repos_path }, force: true)
+ start_gitaly(gitaly_dir)
+ end
end
def start_gitaly(gitaly_dir)
diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
index 9373de5aeab..7038a366144 100644
--- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
@@ -17,10 +17,10 @@ RSpec.shared_examples 'a creatable merge request' do
sign_in(user)
visit project_new_merge_request_path(
target_project,
- merge_request_source_branch: 'fix',
merge_request: {
source_project_id: source_project.id,
target_project_id: target_project.id,
+ source_branch: 'fix',
target_branch: 'master'
})
end
diff --git a/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb b/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb
new file mode 100644
index 00000000000..d87b3181e80
--- /dev/null
+++ b/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb
@@ -0,0 +1,25 @@
+shared_examples 'cluster application helm specs' do |application_name|
+ let(:application) { create(application_name) }
+
+ describe '#files' do
+ subject { application.files }
+
+ context 'when the helm application does not have a ca_cert' do
+ before do
+ application.cluster.application_helm.ca_cert = nil
+ end
+
+ it 'should not include cert files when there is no ca_cert entry' do
+ expect(subject).not_to include(:'ca.pem', :'cert.pem', :'key.pem')
+ end
+ end
+
+ it 'should include cert files when there is a ca_cert entry' do
+ expect(subject).to include(:'ca.pem', :'cert.pem', :'key.pem')
+ expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
+
+ cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem'])
+ expect(cert.not_after).to be < 60.minutes.from_now
+ end
+ end
+end
diff --git a/spec/workers/email_receiver_worker_spec.rb b/spec/workers/email_receiver_worker_spec.rb
index e4e77c667b3..045135255d6 100644
--- a/spec/workers/email_receiver_worker_spec.rb
+++ b/spec/workers/email_receiver_worker_spec.rb
@@ -46,6 +46,21 @@ describe EmailReceiverWorker, :mailer do
should_not_email_anyone
end
end
+
+ context 'when the error is Gitlab::Email::InvalidAttachment' do
+ let(:error) { Gitlab::Email::InvalidAttachment.new("Could not deal with that") }
+
+ it 'reports the error to the sender' do
+ perform_enqueued_jobs do
+ described_class.new.perform(raw_message)
+ end
+
+ email = ActionMailer::Base.deliveries.last
+ expect(email).not_to be_nil
+ expect(email.to).to eq(["jake@adventuretime.ooo"])
+ expect(email.body.parts.last.to_s).to include("Could not deal with that")
+ end
+ end
end
end
diff --git a/spec/workers/stuck_import_jobs_worker_spec.rb b/spec/workers/stuck_import_jobs_worker_spec.rb
index 2169c14218b..e94d2be9850 100644
--- a/spec/workers/stuck_import_jobs_worker_spec.rb
+++ b/spec/workers/stuck_import_jobs_worker_spec.rb
@@ -8,29 +8,29 @@ describe StuckImportJobsWorker do
context 'when the import status was already updated' do
before do
allow(Gitlab::SidekiqStatus).to receive(:completed_jids) do
- project.import_start
- project.import_finish
+ import_state.start
+ import_state.finish
- [project.import_jid]
+ [import_state.jid]
end
end
it 'does not mark the project as failed' do
worker.perform
- expect(project.reload.import_status).to eq('finished')
+ expect(import_state.reload.status).to eq('finished')
end
end
context 'when the import status was not updated' do
before do
- allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([project.import_jid])
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([import_state.jid])
end
it 'marks the project as failed' do
worker.perform
- expect(project.reload.import_status).to eq('failed')
+ expect(import_state.reload.status).to eq('failed')
end
end
end
@@ -41,27 +41,27 @@ describe StuckImportJobsWorker do
end
it 'does not mark the project as failed' do
- expect { worker.perform }.not_to change { project.reload.import_status }
+ expect { worker.perform }.not_to change { import_state.reload.status }
end
end
end
describe 'with scheduled import_status' do
it_behaves_like 'project import job detection' do
- let(:project) { create(:project, :import_scheduled) }
+ let(:import_state) { create(:project, :import_scheduled).import_state }
before do
- project.import_state.update(jid: '123')
+ import_state.update(jid: '123')
end
end
end
describe 'with started import_status' do
it_behaves_like 'project import job detection' do
- let(:project) { create(:project, :import_started) }
+ let(:import_state) { create(:project, :import_started).import_state }
before do
- project.import_state.update(jid: '123')
+ import_state.update(jid: '123')
end
end
end
diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore
index 39b6783cef8..69eda01429a 100644
--- a/vendor/gitignore/Android.gitignore
+++ b/vendor/gitignore/Android.gitignore
@@ -1,6 +1,7 @@
# Built application files
*.apk
*.ap_
+*.aab
# Files for the ART/Dalvik VM
*.dex
@@ -43,8 +44,9 @@ captures/
.idea/caches
# Keystore files
-# Uncomment the following line if you do not want to check your keystore files in.
+# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
+#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
diff --git a/vendor/gitignore/Delphi.gitignore b/vendor/gitignore/Delphi.gitignore
index 000ee5f104b..9532800ba22 100644
--- a/vendor/gitignore/Delphi.gitignore
+++ b/vendor/gitignore/Delphi.gitignore
@@ -64,3 +64,6 @@ __recovery/
# Castalia statistics file (since XE7 Castalia is distributed with Delphi)
*.stat
+
+# Boss dependency manager vendor folder https://github.com/HashLoad/boss
+modules/
diff --git a/vendor/gitignore/Elixir.gitignore b/vendor/gitignore/Elixir.gitignore
index 86e4c3f3905..b263cd10f37 100644
--- a/vendor/gitignore/Elixir.gitignore
+++ b/vendor/gitignore/Elixir.gitignore
@@ -7,3 +7,4 @@ erl_crash.dump
*.ez
*.beam
/config/*.secret.exs
+.elixir_ls/
diff --git a/vendor/gitignore/Global/Images.gitignore b/vendor/gitignore/Global/Images.gitignore
new file mode 100644
index 00000000000..97dcdbe6a95
--- /dev/null
+++ b/vendor/gitignore/Global/Images.gitignore
@@ -0,0 +1,63 @@
+# JPEG
+*.jpg
+*.jpeg
+*.jpe
+*.jif
+*.jfif
+*.jfi
+
+# JPEG 2000
+*.jp2
+*.j2k
+*.jpf
+*.jpx
+*.jpm
+*.mj2
+
+# JPEG XR
+*.jxr
+*.hdp
+*.wdp
+
+# Graphics Interchange Format
+*.gif
+
+# RAW
+*.raw
+
+# Web P
+*.webp
+
+# Portable Network Graphics
+*.png
+
+# Animated Portable Network Graphics
+*.apng
+
+# Multiple-image Network Graphics
+*.mng
+
+# Tagged Image File Format
+*.tiff
+*.tif
+
+# Scalable Vector Graphics
+*.svg
+*.svgz
+
+# Portable Document Format
+*.pdf
+
+# X BitMap
+*.xbm
+
+# BMP
+*.bmp
+*.dib
+
+# ICO
+*.ico
+
+# 3D Images
+*.3dm
+*.max
diff --git a/vendor/gitignore/Global/NetBeans.gitignore b/vendor/gitignore/Global/NetBeans.gitignore
index 254108cd23b..863bc7fa66e 100644
--- a/vendor/gitignore/Global/NetBeans.gitignore
+++ b/vendor/gitignore/Global/NetBeans.gitignore
@@ -1,4 +1,4 @@
-nbproject/private/
+**/nbproject/private/
build/
nbbuild/
dist/
diff --git a/vendor/gitignore/Global/PSoCCreator.gitignore b/vendor/gitignore/Global/PSoCCreator.gitignore
new file mode 100644
index 00000000000..15ae040bcda
--- /dev/null
+++ b/vendor/gitignore/Global/PSoCCreator.gitignore
@@ -0,0 +1,18 @@
+# Project Settings
+*.cywrk.*
+*.cyprj.*
+
+# Generated Assets and Resources
+Debug/
+Release/
+Export/
+*/codegentemp
+*/Generated_Source
+*_datasheet.pdf
+*_timing.html
+*.cycdx
+*.cyfit
+*.rpt
+*.svd
+*.log
+*.zip
diff --git a/vendor/gitignore/Global/Xcode.gitignore b/vendor/gitignore/Global/Xcode.gitignore
index cd0c7d3e45a..b01314d3a64 100644
--- a/vendor/gitignore/Global/Xcode.gitignore
+++ b/vendor/gitignore/Global/Xcode.gitignore
@@ -2,17 +2,11 @@
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
-## User settings
-xcuserdata/
-
-## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
-*.xcscmblueprint
-*.xccheckout
-
-## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
+## Build generated
build/
DerivedData/
-*.moved-aside
+
+## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
@@ -21,3 +15,65 @@ DerivedData/
!default.mode2v3
*.perspectivev3
!default.perspectivev3
+xcuserdata/
+
+## Other
+*.moved-aside
+*.xccheckout
+*.xcscmblueprint
+
+## Obj-C/Swift specific
+*.hmap
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+## Playgrounds
+timeline.xctimeline
+playground.xcworkspace
+
+# Swift Package Manager
+#
+# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
+# Packages/
+# Package.pins
+# Package.resolved
+.build/
+
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+# Pods/
+#
+# Add this line if you want to avoid checking in source code from the Xcode workspace
+# *.xcworkspace
+
+# Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+Carthage/Build
+
+# fastlane
+#
+# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
+# screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
+
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots/**/*.png
+fastlane/test_output
+
+# Code Injection
+#
+# After new code Injection tools there's a generated folder /iOSInjectionProject
+# https://github.com/johnno1962/injectionforxcode
+
+iOSInjectionProject/
+
diff --git a/vendor/gitignore/Laravel.gitignore b/vendor/gitignore/Laravel.gitignore
index 67e2146f2bc..6552ddf8a06 100644
--- a/vendor/gitignore/Laravel.gitignore
+++ b/vendor/gitignore/Laravel.gitignore
@@ -1,4 +1,4 @@
-vendor/
+/vendor/
node_modules/
npm-debug.log
yarn-error.log
diff --git a/vendor/gitignore/Magento.gitignore b/vendor/gitignore/Magento.gitignore
index 6f1fa223992..abe6d79fedb 100644
--- a/vendor/gitignore/Magento.gitignore
+++ b/vendor/gitignore/Magento.gitignore
@@ -2,6 +2,8 @@
# Magento Default Files #
#--------------------------#
+/PATCH_*.sh
+
/app/etc/local.xml
/media/*
diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore
index c221276ebae..e1da6ae8ea5 100644
--- a/vendor/gitignore/Node.gitignore
+++ b/vendor/gitignore/Node.gitignore
@@ -71,3 +71,6 @@ typings/
# Serverless directories
.serverless
+
+# FuseBox cache
+.fusebox/
diff --git a/vendor/gitignore/Python.gitignore b/vendor/gitignore/Python.gitignore
index 6f7a6d9c3d7..510c73d0fdb 100644
--- a/vendor/gitignore/Python.gitignore
+++ b/vendor/gitignore/Python.gitignore
@@ -109,3 +109,6 @@ venv.bak/
.mypy_cache/
.dmypy.json
dmypy.json
+
+# Pyre type checker
+.pyre/
diff --git a/vendor/gitignore/Rails.gitignore b/vendor/gitignore/Rails.gitignore
index 78eb74fdc26..38ba1b5b38c 100644
--- a/vendor/gitignore/Rails.gitignore
+++ b/vendor/gitignore/Rails.gitignore
@@ -50,6 +50,7 @@ node_modules/
# Ignore precompiled javascript packs
/public/packs
/public/packs-test
+/public/assets
# Ignore yarn files
/yarn-error.log
diff --git a/vendor/gitignore/Unity.gitignore b/vendor/gitignore/Unity.gitignore
index 0210746b1a5..833e6d4291c 100644
--- a/vendor/gitignore/Unity.gitignore
+++ b/vendor/gitignore/Unity.gitignore
@@ -23,6 +23,7 @@ ExportedObj/
*.svd
*.pdb
*.opendb
+*.VC.db
# Unity3D generated meta files
*.pidb.meta