Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDJ Mountney <david@twkie.net>2018-10-25 18:48:14 +0300
committerDJ Mountney <david@twkie.net>2018-10-25 18:48:14 +0300
commitfee6989fa003395a6188f8ca452adab25d8ece6b (patch)
treeb18aa69513c0158a5e71f5d3479b1dba0cb2c00a
parent34d84fd29fe346dbf95a0cf76de803b6e61c45c6 (diff)
parent4de9004175e7c1dfd0366c41d70573d4d686b292 (diff)
Merge remote-tracking branch 'origin/master' into dev-master
-rw-r--r--.gitlab-ci.yml106
-rw-r--r--.gitlab/issue_templates/Security developer workflow.md4
-rw-r--r--.ruby-version2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile3
-rw-r--r--Gemfile.lock4
-rw-r--r--Gemfile.rails5.lock4
-rw-r--r--app/assets/javascripts/activities.js10
-rw-r--r--app/assets/javascripts/breadcrumb.js15
-rw-r--r--app/assets/javascripts/build_artifacts.js18
-rw-r--r--app/assets/javascripts/ci_variable_list/ajax_variable_list.js56
-rw-r--r--app/assets/javascripts/ci_variable_list/ci_variable_list.js37
-rw-r--r--app/assets/javascripts/ci_variable_list/native_form_variable_list.js5
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js8
-rw-r--r--app/assets/javascripts/comment_type_toggle.js56
-rw-r--r--app/assets/javascripts/commit/image_file.js320
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js10
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue122
-rw-r--r--app/assets/javascripts/commit_merge_requests.js7
-rw-r--r--app/assets/javascripts/commits.js38
-rw-r--r--app/assets/javascripts/commons/bootstrap.js12
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js24
-rw-r--r--app/assets/javascripts/contextual_sidebar.js7
-rw-r--r--app/assets/javascripts/create_item_dropdown.js13
-rw-r--r--app/assets/javascripts/create_label.js63
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue6
-rw-r--r--app/assets/javascripts/deploy_keys/service/index.js9
-rw-r--r--app/assets/javascripts/diff.js14
-rw-r--r--app/assets/javascripts/diffs/components/app.vue18
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue4
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue116
-rw-r--r--app/assets/javascripts/diffs/store/actions.js24
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js94
-rw-r--r--app/assets/javascripts/dropzone_input.js41
-rw-r--r--app/assets/javascripts/due_date_select.js3
-rw-r--r--app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js55
-rw-r--r--app/assets/javascripts/experimental_flags.js2
-rw-r--r--app/assets/javascripts/files_comment_button.js12
-rw-r--r--app/assets/javascripts/filterable_list.js25
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js5
-rw-r--r--app/assets/javascripts/flash.js42
-rw-r--r--app/assets/javascripts/fly_out_nav.js52
-rw-r--r--app/assets/javascripts/gl_field_error.js3
-rw-r--r--app/assets/javascripts/gl_field_errors.js14
-rw-r--r--app/assets/javascripts/gl_form.js19
-rw-r--r--app/assets/javascripts/group_avatar.js5
-rw-r--r--app/assets/javascripts/group_label_subscription.js10
-rw-r--r--app/assets/javascripts/groups/components/item_stats.vue72
-rw-r--r--app/assets/javascripts/groups/components/item_stats_value.vue86
-rw-r--r--app/assets/javascripts/groups/new_group_child.js22
-rw-r--r--app/assets/javascripts/groups/store/groups_store.js23
-rw-r--r--app/assets/javascripts/groups/transfer_dropdown.js2
-rw-r--r--app/assets/javascripts/groups_select.js9
-rw-r--r--app/assets/javascripts/helpers/avatar_helper.js4
-rw-r--r--app/assets/javascripts/image_diff/image_diff.js6
-rw-r--r--app/assets/javascripts/image_diff/init_discussion_tab.js3
-rw-r--r--app/assets/javascripts/image_diff/replaced_image_diff.js15
-rw-r--r--app/assets/javascripts/importer_status.js122
-rw-r--r--app/assets/javascripts/init_changes_dropdown.js2
-rw-r--r--app/assets/javascripts/init_notes.js8
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js5
-rw-r--r--app/assets/javascripts/issuable/auto_width_dropdown_select.js5
-rw-r--r--app/assets/javascripts/issuable_bulk_update_actions.js10
-rw-r--r--app/assets/javascripts/issuable_form.js34
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue6
-rw-r--r--app/assets/javascripts/jobs/components/job_container_item.vue11
-rw-r--r--app/assets/javascripts/jobs/components/stuck_block.vue16
-rw-r--r--app/assets/javascripts/jobs/store/getters.js11
-rw-r--r--app/assets/javascripts/lib/utils/datefix.js28
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js106
-rw-r--r--app/assets/javascripts/lib/utils/pretty_time.js63
-rw-r--r--app/assets/javascripts/member_expiration_date.js2
-rw-r--r--app/assets/javascripts/mr_notes/index.js2
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue5
-rw-r--r--app/assets/javascripts/notes/components/discussion_filter.vue82
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue11
-rw-r--r--app/assets/javascripts/notes/discussion_filters.js33
-rw-r--r--app/assets/javascripts/notes/index.js3
-rw-r--r--app/assets/javascripts/notes/services/notes_service.js5
-rw-r--r--app/assets/javascripts/notes/stores/actions.js25
-rw-r--r--app/assets/javascripts/notes/stores/getters.js6
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js4
-rw-r--r--app/assets/javascripts/notes/stores/utils.js12
-rw-r--r--app/assets/javascripts/pager.js11
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue192
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue34
-rw-r--r--app/assets/javascripts/vue_shared/components/pikaday.vue2
-rw-r--r--app/assets/stylesheets/framework/common.scss10
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss1
-rw-r--r--app/assets/stylesheets/pages/diff.scss14
-rw-r--r--app/assets/stylesheets/pages/issues.scss23
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss10
-rw-r--r--app/assets/stylesheets/pages/notes.scss21
-rw-r--r--app/controllers/concerns/issuable_actions.rb33
-rw-r--r--app/controllers/concerns/notes_actions.rb17
-rw-r--r--app/controllers/projects/notes_controller.rb2
-rw-r--r--app/finders/notes_finder.rb6
-rw-r--r--app/helpers/groups_helper.rb9
-rw-r--r--app/helpers/issuables_helper.rb4
-rw-r--r--app/models/ci/job_artifact.rb12
-rw-r--r--app/models/ci/pipeline.rb10
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/platforms/kubernetes.rb6
-rw-r--r--app/models/note.rb9
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/project_services/kubernetes_service.rb6
-rw-r--r--app/models/user.rb8
-rw-r--r--app/models/user_preference.rb52
-rw-r--r--app/serializers/build_details_entity.rb1
-rw-r--r--app/serializers/current_user_entity.rb8
-rw-r--r--app/serializers/merge_request_user_entity.rb2
-rw-r--r--app/serializers/user_preference_entity.rb10
-rw-r--r--app/services/clusters/gcp/finalize_creation_service.rb7
-rw-r--r--app/views/instance_statistics/conversational_development_index/_disabled.html.haml2
-rw-r--r--app/views/layouts/_flash.html.haml2
-rw-r--r--app/views/layouts/_page.html.haml4
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml14
-rw-r--r--app/views/projects/issues/_discussion.html.haml2
-rw-r--r--app/views/projects/issues/_new_branch.html.haml3
-rw-r--r--app/views/projects/issues/show.html.haml7
-rw-r--r--app/views/projects/merge_requests/show.html.haml6
-rw-r--r--app/views/projects/pipelines/_info.html.haml6
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml15
-rw-r--r--app/views/shared/issuable/_sidebar_todo.html.haml4
-rw-r--r--app/views/shared/runners/show.html.haml2
-rwxr-xr-xbin/secpick14
-rw-r--r--changelogs/unreleased/26723-discussion-filters.yml5
-rw-r--r--changelogs/unreleased/32959-update-todo-icon.yml5
-rw-r--r--changelogs/unreleased/42611-removed-branch-link.yml5
-rw-r--r--changelogs/unreleased/52059-filter-milestone-by-none-any.yml5
-rw-r--r--changelogs/unreleased/52202-consider-moving-isjobstuck-verification-to-backend.yml5
-rw-r--r--changelogs/unreleased/52840-fix-runners-details-page.yml5
-rw-r--r--changelogs/unreleased/53013-duplicate-escape.yml5
-rw-r--r--changelogs/unreleased/53023-endless-scroll-loader-is-visible-on-user-profile-overview-page.yml4
-rw-r--r--changelogs/unreleased/53055-combine-date-util-functions.yml5
-rw-r--r--changelogs/unreleased/53070-fix-enable-usage-ping-link.yml5
-rw-r--r--changelogs/unreleased/add-role-binding-to-kubeclient.yml5
-rw-r--r--changelogs/unreleased/gt-add-transparent-background-to-markdown-header-tabs.yml5
-rw-r--r--changelogs/unreleased/lfs-project-attribute-alias.yml5
-rw-r--r--changelogs/unreleased/mr-file-list.yml5
-rw-r--r--changelogs/unreleased/sh-pages-eof-error.yml5
-rw-r--r--changelogs/unreleased/support-license-management-and-performance.yml5
-rw-r--r--changelogs/unreleased/update-runner-chart-to-0-1-35.yml5
-rw-r--r--config/gitlab.yml.example3
-rw-r--r--danger/specs/Dangerfile6
-rw-r--r--db/migrate/20180925200829_create_user_preferences.rb31
-rw-r--r--db/schema.rb11
-rw-r--r--doc/administration/pages/index.md27
-rw-r--r--doc/administration/pages/source.md38
-rw-r--r--doc/ci/runners/README.md2
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--doc/development/feature_flags.md5
-rw-r--r--doc/development/i18n/proofreader.md1
-rw-r--r--doc/install/installation.md6
-rw-r--r--doc/topics/autodevops/index.md115
-rw-r--r--doc/update/11.3-to-11.4.md6
-rw-r--r--doc/user/project/milestones/index.md3
-rw-r--r--doc/user/project/repository/branches/index.md9
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata.rb7
-rw-r--r--lib/gitlab/ci/config/entry/reports.rb4
-rw-r--r--lib/gitlab/ci/templates/Android.gitlab-ci.yml62
-rw-r--r--lib/gitlab/database/migration_helpers.rb2
-rw-r--r--lib/gitlab/ee_compat_check.rb10
-rw-r--r--lib/gitlab/kubernetes/kube_client.rb70
-rw-r--r--lib/gitlab/kubernetes/role_binding.rb47
-rw-r--r--lib/gitlab/setup_helper.rb5
-rw-r--r--lib/tasks/haml-lint.rake11
-rw-r--r--locale/gitlab.pot24
-rw-r--r--package.json2
-rwxr-xr-xscripts/review_apps/review-apps.sh37
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb7
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb8
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb31
-rw-r--r--spec/factories/ci/runners.rb10
-rw-r--r--spec/factories/user_preferences.rb12
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb10
-rw-r--r--spec/features/projects/jobs_spec.rb61
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb18
-rw-r--r--spec/finders/notes_finder_spec.rb21
-rw-r--r--spec/fixtures/api/schemas/job/job_details.json3
-rw-r--r--spec/javascripts/collapsed_sidebar_todo_spec.js12
-rw-r--r--spec/javascripts/datetime_utility_spec.js160
-rw-r--r--spec/javascripts/diffs/components/tree_list_spec.js72
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js9
-rw-r--r--spec/javascripts/diffs/store/mutations_spec.js70
-rw-r--r--spec/javascripts/diffs/store/utils_spec.js22
-rw-r--r--spec/javascripts/flash_spec.js4
-rw-r--r--spec/javascripts/jobs/components/job_app_spec.js150
-rw-r--r--spec/javascripts/jobs/store/getters_spec.js30
-rw-r--r--spec/javascripts/lib/utils/datefix_spec.js27
-rw-r--r--spec/javascripts/notes/components/discussion_filter_spec.js60
-rw-r--r--spec/javascripts/notes/components/note_app_spec.js3
-rw-r--r--spec/javascripts/notes/mock_data.js15
-rw-r--r--spec/javascripts/pretty_time_spec.js135
-rw-r--r--spec/javascripts/vue_shared/components/file_row_spec.js36
-rw-r--r--spec/lib/gitaly/server_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb30
-rw-r--r--spec/lib/gitlab/ci/config/entry/reports_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/kube_client_spec.rb113
-rw-r--r--spec/lib/gitlab/kubernetes/role_binding_spec.rb47
-rw-r--r--spec/models/ci/pipeline_spec.rb35
-rw-r--r--spec/models/clusters/applications/runner_spec.rb6
-rw-r--r--spec/models/note_spec.rb24
-rw-r--r--spec/models/user_preference_spec.rb32
-rw-r--r--spec/models/user_spec.rb9
-rw-r--r--spec/services/ci/retry_build_service_spec.rb1
-rw-r--r--spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb1
-rw-r--r--spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb1
-rw-r--r--spec/support/helpers/ci_artifact_metadata_generator.rb48
-rw-r--r--spec/support/helpers/test_env.rb11
-rw-r--r--spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb54
-rw-r--r--spec/support/stored_repositories.rb4
-rw-r--r--spec/views/shared/runners/show.html.haml_spec.rb155
-rw-r--r--spec/workers/repository_check/batch_worker_spec.rb2
-rw-r--r--yarn.lock7
221 files changed, 3513 insertions, 1686 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c652b6c75e2..c3163b687b4 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.18-chrome-69.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.5-golang-1.9-git-2.18-chrome-69.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29"
.dedicated-runner: &dedicated-runner
retry: 1
@@ -6,7 +6,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git
- gitlab-org
.default-cache: &default-cache
- key: "ruby-2.4.4-debian-stretch-with-yarn"
+ key: "ruby-2.4.5-debian-stretch-with-yarn"
paths:
- vendor/ruby
- .yarn-cache/
@@ -75,11 +75,6 @@ stages:
- mysql:5.7
- redis:alpine
-.rails5-variables: &rails5-variables
- script:
- - export RAILS5=${RAILS5}
- - export BUNDLE_GEMFILE=${BUNDLE_GEMFILE}
-
.rails5: &rails5
allow_failure: true
only:
@@ -139,7 +134,7 @@ stages:
- export SCRIPT_NAME="${SCRIPT_NAME:-$CI_JOB_NAME}"
- apk add --update openssl
- wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/$SCRIPT_NAME
- - chmod 755 $SCRIPT_NAME
+ - chmod 755 $(basename $SCRIPT_NAME)
.rake-exec: &rake-exec
<<: *dedicated-no-docs-no-db-pull-cache-job
@@ -150,7 +145,6 @@ stages:
<<: *dedicated-runner
<<: *except-docs-and-qa
<<: *pull-cache
- <<: *rails5-variables
stage: test
script:
- JOB_NAME=( $CI_JOB_NAME )
@@ -594,7 +588,7 @@ static-analysis:
script:
- scripts/static-analysis
cache:
- key: "ruby-2.4.4-debian-stretch-with-yarn-and-rubocop"
+ key: "ruby-2.4.5-debian-stretch-with-yarn-and-rubocop"
paths:
- vendor/ruby
- .yarn-cache/
@@ -723,7 +717,7 @@ gitlab:assets:compile:
- public/assets/
karma:
- <<: *dedicated-no-docs-and-no-qa-pull-cache-job
+ <<: *dedicated-no-docs-pull-cache-job
<<: *use-pg
dependencies:
- compile-assets
@@ -929,3 +923,93 @@ no_ee_check:
- scripts/no-ee-check
only:
- //@gitlab-org/gitlab-ce
+
+# GitLab Review apps
+review:
+ image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
+ stage: test
+ allow_failure: true
+ before_script:
+ - gem install gitlab --no-document
+ variables:
+ GIT_DEPTH: "1"
+ HOST_SUFFIX: "$CI_ENVIRONMENT_SLUG"
+ DOMAIN: "-$CI_ENVIRONMENT_SLUG.$REVIEW_APPS_DOMAIN"
+ GITLAB_HELM_CHART_REF: "master"
+ script:
+ - export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION)
+ - export GITALY_VERSION=$(<GITALY_SERVER_VERSION)
+ - export GITLAB_WORKHORSE_VERSION=$(<GITLAB_WORKHORSE_VERSION)
+ - source ./scripts/review_apps/review-apps.sh
+ - BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng
+ - check_kube_domain
+ - download_gitlab_chart
+ - ensure_namespace
+ - install_tiller
+ - create_secret
+ - install_external_dns
+ - deploy
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ url: https://gitlab-$CI_ENVIRONMENT_SLUG.$REVIEW_APPS_DOMAIN
+ on_stop: stop_review
+ only:
+ refs:
+ - branches@gitlab-org/gitlab-ce
+ - branches@gitlab-org/gitlab-ee
+ kubernetes: active
+ except:
+ refs:
+ - master
+ - /(^docs[\/-].*|.*-docs$)/
+
+stop_review:
+ <<: *single-script-job
+ image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
+ stage: test
+ allow_failure: true
+ cache: {}
+ dependencies: []
+ variables:
+ SCRIPT_NAME: "review_apps/review-apps.sh"
+ script:
+ - source $(basename "${SCRIPT_NAME}")
+ - delete
+ - cleanup
+ when: manual
+ environment:
+ name: review/$CI_COMMIT_REF_NAME
+ action: stop
+ only:
+ refs:
+ - branches@gitlab-org/gitlab-ce
+ - branches@gitlab-org/gitlab-ee
+ kubernetes: active
+ except:
+ - master
+ - /(^docs[\/-].*|.*-docs$)/
+
+schedule:review_apps_cleanup:
+ <<: *dedicated-no-docs-pull-cache-job
+ image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
+ stage: build
+ allow_failure: true
+ cache: {}
+ dependencies: []
+ before_script:
+ - gem install gitlab --no-document
+ variables:
+ GIT_DEPTH: "1"
+ script:
+ - ruby -rrubygems scripts/review_apps/automated_cleanup.rb
+ environment:
+ name: review/auto-cleanup
+ action: stop
+ only:
+ refs:
+ - schedules@gitlab-org/gitlab-ce
+ - schedules@gitlab-org/gitlab-ee
+ kubernetes: active
+ except:
+ - tags
+ - /(^docs[\/-].*|.*-docs$)/
diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md
index 64b54b171f7..69cf7fe1548 100644
--- a/.gitlab/issue_templates/Security developer workflow.md
+++ b/.gitlab/issue_templates/Security developer workflow.md
@@ -16,7 +16,6 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Add a link to the MR to the [links section](#links)
- [ ] Add a link to an EE MR if required
- [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**.
-- [ ] Assign the MR to a RM once is reviewed and ready to be merged. Check the [RM list] to see who to ping.
#### Backports
@@ -26,7 +25,8 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Create the branch `security-X-Y` from `X-Y-stable` if it doesn't exist (and make sure it's up to date with stable)
- [ ] Create each MR targetting the security branch `security-X-Y`
- [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR
-- [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager.
+- [ ] Add the ~"Merge into Security" label to all of the MRs.
+- [ ] Make sure all MRs have a link in the [links section](#links)
[secpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script
diff --git a/.ruby-version b/.ruby-version
index 79a614418f7..59aa62c1fa4 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.4.4
+2.4.5
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 33e061fe7a0..bcc9c2840a7 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.125.1
+0.126.0
diff --git a/Gemfile b/Gemfile
index 64d87baf697..c442ed9065e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -417,8 +417,7 @@ end
gem 'gitaly-proto', '~> 0.118.1', require: 'gitaly'
gem 'grpc', '~> 1.15.0'
-# Locked until https://github.com/google/protobuf/issues/4210 is closed
-gem 'google-protobuf', '= 3.5.1'
+gem 'google-protobuf', '~> 3.6'
gem 'toml-rb', '~> 1.0.0', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index a39788bee9f..bf16bef4f32 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -303,7 +303,7 @@ GEM
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
- google-protobuf (3.5.1)
+ google-protobuf (3.6.1)
googleapis-common-protos-types (1.0.2)
google-protobuf (~> 3.0)
googleauth (0.6.6)
@@ -1005,7 +1005,7 @@ DEPENDENCIES
gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2)
google-api-client (~> 0.23)
- google-protobuf (= 3.5.1)
+ google-protobuf (~> 3.6)
gpgme
grape (~> 1.1)
grape-entity (~> 0.7.1)
diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock
index 1421edb1d39..81547303ed2 100644
--- a/Gemfile.rails5.lock
+++ b/Gemfile.rails5.lock
@@ -306,7 +306,7 @@ GEM
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
- google-protobuf (3.5.1)
+ google-protobuf (3.6.1)
googleapis-common-protos-types (1.0.2)
google-protobuf (~> 3.0)
googleauth (0.6.6)
@@ -1014,7 +1014,7 @@ DEPENDENCIES
gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2)
google-api-client (~> 0.23)
- google-protobuf (= 3.5.1)
+ google-protobuf (~> 3.6)
gpgme
grape (~> 1.1)
grape-entity (~> 0.7.1)
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
index de4566bb119..05de970e387 100644
--- a/app/assets/javascripts/activities.js
+++ b/app/assets/javascripts/activities.js
@@ -6,10 +6,12 @@ import Pager from './pager';
import { localTimeAgo } from './lib/utils/datetime_utility';
export default class Activities {
- constructor() {
- Pager.init(20, true, false, data => data, this.updateTooltips);
+ constructor(container = '') {
+ this.container = container;
- $('.event-filter-link').on('click', (e) => {
+ Pager.init(20, true, false, data => data, this.updateTooltips, this.container);
+
+ $('.event-filter-link').on('click', e => {
e.preventDefault();
this.toggleFilter(e.currentTarget);
this.reloadActivities();
@@ -22,7 +24,7 @@ export default class Activities {
reloadActivities() {
$('.content_list').html('');
- Pager.init(20, true, false, data => data, this.updateTooltips);
+ Pager.init(20, true, false, data => data, this.updateTooltips, this.container);
}
toggleFilter(sender) {
diff --git a/app/assets/javascripts/breadcrumb.js b/app/assets/javascripts/breadcrumb.js
index 1474d93dde6..a37838694ec 100644
--- a/app/assets/javascripts/breadcrumb.js
+++ b/app/assets/javascripts/breadcrumb.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-export const addTooltipToEl = (el) => {
+export const addTooltipToEl = el => {
const textEl = el.querySelector('.js-breadcrumb-item-text');
if (textEl && textEl.scrollWidth > textEl.offsetWidth) {
@@ -14,17 +14,18 @@ export default () => {
const breadcrumbs = document.querySelector('.js-breadcrumbs-list');
if (breadcrumbs) {
- const topLevelLinks = [...breadcrumbs.children].filter(el => !el.classList.contains('dropdown'))
+ const topLevelLinks = [...breadcrumbs.children]
+ .filter(el => !el.classList.contains('dropdown'))
.map(el => el.querySelector('a'))
.filter(el => el);
const $expander = $('.js-breadcrumbs-collapsed-expander');
topLevelLinks.forEach(el => addTooltipToEl(el));
- $expander.closest('.dropdown')
- .on('show.bs.dropdown hide.bs.dropdown', (e) => {
- $('.js-breadcrumbs-collapsed-expander', e.currentTarget).toggleClass('open')
- .tooltip('hide');
- });
+ $expander.closest('.dropdown').on('show.bs.dropdown hide.bs.dropdown', e => {
+ $('.js-breadcrumbs-collapsed-expander', e.currentTarget)
+ .toggleClass('open')
+ .tooltip('hide');
+ });
}
};
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
index e338376fcaa..97a1645aa51 100644
--- a/app/assets/javascripts/build_artifacts.js
+++ b/app/assets/javascripts/build_artifacts.js
@@ -12,16 +12,16 @@ export default class BuildArtifacts {
}
// eslint-disable-next-line class-methods-use-this
disablePropagation() {
- $('.top-block').on('click', '.download', function (e) {
+ $('.top-block').on('click', '.download', function(e) {
return e.stopPropagation();
});
- return $('.tree-holder').on('click', 'tr[data-link] a', function (e) {
+ return $('.tree-holder').on('click', 'tr[data-link] a', function(e) {
return e.stopImmediatePropagation();
});
}
// eslint-disable-next-line class-methods-use-this
setupEntryClick() {
- return $('.tree-holder').on('click', 'tr[data-link]', function () {
+ return $('.tree-holder').on('click', 'tr[data-link]', function() {
visitUrl(this.dataset.link, convertPermissionToBoolean(this.dataset.externalLink));
});
}
@@ -37,11 +37,15 @@ export default class BuildArtifacts {
// We want the tooltip to show if you hover anywhere on the row
// But be placed below and in the middle of the file name
$('.js-artifact-tree-row')
- .on('mouseenter', (e) => {
- $(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('show');
+ .on('mouseenter', e => {
+ $(e.currentTarget)
+ .find('.js-artifact-tree-tooltip')
+ .tooltip('show');
})
- .on('mouseleave', (e) => {
- $(e.currentTarget).find('.js-artifact-tree-tooltip').tooltip('hide');
+ .on('mouseleave', e => {
+ $(e.currentTarget)
+ .find('.js-artifact-tree-tooltip')
+ .tooltip('hide');
});
}
}
diff --git a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
index b33adff609f..1089d0a72d3 100644
--- a/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ajax_variable_list.js
@@ -7,11 +7,13 @@ import statusCodes from '../lib/utils/http_status';
import VariableList from './ci_variable_list';
function generateErrorBoxContent(errors) {
- const errorList = [].concat(errors).map(errorString => `
+ const errorList = [].concat(errors).map(
+ errorString => `
<li>
${_.escape(errorString)}
</li>
- `);
+ `,
+ );
return `
<p>
@@ -25,13 +27,7 @@ function generateErrorBoxContent(errors) {
// Used for the variable list on CI/CD projects/groups settings page
export default class AjaxVariableList {
- constructor({
- container,
- saveButton,
- errorBox,
- formField = 'variables',
- saveEndpoint,
- }) {
+ constructor({ container, saveButton, errorBox, formField = 'variables', saveEndpoint }) {
this.container = container;
this.saveButton = saveButton;
this.errorBox = errorBox;
@@ -58,18 +54,21 @@ export default class AjaxVariableList {
// to match it up in `updateRowsWithPersistedVariables`
this.variableList.toggleEnableRow(false);
- return axios.patch(this.saveEndpoint, {
- variables_attributes: this.variableList.getAllData(),
- }, {
- // We want to be able to process the `res.data` from a 400 error response
- // and print the validation messages such as duplicate variable keys
- validateStatus: status => (
- status >= statusCodes.OK &&
- status < statusCodes.MULTIPLE_CHOICES
- ) ||
- status === statusCodes.BAD_REQUEST,
- })
- .then((res) => {
+ return axios
+ .patch(
+ this.saveEndpoint,
+ {
+ variables_attributes: this.variableList.getAllData(),
+ },
+ {
+ // We want to be able to process the `res.data` from a 400 error response
+ // and print the validation messages such as duplicate variable keys
+ validateStatus: status =>
+ (status >= statusCodes.OK && status < statusCodes.MULTIPLE_CHOICES) ||
+ status === statusCodes.BAD_REQUEST,
+ },
+ )
+ .then(res => {
loadingIcon.classList.toggle('hide', true);
this.variableList.toggleEnableRow(true);
@@ -90,18 +89,21 @@ export default class AjaxVariableList {
}
updateRowsWithPersistedVariables(persistedVariables = []) {
- const persistedVariableMap = [].concat(persistedVariables).reduce((variableMap, variable) => ({
- ...variableMap,
- [variable.key]: variable,
- }), {});
+ const persistedVariableMap = [].concat(persistedVariables).reduce(
+ (variableMap, variable) => ({
+ ...variableMap,
+ [variable.key]: variable,
+ }),
+ {},
+ );
- this.container.querySelectorAll('.js-row').forEach((row) => {
+ this.container.querySelectorAll('.js-row').forEach(row => {
// If we submitted a row that was destroyed, remove it so we don't try
// to destroy it again which would cause a BE error
const destroyInput = row.querySelector('.js-ci-variable-input-destroy');
if (convertPermissionToBoolean(destroyInput.value)) {
row.remove();
- // Update the ID input so any future edits and `_destroy` will apply on the BE
+ // Update the ID input so any future edits and `_destroy` will apply on the BE
} else {
const key = row.querySelector('.js-ci-variable-input-key').value;
const persistedVariable = persistedVariableMap[key];
diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
index 47efb3a8cee..7bdc18ce03e 100644
--- a/app/assets/javascripts/ci_variable_list/ci_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js
@@ -16,10 +16,7 @@ function createEnvironmentItem(value) {
}
export default class VariableList {
- constructor({
- container,
- formField,
- }) {
+ constructor({ container, formField }) {
this.$container = $(container);
this.formField = formField;
this.environmentDropdownMap = new WeakMap();
@@ -71,7 +68,7 @@ export default class VariableList {
this.initRow(rowEl);
});
- this.$container.on('click', '.js-row-remove-button', (e) => {
+ this.$container.on('click', '.js-row-remove-button', e => {
e.preventDefault();
this.removeRow($(e.currentTarget).closest('.js-row'));
});
@@ -81,7 +78,7 @@ export default class VariableList {
.join(',');
// Remove any empty rows except the last row
- this.$container.on('blur', inputSelector, (e) => {
+ this.$container.on('blur', inputSelector, e => {
const $row = $(e.currentTarget).closest('.js-row');
if ($row.is(':not(:last-child)') && !this.checkIfRowTouched($row)) {
@@ -136,7 +133,7 @@ export default class VariableList {
$rowClone.removeAttr('data-is-persisted');
// Reset the inputs to their defaults
- Object.keys(this.inputMap).forEach((name) => {
+ Object.keys(this.inputMap).forEach(name => {
const entry = this.inputMap[name];
$rowClone.find(entry.selector).val(entry.default);
});
@@ -171,7 +168,7 @@ export default class VariableList {
}
checkIfRowTouched($row) {
- return Object.keys(this.inputMap).some((name) => {
+ return Object.keys(this.inputMap).some(name => {
const entry = this.inputMap[name];
const $el = $row.find(entry.selector);
return $el.length && $el.val() !== entry.default;
@@ -190,11 +187,14 @@ export default class VariableList {
getAllData() {
// Ignore the last empty row because we don't want to try persist
// a blank variable and run into validation problems.
- const validRows = this.$container.find('.js-row').toArray().slice(0, -1);
+ const validRows = this.$container
+ .find('.js-row')
+ .toArray()
+ .slice(0, -1);
- return validRows.map((rowEl) => {
+ return validRows.map(rowEl => {
const resultant = {};
- Object.keys(this.inputMap).forEach((name) => {
+ Object.keys(this.inputMap).forEach(name => {
const entry = this.inputMap[name];
const $input = $(rowEl).find(entry.selector);
if ($input.length) {
@@ -207,11 +207,16 @@ export default class VariableList {
}
getEnvironmentValues() {
- const valueMap = this.$container.find(this.inputMap.environment_scope.selector).toArray()
- .reduce((prevValueMap, envInput) => ({
- ...prevValueMap,
- [envInput.value]: envInput.value,
- }), {});
+ const valueMap = this.$container
+ .find(this.inputMap.environment_scope.selector)
+ .toArray()
+ .reduce(
+ (prevValueMap, envInput) => ({
+ ...prevValueMap,
+ [envInput.value]: envInput.value,
+ }),
+ {},
+ );
return Object.keys(valueMap).map(createEnvironmentItem);
}
diff --git a/app/assets/javascripts/ci_variable_list/native_form_variable_list.js b/app/assets/javascripts/ci_variable_list/native_form_variable_list.js
index 7cd5916ac9c..e7111c666a2 100644
--- a/app/assets/javascripts/ci_variable_list/native_form_variable_list.js
+++ b/app/assets/javascripts/ci_variable_list/native_form_variable_list.js
@@ -2,10 +2,7 @@ import $ from 'jquery';
import VariableList from './ci_variable_list';
// Used for the variable list on scheduled pipeline edit page
-export default function setupNativeFormVariableList({
- container,
- formField = 'variables',
-}) {
+export default function setupNativeFormVariableList({ container, formField = 'variables' }) {
const $container = $(container);
const variableList = new VariableList({
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index d90db7b103c..106ac3cb516 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -76,12 +76,8 @@ export default class ClusterStore {
this.state.status = serverState.status;
this.state.statusReason = serverState.status_reason;
- serverState.applications.forEach((serverAppEntry) => {
- const {
- name: appId,
- status,
- status_reason: statusReason,
- } = serverAppEntry;
+ serverState.applications.forEach(serverAppEntry => {
+ const { name: appId, status, status_reason: statusReason } = serverAppEntry;
this.state.applications[appId] = {
...(this.state.applications[appId] || {}),
diff --git a/app/assets/javascripts/comment_type_toggle.js b/app/assets/javascripts/comment_type_toggle.js
index c74184949df..a259667bb75 100644
--- a/app/assets/javascripts/comment_type_toggle.js
+++ b/app/assets/javascripts/comment_type_toggle.js
@@ -24,36 +24,44 @@ class CommentTypeToggle {
setConfig() {
const config = {
- InputSetter: [{
- input: this.noteTypeInput,
- valueAttribute: 'data-value',
- },
- {
- input: this.submitButton,
- valueAttribute: 'data-submit-text',
- }],
+ InputSetter: [
+ {
+ input: this.noteTypeInput,
+ valueAttribute: 'data-value',
+ },
+ {
+ input: this.submitButton,
+ valueAttribute: 'data-submit-text',
+ },
+ ],
};
if (this.closeButton) {
- config.InputSetter.push({
- input: this.closeButton,
- valueAttribute: 'data-close-text',
- }, {
- input: this.closeButton,
- valueAttribute: 'data-close-text',
- inputAttribute: 'data-alternative-text',
- });
+ config.InputSetter.push(
+ {
+ input: this.closeButton,
+ valueAttribute: 'data-close-text',
+ },
+ {
+ input: this.closeButton,
+ valueAttribute: 'data-close-text',
+ inputAttribute: 'data-alternative-text',
+ },
+ );
}
if (this.reopenButton) {
- config.InputSetter.push({
- input: this.reopenButton,
- valueAttribute: 'data-reopen-text',
- }, {
- input: this.reopenButton,
- valueAttribute: 'data-reopen-text',
- inputAttribute: 'data-alternative-text',
- });
+ config.InputSetter.push(
+ {
+ input: this.reopenButton,
+ valueAttribute: 'data-reopen-text',
+ },
+ {
+ input: this.reopenButton,
+ valueAttribute: 'data-reopen-text',
+ inputAttribute: 'data-alternative-text',
+ },
+ );
}
return config;
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index 30d9b656fec..d4ecfa4aa93 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -9,44 +9,60 @@ const viewModes = ['two-up', 'swipe'];
export default class ImageFile {
constructor(file) {
this.file = file;
- this.requestImageInfo($('.two-up.view .frame.deleted img', this.file), (function(_this) {
- return function(deletedWidth, deletedHeight) {
- return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(width, height) {
- _this.initViewModes();
-
- // Load two-up view after images are loaded
- // so that we can display the correct width and height information
- const $images = $('.two-up.view img', _this.file);
-
- $images.waitForImages(function() {
- _this.initView('two-up');
+ this.requestImageInfo(
+ $('.two-up.view .frame.deleted img', this.file),
+ (function(_this) {
+ return function(deletedWidth, deletedHeight) {
+ return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(
+ width,
+ height,
+ ) {
+ _this.initViewModes();
+
+ // Load two-up view after images are loaded
+ // so that we can display the correct width and height information
+ const $images = $('.two-up.view img', _this.file);
+
+ $images.waitForImages(function() {
+ _this.initView('two-up');
+ });
});
- });
- };
- })(this));
+ };
+ })(this),
+ );
}
initViewModes() {
const viewMode = viewModes[0];
$('.view-modes', this.file).removeClass('hide');
- $('.view-modes-menu', this.file).on('click', 'li', (function(_this) {
- return function(event) {
- if (!$(event.currentTarget).hasClass('active')) {
- return _this.activateViewMode(event.currentTarget.className);
- }
- };
- })(this));
+ $('.view-modes-menu', this.file).on(
+ 'click',
+ 'li',
+ (function(_this) {
+ return function(event) {
+ if (!$(event.currentTarget).hasClass('active')) {
+ return _this.activateViewMode(event.currentTarget.className);
+ }
+ };
+ })(this),
+ );
return this.activateViewMode(viewMode);
}
activateViewMode(viewMode) {
- $('.view-modes-menu li', this.file).removeClass('active').filter("." + viewMode).addClass('active');
- return $(".view:visible:not(." + viewMode + ")", this.file).fadeOut(200, (function(_this) {
- return function() {
- $(".view." + viewMode, _this.file).fadeIn(200);
- return _this.initView(viewMode);
- };
- })(this));
+ $('.view-modes-menu li', this.file)
+ .removeClass('active')
+ .filter('.' + viewMode)
+ .addClass('active');
+ return $('.view:visible:not(.' + viewMode + ')', this.file).fadeOut(
+ 200,
+ (function(_this) {
+ return function() {
+ $('.view.' + viewMode, _this.file).fadeIn(200);
+ return _this.initView(viewMode);
+ };
+ })(this),
+ );
}
initView(viewMode) {
@@ -63,135 +79,154 @@ export default class ImageFile {
$body.css('user-select', 'none');
});
- $body.off('mouseup').off('mousemove').on('mouseup', function() {
- dragging = false;
- $body.css('user-select', '');
- })
- .on('mousemove', function(e) {
- var left;
- if (!dragging) return;
-
- left = e.pageX - ($offsetEl.offset().left + padding);
-
- callback(e, left);
- });
+ $body
+ .off('mouseup')
+ .off('mousemove')
+ .on('mouseup', function() {
+ dragging = false;
+ $body.css('user-select', '');
+ })
+ .on('mousemove', function(e) {
+ var left;
+ if (!dragging) return;
+
+ left = e.pageX - ($offsetEl.offset().left + padding);
+
+ callback(e, left);
+ });
}
prepareFrames(view) {
var maxHeight, maxWidth;
maxWidth = 0;
maxHeight = 0;
- $('.frame', view).each((function(_this) {
- return function(index, frame) {
- var height, width;
- width = $(frame).width();
- height = $(frame).height();
- maxWidth = width > maxWidth ? width : maxWidth;
- return maxHeight = height > maxHeight ? height : maxHeight;
- };
- })(this)).css({
- width: maxWidth,
- height: maxHeight
- });
+ $('.frame', view)
+ .each(
+ (function(_this) {
+ return function(index, frame) {
+ var height, width;
+ width = $(frame).width();
+ height = $(frame).height();
+ maxWidth = width > maxWidth ? width : maxWidth;
+ return (maxHeight = height > maxHeight ? height : maxHeight);
+ };
+ })(this),
+ )
+ .css({
+ width: maxWidth,
+ height: maxHeight,
+ });
return [maxWidth, maxHeight];
}
views = {
'two-up': function() {
- return $('.two-up.view .wrap', this.file).each((function(_this) {
- return function(index, wrap) {
- $('img', wrap).each(function() {
- var currentWidth;
- currentWidth = $(this).width();
- if (currentWidth > availWidth / 2) {
- return $(this).width(availWidth / 2);
- }
- });
- return _this.requestImageInfo($('img', wrap), function(width, height) {
- $('.image-info .meta-width', wrap).text(width + "px");
- $('.image-info .meta-height', wrap).text(height + "px");
- return $('.image-info', wrap).removeClass('hide');
- });
- };
- })(this));
+ return $('.two-up.view .wrap', this.file).each(
+ (function(_this) {
+ return function(index, wrap) {
+ $('img', wrap).each(function() {
+ var currentWidth;
+ currentWidth = $(this).width();
+ if (currentWidth > availWidth / 2) {
+ return $(this).width(availWidth / 2);
+ }
+ });
+ return _this.requestImageInfo($('img', wrap), function(width, height) {
+ $('.image-info .meta-width', wrap).text(width + 'px');
+ $('.image-info .meta-height', wrap).text(height + 'px');
+ return $('.image-info', wrap).removeClass('hide');
+ });
+ };
+ })(this),
+ );
},
- 'swipe': function() {
+ swipe() {
var maxHeight, maxWidth;
maxWidth = 0;
maxHeight = 0;
- return $('.swipe.view', this.file).each((function(_this) {
- return function(index, view) {
- var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref;
- ref = _this.prepareFrames(view), [maxWidth, maxHeight] = ref;
- $swipeFrame = $('.swipe-frame', view);
- $swipeWrap = $('.swipe-wrap', view);
- $swipeBar = $('.swipe-bar', view);
-
- $swipeFrame.css({
- width: maxWidth + 16,
- height: maxHeight + 28
- });
- $swipeWrap.css({
- width: maxWidth + 1,
- height: maxHeight + 2
- });
- // Set swipeBar left position to match image frame
- $swipeBar.css({
- left: 1
- });
-
- wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10);
-
- _this.initDraggable($swipeBar, wrapPadding, function(e, left) {
- if (left > 0 && left < $swipeFrame.width() - (wrapPadding * 2)) {
- $swipeWrap.width((maxWidth + 1) - left);
- $swipeBar.css('left', left);
- }
- });
- };
- })(this));
+ return $('.swipe.view', this.file).each(
+ (function(_this) {
+ return function(index, view) {
+ var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref;
+ (ref = _this.prepareFrames(view)), ([maxWidth, maxHeight] = ref);
+ $swipeFrame = $('.swipe-frame', view);
+ $swipeWrap = $('.swipe-wrap', view);
+ $swipeBar = $('.swipe-bar', view);
+
+ $swipeFrame.css({
+ width: maxWidth + 16,
+ height: maxHeight + 28,
+ });
+ $swipeWrap.css({
+ width: maxWidth + 1,
+ height: maxHeight + 2,
+ });
+ // Set swipeBar left position to match image frame
+ $swipeBar.css({
+ left: 1,
+ });
+
+ wrapPadding = parseInt($swipeWrap.css('right').replace('px', ''), 10);
+
+ _this.initDraggable($swipeBar, wrapPadding, function(e, left) {
+ if (left > 0 && left < $swipeFrame.width() - wrapPadding * 2) {
+ $swipeWrap.width(maxWidth + 1 - left);
+ $swipeBar.css('left', left);
+ }
+ });
+ };
+ })(this),
+ );
},
'onion-skin': function() {
var dragTrackWidth, maxHeight, maxWidth;
maxWidth = 0;
maxHeight = 0;
dragTrackWidth = $('.drag-track', this.file).width() - $('.dragger', this.file).width();
- return $('.onion-skin.view', this.file).each((function(_this) {
- return function(index, view) {
- var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false;
- ref = _this.prepareFrames(view), [maxWidth, maxHeight] = ref;
- $frame = $('.onion-skin-frame', view);
- $frameAdded = $('.frame.added', view);
- $track = $('.drag-track', view);
- $dragger = $('.dragger', $track);
-
- $frame.css({
- width: maxWidth + 16,
- height: maxHeight + 28
- });
- $('.swipe-wrap', view).css({
- width: maxWidth + 1,
- height: maxHeight + 2
- });
- $dragger.css({
- left: dragTrackWidth
- });
-
- $frameAdded.css('opacity', 1);
- framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
-
- _this.initDraggable($dragger, framePadding, function(e, left) {
- var opacity = left / dragTrackWidth;
-
- if (opacity >= 0 && opacity <= 1) {
- $dragger.css('left', left);
- $frameAdded.css('opacity', opacity);
- }
- });
- };
- })(this));
- }
- }
+ return $('.onion-skin.view', this.file).each(
+ (function(_this) {
+ return function(index, view) {
+ var $frame,
+ $track,
+ $dragger,
+ $frameAdded,
+ framePadding,
+ ref,
+ dragging = false;
+ (ref = _this.prepareFrames(view)), ([maxWidth, maxHeight] = ref);
+ $frame = $('.onion-skin-frame', view);
+ $frameAdded = $('.frame.added', view);
+ $track = $('.drag-track', view);
+ $dragger = $('.dragger', $track);
+
+ $frame.css({
+ width: maxWidth + 16,
+ height: maxHeight + 28,
+ });
+ $('.swipe-wrap', view).css({
+ width: maxWidth + 1,
+ height: maxHeight + 2,
+ });
+ $dragger.css({
+ left: dragTrackWidth,
+ });
+
+ $frameAdded.css('opacity', 1);
+ framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
+
+ _this.initDraggable($dragger, framePadding, function(e, left) {
+ var opacity = left / dragTrackWidth;
+
+ if (opacity >= 0 && opacity <= 1) {
+ $dragger.css('left', left);
+ $frameAdded.css('opacity', opacity);
+ }
+ });
+ };
+ })(this),
+ );
+ },
+ };
requestImageInfo(img, callback) {
const domImg = img.get(0);
@@ -199,11 +234,14 @@ export default class ImageFile {
if (domImg.complete) {
return callback.call(this, domImg.naturalWidth, domImg.naturalHeight);
} else {
- return img.on('load', (function(_this) {
- return function() {
- return callback.call(_this, domImg.naturalWidth, domImg.naturalHeight);
- };
- })(this));
+ return img.on(
+ 'load',
+ (function(_this) {
+ return function() {
+ return callback.call(_this, domImg.naturalWidth, domImg.naturalHeight);
+ };
+ })(this),
+ );
}
}
}
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index 3d89bf1316e..340a93e4e66 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -19,11 +19,13 @@ export default () => {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
if (pipelineTableViewEl) {
- // Update MR and Commits tabs
- pipelineTableViewEl.addEventListener('update-pipelines-count', (event) => {
- if (event.detail.pipelines &&
+ // Update MR and Commits tabs
+ pipelineTableViewEl.addEventListener('update-pipelines-count', event => {
+ if (
+ event.detail.pipelines &&
event.detail.pipelines.count &&
- event.detail.pipelines.count.all) {
+ event.detail.pipelines.count.all
+ ) {
const badge = document.querySelector('.js-pipelines-mr-count');
badge.textContent = event.detail.pipelines.count.all;
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index 4849b0fa3db..a2aa3d197e3 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -1,77 +1,73 @@
<script>
- import PipelinesService from '../../pipelines/services/pipelines_service';
- import PipelineStore from '../../pipelines/stores/pipelines_store';
- import pipelinesMixin from '../../pipelines/mixins/pipelines';
+import PipelinesService from '../../pipelines/services/pipelines_service';
+import PipelineStore from '../../pipelines/stores/pipelines_store';
+import pipelinesMixin from '../../pipelines/mixins/pipelines';
- export default {
- mixins: [
- pipelinesMixin,
- ],
- props: {
- endpoint: {
- type: String,
- required: true,
- },
- helpPagePath: {
- type: String,
- required: true,
- },
- autoDevopsHelpPath: {
- type: String,
- required: true,
- },
- errorStateSvgPath: {
- type: String,
- required: true,
- },
- viewType: {
- type: String,
- required: false,
- default: 'child',
- },
+export default {
+ mixins: [pipelinesMixin],
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
},
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ autoDevopsHelpPath: {
+ type: String,
+ required: true,
+ },
+ errorStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ viewType: {
+ type: String,
+ required: false,
+ default: 'child',
+ },
+ },
- data() {
- const store = new PipelineStore();
+ data() {
+ const store = new PipelineStore();
- return {
- store,
- state: store.state,
- };
- },
+ return {
+ store,
+ state: store.state,
+ };
+ },
- computed: {
- shouldRenderTable() {
- return !this.isLoading &&
- this.state.pipelines.length > 0 &&
- !this.hasError;
- },
- shouldRenderErrorState() {
- return this.hasError && !this.isLoading;
- },
+ computed: {
+ shouldRenderTable() {
+ return !this.isLoading && this.state.pipelines.length > 0 && !this.hasError;
},
- created() {
- this.service = new PipelinesService(this.endpoint);
+ shouldRenderErrorState() {
+ return this.hasError && !this.isLoading;
},
- methods: {
- successCallback(resp) {
- // depending of the endpoint the response can either bring a `pipelines` key or not.
- const pipelines = resp.data.pipelines || resp.data;
- this.setCommonData(pipelines);
+ },
+ created() {
+ this.service = new PipelinesService(this.endpoint);
+ },
+ methods: {
+ successCallback(resp) {
+ // depending of the endpoint the response can either bring a `pipelines` key or not.
+ const pipelines = resp.data.pipelines || resp.data;
+ this.setCommonData(pipelines);
- const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
- detail: {
- pipelines: resp.data,
- },
- });
+ const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
+ detail: {
+ pipelines: resp.data,
+ },
+ });
- // notifiy to update the count in tabs
- if (this.$el.parentElement) {
- this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
- }
- },
+ // notifiy to update the count in tabs
+ if (this.$el.parentElement) {
+ this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
+ }
},
- };
+ },
+};
</script>
<template>
<div class="content-list pipelines">
diff --git a/app/assets/javascripts/commit_merge_requests.js b/app/assets/javascripts/commit_merge_requests.js
index 102b4ee8463..3a0ab119df6 100644
--- a/app/assets/javascripts/commit_merge_requests.js
+++ b/app/assets/javascripts/commit_merge_requests.js
@@ -50,7 +50,7 @@ export function createContent(mergeRequests) {
if (mergeRequests.length === 0) {
$content.text(s__('Commits|No related merge requests found'));
} else {
- mergeRequests.forEach((mergeRequest) => {
+ mergeRequests.forEach(mergeRequest => {
const $header = createHeader($content.children().length, mergeRequests.length);
const $item = createItem(mergeRequest);
$content.append($header);
@@ -64,8 +64,9 @@ export function createContent(mergeRequests) {
export function fetchCommitMergeRequests() {
const $container = $('.merge-requests');
- axios.get($container.data('projectCommitPath'))
- .then((response) => {
+ axios
+ .get($container.data('projectCommitPath'))
+ .then(response => {
const $content = createContent(response.data);
$container.html($content);
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index 9a3ea7a55b6..54e2589c707 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -32,22 +32,31 @@ export default class CommitsList {
if (search === this.lastSearch) return Promise.resolve();
const commitsUrl = `${form.attr('action')}?${form.serialize()}`;
this.content.fadeTo('fast', 0.5);
- const params = form.serializeArray().reduce((acc, obj) => Object.assign(acc, {
- [obj.name]: obj.value,
- }), {});
+ const params = form.serializeArray().reduce(
+ (acc, obj) =>
+ Object.assign(acc, {
+ [obj.name]: obj.value,
+ }),
+ {},
+ );
- return axios.get(form.attr('action'), {
- params,
- })
+ return axios
+ .get(form.attr('action'), {
+ params,
+ })
.then(({ data }) => {
this.lastSearch = search;
this.content.html(data.html);
this.content.fadeTo('fast', 1.0);
// Change url so if user reload a page - search results are saved
- window.history.replaceState({
- page: commitsUrl,
- }, document.title, commitsUrl);
+ window.history.replaceState(
+ {
+ page: commitsUrl,
+ },
+ document.title,
+ commitsUrl,
+ );
})
.catch(() => {
this.content.fadeTo('fast', 1.0);
@@ -75,8 +84,15 @@ export default class CommitsList {
processedData = $processedData.not(`li.js-commit-header[data-day='${loadedShownDayFirst}']`);
// Update commits count in the previous commits header.
- commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length);
- $commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${pluralize('commit', commitsCount)}`);
+ commitsCount += Number(
+ $(processedData)
+ .nextUntil('li.js-commit-header')
+ .first()
+ .find('li.commit').length,
+ );
+ $commitsHeadersLast
+ .find('span.commits-count')
+ .text(`${commitsCount} ${pluralize('commit', commitsCount)}`);
}
localTimeAgo($processedData.find('.js-timeago'));
diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js
index 50e2949ab55..fba30aea9ae 100644
--- a/app/assets/javascripts/commons/bootstrap.js
+++ b/app/assets/javascripts/commons/bootstrap.js
@@ -5,6 +5,14 @@ import 'bootstrap';
// custom jQuery functions
$.fn.extend({
- disable() { return $(this).prop('disabled', true).addClass('disabled'); },
- enable() { return $(this).prop('disabled', false).removeClass('disabled'); },
+ disable() {
+ return $(this)
+ .prop('disabled', true)
+ .addClass('disabled');
+ },
+ enable() {
+ return $(this)
+ .prop('disabled', false)
+ .removeClass('disabled');
+ },
});
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
index b0c85c2572e..1000c310e35 100644
--- a/app/assets/javascripts/confirm_danger_modal.js
+++ b/app/assets/javascripts/confirm_danger_modal.js
@@ -13,19 +13,23 @@ function openConfirmDangerModal($form, text) {
$submit.disable();
$input.focus();
- $('.js-confirm-danger-input').off('input').on('input', function handleInput() {
- const confirmText = rstrip($(this).val());
- if (confirmText === confirmTextMatch) {
- $submit.enable();
- } else {
- $submit.disable();
- }
- });
- $('.js-confirm-danger-submit').off('click').on('click', () => $form.submit());
+ $('.js-confirm-danger-input')
+ .off('input')
+ .on('input', function handleInput() {
+ const confirmText = rstrip($(this).val());
+ if (confirmText === confirmTextMatch) {
+ $submit.enable();
+ } else {
+ $submit.disable();
+ }
+ });
+ $('.js-confirm-danger-submit')
+ .off('click')
+ .on('click', () => $form.submit());
}
export default function initConfirmDangerModal() {
- $(document).on('click', '.js-confirm-danger', (e) => {
+ $(document).on('click', '.js-confirm-danger', e => {
e.preventDefault();
const $btn = $(e.target);
const $form = $btn.closest('form');
diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js
index 3a50e73ad85..dff0adba25a 100644
--- a/app/assets/javascripts/contextual_sidebar.js
+++ b/app/assets/javascripts/contextual_sidebar.js
@@ -20,8 +20,11 @@ export default class ContextualSidebar {
}
bindEvents() {
- document.addEventListener('click', (e) => {
- if (!e.target.closest('.nav-sidebar') && (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md')) {
+ document.addEventListener('click', e => {
+ if (
+ !e.target.closest('.nav-sidebar') &&
+ (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md')
+ ) {
this.toggleCollapsedSidebar(true);
}
});
diff --git a/app/assets/javascripts/create_item_dropdown.js b/app/assets/javascripts/create_item_dropdown.js
index 8ef9aa7f529..916b190f469 100644
--- a/app/assets/javascripts/create_item_dropdown.js
+++ b/app/assets/javascripts/create_item_dropdown.js
@@ -36,7 +36,7 @@ export default class CreateItemDropdown {
},
selectable: true,
toggleLabel(selected) {
- return (selected && 'id' in selected) ? _.escape(selected.title) : this.defaultToggleLabel;
+ return selected && 'id' in selected ? _.escape(selected.title) : this.defaultToggleLabel;
},
fieldName: this.fieldName,
text(item) {
@@ -46,7 +46,7 @@ export default class CreateItemDropdown {
return _.escape(item.id);
},
onFilter: this.toggleCreateNewButton.bind(this),
- clicked: (options) => {
+ clicked: options => {
options.e.preventDefault();
this.onSelect();
},
@@ -77,9 +77,8 @@ export default class CreateItemDropdown {
getData(term, callback) {
this.getDataOption(term, (data = []) => {
// Ensure the selected item isn't already in the data to avoid duplicates
- const alreadyHasSelectedItem = this.selectedItem && data.some(item =>
- item.id === this.selectedItem.id,
- );
+ const alreadyHasSelectedItem =
+ this.selectedItem && data.some(item => item.id === this.selectedItem.id);
let uniqueData = data;
if (!alreadyHasSelectedItem) {
@@ -106,9 +105,7 @@ export default class CreateItemDropdown {
if (newValue) {
this.selectedItem = this.createNewItemFromValue(newValue);
- this.$dropdownContainer
- .find('.js-dropdown-create-new-item code')
- .text(newValue);
+ this.$dropdownContainer.find('.js-dropdown-create-new-item code').text(newValue);
}
this.toggleFooter(!newValue);
diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js
index a999c21b2e9..28ca7d97314 100644
--- a/app/assets/javascripts/create_label.js
+++ b/app/assets/javascripts/create_label.js
@@ -37,7 +37,7 @@ export default class CreateLabelDropdown {
addBinding() {
const self = this;
- this.$colorSuggestions.on('click', function (e) {
+ this.$colorSuggestions.on('click', function(e) {
const $this = $(this);
self.addColorValue(e, $this);
});
@@ -47,7 +47,7 @@ export default class CreateLabelDropdown {
this.$dropdownBack.on('click', this.resetForm.bind(this));
- this.$cancelButton.on('click', function (e) {
+ this.$cancelButton.on('click', function(e) {
e.preventDefault();
e.stopPropagation();
@@ -79,13 +79,9 @@ export default class CreateLabelDropdown {
}
resetForm() {
- this.$newLabelField
- .val('')
- .trigger('change');
+ this.$newLabelField.val('').trigger('change');
- this.$newColorField
- .val('')
- .trigger('change');
+ this.$newColorField.val('').trigger('change');
this.$colorPreview
.css('background-color', '')
@@ -97,31 +93,34 @@ export default class CreateLabelDropdown {
e.preventDefault();
e.stopPropagation();
- Api.newLabel(this.namespacePath, this.projectPath, {
- title: this.$newLabelField.val(),
- color: this.$newColorField.val(),
- }, (label) => {
- this.$newLabelCreateButton.enable();
-
- if (label.message) {
- let errors;
-
- if (typeof label.message === 'string') {
- errors = label.message;
+ Api.newLabel(
+ this.namespacePath,
+ this.projectPath,
+ {
+ title: this.$newLabelField.val(),
+ color: this.$newColorField.val(),
+ },
+ label => {
+ this.$newLabelCreateButton.enable();
+
+ if (label.message) {
+ let errors;
+
+ if (typeof label.message === 'string') {
+ errors = label.message;
+ } else {
+ errors = Object.keys(label.message)
+ .map(key => `${humanize(key)} ${label.message[key].join(', ')}`)
+ .join('<br/>');
+ }
+
+ this.$newLabelError.html(errors).show();
} else {
- errors = Object.keys(label.message).map(key =>
- `${humanize(key)} ${label.message[key].join(', ')}`,
- ).join('<br/>');
- }
+ this.$dropdownBack.trigger('click');
- this.$newLabelError
- .html(errors)
- .show();
- } else {
- this.$dropdownBack.trigger('click');
-
- $(document).trigger('created.label', label);
- }
- });
+ $(document).trigger('created.label', label);
+ }
+ },
+ );
}
}
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index aa52f120fe7..3589599986d 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -95,8 +95,10 @@ export default {
.catch(() => new Flash(s__('DeployKeys|Error enabling deploy key')));
},
disableKey(deployKey, callback) {
- // eslint-disable-next-line no-alert
- if (window.confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) {
+ if (
+ // eslint-disable-next-line no-alert
+ window.confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))
+ ) {
this.service
.disableKey(deployKey.id)
.then(this.fetchKeys)
diff --git a/app/assets/javascripts/deploy_keys/service/index.js b/app/assets/javascripts/deploy_keys/service/index.js
index 9dc3b21f6f6..268a37008c5 100644
--- a/app/assets/javascripts/deploy_keys/service/index.js
+++ b/app/assets/javascripts/deploy_keys/service/index.js
@@ -8,17 +8,14 @@ export default class DeployKeysService {
}
getKeys() {
- return this.axios.get()
- .then(response => response.data);
+ return this.axios.get().then(response => response.data);
}
enableKey(id) {
- return this.axios.put(`${id}/enable`)
- .then(response => response.data);
+ return this.axios.put(`${id}/enable`).then(response => response.data);
}
disableKey(id) {
- return this.axios.put(`${id}/disable`)
- .then(response => response.data);
+ return this.axios.put(`${id}/disable`).then(response => response.data);
}
}
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index a044fc1ab42..245f1a7c558 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -21,9 +21,12 @@ export default class Diff {
});
const tab = document.getElementById('diffs');
- if (!tab || (tab && tab.dataset && tab.dataset.isLocked !== '')) FilesCommentButton.init($diffFile);
+ if (!tab || (tab && tab.dataset && tab.dataset.isLocked !== ''))
+ FilesCommentButton.init($diffFile);
- const firstFile = $('.files').first().get(0);
+ const firstFile = $('.files')
+ .first()
+ .get(0);
const canCreateNote = firstFile && firstFile.hasAttribute('data-can-create-note');
$diffFile.each((index, file) => imageDiffHelper.initImageDiff(file, canCreateNote));
@@ -73,9 +76,10 @@ export default class Diff {
const view = file.data('view');
const params = { since, to, bottom, offset, unfold, view };
- axios.get(link, { params })
- .then(({ data }) => $target.parent().replaceWith(data))
- .catch(() => flash(__('An error occurred while loading diff')));
+ axios
+ .get(link, { params })
+ .then(({ data }) => $target.parent().replaceWith(data))
+ .catch(() => flash(__('An error occurred while loading diff')));
}
openAnchoredDiff(cb) {
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index edca45f22f9..a8d615dd8f0 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -41,6 +41,11 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ assignedDiscussions: false,
+ };
+ },
computed: {
...mapState({
isLoading: state => state.diffs.isLoading,
@@ -58,9 +63,9 @@ export default {
plainDiffPath: state => state.diffs.plainDiffPath,
emailPatchPath: state => state.diffs.emailPatchPath,
}),
- ...mapState('diffs', ['showTreeList']),
+ ...mapState('diffs', ['showTreeList', 'isLoading']),
...mapGetters('diffs', ['isParallelView']),
- ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
+ ...mapGetters(['isNotesFetched', 'getNoteableData']),
targetBranch() {
return {
branchName: this.targetBranchName,
@@ -147,11 +152,12 @@ export default {
}
},
setDiscussions() {
- if (this.isNotesFetched) {
+ if (this.isNotesFetched && !this.assignedDiscussions && !this.isLoading) {
requestIdleCallback(
- () => {
- this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode);
- },
+ () =>
+ this.assignDiscussionsToDiff().then(() => {
+ this.assignedDiscussions = true;
+ }),
{ timeout: 1000 },
);
}
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index f72c7a84e5c..958e57c5652 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -29,7 +29,7 @@ export default {
},
computed: {
...mapState('diffs', ['currentDiffFileId']),
- ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']),
+ ...mapGetters(['isNotesFetched']),
isCollapsed() {
return this.file.collapsed || false;
},
@@ -79,7 +79,7 @@ export default {
.then(() => {
requestIdleCallback(
() => {
- this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode);
+ this.assignDiscussionsToDiff();
},
{ timeout: 1000 },
);
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index cfe4273742f..34e836a570a 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -1,17 +1,30 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
+import { TooltipDirective as Tooltip } from '@gitlab-org/gitlab-ui';
+import { convertPermissionToBoolean } from '~/lib/utils/common_utils';
import Icon from '~/vue_shared/components/icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue';
import FileRowStats from './file_row_stats.vue';
+const treeListStorageKey = 'mr_diff_tree_list';
+
export default {
+ directives: {
+ Tooltip,
+ },
components: {
Icon,
FileRow,
},
data() {
+ const treeListStored = localStorage.getItem(treeListStorageKey);
+ const renderTreeList = treeListStored !== null ?
+ convertPermissionToBoolean(treeListStored) : true;
+
return {
search: '',
+ renderTreeList,
+ focusSearch: false,
};
},
computed: {
@@ -20,15 +33,35 @@ export default {
filteredTreeList() {
const search = this.search.toLowerCase().trim();
- if (search === '') return this.tree;
+ if (search === '') return this.renderTreeList ? this.tree : this.allBlobs;
return this.allBlobs.filter(f => f.name.toLowerCase().indexOf(search) >= 0);
},
+ rowDisplayTextKey() {
+ if (this.renderTreeList && this.search.trim() === '') {
+ return 'name';
+ }
+
+ return 'path';
+ },
},
methods: {
...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']),
clearSearch() {
this.search = '';
+ this.toggleFocusSearch(false);
+ },
+ toggleRenderTreeList(toggle) {
+ this.renderTreeList = toggle;
+ localStorage.setItem(treeListStorageKey, this.renderTreeList);
+ },
+ toggleFocusSearch(toggle) {
+ this.focusSearch = toggle;
+ },
+ blurSearch() {
+ if (this.search.trim() === '') {
+ this.toggleFocusSearch(false);
+ }
},
},
FileRowStats,
@@ -37,28 +70,67 @@ export default {
<template>
<div class="tree-list-holder d-flex flex-column">
- <div class="append-bottom-8 position-relative tree-list-search">
- <icon
- name="search"
- class="position-absolute tree-list-icon"
- />
- <input
- v-model="search"
- :placeholder="s__('MergeRequest|Filter files')"
- type="search"
- class="form-control"
- />
- <button
- v-show="search"
- :aria-label="__('Clear search')"
- type="button"
- class="position-absolute tree-list-icon tree-list-clear-icon border-0 p-0"
- @click="clearSearch"
- >
+ <div class="append-bottom-8 position-relative tree-list-search d-flex">
+ <div class="flex-fill d-flex">
<icon
- name="close"
+ name="search"
+ class="position-absolute tree-list-icon"
+ />
+ <input
+ v-model="search"
+ :placeholder="s__('MergeRequest|Filter files')"
+ type="search"
+ class="form-control"
+ @focus="toggleFocusSearch(true)"
+ @blur="blurSearch"
/>
- </button>
+ <button
+ v-show="search"
+ :aria-label="__('Clear search')"
+ type="button"
+ class="position-absolute bg-transparent tree-list-icon tree-list-clear-icon border-0 p-0"
+ @click="clearSearch"
+ >
+ <icon
+ name="close"
+ />
+ </button>
+ </div>
+ <div
+ v-show="!focusSearch"
+ class="btn-group prepend-left-8 tree-list-view-toggle"
+ >
+ <button
+ v-tooltip.hover
+ :aria-label="__('List view')"
+ :title="__('List view')"
+ :class="{
+ active: !renderTreeList
+ }"
+ class="btn btn-default pt-0 pb-0 d-flex align-items-center"
+ type="button"
+ @click="toggleRenderTreeList(false)"
+ >
+ <icon
+ name="hamburger"
+ />
+ </button>
+ <button
+ v-tooltip.hover
+ :aria-label="__('Tree view')"
+ :title="__('Tree view')"
+ :class="{
+ active: renderTreeList
+ }"
+ class="btn btn-default pt-0 pb-0 d-flex align-items-center"
+ type="button"
+ @click="toggleRenderTreeList(true)"
+ >
+ <icon
+ name="file-tree"
+ />
+ </button>
+ </div>
</div>
<div
class="tree-list-scroll"
@@ -72,6 +144,8 @@ export default {
:hide-extra-on-tree="true"
:extra-component="$options.FileRowStats"
:show-changed-icon="true"
+ :display-text-key="rowDisplayTextKey"
+ :should-truncate-start="true"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="scrollToFile"
/>
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 1e0b27b538d..ca8ae605cb4 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -5,7 +5,6 @@ import createFlash from '~/flash';
import { s__ } from '~/locale';
import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils';
import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility';
-import { reduceDiscussionsToLineCodes } from '../../notes/stores/utils';
import { getDiffPositionByLineCode, getNoteFormData } from './utils';
import * as types from './mutation_types';
import {
@@ -36,18 +35,17 @@ export const fetchDiffFiles = ({ state, commit }) => {
// This is adding line discussions to the actual lines in the diff tree
// once for parallel and once for inline mode
-export const assignDiscussionsToDiff = ({ state, commit }, allLineDiscussions) => {
+export const assignDiscussionsToDiff = (
+ { commit, state, rootState },
+ discussions = rootState.notes.discussions,
+) => {
const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
- Object.values(allLineDiscussions).forEach(discussions => {
- if (discussions.length > 0) {
- const { fileHash } = discussions[0];
- commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, {
- fileHash,
- discussions,
- diffPositionByLineCode,
- });
- }
+ discussions.filter(discussion => discussion.diff_discussion).forEach(discussion => {
+ commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, {
+ discussion,
+ diffPositionByLineCode,
+ });
});
};
@@ -190,9 +188,7 @@ export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => {
return dispatch('saveNote', postData, { root: true })
.then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
- .then(discussion =>
- dispatch('assignDiscussionsToDiff', reduceDiscussionsToLineCodes([discussion])),
- )
+ .then(discussion => dispatch('assignDiscussionsToDiff', [discussion]))
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
};
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 0b4485ecdb5..5a8aebd2086 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -90,53 +90,67 @@ export default {
}));
},
- [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, discussions, diffPositionByLineCode }) {
- const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash);
- const firstDiscussion = discussions[0];
- const isDiffDiscussion = firstDiscussion.diff_discussion;
- const hasLineCode = firstDiscussion.line_code;
- const diffPosition = diffPositionByLineCode[firstDiscussion.line_code];
-
- if (
- selectedFile &&
- isDiffDiscussion &&
- hasLineCode &&
- diffPosition &&
+ [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode }) {
+ const { latestDiff } = state;
+
+ const discussionLineCode = discussion.line_code;
+ const fileHash = discussion.diff_file.file_hash;
+ const lineCheck = ({ lineCode }) =>
+ lineCode === discussionLineCode &&
isDiscussionApplicableToLine({
- discussion: firstDiscussion,
- diffPosition,
- latestDiff: state.latestDiff,
- })
- ) {
- const targetLine = selectedFile.parallelDiffLines.find(
- line =>
- (line.left && line.left.lineCode === firstDiscussion.line_code) ||
- (line.right && line.right.lineCode === firstDiscussion.line_code),
- );
- if (targetLine) {
- if (targetLine.left && targetLine.left.lineCode === firstDiscussion.line_code) {
- Object.assign(targetLine.left, {
- discussions,
- });
- } else {
- Object.assign(targetLine.right, {
- discussions,
+ discussion,
+ diffPosition: diffPositionByLineCode[lineCode],
+ latestDiff,
+ });
+
+ state.diffFiles = state.diffFiles.map(diffFile => {
+ if (diffFile.fileHash === fileHash) {
+ const file = { ...diffFile };
+
+ if (file.highlightedDiffLines) {
+ file.highlightedDiffLines = file.highlightedDiffLines.map(line => {
+ if (lineCheck(line)) {
+ return {
+ ...line,
+ discussions: line.discussions.concat(discussion),
+ };
+ }
+
+ return line;
});
}
- }
-
- if (selectedFile.highlightedDiffLines) {
- const targetInlineLine = selectedFile.highlightedDiffLines.find(
- line => line.lineCode === firstDiscussion.line_code,
- );
- if (targetInlineLine) {
- Object.assign(targetInlineLine, {
- discussions,
+ if (file.parallelDiffLines) {
+ file.parallelDiffLines = file.parallelDiffLines.map(line => {
+ const left = line.left && lineCheck(line.left);
+ const right = line.right && lineCheck(line.right);
+
+ if (left || right) {
+ return {
+ left: {
+ ...line.left,
+ discussions: left ? line.left.discussions.concat(discussion) : [],
+ },
+ right: {
+ ...line.right,
+ discussions: right ? line.right.discussions.concat(discussion) : [],
+ },
+ };
+ }
+
+ return line;
});
}
+
+ if (!file.parallelDiffLines || !file.highlightedDiffLines) {
+ file.discussions = file.discussions.concat(discussion);
+ }
+
+ return file;
}
- }
+
+ return diffFile;
+ });
},
[types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) {
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index d2778bcdf1c..9987fbcb6a7 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -136,7 +136,7 @@ export default function dropzoneInput(form) {
// removeAllFiles(true) stops uploading files (if any)
// and remove them from dropzone files queue.
- $cancelButton.on('click', (e) => {
+ $cancelButton.on('click', e => {
e.preventDefault();
e.stopPropagation();
Dropzone.forElement($formDropzone.get(0)).removeAllFiles(true);
@@ -146,8 +146,10 @@ export default function dropzoneInput(form) {
// clear dropzone files queue, change status of failed files to undefined,
// and add that files to the dropzone files queue again.
// addFile() adds file to dropzone files queue and upload it.
- $retryLink.on('click', (e) => {
- const dropzoneInstance = Dropzone.forElement(e.target.closest('.js-main-target-form').querySelector('.div-dropzone'));
+ $retryLink.on('click', e => {
+ const dropzoneInstance = Dropzone.forElement(
+ e.target.closest('.js-main-target-form').querySelector('.div-dropzone'),
+ );
const failedFiles = dropzoneInstance.files;
e.preventDefault();
@@ -156,7 +158,7 @@ export default function dropzoneInput(form) {
// uploading of files that are being uploaded at the moment.
dropzoneInstance.removeAllFiles(true);
- failedFiles.map((failedFile) => {
+ failedFiles.map(failedFile => {
const file = failedFile;
if (file.status === Dropzone.ERROR) {
@@ -168,7 +170,7 @@ export default function dropzoneInput(form) {
});
});
// eslint-disable-next-line consistent-return
- handlePaste = (event) => {
+ handlePaste = event => {
const pasteEvent = event.originalEvent;
if (pasteEvent.clipboardData && pasteEvent.clipboardData.items) {
const image = isImage(pasteEvent);
@@ -182,7 +184,7 @@ export default function dropzoneInput(form) {
}
};
- isImage = (data) => {
+ isImage = data => {
let i = 0;
while (i < data.clipboardData.items.length) {
const item = data.clipboardData.items[i];
@@ -203,8 +205,12 @@ export default function dropzoneInput(form) {
const caretStart = textarea.selectionStart;
const caretEnd = textarea.selectionEnd;
const textEnd = $(child).val().length;
- const beforeSelection = $(child).val().substring(0, caretStart);
- const afterSelection = $(child).val().substring(caretEnd, textEnd);
+ const beforeSelection = $(child)
+ .val()
+ .substring(0, caretStart);
+ const afterSelection = $(child)
+ .val()
+ .substring(caretEnd, textEnd);
$(child).val(beforeSelection + formattedText + afterSelection);
textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length);
textarea.style.height = `${textarea.scrollHeight}px`;
@@ -212,11 +218,11 @@ export default function dropzoneInput(form) {
return formTextarea.trigger('input');
};
- addFileToForm = (path) => {
+ addFileToForm = path => {
$(form).append(`<input type="hidden" name="files[]" value="${_.escape(path)}">`);
};
- getFilename = (e) => {
+ getFilename = e => {
let value;
if (window.clipboardData && window.clipboardData.getData) {
value = window.clipboardData.getData('Text');
@@ -231,7 +237,7 @@ export default function dropzoneInput(form) {
const closeSpinner = () => $uploadingProgressContainer.addClass('hide');
- const showError = (message) => {
+ const showError = message => {
$uploadingErrorContainer.removeClass('hide');
$uploadingErrorMessage.html(message);
};
@@ -252,14 +258,15 @@ export default function dropzoneInput(form) {
showSpinner();
closeAlertMessage();
- axios.post(uploadsPath, formData)
+ axios
+ .post(uploadsPath, formData)
.then(({ data }) => {
const md = data.link.markdown;
insertToTextArea(filename, md);
closeSpinner();
})
- .catch((e) => {
+ .catch(e => {
showError(e.response.data.message);
closeSpinner();
});
@@ -267,7 +274,8 @@ export default function dropzoneInput(form) {
updateAttachingMessage = (files, messageContainer) => {
let attachingMessage;
- const filesCount = files.filter(file => file.status === 'uploading' || file.status === 'queued').length;
+ const filesCount = files.filter(file => file.status === 'uploading' || file.status === 'queued')
+ .length;
// Dinamycally change uploading files text depending on files number in
// dropzone files queue.
@@ -282,7 +290,10 @@ export default function dropzoneInput(form) {
form.find('.markdown-selector').click(function onMarkdownClick(e) {
e.preventDefault();
- $(this).closest('.gfm-form').find('.div-dropzone').click();
+ $(this)
+ .closest('.gfm-form')
+ .find('.div-dropzone')
+ .click();
formTextarea.focus();
});
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index c7b5a35cc14..dbfcf8cc921 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -3,8 +3,7 @@ import Pikaday from 'pikaday';
import dateFormat from 'dateformat';
import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
-import { timeFor } from './lib/utils/datetime_utility';
-import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
+import { timeFor, parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
import boardsStore from './boards/stores/boards_store';
class DueDateSelect {
diff --git a/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js
index e9defb62cf8..c5f9fcf6358 100644
--- a/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js
+++ b/app/assets/javascripts/emoji/support/is_emoji_unicode_supported.js
@@ -13,9 +13,11 @@ const rainbowCodePoint = 127752; // parseInt('1F308', 16)
function isRainbowFlagEmoji(emojiUnicode) {
const characters = Array.from(emojiUnicode);
// Length 4 because flags are made of 2 characters which are surrogate pairs
- return emojiUnicode.length === 4 &&
+ return (
+ emojiUnicode.length === 4 &&
characters[0].codePointAt(0) === baseFlagCodePoint &&
- characters[1].codePointAt(0) === rainbowCodePoint;
+ characters[1].codePointAt(0) === rainbowCodePoint
+ );
}
// Chrome <57 renders keycaps oddly
@@ -26,22 +28,28 @@ function isKeycapEmoji(emojiUnicode) {
}
// Check for a skin tone variation emoji which aren't always supported
-const tone1 = 127995;// parseInt('1F3FB', 16)
-const tone5 = 127999;// parseInt('1F3FF', 16)
+const tone1 = 127995; // parseInt('1F3FB', 16)
+const tone5 = 127999; // parseInt('1F3FF', 16)
function isSkinToneComboEmoji(emojiUnicode) {
- return emojiUnicode.length > 2 && Array.from(emojiUnicode).some((char) => {
- const cp = char.codePointAt(0);
- return cp >= tone1 && cp <= tone5;
- });
+ return (
+ emojiUnicode.length > 2 &&
+ Array.from(emojiUnicode).some(char => {
+ const cp = char.codePointAt(0);
+ return cp >= tone1 && cp <= tone5;
+ })
+ );
}
// macOS supports most skin tone emoji's but
// doesn't support the skin tone versions of horse racing
-const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
+const horseRacingCodePoint = 127943; // parseInt('1F3C7', 16)
function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
const firstCharacter = Array.from(emojiUnicode)[0];
- return firstCharacter && firstCharacter.codePointAt(0) === horseRacingCodePoint &&
- isSkinToneComboEmoji(emojiUnicode);
+ return (
+ firstCharacter &&
+ firstCharacter.codePointAt(0) === horseRacingCodePoint &&
+ isSkinToneComboEmoji(emojiUnicode)
+ );
}
// Check for `family_*`, `kiss_*`, `couple_*`
@@ -52,7 +60,7 @@ const personEndCodePoint = 128105; // parseInt('1F469', 16)
function isPersonZwjEmoji(emojiUnicode) {
let hasPersonEmoji = false;
let hasZwj = false;
- Array.from(emojiUnicode).forEach((character) => {
+ Array.from(emojiUnicode).forEach(character => {
const cp = character.codePointAt(0);
if (cp === zwj) {
hasZwj = true;
@@ -80,10 +88,7 @@ function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) {
// in `isEmojiUnicodeSupported` logic
function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) {
const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode);
- return (
- (unicodeSupportMap.skinToneModifier && isSkinToneResult) ||
- !isSkinToneResult
- );
+ return (unicodeSupportMap.skinToneModifier && isSkinToneResult) || !isSkinToneResult;
}
// Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice
@@ -91,8 +96,7 @@ function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) {
function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode);
return (
- (unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) ||
- !isHorseRacingSkinToneResult
+ (unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) || !isHorseRacingSkinToneResult
);
}
@@ -100,10 +104,7 @@ function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnico
// in `isEmojiUnicodeSupported` logic
function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode);
- return (
- (unicodeSupportMap.personZwj && isPersonZwjResult) ||
- !isPersonZwjResult
- );
+ return (unicodeSupportMap.personZwj && isPersonZwjResult) || !isPersonZwjResult;
}
// Takes in a support map and determines whether
@@ -111,16 +112,20 @@ function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) {
//
// Combines all the edge case tests into a one-stop shop method
function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) {
- const isOlderThanChrome57 = unicodeSupportMap.meta && unicodeSupportMap.meta.isChrome &&
+ const isOlderThanChrome57 =
+ unicodeSupportMap.meta &&
+ unicodeSupportMap.meta.isChrome &&
unicodeSupportMap.meta.chromeVersion < 57;
// For comments about each scenario, see the comments above each individual respective function
- return unicodeSupportMap[unicodeVersion] &&
+ return (
+ unicodeSupportMap[unicodeVersion] &&
!(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) &&
checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) &&
checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) &&
checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) &&
- checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode);
+ checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode)
+ );
}
export {
diff --git a/app/assets/javascripts/experimental_flags.js b/app/assets/javascripts/experimental_flags.js
index 1d60847147b..42b3fb8c6da 100644
--- a/app/assets/javascripts/experimental_flags.js
+++ b/app/assets/javascripts/experimental_flags.js
@@ -2,7 +2,7 @@ import $ from 'jquery';
import Cookies from 'js-cookie';
export default () => {
- $('.js-experiment-feature-toggle').on('change', (e) => {
+ $('.js-experiment-feature-toggle').on('change', e => {
const el = e.target;
Cookies.set(el.name, el.value, {
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index 6a4874e1ab8..3233f5c4f71 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -25,13 +25,15 @@ export default {
if (!this.userCanCreateNote) {
// data-can-create-note is an empty string when true, otherwise undefined
- this.userCanCreateNote = $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('canCreateNote') === '';
+ this.userCanCreateNote =
+ $diffFile.closest(DIFF_CONTAINER_SELECTOR).data('canCreateNote') === '';
}
this.isParallelView = Cookies.get('diff_view') === 'parallel';
if (this.userCanCreateNote) {
- $diffFile.on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e))
+ $diffFile
+ .on('mouseover', LINE_COLUMN_CLASSES, e => this.showButton(this.isParallelView, e))
.on('mouseleave', LINE_COLUMN_CLASSES, e => this.hideButton(this.isParallelView, e));
}
},
@@ -64,9 +66,11 @@ export default {
},
validateButtonParent(buttonParentElement) {
- return !buttonParentElement.classList.contains(EMPTY_CELL_CLASS) &&
+ return (
+ !buttonParentElement.classList.contains(EMPTY_CELL_CLASS) &&
!buttonParentElement.classList.contains(UNFOLDABLE_LINE_CLASS) &&
!buttonParentElement.classList.contains(NO_COMMENT_CLASS) &&
- !buttonParentElement.parentNode.classList.contains(DIFF_EXPANDED_CLASS);
+ !buttonParentElement.parentNode.classList.contains(DIFF_EXPANDED_CLASS)
+ );
},
};
diff --git a/app/assets/javascripts/filterable_list.js b/app/assets/javascripts/filterable_list.js
index b17ba3c21db..64b09c8b62c 100644
--- a/app/assets/javascripts/filterable_list.js
+++ b/app/assets/javascripts/filterable_list.js
@@ -65,12 +65,15 @@ export default class FilterableList {
this.isBusy = true;
- return axios.get(this.getFilterEndpoint(), {
- params,
- }).then((res) => {
- this.onFilterSuccess(res, params);
- this.onFilterComplete();
- }).catch(() => this.onFilterComplete());
+ return axios
+ .get(this.getFilterEndpoint(), {
+ params,
+ })
+ .then(res => {
+ this.onFilterSuccess(res, params);
+ this.onFilterComplete();
+ })
+ .catch(() => this.onFilterComplete());
}
onFilterSuccess(response, queryData) {
@@ -81,9 +84,13 @@ export default class FilterableList {
// Change url so if user reload a page - search results are saved
const currentPath = this.getPagePath(queryData);
- return window.history.replaceState({
- page: currentPath,
- }, document.title, currentPath);
+ return window.history.replaceState(
+ {
+ page: currentPath,
+ },
+ document.title,
+ currentPath,
+ );
}
onFilterComplete() {
diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
index c4f0c41d3a8..b70125c80ca 100644
--- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
@@ -68,6 +68,11 @@ export const conditions = [
value: 'none',
},
{
+ url: 'milestone_title=Any+Milestone',
+ tokenKey: 'milestone',
+ value: 'any',
+ },
+ {
url: 'milestone_title=%23upcoming',
tokenKey: 'milestone',
value: 'upcoming',
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index a29de9ae899..749c09f897c 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -8,14 +8,19 @@ const hideFlash = (flashEl, fadeTransition = true) => {
});
}
- flashEl.addEventListener('transitionend', () => {
- flashEl.remove();
- window.dispatchEvent(new Event('resize'));
- if (document.body.classList.contains('flash-shown')) document.body.classList.remove('flash-shown');
- }, {
- once: true,
- passive: true,
- });
+ flashEl.addEventListener(
+ 'transitionend',
+ () => {
+ flashEl.remove();
+ window.dispatchEvent(new Event('resize'));
+ if (document.body.classList.contains('flash-shown'))
+ document.body.classList.remove('flash-shown');
+ },
+ {
+ once: true,
+ passive: true,
+ },
+ );
if (!fadeTransition) flashEl.dispatchEvent(new Event('transitionend'));
};
@@ -30,12 +35,12 @@ const createAction = config => `
</a>
`;
-const createFlashEl = (message, type, isInContentWrapper = false) => `
+const createFlashEl = (message, type, isFixedLayout = false) => `
<div
class="flash-${type}"
>
<div
- class="flash-text ${isInContentWrapper ? 'container-fluid container-limited' : ''}"
+ class="flash-text ${isFixedLayout ? 'container-fluid container-limited limit-container-width' : ''}"
>
${_.escape(message)}
</div>
@@ -69,12 +74,13 @@ const createFlash = function createFlash(
addBodyClass = false,
) {
const flashContainer = parent.querySelector('.flash-container');
+ const navigation = parent.querySelector('.content');
if (!flashContainer) return null;
- const isInContentWrapper = flashContainer.parentNode.classList.contains('content-wrapper');
+ const isFixedLayout = navigation ? navigation.parentNode.classList.contains('container-limited') : true;
- flashContainer.innerHTML = createFlashEl(message, type, isInContentWrapper);
+ flashContainer.innerHTML = createFlashEl(message, type, isFixedLayout);
const flashEl = flashContainer.querySelector(`.flash-${type}`);
removeFlashClickListener(flashEl, fadeTransition);
@@ -83,7 +89,9 @@ const createFlash = function createFlash(
flashEl.innerHTML += createAction(actionConfig);
if (actionConfig.clickHandler) {
- flashEl.querySelector('.flash-action').addEventListener('click', e => actionConfig.clickHandler(e));
+ flashEl
+ .querySelector('.flash-action')
+ .addEventListener('click', e => actionConfig.clickHandler(e));
}
}
@@ -94,11 +102,5 @@ const createFlash = function createFlash(
return flashContainer;
};
-export {
- createFlash as default,
- createFlashEl,
- createAction,
- hideFlash,
- removeFlashClickListener,
-};
+export { createFlash as default, createFlashEl, createAction, hideFlash, removeFlashClickListener };
window.Flash = createFlash;
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
index f820f0dc3f0..3ac00c51df4 100644
--- a/app/assets/javascripts/fly_out_nav.js
+++ b/app/assets/javascripts/fly_out_nav.js
@@ -11,9 +11,13 @@ let sidebar;
export const mousePos = [];
-export const setSidebar = (el) => { sidebar = el; };
+export const setSidebar = el => {
+ sidebar = el;
+};
export const getOpenMenu = () => currentOpenMenu;
-export const setOpenMenu = (menu = null) => { currentOpenMenu = menu; };
+export const setOpenMenu = (menu = null) => {
+ currentOpenMenu = menu;
+};
export const slope = (a, b) => (b.y - a.y) / (b.x - a.x);
@@ -21,9 +25,10 @@ let headerHeight = 50;
export const getHeaderHeight = () => headerHeight;
-export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-collapsed-desktop');
+export const isSidebarCollapsed = () =>
+ sidebar && sidebar.classList.contains('sidebar-collapsed-desktop');
-export const canShowActiveSubItems = (el) => {
+export const canShowActiveSubItems = el => {
if (el.classList.contains('active') && !isSidebarCollapsed()) {
return false;
}
@@ -31,7 +36,10 @@ export const canShowActiveSubItems = (el) => {
return true;
};
-export const canShowSubItems = () => bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md' || bp.getBreakpointSize() === 'lg';
+export const canShowSubItems = () =>
+ bp.getBreakpointSize() === 'sm' ||
+ bp.getBreakpointSize() === 'md' ||
+ bp.getBreakpointSize() === 'lg';
export const getHideSubItemsInterval = () => {
if (!currentOpenMenu || !mousePos.length) return 0;
@@ -41,11 +49,12 @@ export const getHideSubItemsInterval = () => {
const currentMousePosY = currentMousePos.y;
const [menuTop, menuBottom] = menuCornerLocs;
- if (currentMousePosY < menuTop.y ||
- currentMousePosY > menuBottom.y) return 0;
+ if (currentMousePosY < menuTop.y || currentMousePosY > menuBottom.y) return 0;
- if (slope(prevMousePos, menuBottom) < slope(currentMousePos, menuBottom) &&
- slope(prevMousePos, menuTop) > slope(currentMousePos, menuTop)) {
+ if (
+ slope(prevMousePos, menuBottom) < slope(currentMousePos, menuBottom) &&
+ slope(prevMousePos, menuTop) > slope(currentMousePos, menuTop)
+ ) {
return HIDE_INTERVAL_TIMEOUT;
}
@@ -56,11 +65,12 @@ export const calculateTop = (boundingRect, outerHeight) => {
const windowHeight = window.innerHeight;
const bottomOverflow = windowHeight - (boundingRect.top + outerHeight);
- return bottomOverflow < 0 ? (boundingRect.top - outerHeight) + boundingRect.height :
- boundingRect.top;
+ return bottomOverflow < 0
+ ? boundingRect.top - outerHeight + boundingRect.height
+ : boundingRect.top;
};
-export const hideMenu = (el) => {
+export const hideMenu = el => {
if (!el) return;
const parentEl = el.parentNode;
@@ -101,7 +111,7 @@ export const moveSubItemsToPosition = (el, subItems) => {
}
};
-export const showSubLevelItems = (el) => {
+export const showSubLevelItems = el => {
const subItems = el.querySelector('.sidebar-sub-level-items');
const isIconOnly = subItems && subItems.classList.contains('is-fly-out-only');
@@ -128,16 +138,20 @@ export const mouseEnterTopItems = (el, timeout = getHideSubItemsInterval()) => {
}, timeout);
};
-export const mouseLeaveTopItem = (el) => {
+export const mouseLeaveTopItem = el => {
const subItems = el.querySelector('.sidebar-sub-level-items');
- if (!canShowSubItems() || !canShowActiveSubItems(el) ||
- (subItems && subItems === currentOpenMenu)) return;
+ if (
+ !canShowSubItems() ||
+ !canShowActiveSubItems(el) ||
+ (subItems && subItems === currentOpenMenu)
+ )
+ return;
el.classList.remove(IS_OVER_CLASS);
};
-export const documentMouseMove = (e) => {
+export const documentMouseMove = e => {
mousePos.push({
x: e.clientX,
y: e.clientY,
@@ -146,7 +160,7 @@ export const documentMouseMove = (e) => {
if (mousePos.length > 6) mousePos.shift();
};
-export const subItemsMouseLeave = (relatedTarget) => {
+export const subItemsMouseLeave = relatedTarget => {
clearTimeout(timeoutId);
if (relatedTarget && !relatedTarget.closest(`.${IS_OVER_CLASS}`)) {
@@ -174,7 +188,7 @@ export default () => {
headerHeight = document.querySelector('.nav-sidebar').offsetTop;
- items.forEach((el) => {
+ items.forEach(el => {
const subItems = el.querySelector('.sidebar-sub-level-items');
if (subItems) {
diff --git a/app/assets/javascripts/gl_field_error.js b/app/assets/javascripts/gl_field_error.js
index 87c6e37b9fb..a5b8c357e8a 100644
--- a/app/assets/javascripts/gl_field_error.js
+++ b/app/assets/javascripts/gl_field_error.js
@@ -116,7 +116,8 @@ export default class GlFieldError {
this.form.focusOnFirstInvalid.apply(this.form);
// For UX, wait til after first invalid submission to check each keyup
- this.inputElement.off('keyup.fieldValidator')
+ this.inputElement
+ .off('keyup.fieldValidator')
.on('keyup.fieldValidator', this.updateValidity.bind(this));
}
diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js
index b9c51045b1d..3764e7ab422 100644
--- a/app/assets/javascripts/gl_field_errors.js
+++ b/app/assets/javascripts/gl_field_errors.js
@@ -16,9 +16,12 @@ export default class GlFieldErrors {
initValidators() {
// register selectors here as needed
const validateSelectors = [':text', ':password', '[type=email]']
- .map(selector => `input${selector}`).join(',');
+ .map(selector => `input${selector}`)
+ .join(',');
- this.state.inputs = this.form.find(validateSelectors).toArray()
+ this.state.inputs = this.form
+ .find(validateSelectors)
+ .toArray()
.filter(input => !input.classList.contains(customValidationFlag))
.map(input => new GlFieldError({ input, formErrors: this }));
@@ -42,7 +45,7 @@ export default class GlFieldErrors {
/* Public method for triggering validity updates manually */
updateFormValidityState() {
- this.state.inputs.forEach((field) => {
+ this.state.inputs.forEach(field => {
if (field.state.submitted) {
field.updateValidity();
}
@@ -50,8 +53,9 @@ export default class GlFieldErrors {
}
focusOnFirstInvalid() {
- const firstInvalid = this.state.inputs
- .filter(input => !input.inputDomElement.validity.valid)[0];
+ const firstInvalid = this.state.inputs.filter(
+ input => !input.inputDomElement.validity.valid,
+ )[0];
firstInvalid.inputElement.focus();
}
}
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index e672284a2d0..f842d2d74db 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -39,7 +39,10 @@ export default class GLForm {
this.form.find('.div-dropzone').remove();
this.form.addClass('gfm-form');
// remove notify commit author checkbox for non-commit notes
- gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
+ gl.utils.disableButtonIfEmptyField(
+ this.form.find('.js-note-text'),
+ this.form.find('.js-comment-button, .js-note-new-discussion'),
+ );
this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
this.autoComplete.setup(this.form.find('.js-gfm-input'), this.enableGFM);
dropzoneInput(this.form);
@@ -55,11 +58,9 @@ export default class GLForm {
}
setupAutosize() {
- this.textarea.off('autosize:resized')
- .on('autosize:resized', this.setHeightData.bind(this));
+ this.textarea.off('autosize:resized').on('autosize:resized', this.setHeightData.bind(this));
- this.textarea.off('mouseup.autosize')
- .on('mouseup.autosize', this.destroyAutosize.bind(this));
+ this.textarea.off('mouseup.autosize').on('mouseup.autosize', this.destroyAutosize.bind(this));
setTimeout(() => {
autosize(this.textarea);
@@ -91,10 +92,14 @@ export default class GLForm {
addEventListeners() {
this.textarea.on('focus', function focusTextArea() {
- $(this).closest('.md-area').addClass('is-focused');
+ $(this)
+ .closest('.md-area')
+ .addClass('is-focused');
});
this.textarea.on('blur', function blurTextArea() {
- $(this).closest('.md-area').removeClass('is-focused');
+ $(this)
+ .closest('.md-area')
+ .removeClass('is-focused');
});
}
}
diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js
index beaac61e887..dcda625f587 100644
--- a/app/assets/javascripts/group_avatar.js
+++ b/app/assets/javascripts/group_avatar.js
@@ -7,8 +7,9 @@ export default function groupAvatar() {
});
$('.js-group-avatar-input').on('change', function onChangeAvatarInput() {
const form = $(this).closest('form');
- // eslint-disable-next-line no-useless-escape
- const filename = $(this).val().replace(/^.*[\\\/]/, '');
+ const filename = $(this)
+ .val()
+ .replace(/^.*[\\\/]/, ''); // eslint-disable-line no-useless-escape
return form.find('.js-avatar-filename').text(filename);
});
}
diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js
index d33e3a37580..9b74560f914 100644
--- a/app/assets/javascripts/group_label_subscription.js
+++ b/app/assets/javascripts/group_label_subscription.js
@@ -23,7 +23,8 @@ export default class GroupLabelSubscription {
event.preventDefault();
const url = this.$unsubscribeButtons.attr('data-url');
- axios.post(url)
+ axios
+ .post(url)
.then(() => {
this.toggleSubscriptionButtons();
this.$unsubscribeButtons.removeAttr('data-url');
@@ -39,7 +40,8 @@ export default class GroupLabelSubscription {
this.$unsubscribeButtons.attr('data-url', url);
- axios.post(url)
+ axios
+ .post(url)
.then(() => GroupLabelSubscription.setNewTooltip($btn))
.then(() => this.toggleSubscriptionButtons())
.catch(() => flash(__('There was an error when subscribing to this label.')));
@@ -58,6 +60,8 @@ export default class GroupLabelSubscription {
const newTitle = tooltipTitles[type];
$('.js-unsubscribe-button', $button.closest('.label-actions-list'))
- .tooltip('hide').attr('title', newTitle).tooltip('_fixTitle');
+ .tooltip('hide')
+ .attr('title', newTitle)
+ .tooltip('_fixTitle');
}
}
diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue
index 87ab5480c15..829924ba63c 100644
--- a/app/assets/javascripts/groups/components/item_stats.vue
+++ b/app/assets/javascripts/groups/components/item_stats.vue
@@ -1,44 +1,44 @@
<script>
- import icon from '~/vue_shared/components/icon.vue';
- import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
- import {
- ITEM_TYPE,
- VISIBILITY_TYPE_ICON,
- GROUP_VISIBILITY_TYPE,
- PROJECT_VISIBILITY_TYPE,
- } from '../constants';
- import itemStatsValue from './item_stats_value.vue';
+import icon from '~/vue_shared/components/icon.vue';
+import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import {
+ ITEM_TYPE,
+ VISIBILITY_TYPE_ICON,
+ GROUP_VISIBILITY_TYPE,
+ PROJECT_VISIBILITY_TYPE,
+} from '../constants';
+import itemStatsValue from './item_stats_value.vue';
- export default {
- components: {
- icon,
- timeAgoTooltip,
- itemStatsValue,
+export default {
+ components: {
+ icon,
+ timeAgoTooltip,
+ itemStatsValue,
+ },
+ props: {
+ item: {
+ type: Object,
+ required: true,
},
- props: {
- item: {
- type: Object,
- required: true,
- },
+ },
+ computed: {
+ visibilityIcon() {
+ return VISIBILITY_TYPE_ICON[this.item.visibility];
},
- computed: {
- visibilityIcon() {
- return VISIBILITY_TYPE_ICON[this.item.visibility];
- },
- visibilityTooltip() {
- if (this.item.type === ITEM_TYPE.GROUP) {
- return GROUP_VISIBILITY_TYPE[this.item.visibility];
- }
- return PROJECT_VISIBILITY_TYPE[this.item.visibility];
- },
- isProject() {
- return this.item.type === ITEM_TYPE.PROJECT;
- },
- isGroup() {
- return this.item.type === ITEM_TYPE.GROUP;
- },
+ visibilityTooltip() {
+ if (this.item.type === ITEM_TYPE.GROUP) {
+ return GROUP_VISIBILITY_TYPE[this.item.visibility];
+ }
+ return PROJECT_VISIBILITY_TYPE[this.item.visibility];
},
- };
+ isProject() {
+ return this.item.type === ITEM_TYPE.PROJECT;
+ },
+ isGroup() {
+ return this.item.type === ITEM_TYPE.GROUP;
+ },
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/groups/components/item_stats_value.vue b/app/assets/javascripts/groups/components/item_stats_value.vue
index ef9f2bca76c..c542ca946d3 100644
--- a/app/assets/javascripts/groups/components/item_stats_value.vue
+++ b/app/assets/javascripts/groups/components/item_stats_value.vue
@@ -1,52 +1,52 @@
<script>
- import tooltip from '~/vue_shared/directives/tooltip';
- import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import icon from '~/vue_shared/components/icon.vue';
- export default {
- components: {
- icon,
+export default {
+ components: {
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ title: {
+ type: String,
+ required: false,
+ default: '',
},
- directives: {
- tooltip,
+ cssClass: {
+ type: String,
+ required: false,
+ default: '',
},
- props: {
- title: {
- type: String,
- required: false,
- default: '',
- },
- cssClass: {
- type: String,
- required: false,
- default: '',
- },
- iconName: {
- type: String,
- required: true,
- },
- tooltipPlacement: {
- type: String,
- required: false,
- default: 'bottom',
- },
- /**
- * value could either be number or string
- * as `memberCount` is always passed as string
- * while `subgroupCount` & `projectCount`
- * are always number
- */
- value: {
- type: [Number, String],
- required: false,
- default: '',
- },
+ iconName: {
+ type: String,
+ required: true,
},
- computed: {
- isValuePresent() {
- return this.value !== '';
- },
+ tooltipPlacement: {
+ type: String,
+ required: false,
+ default: 'bottom',
},
- };
+ /**
+ * value could either be number or string
+ * as `memberCount` is always passed as string
+ * while `subgroupCount` & `projectCount`
+ * are always number
+ */
+ value: {
+ type: [Number, String],
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ isValuePresent() {
+ return this.value !== '';
+ },
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/groups/new_group_child.js b/app/assets/javascripts/groups/new_group_child.js
index a120d501e35..012177479c6 100644
--- a/app/assets/javascripts/groups/new_group_child.js
+++ b/app/assets/javascripts/groups/new_group_child.js
@@ -37,20 +37,22 @@ export default class NewGroupChild {
getDroplabConfig() {
return {
- InputSetter: [{
- input: this.newGroupChildButton,
- valueAttribute: 'data-value',
- inputAttribute: 'data-action',
- }, {
- input: this.newGroupChildButton,
- valueAttribute: 'data-text',
- }],
+ InputSetter: [
+ {
+ input: this.newGroupChildButton,
+ valueAttribute: 'data-value',
+ inputAttribute: 'data-action',
+ },
+ {
+ input: this.newGroupChildButton,
+ valueAttribute: 'data-text',
+ },
+ ],
};
}
bindEvents() {
- this.newGroupChildButton
- .addEventListener('click', this.onClickNewGroupChildButton.bind(this));
+ this.newGroupChildButton.addEventListener('click', this.onClickNewGroupChildButton.bind(this));
}
onClickNewGroupChildButton(e) {
diff --git a/app/assets/javascripts/groups/store/groups_store.js b/app/assets/javascripts/groups/store/groups_store.js
index 4a7569078a1..16f95d5a0cc 100644
--- a/app/assets/javascripts/groups/store/groups_store.js
+++ b/app/assets/javascripts/groups/store/groups_store.js
@@ -17,13 +17,14 @@ export default class GroupsStore {
}
setSearchedGroups(rawGroups) {
- const formatGroups = groups => groups.map((group) => {
- const formattedGroup = this.formatGroupItem(group);
- if (formattedGroup.children && formattedGroup.children.length) {
- formattedGroup.children = formatGroups(formattedGroup.children);
- }
- return formattedGroup;
- });
+ const formatGroups = groups =>
+ groups.map(group => {
+ const formattedGroup = this.formatGroupItem(group);
+ if (formattedGroup.children && formattedGroup.children.length) {
+ formattedGroup.children = formatGroups(formattedGroup.children);
+ }
+ return formattedGroup;
+ });
if (rawGroups && rawGroups.length) {
this.state.groups = formatGroups(rawGroups);
@@ -62,10 +63,10 @@ export default class GroupsStore {
formatGroupItem(rawGroupItem) {
const groupChildren = rawGroupItem.children || [];
- const groupIsOpen = (groupChildren.length > 0) || false;
- const childrenCount = this.hideProjects ?
- rawGroupItem.subgroup_count :
- rawGroupItem.children_count;
+ const groupIsOpen = groupChildren.length > 0 || false;
+ const childrenCount = this.hideProjects
+ ? rawGroupItem.subgroup_count
+ : rawGroupItem.children_count;
return {
id: rawGroupItem.id,
diff --git a/app/assets/javascripts/groups/transfer_dropdown.js b/app/assets/javascripts/groups/transfer_dropdown.js
index e0eb118ddf7..26510fcdb2a 100644
--- a/app/assets/javascripts/groups/transfer_dropdown.js
+++ b/app/assets/javascripts/groups/transfer_dropdown.js
@@ -22,7 +22,7 @@ export default class TransferDropdown {
search: { fields: ['text'] },
data: extraOptions.concat(this.data),
text: item => item.text,
- clicked: (options) => {
+ clicked: options => {
const { e } = options;
e.preventDefault();
this.assignSelected(options.selectedObj);
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index e37fc5c4be6..b4a3037c1b7 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -23,7 +23,7 @@ export default function groupsSelect() {
axios[params.type.toLowerCase()](params.url, {
params: params.data,
})
- .then((res) => {
+ .then(res => {
const results = res.data || [];
const headers = normalizeHeaders(res.headers);
const currentPage = parseInt(headers['X-PAGE'], 10) || 0;
@@ -36,7 +36,8 @@ export default function groupsSelect() {
more,
},
});
- }).catch(params.error);
+ })
+ .catch(params.error);
},
data(search, page) {
return {
@@ -68,7 +69,9 @@ export default function groupsSelect() {
}
},
formatResult(object) {
- return `<div class='group-result'> <div class='group-name'>${object.full_name}</div> <div class='group-path'>${object.full_path}</div> </div>`;
+ return `<div class='group-result'> <div class='group-name'>${
+ object.full_name
+ }</div> <div class='group-path'>${object.full_path}</div> </div>`;
},
formatSelection(object) {
return object.full_name;
diff --git a/app/assets/javascripts/helpers/avatar_helper.js b/app/assets/javascripts/helpers/avatar_helper.js
index d3b1d0f11fd..35ac7b2629c 100644
--- a/app/assets/javascripts/helpers/avatar_helper.js
+++ b/app/assets/javascripts/helpers/avatar_helper.js
@@ -19,7 +19,9 @@ export function renderIdenticon(entity, options = {}) {
const bgClass = getIdenticonBackgroundClass(entity.id);
const title = getIdenticonTitle(entity.name);
- return `<div class="avatar identicon ${_.escape(sizeClass)} ${_.escape(bgClass)}">${_.escape(title)}</div>`;
+ return `<div class="avatar identicon ${_.escape(sizeClass)} ${_.escape(bgClass)}">${_.escape(
+ title,
+ )}</div>`;
}
export function renderAvatar(entity, options = {}) {
diff --git a/app/assets/javascripts/image_diff/image_diff.js b/app/assets/javascripts/image_diff/image_diff.js
index fab0255c378..3587f073a00 100644
--- a/app/assets/javascripts/image_diff/image_diff.js
+++ b/app/assets/javascripts/image_diff/image_diff.js
@@ -60,8 +60,10 @@ export default class ImageDiff {
}
renderBadge(discussionEl, index) {
- const imageBadge = imageDiffHelper
- .generateBadgeFromDiscussionDOM(this.imageFrameEl, discussionEl);
+ const imageBadge = imageDiffHelper.generateBadgeFromDiscussionDOM(
+ this.imageFrameEl,
+ discussionEl,
+ );
this.imageBadges.push(imageBadge);
diff --git a/app/assets/javascripts/image_diff/init_discussion_tab.js b/app/assets/javascripts/image_diff/init_discussion_tab.js
index 2f16c6ef115..dbe4c06a4e9 100644
--- a/app/assets/javascripts/image_diff/init_discussion_tab.js
+++ b/app/assets/javascripts/image_diff/init_discussion_tab.js
@@ -8,5 +8,6 @@ export default () => {
const diffFileEls = document.querySelectorAll('.timeline-content .diff-file.js-image-file');
[...diffFileEls].forEach(diffFileEl =>
- imageDiffHelper.initImageDiff(diffFileEl, canCreateNote, renderCommentBadge));
+ imageDiffHelper.initImageDiff(diffFileEl, canCreateNote, renderCommentBadge),
+ );
};
diff --git a/app/assets/javascripts/image_diff/replaced_image_diff.js b/app/assets/javascripts/image_diff/replaced_image_diff.js
index 4abd13fb472..8d9e65155d8 100644
--- a/app/assets/javascripts/image_diff/replaced_image_diff.js
+++ b/app/assets/javascripts/image_diff/replaced_image_diff.js
@@ -26,7 +26,7 @@ export default class ReplacedImageDiff extends ImageDiff {
this.imageEls = {};
const viewTypeNames = Object.getOwnPropertyNames(viewTypes);
- viewTypeNames.forEach((viewType) => {
+ viewTypeNames.forEach(viewType => {
this.imageEls[viewType] = this.imageFrameEls[viewType].querySelector('img');
});
}
@@ -79,13 +79,12 @@ export default class ReplacedImageDiff extends ImageDiff {
// Re-render indicator in new view
if (indicator.removed) {
- const normalizedIndicator = imageDiffHelper
- .resizeCoordinatesToImageElement(this.imageEl, {
- x: indicator.x,
- y: indicator.y,
- width: indicator.image.width,
- height: indicator.image.height,
- });
+ const normalizedIndicator = imageDiffHelper.resizeCoordinatesToImageElement(this.imageEl, {
+ x: indicator.x,
+ y: indicator.y,
+ width: indicator.image.width,
+ height: indicator.image.height,
+ });
imageDiffHelper.showCommentIndicator(this.imageFrameEl, normalizedIndicator);
}
}
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index eda8cdad908..f1beb1a8ea5 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -60,66 +60,71 @@ class ImporterStatus {
attributes = Object.assign(repoData, attributes);
}
- return axios.post(this.importUrl, attributes)
- .then(({ data }) => {
- const job = $(`tr#repo_${id}`);
- job.attr('id', `project_${data.id}`);
-
- job.find('.import-target').html(`<a href="${data.full_path}">${data.full_path}</a>`);
- $('table.import-jobs tbody').prepend(job);
-
- job.addClass('table-active');
- const connectingVerb = this.ciCdOnly ? __('connecting') : __('importing');
- job.find('.import-actions').html(sprintf(
- _.escape(__('%{loadingIcon} Started')), {
- loadingIcon: `<i class="fa fa-spinner fa-spin" aria-label="${_.escape(connectingVerb)}"></i>`,
- },
- false,
- ));
- })
- .catch((error) => {
- let details = error;
-
- const $statusField = $(`#repo_${this.id} .job-status`);
- $statusField.text(__('Failed'));
-
- if (error.response && error.response.data && error.response.data.errors) {
- details = error.response.data.errors;
- }
-
- flash(sprintf(__('An error occurred while importing project: %{details}'), { details }));
- });
+ return axios
+ .post(this.importUrl, attributes)
+ .then(({ data }) => {
+ const job = $(`tr#repo_${id}`);
+ job.attr('id', `project_${data.id}`);
+
+ job.find('.import-target').html(`<a href="${data.full_path}">${data.full_path}</a>`);
+ $('table.import-jobs tbody').prepend(job);
+
+ job.addClass('table-active');
+ const connectingVerb = this.ciCdOnly ? __('connecting') : __('importing');
+ job.find('.import-actions').html(
+ sprintf(
+ _.escape(__('%{loadingIcon} Started')),
+ {
+ loadingIcon: `<i class="fa fa-spinner fa-spin" aria-label="${_.escape(
+ connectingVerb,
+ )}"></i>`,
+ },
+ false,
+ ),
+ );
+ })
+ .catch(error => {
+ let details = error;
+
+ const $statusField = $(`#repo_${this.id} .job-status`);
+ $statusField.text(__('Failed'));
+
+ if (error.response && error.response.data && error.response.data.errors) {
+ details = error.response.data.errors;
+ }
+
+ flash(sprintf(__('An error occurred while importing project: %{details}'), { details }));
+ });
}
autoUpdate() {
- return axios.get(this.jobsUrl)
- .then(({ data = [] }) => {
- data.forEach((job) => {
- const jobItem = $(`#project_${job.id}`);
- const statusField = jobItem.find('.job-status');
-
- const spinner = '<i class="fa fa-spinner fa-spin"></i>';
-
- switch (job.import_status) {
- case 'finished':
- jobItem.removeClass('table-active').addClass('table-success');
- statusField.html(`<span><i class="fa fa-check"></i> ${__('Done')}</span>`);
- break;
- case 'scheduled':
- statusField.html(`${spinner} ${__('Scheduled')}`);
- break;
- case 'started':
- statusField.html(`${spinner} ${__('Started')}`);
- break;
- case 'failed':
- statusField.html(__('Failed'));
- break;
- default:
- statusField.html(job.import_status);
- break;
- }
- });
+ return axios.get(this.jobsUrl).then(({ data = [] }) => {
+ data.forEach(job => {
+ const jobItem = $(`#project_${job.id}`);
+ const statusField = jobItem.find('.job-status');
+
+ const spinner = '<i class="fa fa-spinner fa-spin"></i>';
+
+ switch (job.import_status) {
+ case 'finished':
+ jobItem.removeClass('table-active').addClass('table-success');
+ statusField.html(`<span><i class="fa fa-check"></i> ${__('Done')}</span>`);
+ break;
+ case 'scheduled':
+ statusField.html(`${spinner} ${__('Scheduled')}`);
+ break;
+ case 'started':
+ statusField.html(`${spinner} ${__('Started')}`);
+ break;
+ case 'failed':
+ statusField.html(__('Failed'));
+ break;
+ default:
+ statusField.html(job.import_status);
+ break;
+ }
});
+ });
}
setAutoUpdate() {
@@ -141,7 +146,4 @@ function initImporterStatus() {
}
}
-export {
- initImporterStatus as default,
- ImporterStatus,
-};
+export { initImporterStatus as default, ImporterStatus };
diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js
index 5c5a6e01848..e708e5d0978 100644
--- a/app/assets/javascripts/init_changes_dropdown.js
+++ b/app/assets/javascripts/init_changes_dropdown.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { stickyMonitor } from './lib/utils/sticky';
-export default (stickyTop) => {
+export default stickyTop => {
stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop);
$('.js-diff-stats-dropdown').glDropdown({
diff --git a/app/assets/javascripts/init_notes.js b/app/assets/javascripts/init_notes.js
index 3c71258e53b..a77828e8cf2 100644
--- a/app/assets/javascripts/init_notes.js
+++ b/app/assets/javascripts/init_notes.js
@@ -2,13 +2,7 @@ import Notes from './notes';
export default () => {
const dataEl = document.querySelector('.js-notes-data');
- const {
- notesUrl,
- notesIds,
- now,
- diffView,
- enableGFM,
- } = JSON.parse(dataEl.innerHTML);
+ const { notesUrl, notesIds, now, diffView, enableGFM } = JSON.parse(dataEl.innerHTML);
// Create a singleton so that we don't need to assign
// into the window object, we can just access the current isntance with Notes.instance
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index bd90d0eaa32..08b858305ab 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -97,7 +97,8 @@ export default class IntegrationSettingsForm {
testSettings(formData) {
this.toggleSubmitBtnState(true);
- return axios.put(this.testEndPoint, formData)
+ return axios
+ .put(this.testEndPoint, formData)
.then(({ data }) => {
if (data.error) {
let flashActions;
@@ -105,7 +106,7 @@ export default class IntegrationSettingsForm {
if (data.test_failed) {
flashActions = {
title: 'Save anyway',
- clickHandler: (e) => {
+ clickHandler: e => {
e.preventDefault();
this.$form.submit();
},
diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
index 07cf1eff279..612c524ca1c 100644
--- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js
+++ b/app/assets/javascripts/issuable/auto_width_dropdown_select.js
@@ -27,7 +27,10 @@ class AutoWidthDropdownSelect {
// We have to look at the parent because
// `offsetParent` on a `display: none;` is `null`
- const offsetParentWidth = $(this).parent().offsetParent().width();
+ const offsetParentWidth = $(this)
+ .parent()
+ .offsetParent()
+ .width();
// Reset any width to let it naturally flow
$dropdown.css('width', 'auto');
if ($dropdown.outerWidth(false) > offsetParentWidth) {
diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js
index 9848bcc2e64..b844e4c5e5b 100644
--- a/app/assets/javascripts/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable_bulk_update_actions.js
@@ -32,7 +32,7 @@ export default {
onFormSubmitFailure() {
this.form.find('[type="submit"]').enable();
- return new Flash("Issue update failed");
+ return new Flash('Issue update failed');
},
getSelectedIssues() {
@@ -63,7 +63,7 @@ export default {
const result = [];
const labelsToKeep = this.$labelDropdown.data('indeterminate');
- this.getLabelsFromSelection().forEach((id) => {
+ this.getLabelsFromSelection().forEach(id => {
if (labelsToKeep.indexOf(id) === -1) {
result.push(id);
}
@@ -89,8 +89,8 @@ export default {
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
add_label_ids: [],
- remove_label_ids: []
- }
+ remove_label_ids: [],
+ },
};
if (this.willUpdateLabels) {
formData.update.add_label_ids = this.$labelDropdown.data('marked');
@@ -134,7 +134,7 @@ export default {
// Collect unique label IDs for all checked issues
this.getElement('.selected-issuable:checked').each((i, el) => {
issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
- issuableLabels.forEach((labelId) => {
+ issuableLabels.forEach(labelId => {
// Store unique IDs
if (uniqueIds.indexOf(labelId) === -1) {
uniqueIds.push(labelId);
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 0140960b367..c81a2230310 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -1,6 +1,3 @@
-/* eslint-disable no-new, no-unused-vars, consistent-return, no-else-return */
-/* global GitLab */
-
import $ from 'jquery';
import Pikaday from 'pikaday';
import Autosave from './autosave';
@@ -8,7 +5,7 @@ import UsersSelect from './users_select';
import GfmAutoComplete from './gfm_auto_complete';
import ZenMode from './zen_mode';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
-import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
+import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
export default class IssuableForm {
constructor(form) {
@@ -19,9 +16,11 @@ export default class IssuableForm {
this.handleSubmit = this.handleSubmit.bind(this);
this.wipRegex = /^\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i;
- new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup();
- new UsersSelect();
- new ZenMode();
+ this.gfmAutoComplete = new GfmAutoComplete(
+ gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources,
+ ).setup();
+ this.usersSelect = new UsersSelect();
+ this.zenMode = new ZenMode();
this.titleField = this.form.find('input[name*="[title]"]');
this.descriptionField = this.form.find('textarea[name*="[description]"]');
@@ -57,8 +56,16 @@ export default class IssuableForm {
}
initAutosave() {
- new Autosave(this.titleField, [document.location.pathname, document.location.search, 'title']);
- return new Autosave(this.descriptionField, [document.location.pathname, document.location.search, 'description']);
+ this.autosave = new Autosave(this.titleField, [
+ document.location.pathname,
+ document.location.search,
+ 'title',
+ ]);
+ return new Autosave(this.descriptionField, [
+ document.location.pathname,
+ document.location.search,
+ 'description',
+ ]);
}
handleSubmit() {
@@ -74,7 +81,7 @@ export default class IssuableForm {
this.$wipExplanation = this.form.find('.js-wip-explanation');
this.$noWipExplanation = this.form.find('.js-no-wip-explanation');
if (!(this.$wipExplanation.length && this.$noWipExplanation.length)) {
- return;
+ return undefined;
}
this.form.on('click', '.js-toggle-wip', this.toggleWip);
this.titleField.on('keyup blur', this.renderWipExplanation);
@@ -89,10 +96,9 @@ export default class IssuableForm {
if (this.workInProgress()) {
this.$wipExplanation.show();
return this.$noWipExplanation.hide();
- } else {
- this.$wipExplanation.hide();
- return this.$noWipExplanation.show();
}
+ this.$wipExplanation.hide();
+ return this.$noWipExplanation.show();
}
toggleWip(event) {
@@ -110,7 +116,7 @@ export default class IssuableForm {
}
addWip() {
- this.titleField.val(`WIP: ${(this.titleField.val())}`);
+ this.titleField.val(`WIP: ${this.titleField.val()}`);
}
initTargetBranchDropdown() {
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index ba14aaeed2c..ac19034f69d 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -77,11 +77,11 @@
'shouldRenderCalloutMessage',
'shouldRenderTriggeredLabel',
'hasEnvironment',
- 'isJobStuck',
'hasTrace',
'emptyStateIllustration',
'isScrollingDown',
'emptyStateAction',
+ 'hasRunnersForProject',
]),
shouldRenderContent() {
@@ -195,9 +195,9 @@
<!-- Body Section -->
<stuck-block
- v-if="isJobStuck"
+ v-if="job.stuck"
class="js-job-stuck"
- :has-no-runners-for-project="job.runners.available"
+ :has-no-runners-for-project="hasRunnersForProject"
:tags="job.tags"
:runners-path="runnerSettingsUrl"
/>
diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job_container_item.vue
index 81cc0823792..6486b25c8a7 100644
--- a/app/assets/javascripts/jobs/components/job_container_item.vue
+++ b/app/assets/javascripts/jobs/components/job_container_item.vue
@@ -1,5 +1,4 @@
<script>
-import _ from 'underscore';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
@@ -9,11 +8,9 @@ export default {
CiIcon,
Icon,
},
-
directives: {
tooltip,
},
-
props: {
job: {
type: Object,
@@ -24,10 +21,9 @@ export default {
required: true,
},
},
-
computed: {
tooltipText() {
- return `${_.escape(this.job.name)} - ${this.job.status.tooltip}`;
+ return `${this.job.name} - ${this.job.status.tooltip}`;
},
},
};
@@ -36,7 +32,10 @@ export default {
<template>
<div
class="build-job"
- :class="{ retried: job.retried, active: isActive }"
+ :class="{
+ retried: job.retried,
+ active: isActive
+ }"
>
<a
v-tooltip
diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue
index a60643b2c65..1d5789b175a 100644
--- a/app/assets/javascripts/jobs/components/stuck_block.vue
+++ b/app/assets/javascripts/jobs/components/stuck_block.vue
@@ -23,14 +23,7 @@ export default {
<template>
<div class="bs-callout bs-callout-warning">
<p
- v-if="hasNoRunnersForProject"
- class="js-stuck-no-runners append-bottom-0"
- >
- {{ s__(`Job|This job is stuck, because the project
- doesn't have any runners online assigned to it.`) }}
- </p>
- <p
- v-else-if="tags.length"
+ v-if="tags.length"
class="js-stuck-with-tags append-bottom-0"
>
{{ s__(`This job is stuck, because you don't have
@@ -44,6 +37,13 @@ export default {
</span>
</p>
<p
+ v-else-if="hasNoRunnersForProject"
+ class="js-stuck-no-runners append-bottom-0"
+ >
+ {{ s__(`Job|This job is stuck, because the project
+ doesn't have any runners online assigned to it.`) }}
+ </p>
+ <p
v-else
class="js-stuck-no-active-runner append-bottom-0"
>
diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js
index 4ce395a9106..4de01f8e532 100644
--- a/app/assets/javascripts/jobs/store/getters.js
+++ b/app/assets/javascripts/jobs/store/getters.js
@@ -41,17 +41,10 @@ export const emptyStateIllustration = state =>
(state.job && state.job.status && state.job.status.illustration) || {};
export const emptyStateAction = state => (state.job && state.job.status && state.job.status.action) || {};
-/**
- * When the job is pending and there are no available runners
- * we need to render the stuck block;
- *
- * @returns {Boolean}
- */
-export const isJobStuck = state =>
- (!_.isEmpty(state.job.status) && state.job.status.group === 'pending') &&
- (!_.isEmpty(state.job.runners) && state.job.runners.available === false);
export const isScrollingDown = state => isScrolledToBottom() && !state.isTraceComplete;
+export const hasRunnersForProject = state => state.job.runners.available && !state.job.runners.online;
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/lib/utils/datefix.js b/app/assets/javascripts/lib/utils/datefix.js
deleted file mode 100644
index 19e4085dbbb..00000000000
--- a/app/assets/javascripts/lib/utils/datefix.js
+++ /dev/null
@@ -1,28 +0,0 @@
-export const pad = (val, len = 2) => `0${val}`.slice(-len);
-
-/**
- * Formats dates in Pickaday
- * @param {String} dateString Date in yyyy-mm-dd format
- * @return {Date} UTC format
- */
-export const parsePikadayDate = dateString => {
- const parts = dateString.split('-');
- const year = parseInt(parts[0], 10);
- const month = parseInt(parts[1] - 1, 10);
- const day = parseInt(parts[2], 10);
-
- return new Date(year, month, day);
-};
-
-/**
- * Used `onSelect` method in pickaday
- * @param {Date} date UTC format
- * @return {String} Date formated in yyyy-mm-dd
- */
-export const pikadayToString = date => {
- const day = pad(date.getDate());
- const month = pad(date.getMonth() + 1);
- const year = date.getFullYear();
-
- return `${year}-${month}-${day}`;
-};
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 833dbefd3dc..1bdf98d0c97 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import _ from 'underscore';
import timeago from 'timeago.js';
import dateFormat from 'dateformat';
import { pluralize } from './text_utility';
@@ -46,6 +47,8 @@ const getMonthNames = abbreviated => {
];
};
+export const pad = (val, len = 2) => `0${val}`.slice(-len);
+
/**
* Given a date object returns the day of the week in English
* @param {date} date
@@ -74,10 +77,10 @@ let timeagoInstance;
/**
* Sets a timeago Instance
*/
-export function getTimeago() {
+export const getTimeago = () => {
if (!timeagoInstance) {
- const localeRemaining = function getLocaleRemaining(number, index) {
- return [
+ const localeRemaining = (number, index) =>
+ [
[s__('Timeago|just now'), s__('Timeago|right now')],
[s__('Timeago|%s seconds ago'), s__('Timeago|%s seconds remaining')],
[s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')],
@@ -93,9 +96,9 @@ export function getTimeago() {
[s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')],
[s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')],
][index];
- };
- const locale = function getLocale(number, index) {
- return [
+
+ const locale = (number, index) =>
+ [
[s__('Timeago|just now'), s__('Timeago|right now')],
[s__('Timeago|%s seconds ago'), s__('Timeago|in %s seconds')],
[s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')],
@@ -111,7 +114,6 @@ export function getTimeago() {
[s__('Timeago|1 year ago'), s__('Timeago|in 1 year')],
[s__('Timeago|%s years ago'), s__('Timeago|in %s years')],
][index];
- };
timeago.register(timeagoLanguageCode, locale);
timeago.register(`${timeagoLanguageCode}-remaining`, localeRemaining);
@@ -119,7 +121,7 @@ export function getTimeago() {
}
return timeagoInstance;
-}
+};
/**
* For the given element, renders a timeago instance.
@@ -184,7 +186,7 @@ export const getDayDifference = (a, b) => {
* @param {Number} seconds
* @return {String}
*/
-export function timeIntervalInWords(intervalInSeconds) {
+export const timeIntervalInWords = intervalInSeconds => {
const secondsInteger = parseInt(intervalInSeconds, 10);
const minutes = Math.floor(secondsInteger / 60);
const seconds = secondsInteger - minutes * 60;
@@ -196,9 +198,9 @@ export function timeIntervalInWords(intervalInSeconds) {
text = `${seconds} ${pluralize('second', seconds)}`;
}
return text;
-}
+};
-export function dateInWords(date, abbreviated = false, hideYear = false) {
+export const dateInWords = (date, abbreviated = false, hideYear = false) => {
if (!date) return date;
const month = date.getMonth();
@@ -240,7 +242,7 @@ export function dateInWords(date, abbreviated = false, hideYear = false) {
}
return `${monthName} ${date.getDate()}, ${year}`;
-}
+};
/**
* Returns month name based on provided date.
@@ -391,3 +393,83 @@ export const formatTime = milliseconds => {
formattedTime += remainingSeconds;
return formattedTime;
};
+
+/**
+ * Formats dates in Pickaday
+ * @param {String} dateString Date in yyyy-mm-dd format
+ * @return {Date} UTC format
+ */
+export const parsePikadayDate = dateString => {
+ const parts = dateString.split('-');
+ const year = parseInt(parts[0], 10);
+ const month = parseInt(parts[1] - 1, 10);
+ const day = parseInt(parts[2], 10);
+
+ return new Date(year, month, day);
+};
+
+/**
+ * Used `onSelect` method in pickaday
+ * @param {Date} date UTC format
+ * @return {String} Date formated in yyyy-mm-dd
+ */
+export const pikadayToString = date => {
+ const day = pad(date.getDate());
+ const month = pad(date.getMonth() + 1);
+ const year = date.getFullYear();
+
+ return `${year}-${month}-${day}`;
+};
+
+/**
+ * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # }
+ * Seconds can be negative or positive, zero or non-zero. Can be configured for any day
+ * or week length.
+ */
+export const parseSeconds = (seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) => {
+ const DAYS_PER_WEEK = daysPerWeek;
+ const HOURS_PER_DAY = hoursPerDay;
+ const MINUTES_PER_HOUR = 60;
+ const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR;
+ const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
+
+ const timePeriodConstraints = {
+ weeks: MINUTES_PER_WEEK,
+ days: MINUTES_PER_DAY,
+ hours: MINUTES_PER_HOUR,
+ minutes: 1,
+ };
+
+ let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR);
+
+ return _.mapObject(timePeriodConstraints, minutesPerPeriod => {
+ const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
+
+ unorderedMinutes -= periodCount * minutesPerPeriod;
+
+ return periodCount;
+ });
+};
+
+/**
+ * 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.
+ */
+export const stringifyTime = timeObject => {
+ const reducedTime = _.reduce(
+ timeObject,
+ (memo, unitValue, unitName) => {
+ const isNonZero = !!unitValue;
+ return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
+ },
+ '',
+ ).trim();
+ return reducedTime.length ? reducedTime : '0m';
+};
+
+/**
+ * Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns
+ * the first non-zero unit/value pair.
+ */
+export const abbreviateTime = timeStr =>
+ timeStr.split(' ').filter(unitStr => unitStr.charAt(0) !== '0')[0];
diff --git a/app/assets/javascripts/lib/utils/pretty_time.js b/app/assets/javascripts/lib/utils/pretty_time.js
deleted file mode 100644
index d92b8a7179f..00000000000
--- a/app/assets/javascripts/lib/utils/pretty_time.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import _ from 'underscore';
-
-/*
- * TODO: Make these methods more configurable (e.g. stringifyTime condensed or
- * non-condensed, abbreviateTimelengths)
- * */
-
-/*
- * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # }
- * Seconds can be negative or positive, zero or non-zero. Can be configured for any day
- * or week length.
-*/
-
-export function parseSeconds(seconds, { daysPerWeek = 5, hoursPerDay = 8 } = {}) {
- const DAYS_PER_WEEK = daysPerWeek;
- const HOURS_PER_DAY = hoursPerDay;
- const MINUTES_PER_HOUR = 60;
- const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR;
- const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
-
- const timePeriodConstraints = {
- weeks: MINUTES_PER_WEEK,
- days: MINUTES_PER_DAY,
- hours: MINUTES_PER_HOUR,
- minutes: 1,
- };
-
- let unorderedMinutes = Math.abs(seconds / MINUTES_PER_HOUR);
-
- return _.mapObject(timePeriodConstraints, minutesPerPeriod => {
- const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
-
- unorderedMinutes -= periodCount * minutesPerPeriod;
-
- return periodCount;
- });
-}
-
-/*
-* 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.
-*/
-
-export function stringifyTime(timeObject) {
- const reducedTime = _.reduce(
- timeObject,
- (memo, unitValue, unitName) => {
- const isNonZero = !!unitValue;
- return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
- },
- '',
- ).trim();
- return reducedTime.length ? reducedTime : '0m';
-}
-
-/*
-* Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns
-* the first non-zero unit/value pair.
-*/
-
-export function abbreviateTime(timeStr) {
- return timeStr.split(' ').filter(unitStr => unitStr.charAt(0) !== '0')[0];
-}
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
index df5cd1b8c51..0beedcacf33 100644
--- a/app/assets/javascripts/member_expiration_date.js
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import Pikaday from 'pikaday';
-import { parsePikadayDate, pikadayToString } from './lib/utils/datefix';
+import { parsePikadayDate, pikadayToString } from './lib/utils/datetime_utility';
// Add datepickers to all `js-access-expiration-date` elements. If those elements are
// children of an element with the `clearable-input` class, and have a sibling
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index 8aabb840847..1c98683c597 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -4,6 +4,7 @@ import { mapActions, mapState, mapGetters } from 'vuex';
import initDiffsApp from '../diffs';
import notesApp from '../notes/components/notes_app.vue';
import discussionCounter from '../notes/components/discussion_counter.vue';
+import initDiscussionFilters from '../notes/discussion_filters';
import store from './stores';
import MergeRequest from '../merge_request';
@@ -88,5 +89,6 @@ export default function initMrNotes() {
},
});
+ initDiscussionFilters(store);
initDiffsApp(store);
}
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
index ad6e7cf501d..1f80f24e045 100644
--- a/app/assets/javascripts/notes/components/discussion_counter.vue
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -56,10 +56,11 @@ export default {
</script>
<template>
- <div class="line-resolve-all-container prepend-top-10">
+ <div
+ v-if="discussionCount > 0"
+ class="line-resolve-all-container prepend-top-8">
<div>
<div
- v-if="discussionCount > 0"
:class="{ 'has-next-btn': hasNextButton }"
class="line-resolve-all">
<span
diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue
new file mode 100644
index 00000000000..27972682ca1
--- /dev/null
+++ b/app/assets/javascripts/notes/components/discussion_filter.vue
@@ -0,0 +1,82 @@
+<script>
+import $ from 'jquery';
+import Icon from '~/vue_shared/components/icon.vue';
+import { mapGetters, mapActions } from 'vuex';
+
+export default {
+ components: {
+ Icon,
+ },
+ props: {
+ filters: {
+ type: Array,
+ required: true,
+ },
+ defaultValue: {
+ type: Number,
+ default: null,
+ required: false,
+ },
+ },
+ data() {
+ return { currentValue: this.defaultValue };
+ },
+ computed: {
+ ...mapGetters([
+ 'getNotesDataByProp',
+ ]),
+ currentFilter() {
+ if (!this.currentValue) return this.filters[0];
+ return this.filters.find(filter => filter.value === this.currentValue);
+ },
+ },
+ methods: {
+ ...mapActions(['filterDiscussion']),
+ selectFilter(value) {
+ const filter = parseInt(value, 10);
+
+ // close dropdown
+ $(this.$refs.dropdownToggle).dropdown('toggle');
+
+ if (filter === this.currentValue) return;
+ this.currentValue = filter;
+ this.filterDiscussion({ path: this.getNotesDataByProp('discussionsPath'), filter });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="discussion-filter-container d-inline-block align-bottom">
+ <button
+ id="discussion-filter-dropdown"
+ ref="dropdownToggle"
+ class="btn btn-default"
+ data-toggle="dropdown"
+ aria-expanded="false"
+ >
+ {{ currentFilter.title }}
+ <icon name="chevron-down" />
+ </button>
+ <div
+ class="dropdown-menu dropdown-menu-selectable dropdown-menu-right"
+ aria-labelledby="discussion-filter-dropdown">
+ <div class="dropdown-content">
+ <ul>
+ <li
+ v-for="filter in filters"
+ :key="filter.value"
+ >
+ <button
+ :class="{ 'is-active': filter.value === currentValue }"
+ type="button"
+ @click="selectFilter(filter.value)"
+ >
+ {{ filter.title }}
+ </button>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 618a1581d8f..b0faa443a18 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -50,11 +50,11 @@ export default {
},
data() {
return {
- isLoading: true,
+ currentFilter: null,
};
},
computed: {
- ...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount']),
+ ...mapGetters(['isNotesFetched', 'discussions', 'getNotesDataByProp', 'discussionCount', 'isLoading']),
noteableType() {
return this.noteableData.noteableType;
},
@@ -102,6 +102,7 @@ export default {
},
methods: {
...mapActions({
+ setLoadingState: 'setLoadingState',
fetchDiscussions: 'fetchDiscussions',
poll: 'poll',
actionToggleAward: 'toggleAward',
@@ -133,19 +134,19 @@ export default {
return discussion.individual_note ? { note: discussion.notes[0] } : { discussion };
},
fetchNotes() {
- return this.fetchDiscussions(this.getNotesDataByProp('discussionsPath'))
+ return this.fetchDiscussions({ path: this.getNotesDataByProp('discussionsPath') })
.then(() => {
this.initPolling();
})
.then(() => {
- this.isLoading = false;
+ this.setLoadingState(false);
this.setNotesFetchedState(true);
eventHub.$emit('fetchedNotesData');
})
.then(() => this.$nextTick())
.then(() => this.checkLocationHash())
.catch(() => {
- this.isLoading = false;
+ this.setLoadingState(false);
this.setNotesFetchedState(true);
Flash('Something went wrong while fetching comments. Please try again.');
});
diff --git a/app/assets/javascripts/notes/discussion_filters.js b/app/assets/javascripts/notes/discussion_filters.js
new file mode 100644
index 00000000000..012ffc4093e
--- /dev/null
+++ b/app/assets/javascripts/notes/discussion_filters.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import DiscussionFilter from './components/discussion_filter.vue';
+
+export default (store) => {
+ const discussionFilterEl = document.getElementById('js-vue-discussion-filter');
+
+ if (discussionFilterEl) {
+ const { defaultFilter, notesFilters } = discussionFilterEl.dataset;
+ const defaultValue = defaultFilter ? parseInt(defaultFilter, 10) : null;
+ const filterValues = notesFilters ? JSON.parse(notesFilters) : {};
+ const filters = Object.keys(filterValues).map(entry =>
+ ({ title: entry, value: filterValues[entry] }));
+
+ return new Vue({
+ el: discussionFilterEl,
+ name: 'DiscussionFilter',
+ components: {
+ DiscussionFilter,
+ },
+ store,
+ render(createElement) {
+ return createElement('discussion-filter', {
+ props: {
+ filters,
+ defaultValue,
+ },
+ });
+ },
+ });
+ }
+
+ return null;
+};
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index 3aef30c608c..2f715c85fa6 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -1,10 +1,13 @@
import Vue from 'vue';
import notesApp from './components/notes_app.vue';
+import initDiscussionFilters from './discussion_filters';
import createStore from './stores';
document.addEventListener('DOMContentLoaded', () => {
const store = createStore();
+ initDiscussionFilters(store);
+
return new Vue({
el: '#js-vue-notes',
components: {
diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js
index f5dce94caad..47a6f07cce2 100644
--- a/app/assets/javascripts/notes/services/notes_service.js
+++ b/app/assets/javascripts/notes/services/notes_service.js
@@ -5,8 +5,9 @@ import * as constants from '../constants';
Vue.use(VueResource);
export default {
- fetchDiscussions(endpoint) {
- return Vue.http.get(endpoint);
+ fetchDiscussions(endpoint, filter) {
+ const config = filter !== undefined ? { params: { notes_filter: filter } } : null;
+ return Vue.http.get(endpoint, config);
},
deleteNote(endpoint) {
return Vue.http.delete(endpoint);
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 7ab7e5a9abb..b5dd49bc6c9 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -11,6 +11,7 @@ import loadAwardsHandler from '../../awards_handler';
import sidebarTimeTrackingEventHub from '../../sidebar/event_hub';
import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub';
+import { __ } from '~/locale';
let eTagPoll;
@@ -36,9 +37,9 @@ export const setNotesFetchedState = ({ commit }, state) =>
export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
-export const fetchDiscussions = ({ commit }, path) =>
+export const fetchDiscussions = ({ commit }, { path, filter }) =>
service
- .fetchDiscussions(path)
+ .fetchDiscussions(path, filter)
.then(res => res.json())
.then(discussions => {
commit(types.SET_INITIAL_DISCUSSIONS, discussions);
@@ -251,7 +252,7 @@ const pollSuccessCallBack = (resp, commit, state, getters, dispatch) => {
if (discussion) {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
} else if (note.type === constants.DIFF_NOTE) {
- dispatch('fetchDiscussions', state.notesData.discussionsPath);
+ dispatch('fetchDiscussions', { path: state.notesData.discussionsPath });
} else {
commit(types.ADD_NEW_NOTE, note);
}
@@ -345,5 +346,23 @@ export const updateMergeRequestWidget = () => {
mrWidgetEventHub.$emit('mr.discussion.updated');
};
+export const setLoadingState = ({ commit }, data) => {
+ commit(types.SET_NOTES_LOADING_STATE, data);
+};
+
+export const filterDiscussion = ({ dispatch }, { path, filter }) => {
+ dispatch('setLoadingState', true);
+ dispatch('fetchDiscussions', { path, filter })
+ .then(() => {
+ dispatch('setLoadingState', false);
+ dispatch('setNotesFetchedState', true);
+ })
+ .catch(() => {
+ dispatch('setLoadingState', false);
+ dispatch('setNotesFetchedState', true);
+ Flash(__('Something went wrong while fetching comments. Please try again.'));
+ });
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index a829149a17e..e4f36154fcd 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -1,6 +1,5 @@
import _ from 'underscore';
import * as constants from '../constants';
-import { reduceDiscussionsToLineCodes } from './utils';
import { collapseSystemNotes } from './collapse_utils';
export const discussions = state => collapseSystemNotes(state.discussions);
@@ -11,6 +10,8 @@ export const getNotesData = state => state.notesData;
export const isNotesFetched = state => state.isNotesFetched;
+export const isLoading = state => state.isLoading;
+
export const getNotesDataByProp = state => prop => state.notesData[prop];
export const getNoteableData = state => state.noteableData;
@@ -29,9 +30,6 @@ export const notesById = state =>
return acc;
}, {});
-export const discussionsStructuredByLineCode = state =>
- reduceDiscussionsToLineCodes(state.discussions);
-
export const noteableType = state => {
const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants;
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index 61dbb075586..400142668ea 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -11,6 +11,7 @@ export default () => ({
// View layer
isToggleStateButtonLoading: false,
isNotesFetched: false,
+ isLoading: true,
// holds endpoints and permissions provided through haml
notesData: {
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index 6f374f78691..2fa53aef1d4 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -14,6 +14,7 @@ export const UPDATE_NOTE = 'UPDATE_NOTE';
export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES';
export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE';
+export const SET_NOTES_LOADING_STATE = 'SET_NOTES_LOADING_STATE';
// DISCUSSION
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 73e55705f39..65085452139 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -216,6 +216,10 @@ export default {
Object.assign(state, { isNotesFetched: value });
},
+ [types.SET_NOTES_LOADING_STATE](state, value) {
+ state.isLoading = value;
+ },
+
[types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) {
const discussion = utils.findNoteObjectById(state.discussions, discussionId);
diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js
index 0e41ff03d67..dd57539e4d8 100644
--- a/app/assets/javascripts/notes/stores/utils.js
+++ b/app/assets/javascripts/notes/stores/utils.js
@@ -25,18 +25,6 @@ export const getQuickActionText = note => {
return text;
};
-export const reduceDiscussionsToLineCodes = selectedDiscussions =>
- selectedDiscussions.reduce((acc, note) => {
- if (note.diff_discussion && note.line_code) {
- // For context about line notes: there might be multiple notes with the same line code
- const items = acc[note.line_code] || [];
- items.push(note);
-
- Object.assign(acc, { [note.line_code]: items });
- }
- return acc;
- }, {});
-
export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim();
diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js
index 3b58c54b3f4..386a9b2c740 100644
--- a/app/assets/javascripts/pager.js
+++ b/app/assets/javascripts/pager.js
@@ -7,14 +7,21 @@ const ENDLESS_SCROLL_BOTTOM_PX = 400;
const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000;
export default {
- init(limit = 0, preload = false, disable = false, prepareData = $.noop, callback = $.noop) {
+ init(
+ limit = 0,
+ preload = false,
+ disable = false,
+ prepareData = $.noop,
+ callback = $.noop,
+ container = '',
+ ) {
this.url = $('.content_list').data('href') || removeParams(['limit', 'offset']);
this.limit = limit;
this.offset = parseInt(getParameterByName('offset'), 10) || this.limit;
this.disable = disable;
this.prepareData = prepareData;
this.callback = callback;
- this.loading = $('.loading').first();
+ this.loading = $(`${container} .loading`).first();
if (preload) {
this.offset = 0;
this.getOld();
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index 1de9945baad..04bcb16f036 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -170,7 +170,7 @@ export default class UserTabs {
this.loadActivityCalendar('activity');
// eslint-disable-next-line no-new
- new Activities();
+ new Activities('#activity');
this.loaded.activity = true;
}
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
index 1d030c4f67f..259858e4b46 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue
@@ -1,111 +1,111 @@
<script>
- import { __, sprintf } from '~/locale';
- import { abbreviateTime } from '~/lib/utils/pretty_time';
- import icon from '~/vue_shared/components/icon.vue';
- import tooltip from '~/vue_shared/directives/tooltip';
+import { __, sprintf } from '~/locale';
+import { abbreviateTime } from '~/lib/utils/datetime_utility';
+import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
- export default {
- name: 'TimeTrackingCollapsedState',
- components: {
- icon,
+export default {
+ name: 'TimeTrackingCollapsedState',
+ components: {
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ showComparisonState: {
+ type: Boolean,
+ required: true,
},
- directives: {
- tooltip,
+ showSpentOnlyState: {
+ type: Boolean,
+ required: true,
},
- props: {
- showComparisonState: {
- type: Boolean,
- required: true,
- },
- showSpentOnlyState: {
- type: Boolean,
- required: true,
- },
- showEstimateOnlyState: {
- type: Boolean,
- required: true,
- },
- showNoTimeTrackingState: {
- type: Boolean,
- required: true,
- },
- timeSpentHumanReadable: {
- type: String,
- required: false,
- default: '',
- },
- timeEstimateHumanReadable: {
- type: String,
- required: false,
- default: '',
- },
+ showEstimateOnlyState: {
+ type: Boolean,
+ required: true,
},
- computed: {
- timeSpent() {
- return this.abbreviateTime(this.timeSpentHumanReadable);
- },
- timeEstimate() {
- return this.abbreviateTime(this.timeEstimateHumanReadable);
- },
- divClass() {
- if (this.showComparisonState) {
- return 'compare';
- } else if (this.showEstimateOnlyState) {
- return 'estimate-only';
- } else if (this.showSpentOnlyState) {
- return 'spend-only';
- } else if (this.showNoTimeTrackingState) {
- return 'no-tracking';
- }
+ showNoTimeTrackingState: {
+ type: Boolean,
+ required: true,
+ },
+ timeSpentHumanReadable: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ timeEstimateHumanReadable: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ timeSpent() {
+ return this.abbreviateTime(this.timeSpentHumanReadable);
+ },
+ timeEstimate() {
+ return this.abbreviateTime(this.timeEstimateHumanReadable);
+ },
+ divClass() {
+ if (this.showComparisonState) {
+ return 'compare';
+ } else if (this.showEstimateOnlyState) {
+ return 'estimate-only';
+ } else if (this.showSpentOnlyState) {
+ return 'spend-only';
+ } else if (this.showNoTimeTrackingState) {
+ return 'no-tracking';
+ }
+ return '';
+ },
+ spanClass() {
+ if (this.showComparisonState) {
return '';
- },
- spanClass() {
- if (this.showComparisonState) {
- return '';
- } else if (this.showEstimateOnlyState || this.showSpentOnlyState) {
- return 'bold';
- } else if (this.showNoTimeTrackingState) {
- return 'no-value';
- }
+ } else if (this.showEstimateOnlyState || this.showSpentOnlyState) {
+ return 'bold';
+ } else if (this.showNoTimeTrackingState) {
+ return 'no-value';
+ }
- return '';
- },
- text() {
- if (this.showComparisonState) {
- return `${this.timeSpent} / ${this.timeEstimate}`;
- } else if (this.showEstimateOnlyState) {
- return `-- / ${this.timeEstimate}`;
- } else if (this.showSpentOnlyState) {
- return `${this.timeSpent} / --`;
- } else if (this.showNoTimeTrackingState) {
- return 'None';
- }
+ return '';
+ },
+ text() {
+ if (this.showComparisonState) {
+ return `${this.timeSpent} / ${this.timeEstimate}`;
+ } else if (this.showEstimateOnlyState) {
+ return `-- / ${this.timeEstimate}`;
+ } else if (this.showSpentOnlyState) {
+ return `${this.timeSpent} / --`;
+ } else if (this.showNoTimeTrackingState) {
+ return 'None';
+ }
- return '';
- },
- timeTrackedTooltipText() {
- let title;
- if (this.showComparisonState) {
- title = __('Time remaining');
- } else if (this.showEstimateOnlyState) {
- title = __('Estimated');
- } else if (this.showSpentOnlyState) {
- title = __('Time spent');
- }
+ return '';
+ },
+ timeTrackedTooltipText() {
+ let title;
+ if (this.showComparisonState) {
+ title = __('Time remaining');
+ } else if (this.showEstimateOnlyState) {
+ title = __('Estimated');
+ } else if (this.showSpentOnlyState) {
+ title = __('Time spent');
+ }
- return sprintf('%{title}: %{text}', ({ title, text: this.text }));
- },
- tooltipText() {
- return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText;
- },
+ return sprintf('%{title}: %{text}', { title, text: this.text });
+ },
+ tooltipText() {
+ return this.showNoTimeTrackingState ? __('Time tracking') : this.timeTrackedTooltipText;
},
- methods: {
- abbreviateTime(timeStr) {
- return abbreviateTime(timeStr);
- },
+ },
+ methods: {
+ abbreviateTime(timeStr) {
+ return abbreviateTime(timeStr);
},
- };
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
index dc599e1b9fc..e74912d628f 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue
@@ -1,5 +1,5 @@
<script>
-import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time';
+import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import tooltip from '../../../vue_shared/directives/tooltip';
export default {
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 36a345130c0..2d89a156117 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -34,10 +34,21 @@ export default {
required: false,
default: false,
},
+ displayTextKey: {
+ type: String,
+ required: false,
+ default: 'name',
+ },
+ shouldTruncateStart: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
mouseOver: false,
+ truncateStart: 0,
};
},
computed: {
@@ -60,6 +71,15 @@ export default {
'is-open': this.file.opened,
};
},
+ outputText() {
+ const text = this.file[this.displayTextKey];
+
+ if (this.truncateStart === 0) {
+ return text;
+ }
+
+ return `...${text.substring(this.truncateStart, text.length)}`;
+ },
},
watch: {
'file.active': function fileActiveWatch(active) {
@@ -72,6 +92,15 @@ export default {
if (this.hasPathAtCurrentRoute()) {
this.scrollIntoView(true);
}
+
+ if (this.shouldTruncateStart) {
+ const { scrollWidth, offsetWidth } = this.$refs.textOutput;
+ const textOverflow = scrollWidth - offsetWidth;
+
+ if (textOverflow > 0) {
+ this.truncateStart = Math.ceil(textOverflow / 5) + 3;
+ }
+ }
},
methods: {
toggleTreeOpen(path) {
@@ -139,6 +168,7 @@ export default {
class="file-row-name-container"
>
<span
+ ref="textOutput"
:style="levelIndentation"
class="file-row-name str-truncated"
>
@@ -156,7 +186,7 @@ export default {
:size="16"
class="append-right-5"
/>
- {{ file.name }}
+ {{ outputText }}
</span>
<component
:is="extraComponent"
@@ -175,6 +205,8 @@ export default {
:hide-extra-on-tree="hideExtraOnTree"
:extra-component="extraComponent"
:show-changed-icon="showChangedIcon"
+ :display-text-key="displayTextKey"
+ :should-truncate-start="shouldTruncateStart"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="clickedFile"
/>
diff --git a/app/assets/javascripts/vue_shared/components/pikaday.vue b/app/assets/javascripts/vue_shared/components/pikaday.vue
index 782d8e3abf6..26c99aecae4 100644
--- a/app/assets/javascripts/vue_shared/components/pikaday.vue
+++ b/app/assets/javascripts/vue_shared/components/pikaday.vue
@@ -1,6 +1,6 @@
<script>
import Pikaday from 'pikaday';
-import { parsePikadayDate, pikadayToString } from '../../lib/utils/datefix';
+import { parsePikadayDate, pikadayToString } from '~/lib/utils/datetime_utility';
export default {
name: 'DatePicker',
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 3c9505a21d6..fa753b13e5f 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -334,6 +334,14 @@ img.emoji {
}
}
+.outline-0 {
+ outline: 0;
+
+ &:focus {
+ outline: 0;
+ }
+}
+
/** COMMON CLASSES **/
.prepend-top-0 { margin-top: 0; }
.prepend-top-2 { margin-top: 2px; }
@@ -369,3 +377,5 @@ img.emoji {
.flex-align-self-center { align-self: center; }
.flex-grow { flex-grow: 1; }
.flex-no-shrink { flex-shrink: 0; }
+.mw-460 { max-width: 460px; }
+.ws-initial { white-space: initial; }
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index bf6f66d30ff..f47dfe1b563 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -37,6 +37,7 @@
button {
padding-top: 0;
+ background-color: transparent;
}
&.active a,
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 8d884ad6891..52c91266ff4 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -1027,8 +1027,12 @@
overflow-x: auto;
}
-.tree-list-search .form-control {
- padding-left: 30px;
+.tree-list-search {
+ flex: 0 0 34px;
+
+ .form-control {
+ padding-left: 30px;
+ }
}
.tree-list-icon {
@@ -1063,3 +1067,9 @@
}
}
}
+
+.tree-list-view-toggle {
+ svg {
+ top: 0;
+ }
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 0f95fb911e1..8ea34f5d19d 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -185,7 +185,17 @@ ul.related-merge-requests > li {
}
.new-branch-col {
- padding-top: 10px;
+ font-size: 0;
+
+ .discussion-filter-container {
+ &:not(:only-child) {
+ margin-right: $gl-padding-8;
+ }
+
+ @include media-breakpoint-down(md) {
+ margin-top: $gl-padding-8;
+ }
+ }
}
.create-mr-dropdown-wrap {
@@ -205,6 +215,10 @@ ul.related-merge-requests > li {
.btn-group:not(.hidden) {
display: flex;
+
+ @include media-breakpoint-down(md) {
+ margin-top: $gl-padding-8;
+ }
}
.js-create-merge-request {
@@ -251,7 +265,6 @@ ul.related-merge-requests > li {
.new-branch-col {
padding-top: 0;
- text-align: right;
align-self: center;
}
@@ -262,3 +275,9 @@ ul.related-merge-requests > li {
}
}
}
+
+@include media-breakpoint-up(lg) {
+ .new-branch-col {
+ text-align: right;
+ }
+}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 2feb7464ecb..fa6afbf81de 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -818,9 +818,17 @@
display: flex;
justify-content: space-between;
- @include media-breakpoint-down(xs) {
+ @include media-breakpoint-down(md) {
flex-direction: column-reverse;
}
+
+ .discussion-filter-container {
+ margin-top: $gl-padding-8;
+
+ &:not(:only-child) {
+ padding-right: $gl-padding-8;
+ }
+ }
}
.limit-container-width:not(.container-limited) {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index bfba1bf1b2b..be535ade0a6 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -618,7 +618,6 @@ ul.notes {
.line-resolve-all-container {
@include notes-media('min', map-get($grid-breakpoints, sm)) {
margin-right: 0;
- padding-left: $gl-padding;
}
> div {
@@ -756,3 +755,23 @@ ul.notes {
margin-top: 4px;
}
}
+
+.discussion-filter-container {
+
+ .btn > svg {
+ width: $gl-col-padding;
+ height: $gl-col-padding;
+ }
+
+ .dropdown-menu {
+ margin-bottom: $gl-padding-4;
+
+ @include media-breakpoint-down(md) {
+ margin-left: $btn-side-margin + $contextual-sidebar-collapsed-width;
+ }
+
+ @include media-breakpoint-down(xs) {
+ margin-left: $btn-side-margin;
+ }
+ }
+}
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 07e01e903ea..ad9cc0925b7 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -2,6 +2,7 @@
module IssuableActions
extend ActiveSupport::Concern
+ include Gitlab::Utils::StrongMemoize
included do
before_action :labels, only: [:show, :new, :edit]
@@ -95,10 +96,14 @@ module IssuableActions
def discussions
notes = issuable.discussion_notes
.inc_relations_for_view
+ .with_notes_filter(notes_filter)
.includes(:noteable)
.fresh
- notes = ResourceEvents::MergeIntoNotesService.new(issuable, current_user).execute(notes)
+ if notes_filter != UserPreference::NOTES_FILTERS[:only_comments]
+ notes = ResourceEvents::MergeIntoNotesService.new(issuable, current_user).execute(notes)
+ end
+
notes = prepare_notes_for_rendering(notes)
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
@@ -110,6 +115,32 @@ module IssuableActions
private
+ def notes_filter
+ strong_memoize(:notes_filter) do
+ notes_filter_param = params[:notes_filter]&.to_i
+
+ # GitLab Geo does not expect database UPDATE or INSERT statements to happen
+ # on GET requests.
+ # This is just a fail-safe in case notes_filter is sent via GET request in GitLab Geo.
+ if Gitlab::Database.read_only?
+ notes_filter_param || current_user&.notes_filter_for(issuable)
+ else
+ notes_filter = current_user&.set_notes_filter(notes_filter_param, issuable) || notes_filter_param
+
+ # We need to invalidate the cache for polling notes otherwise it will
+ # ignore the filter.
+ # The ideal would be to invalidate the cache for each user.
+ issuable.expire_note_etag_cache if notes_filter_updated?
+
+ notes_filter
+ end
+ end
+ end
+
+ def notes_filter_updated?
+ current_user&.user_preference&.previous_changes&.any?
+ end
+
def discussion_serializer
DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user, note_entity: ProjectNoteEntity)
end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 3a45d6205ab..777b147e2dd 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -17,10 +17,17 @@ module NotesActions
notes_json = { notes: [], last_fetched_at: current_fetched_at }
- notes = notes_finder.execute
- .inc_relations_for_view
+ notes = notes_finder
+ .execute
+ .inc_relations_for_view
+
+ if notes_filter != UserPreference::NOTES_FILTERS[:only_comments]
+ notes =
+ ResourceEvents::MergeIntoNotesService
+ .new(noteable, current_user, last_fetched_at: current_fetched_at)
+ .execute(notes)
+ end
- notes = ResourceEvents::MergeIntoNotesService.new(noteable, current_user, last_fetched_at: current_fetched_at).execute(notes)
notes = prepare_notes_for_rendering(notes)
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
@@ -224,6 +231,10 @@ module NotesActions
request.headers['X-Last-Fetched-At']
end
+ def notes_filter
+ current_user&.notes_filter_for(params[:target_type])
+ end
+
def notes_finder
@notes_finder ||= NotesFinder.new(project, current_user, finder_params)
end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 4bac763d000..3152a38fd8e 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -68,7 +68,7 @@ class Projects::NotesController < Projects::ApplicationController
alias_method :awardable, :note
def finder_params
- params.merge(last_fetched_at: last_fetched_at)
+ params.merge(last_fetched_at: last_fetched_at, notes_filter: notes_filter)
end
def authorize_admin_note!
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index c67c2065440..817aac8b5d5 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -24,6 +24,8 @@ class NotesFinder
def execute
notes = init_collection
notes = since_fetch_at(notes)
+ notes = notes.with_notes_filter(@params[:notes_filter]) if notes_filter?
+
notes.fresh
end
@@ -134,4 +136,8 @@ class NotesFinder
last_fetched_at = Time.at(@params.fetch(:last_fetched_at, 0).to_i)
notes.updated_after(last_fetched_at - FETCH_OVERLAP)
end
+
+ def notes_filter?
+ @params[:notes_filter].present?
+ end
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index f573fd399a5..0c313e9e6d3 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -1,6 +1,15 @@
# frozen_string_literal: true
module GroupsHelper
+ def group_overview_nav_link_paths
+ %w[
+ groups#show
+ groups#activity
+ groups#subgroups
+ analytics#show
+ ]
+ end
+
def group_nav_link_paths
%w[groups#projects groups#edit badges#index ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 97406fefd43..6069640b9c8 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -386,8 +386,8 @@ module IssuablesHelper
{
todo_text: "Add todo",
mark_text: "Mark todo as done",
- todo_icon: (is_collapsed ? icon('plus-square') : nil),
- mark_icon: (is_collapsed ? icon('check-square', class: 'todo-undone') : nil),
+ todo_icon: (is_collapsed ? sprite_icon('todo-add') : nil),
+ mark_icon: (is_collapsed ? sprite_icon('todo-done', css_class: 'todo-undone') : nil),
issuable_id: issuable.id,
issuable_type: issuable.class.name.underscore,
url: project_todos_path(@project),
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 2b28b702b05..34a889057ab 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -19,7 +19,9 @@ module Ci
sast: 'gl-sast-report.json',
dependency_scanning: 'gl-dependency-scanning-report.json',
container_scanning: 'gl-container-scanning-report.json',
- dast: 'gl-dast-report.json'
+ dast: 'gl-dast-report.json',
+ license_management: 'gl-license-management-report.json',
+ performance: 'performance.json'
}.freeze
TYPE_AND_FORMAT_PAIRS = {
@@ -35,7 +37,9 @@ module Ci
sast: :raw,
dependency_scanning: :raw,
container_scanning: :raw,
- dast: :raw
+ dast: :raw,
+ license_management: :raw,
+ performance: :raw
}.freeze
belongs_to :project
@@ -80,7 +84,9 @@ module Ci
dependency_scanning: 6, ## EE-specific
container_scanning: 7, ## EE-specific
dast: 8, ## EE-specific
- codequality: 9 ## EE-specific
+ codequality: 9, ## EE-specific
+ license_management: 10, ## EE-specific
+ performance: 11 ## EE-specific
}
enum file_format: {
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 17024e8a0af..aeee7f0a5d2 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -268,6 +268,12 @@ module Ci
stage unless stage.statuses_count.zero?
end
+ def ref_exists?
+ project.repository.ref_exists?(git_ref)
+ rescue Gitlab::Git::Repository::NoRepository
+ false
+ end
+
##
# TODO We do not completely switch to persisted stages because of
# race conditions with setting statuses gitlab-ce#23257.
@@ -674,11 +680,11 @@ module Ci
def push_details
strong_memoize(:push_details) do
- Gitlab::Git::Push.new(project, before_sha, sha, push_ref)
+ Gitlab::Git::Push.new(project, before_sha, sha, git_ref)
end
end
- def push_ref
+ def git_ref
if branch?
Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
elsif tag?
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 43bf852c7ec..b311f5e0617 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ActiveRecord::Base
- VERSION = '0.1.34'.freeze
+ VERSION = '0.1.35'.freeze
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb
index e8e943872de..f0f791742f4 100644
--- a/app/models/clusters/platforms/kubernetes.rb
+++ b/app/models/clusters/platforms/kubernetes.rb
@@ -107,7 +107,7 @@ module Clusters
end
def kubeclient
- @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io'])
+ @kubeclient ||= build_kube_client!
end
private
@@ -136,7 +136,7 @@ module Clusters
Gitlab::NamespaceSanitizer.sanitize(slug)
end
- def build_kube_client!(api_groups: ['api'], api_version: 'v1')
+ def build_kube_client!
raise "Incomplete settings" unless api_url && actual_namespace
unless (username && password) || token
@@ -145,8 +145,6 @@ module Clusters
Gitlab::Kubernetes::KubeClient.new(
api_url,
- api_groups,
- api_version,
auth_options: kubeclient_auth_options,
ssl_options: kubeclient_ssl_options,
http_proxy_uri: ENV['http_proxy']
diff --git a/app/models/note.rb b/app/models/note.rb
index 95e1d3afa00..e1bd943e8e4 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -110,6 +110,15 @@ class Note < ActiveRecord::Base
:system_note_metadata, :note_diff_file)
end
+ scope :with_notes_filter, -> (notes_filter) do
+ case notes_filter
+ when UserPreference::NOTES_FILTERS[:only_comments]
+ user
+ else
+ all
+ end
+ end
+
scope :diff_notes, -> { where(type: %w(LegacyDiffNote DiffNote)) }
scope :new_diff_notes, -> { where(type: 'DiffNote') }
scope :non_diff_notes, -> { where(type: ['Note', 'DiscussionNote', nil]) }
diff --git a/app/models/project.rb b/app/models/project.rb
index be99408fcea..382fb4f463a 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -548,6 +548,8 @@ class Project < ActiveRecord::Base
self[:lfs_enabled] && Gitlab.config.lfs.enabled
end
+ alias_method :lfs_enabled, :lfs_enabled?
+
def auto_devops_enabled?
if auto_devops&.enabled.nil?
has_auto_devops_implicitly_enabled?
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
index f119555f16b..798944d0c06 100644
--- a/app/models/project_services/kubernetes_service.rb
+++ b/app/models/project_services/kubernetes_service.rb
@@ -144,7 +144,7 @@ class KubernetesService < DeploymentService
end
def kubeclient
- @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io'])
+ @kubeclient ||= build_kube_client!
end
def deprecated?
@@ -182,13 +182,11 @@ class KubernetesService < DeploymentService
slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '')
end
- def build_kube_client!(api_groups: ['api'], api_version: 'v1')
+ def build_kube_client!
raise "Incomplete settings" unless api_url && actual_namespace && token
Gitlab::Kubernetes::KubeClient.new(
api_url,
- api_groups,
- api_version,
auth_options: kubeclient_auth_options,
ssl_options: kubeclient_ssl_options,
http_proxy_uri: ENV['http_proxy']
diff --git a/app/models/user.rb b/app/models/user.rb
index 34efb22b359..ca7fc3b058f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -152,6 +152,7 @@ class User < ActiveRecord::Base
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
has_one :status, class_name: 'UserStatus'
+ has_one :user_preference
#
# Validations
@@ -224,6 +225,8 @@ class User < ActiveRecord::Base
enum project_view: [:readme, :activity, :files]
delegate :path, to: :namespace, allow_nil: true, prefix: true
+ delegate :notes_filter_for, to: :user_preference
+ delegate :set_notes_filter, to: :user_preference
state_machine :state, initial: :active do
event :block do
@@ -1367,6 +1370,11 @@ class User < ActiveRecord::Base
!consented_usage_stats? && 7.days.ago > self.created_at && !has_current_license? && User.single_user?
end
+ # Avoid migrations only building user preference object when needed.
+ def user_preference
+ super.presence || build_user_preference
+ end
+
def todos_limited_to(ids)
todos.where(id: ids)
end
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
new file mode 100644
index 00000000000..6cd91abc261
--- /dev/null
+++ b/app/models/user_preference.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+class UserPreference < ActiveRecord::Base
+ # We could use enums, but Rails 4 doesn't support multiple
+ # enum options with same name for multiple fields, also it creates
+ # extra methods that aren't really needed here.
+ NOTES_FILTERS = { all_notes: 0, only_comments: 1 }.freeze
+
+ belongs_to :user
+
+ validates :issue_notes_filter, :merge_request_notes_filter, inclusion: { in: NOTES_FILTERS.values }, presence: true
+
+ class << self
+ def notes_filters
+ {
+ s_('Notes|Show all activity') => NOTES_FILTERS[:all_notes],
+ s_('Notes|Show comments only') => NOTES_FILTERS[:only_comments]
+ }
+ end
+ end
+
+ def set_notes_filter(filter_id, issuable)
+ # No need to update the column if the value is already set.
+ if filter_id && NOTES_FILTERS.values.include?(filter_id)
+ field = notes_filter_field_for(issuable)
+ self[field] = filter_id
+
+ save if attribute_changed?(field)
+ end
+
+ notes_filter_for(issuable)
+ end
+
+ # Returns the current discussion filter for a given issuable
+ # or issuable type.
+ def notes_filter_for(resource)
+ self[notes_filter_field_for(resource)]
+ end
+
+ private
+
+ def notes_filter_field_for(resource)
+ field_key =
+ if resource.is_a?(Issuable)
+ resource.model_name.param_key
+ else
+ resource
+ end
+
+ "#{field_key}_notes_filter"
+ end
+end
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 066a5b1885c..9ddce0d2c80 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -5,6 +5,7 @@ class BuildDetailsEntity < JobEntity
expose :tag_list, as: :tags
expose :has_trace?, as: :has_trace
expose :stage
+ expose :stuck?, as: :stuck
expose :user, using: UserEntity
expose :runner, using: RunnerEntity
expose :pipeline, using: PipelineEntity
diff --git a/app/serializers/current_user_entity.rb b/app/serializers/current_user_entity.rb
new file mode 100644
index 00000000000..71d14e727dd
--- /dev/null
+++ b/app/serializers/current_user_entity.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+# Always use this entity when rendering data for current user
+# for attributes that does not need to be visible to other users
+# like user preferences.
+class CurrentUserEntity < UserEntity
+ expose :user_preference, using: UserPreferenceEntity
+end
diff --git a/app/serializers/merge_request_user_entity.rb b/app/serializers/merge_request_user_entity.rb
index fd2d2897113..53257b0602c 100644
--- a/app/serializers/merge_request_user_entity.rb
+++ b/app/serializers/merge_request_user_entity.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class MergeRequestUserEntity < UserEntity
+class MergeRequestUserEntity < CurrentUserEntity
include RequestAwareEntity
include BlobHelper
include TreeHelper
diff --git a/app/serializers/user_preference_entity.rb b/app/serializers/user_preference_entity.rb
new file mode 100644
index 00000000000..fbdaab459b3
--- /dev/null
+++ b/app/serializers/user_preference_entity.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class UserPreferenceEntity < Grape::Entity
+ expose :issue_notes_filter
+ expose :merge_request_notes_filter
+
+ expose :notes_filters do |user_preference|
+ UserPreference.notes_filters
+ end
+end
diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb
index 3ae0a4a19d0..6ee63db8eb9 100644
--- a/app/services/clusters/gcp/finalize_creation_service.rb
+++ b/app/services/clusters/gcp/finalize_creation_service.rb
@@ -60,18 +60,15 @@ module Clusters
'https://' + gke_cluster.endpoint,
Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate),
gke_cluster.master_auth.username,
- gke_cluster.master_auth.password,
- api_groups: ['api', 'apis/rbac.authorization.k8s.io']
+ gke_cluster.master_auth.password
)
end
- def build_kube_client!(api_url, ca_pem, username, password, api_groups: ['api'], api_version: 'v1')
+ def build_kube_client!(api_url, ca_pem, username, password)
raise "Incomplete settings" unless api_url && username && password
Gitlab::Kubernetes::KubeClient.new(
api_url,
- api_groups,
- api_version,
auth_options: { username: username, password: password },
ssl_options: kubeclient_ssl_options(ca_pem),
http_proxy_uri: ENV['http_proxy']
diff --git a/app/views/instance_statistics/conversational_development_index/_disabled.html.haml b/app/views/instance_statistics/conversational_development_index/_disabled.html.haml
index 0a5717f75e1..b854e15d36f 100644
--- a/app/views/instance_statistics/conversational_development_index/_disabled.html.haml
+++ b/app/views/instance_statistics/conversational_development_index/_disabled.html.haml
@@ -11,4 +11,4 @@
%p
= _('Enable usage ping to get an overview of how you are using GitLab from a feature perspective.')
- if current_user.admin?
- = link_to _('Enable usage ping'), admin_application_settings_path(anchor: 'usage-statistics'), class: 'btn btn-primary'
+ = link_to _('Enable usage ping'), metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), class: 'btn btn-primary'
diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml
index 8bd5708d490..2cdaa85bdaa 100644
--- a/app/views/layouts/_flash.html.haml
+++ b/app/views/layouts/_flash.html.haml
@@ -6,5 +6,5 @@
-# Don't show a flash message if the message is nil
- if value
%div{ class: "flash-#{key}" }
- %div{ class: "#{container_class} #{extra_flash_class}" }
+ %div{ class: "#{(container_class unless fluid_layout)} #{(extra_flash_class unless @no_container)} #{@content_class}" }
%span= value
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 1420b0a4973..1b2a4cd6780 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -6,12 +6,12 @@
.mobile-overlay
.alert-wrapper
= render "layouts/broadcast"
- = render 'layouts/header/read_only_banner'
+ = render "layouts/header/read_only_banner"
= yield :flash_message
= render "shared/ping_consent"
- unless @hide_breadcrumbs
= render "layouts/nav/breadcrumbs"
- = render "layouts/flash"
+ = render "layouts/flash", extra_flash_class: 'limit-container-width'
.d-flex
%div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
.content{ id: "content-body" }
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 4aa22138498..163556f4509 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -12,7 +12,7 @@
= @group.name
%ul.sidebar-top-level-items.qa-group-sidebar
- if group_sidebar_link?(:overview)
- = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups', 'analytics#show'], html_options: { class: 'home' }) do
+ = nav_link(path: group_overview_nav_link_paths, html_options: { class: 'home' }) do
= link_to group_path(@group) do
.nav-icon-container
= sprite_icon('home')
@@ -36,6 +36,16 @@
%span
= _('Activity')
+ = render_if_exists 'groups/sidebar/security_dashboard'
+
+ - if group_sidebar_link?(:contribution_analytics)
+ = nav_link(path: 'analytics#show') do
+ = link_to group_analytics_path(@group), title: 'Contribution Analytics', data: {placement: 'right'} do
+ %span
+ Contribution Analytics
+
+ = render_if_exists "layouts/nav/ee/epic_link", group: @group
+
- if group_sidebar_link?(:issues)
= nav_link(path: issues_sub_menu_items) do
= link_to issues_group_path(@group) do
@@ -132,4 +142,6 @@
%span
= _('CI / CD')
+ = render_if_exists "groups/ee/settings_nav"
+
= render 'shared/sidebar_toggle_button'
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 28998acdc13..4917f4b8903 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -10,4 +10,4 @@
noteable_data: serialize_issuable(@issue),
noteable_type: 'Issue',
target_type: 'issue',
- current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } }
+ current_user_data: UserSerializer.new.represent(current_user, {only_path: true}, CurrentUserEntity).to_json } }
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index a678cb6f058..5374f4a1de0 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -8,12 +8,13 @@
- create_branch_path = project_branches_path(@project, branch_name: @issue.to_branch_name, ref: @project.default_branch, issue_iid: @issue.iid)
- refs_path = refs_namespace_project_path(@project.namespace, @project, search: '')
- .create-mr-dropdown-wrap{ data: { can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path } }
+ .create-mr-dropdown-wrap.d-inline-block{ data: { can_create_path: can_create_path, create_mr_path: create_mr_path, create_branch_path: create_branch_path, refs_path: refs_path } }
.btn-group.unavailable
%button.btn.btn-grouped{ type: 'button', disabled: 'disabled' }
= icon('spinner', class: 'fa-spin')
%span.text
Checking branch availability…
+
.btn-group.available.hidden
%button.btn.js-create-merge-request.btn-success.btn-inverted{ type: 'button', data: { action: data_action } }
= value
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index c39fd0063be..b50b3ca207b 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -77,11 +77,12 @@
#related-branches{ data: { url: related_branches_project_issue_path(@project, @issue) } }
// This element is filled in using JavaScript.
- .content-block.emoji-block
+ .content-block.emoji-block.emoji-block-sticky
.row
- .col-sm-8.js-noteable-awards
+ .col-md-12.col-lg-6.js-noteable-awards
= render 'award_emoji/awards_block', awardable: @issue, inline: true
- .col-sm-4.new-branch-col
+ .col-md-12.col-lg-6.new-branch-col
+ #js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@issue), notes_filters: UserPreference.notes_filters.to_json } }
= render 'new_branch' unless @issue.confidential?
%section.issuable-discussion
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index ef2fa8668c0..efc2d88172e 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -51,8 +51,10 @@
= tab_link_for @merge_request, :diffs do
Changes
%span.badge.badge-pill= @merge_request.diff_size
-
- #js-vue-discussion-counter
+ .d-inline-flex.flex-wrap
+ #js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(@merge_request),
+ notes_filters: UserPreference.notes_filters.to_json } }
+ #js-vue-discussion-counter
.tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index dbb563f51ea..2575efc0981 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -13,7 +13,11 @@
= pluralize @pipeline.total_size, "job"
- if @pipeline.ref
from
- = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name"
+ - if @pipeline.ref_exists?
+ = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name"
+ - else
+ %span.ref-name
+ = @pipeline.ref
- if @pipeline.duration
in
= time_interval_in_words(@pipeline.duration)
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index c4d177361e7..cb45928d9a5 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -36,7 +36,7 @@
%button.btn.btn-link{ type: 'button' }
= sprite_icon('search')
%span
- Press Enter or click to search
+ = _('Press Enter or click to search')
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
%button.btn.btn-link{ type: 'button' }
@@ -61,7 +61,7 @@
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link{ type: 'button' }
- No Assignee
+ = _('No Assignee')
%li.divider.droplab-item-ignore
- if current_user
= render 'shared/issuable/user_dropdown_item',
@@ -74,13 +74,16 @@
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link{ type: 'button' }
- No Milestone
+ = _('None')
+ %li.filter-dropdown-item{ data: { value: 'any' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Any')
%li.filter-dropdown-item{ data: { value: 'upcoming' } }
%button.btn.btn-link{ type: 'button' }
- Upcoming
+ = _('Upcoming')
%li.filter-dropdown-item{ 'data-value' => 'started' }
%button.btn.btn-link{ type: 'button' }
- Started
+ = _('Started')
%li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
@@ -90,7 +93,7 @@
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link{ type: 'button' }
- No Label
+ = _('No Label')
%li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item
diff --git a/app/views/shared/issuable/_sidebar_todo.html.haml b/app/views/shared/issuable/_sidebar_todo.html.haml
index 583b33a8a1b..660ee6d5777 100644
--- a/app/views/shared/issuable/_sidebar_todo.html.haml
+++ b/app/views/shared/issuable/_sidebar_todo.html.haml
@@ -1,6 +1,6 @@
- is_collapsed = local_assigns.fetch(:is_collapsed, false)
-- mark_content = is_collapsed ? icon('check-square', class: 'todo-undone') : _('Mark todo as done')
-- todo_content = is_collapsed ? icon('plus-square') : _('Add todo')
+- mark_content = is_collapsed ? sprite_icon('todo-done', css_class: 'todo-undone') : _('Mark todo as done')
+- todo_content = is_collapsed ? sprite_icon('todo-add') : _('Add todo')
%button.issuable-todo-btn.js-issuable-todo{ type: 'button',
class: (is_collapsed ? 'btn-blank sidebar-collapsed-icon dont-change-state has-tooltip' : 'btn btn-default issuable-header-btn float-right'),
diff --git a/app/views/shared/runners/show.html.haml b/app/views/shared/runners/show.html.haml
index 362569bfbaf..f62eed694d2 100644
--- a/app/views/shared/runners/show.html.haml
+++ b/app/views/shared/runners/show.html.haml
@@ -24,7 +24,7 @@
%td= @runner.active? ? 'Yes' : 'No'
%tr
%td Protected
- %td= @runner.active? ? _('Yes') : _('No')
+ %td= @runner.ref_protected? ? 'Yes' : 'No'
%tr
%td Can run untagged jobs
%td= @runner.run_untagged? ? 'Yes' : 'No'
diff --git a/bin/secpick b/bin/secpick
index 5e30c8e72c5..2b263d452c9 100755
--- a/bin/secpick
+++ b/bin/secpick
@@ -15,7 +15,7 @@ parser = OptionParser.new do |opts|
options[:version] = version&.tr('.', '-')
end
- opts.on('-b', '--branch security-fix-branch', 'Original branch name') do |branch|
+ opts.on('-b', '--branch security-fix-branch', 'Original branch name (optional, defaults to current)') do |branch|
options[:branch] = branch
end
@@ -32,15 +32,21 @@ end
parser.parse!
+options[:branch] ||= `git rev-parse --abbrev-ref HEAD`
+
abort("Missing options. Use #{$0} --help to see the list of options available".red) if options.values.include?(nil)
abort("Wrong version format #{options[:version].bold}".red) unless options[:version] =~ /\A\d*\-\d*\Z/
-branch = "#{options[:branch]}-#{options[:version]}"
+ee = File.exist?('./CHANGELOG-EE.md')
+original_branch = options[:branch].strip
+branch = "#{original_branch}-#{options[:version]}"
branch.prepend("#{BRANCH_PREFIX}-") unless branch.start_with?("#{BRANCH_PREFIX}-")
branch = branch.freeze
-stable_branch = "#{BRANCH_PREFIX}-#{options[:version]}".freeze
+stable_branch = "#{BRANCH_PREFIX}-#{options[:version]}".tap do |name|
+ name << "-ee" if ee
+end.freeze
-command = "git fetch #{REMOTE} #{stable_branch} && git checkout #{stable_branch} && git pull #{REMOTE} #{stable_branch} && git checkout -B #{branch} && git cherry-pick #{options[:sha]} && git push #{REMOTE} #{branch}"
+command = "git fetch #{REMOTE} #{stable_branch} && git checkout #{stable_branch} && git pull #{REMOTE} #{stable_branch} && git checkout -B #{branch} && git cherry-pick #{options[:sha]} && git push #{REMOTE} #{branch} && git checkout #{original_branch}"
_stdin, stdout, stderr = Open3.popen3(command)
diff --git a/changelogs/unreleased/26723-discussion-filters.yml b/changelogs/unreleased/26723-discussion-filters.yml
new file mode 100644
index 00000000000..3abe95bf30d
--- /dev/null
+++ b/changelogs/unreleased/26723-discussion-filters.yml
@@ -0,0 +1,5 @@
+---
+title: Filter notes by comments or activity for issues and merge requests
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/32959-update-todo-icon.yml b/changelogs/unreleased/32959-update-todo-icon.yml
new file mode 100644
index 00000000000..f08fd6aa89f
--- /dev/null
+++ b/changelogs/unreleased/32959-update-todo-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Update Todo icons in collapsed sidebar for Issues and MRs
+merge_request: 22534
+author:
+type: changed
diff --git a/changelogs/unreleased/42611-removed-branch-link.yml b/changelogs/unreleased/42611-removed-branch-link.yml
new file mode 100644
index 00000000000..03a206871b4
--- /dev/null
+++ b/changelogs/unreleased/42611-removed-branch-link.yml
@@ -0,0 +1,5 @@
+---
+title: Only render link to branch when branch still exists in pipeline page
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/52059-filter-milestone-by-none-any.yml b/changelogs/unreleased/52059-filter-milestone-by-none-any.yml
new file mode 100644
index 00000000000..5511440c0b9
--- /dev/null
+++ b/changelogs/unreleased/52059-filter-milestone-by-none-any.yml
@@ -0,0 +1,5 @@
+---
+title: Added `Any` option to milestones filter
+merge_request: 22351
+author: Heinrich Lee Yu
+type: added
diff --git a/changelogs/unreleased/52202-consider-moving-isjobstuck-verification-to-backend.yml b/changelogs/unreleased/52202-consider-moving-isjobstuck-verification-to-backend.yml
new file mode 100644
index 00000000000..0efd97d91b8
--- /dev/null
+++ b/changelogs/unreleased/52202-consider-moving-isjobstuck-verification-to-backend.yml
@@ -0,0 +1,5 @@
+---
+title: Renders stuck block when runners are stuck
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/52840-fix-runners-details-page.yml b/changelogs/unreleased/52840-fix-runners-details-page.yml
new file mode 100644
index 00000000000..b061390fcf0
--- /dev/null
+++ b/changelogs/unreleased/52840-fix-runners-details-page.yml
@@ -0,0 +1,5 @@
+---
+title: Fix rendering of 'Protected' value on Runner details page
+merge_request: 22459
+author:
+type: fixed
diff --git a/changelogs/unreleased/53013-duplicate-escape.yml b/changelogs/unreleased/53013-duplicate-escape.yml
new file mode 100644
index 00000000000..c5ec2322fb5
--- /dev/null
+++ b/changelogs/unreleased/53013-duplicate-escape.yml
@@ -0,0 +1,5 @@
+---
+title: Remove duplicate escape in job sidebar
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/53023-endless-scroll-loader-is-visible-on-user-profile-overview-page.yml b/changelogs/unreleased/53023-endless-scroll-loader-is-visible-on-user-profile-overview-page.yml
new file mode 100644
index 00000000000..0377e10fe9e
--- /dev/null
+++ b/changelogs/unreleased/53023-endless-scroll-loader-is-visible-on-user-profile-overview-page.yml
@@ -0,0 +1,4 @@
+title: Adds container to pager to enable scoping
+merge_request: 22529
+? author
+type: other
diff --git a/changelogs/unreleased/53055-combine-date-util-functions.yml b/changelogs/unreleased/53055-combine-date-util-functions.yml
new file mode 100644
index 00000000000..56d4406f1bf
--- /dev/null
+++ b/changelogs/unreleased/53055-combine-date-util-functions.yml
@@ -0,0 +1,5 @@
+---
+title: Combine all datetime library functions into 'datetime_utility.js'
+merge_request: 22570
+author:
+type: other
diff --git a/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml b/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml
new file mode 100644
index 00000000000..605d3679159
--- /dev/null
+++ b/changelogs/unreleased/53070-fix-enable-usage-ping-link.yml
@@ -0,0 +1,5 @@
+---
+title: "fix link to enable usage ping from convdev index"
+merge_request: 22545
+author: Anand Capur
+type: fixed
diff --git a/changelogs/unreleased/add-role-binding-to-kubeclient.yml b/changelogs/unreleased/add-role-binding-to-kubeclient.yml
new file mode 100644
index 00000000000..bc343116eb4
--- /dev/null
+++ b/changelogs/unreleased/add-role-binding-to-kubeclient.yml
@@ -0,0 +1,5 @@
+---
+title: Allow kubeclient to call RoleBinding methods
+merge_request: 22524
+author:
+type: other
diff --git a/changelogs/unreleased/gt-add-transparent-background-to-markdown-header-tabs.yml b/changelogs/unreleased/gt-add-transparent-background-to-markdown-header-tabs.yml
new file mode 100644
index 00000000000..2ba52e07324
--- /dev/null
+++ b/changelogs/unreleased/gt-add-transparent-background-to-markdown-header-tabs.yml
@@ -0,0 +1,5 @@
+---
+title: Add transparent background to markdown header tabs
+merge_request: 22565
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/lfs-project-attribute-alias.yml b/changelogs/unreleased/lfs-project-attribute-alias.yml
new file mode 100644
index 00000000000..883869f651a
--- /dev/null
+++ b/changelogs/unreleased/lfs-project-attribute-alias.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve LFS not correctly showing enabled
+merge_request: 22501
+author:
+type: fixed
diff --git a/changelogs/unreleased/mr-file-list.yml b/changelogs/unreleased/mr-file-list.yml
new file mode 100644
index 00000000000..0a2a5e0c1cc
--- /dev/null
+++ b/changelogs/unreleased/mr-file-list.yml
@@ -0,0 +1,5 @@
+---
+title: Switch between tree list & file list in diffs file browser
+merge_request: 22191
+author:
+type: added
diff --git a/changelogs/unreleased/sh-pages-eof-error.yml b/changelogs/unreleased/sh-pages-eof-error.yml
new file mode 100644
index 00000000000..497a74c1458
--- /dev/null
+++ b/changelogs/unreleased/sh-pages-eof-error.yml
@@ -0,0 +1,5 @@
+---
+title: Fix EOF detection with CI artifacts metadata
+merge_request: 22479
+author:
+type: fixed
diff --git a/changelogs/unreleased/support-license-management-and-performance.yml b/changelogs/unreleased/support-license-management-and-performance.yml
new file mode 100644
index 00000000000..2e65dba5e76
--- /dev/null
+++ b/changelogs/unreleased/support-license-management-and-performance.yml
@@ -0,0 +1,5 @@
+---
+title: Support licenses and performance
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/update-runner-chart-to-0-1-35.yml b/changelogs/unreleased/update-runner-chart-to-0-1-35.yml
new file mode 100644
index 00000000000..3b8029c8d96
--- /dev/null
+++ b/changelogs/unreleased/update-runner-chart-to-0-1-35.yml
@@ -0,0 +1,5 @@
+---
+title: Update used version of Runner Helm Chart to 0.1.35
+merge_request: 22541
+author:
+type: other
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 749cdd0f869..a4db125f831 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -772,9 +772,6 @@ test:
default:
path: tmp/tests/repositories/
gitaly_address: unix:tmp/tests/gitaly/gitaly.socket
- broken:
- path: tmp/tests/non-existent-repositories
- gitaly_address: unix:tmp/tests/gitaly/gitaly.socket
gitaly:
client_path: tmp/tests/gitaly
diff --git a/danger/specs/Dangerfile b/danger/specs/Dangerfile
index 97188df8785..a526bb8adaa 100644
--- a/danger/specs/Dangerfile
+++ b/danger/specs/Dangerfile
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
NO_SPECS_LABELS = %w[backstage Documentation QA].freeze
NO_NEW_SPEC_MESSAGE = <<~MSG.freeze
You've made some app changes, but didn't add any tests.
@@ -9,8 +11,8 @@ def presented_no_changelog_labels
NO_SPECS_LABELS.map { |label| "~#{label}" }.join(', ')
end
-has_app_changes = !git.modified_files.grep(%r{\A(ee/)?(app|lib|db/(geo/)?(post_)?migrate)/}).empty?
-has_spec_changes = !git.modified_files.grep(%r{\A(ee/)?spec/}).empty?
+has_app_changes = !helper.all_changed_files.grep(%r{\A(ee/)?(app|lib|db/(geo/)?(post_)?migrate)/}).empty?
+has_spec_changes = !helper.all_changed_files.grep(%r{\A(ee/)?spec/}).empty?
new_specs_needed = (gitlab.mr_labels & NO_SPECS_LABELS).empty?
if has_app_changes && !has_spec_changes && new_specs_needed
diff --git a/db/migrate/20180925200829_create_user_preferences.rb b/db/migrate/20180925200829_create_user_preferences.rb
new file mode 100644
index 00000000000..755cabdabde
--- /dev/null
+++ b/db/migrate/20180925200829_create_user_preferences.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class CreateUserPreferences < ActiveRecord::Migration
+ DOWNTIME = false
+
+ class UserPreference < ActiveRecord::Base
+ self.table_name = 'user_preferences'
+
+ NOTES_FILTERS = { all_notes: 0, comments: 1 }.freeze
+ end
+
+ def change
+ create_table :user_preferences do |t|
+ t.references :user,
+ null: false,
+ index: { unique: true },
+ foreign_key: { on_delete: :cascade }
+
+ t.integer :issue_notes_filter,
+ default: UserPreference::NOTES_FILTERS[:all_notes],
+ null: false, limit: 2
+
+ t.integer :merge_request_notes_filter,
+ default: UserPreference::NOTES_FILTERS[:all_notes],
+ null: false,
+ limit: 2
+
+ t.timestamps_with_timezone null: false
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 50989960aa9..ddfccbba678 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -2134,6 +2134,16 @@ ActiveRecord::Schema.define(version: 20181013005024) do
add_index "user_interacted_projects", ["project_id", "user_id"], name: "index_user_interacted_projects_on_project_id_and_user_id", unique: true, using: :btree
add_index "user_interacted_projects", ["user_id"], name: "index_user_interacted_projects_on_user_id", using: :btree
+ create_table "user_preferences", force: :cascade do |t|
+ t.integer "user_id", null: false
+ t.integer "issue_notes_filter", limit: 2, default: 0, null: false
+ t.integer "merge_request_notes_filter", limit: 2, default: 0, null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ end
+
+ add_index "user_preferences", ["user_id"], name: "index_user_preferences_on_user_id", unique: true, using: :btree
+
create_table "user_statuses", primary_key: "user_id", force: :cascade do |t|
t.integer "cached_markdown_version"
t.string "emoji", default: "speech_balloon", null: false
@@ -2460,6 +2470,7 @@ ActiveRecord::Schema.define(version: 20181013005024) do
add_foreign_key "user_custom_attributes", "users", on_delete: :cascade
add_foreign_key "user_interacted_projects", "projects", name: "fk_722ceba4f7", on_delete: :cascade
add_foreign_key "user_interacted_projects", "users", name: "fk_0894651f08", on_delete: :cascade
+ add_foreign_key "user_preferences", "users", on_delete: :cascade
add_foreign_key "user_statuses", "users", on_delete: :cascade
add_foreign_key "user_synced_attributes_metadata", "users", on_delete: :cascade
add_foreign_key "users", "application_setting_terms", column: "accepted_term_id", name: "fk_789cd90b35", on_delete: :cascade
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 2952a98626a..d8345f2d6bd 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -242,6 +242,33 @@ verification requirement. Navigate to `Admin area âž” Settings` and uncheck
**Require users to prove ownership of custom domains** in the Pages section.
This setting is enabled by default.
+### Access control
+
+Access control was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/33422)
+in GitLab 11.5. It can be configured per-project, and allows access to a Pages
+site to be controlled based on a user's membership to that project.
+
+Access control works by registering the Pages daemon as an OAuth application
+with GitLab. Whenever a request to access a private Pages site is made by an
+unauthenticated user, the Pages daemon redirects the user to GitLab. If
+authentication is successful, the user is redirected back to Pages with a token,
+which is persisted in a cookie. The cookies are signed with a secret key, so
+tampering can be detected.
+
+Each request to view a resource in a private site is authenticated by Pages
+using that token. For each request it receives, it makes a request to the GitLab
+API to check that the user is authorized to read that site.
+
+Pages access control is currently disabled by default. To enable it, you must:
+
+1. Enable it in `/etc/gitlab/gitlab.rb`
+
+ ```ruby
+ gitlab_pages['access_control'] = true
+ ```
+
+1. [Reconfigure GitLab][reconfigure]
+
## Activate verbose logging for daemon
Verbose logging was [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/2533) in
diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md
index 295905a7625..ddff54be575 100644
--- a/doc/administration/pages/source.md
+++ b/doc/administration/pages/source.md
@@ -391,6 +391,44 @@ the first one with a backslash (\). For example `pages.example.io` would be:
server_name ~^.*\.pages\.example\.io$;
```
+## Access control
+
+Access control was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/33422)
+in GitLab 11.5. It can be configured per-project, and allows access to a Pages
+site to be controlled based on a user's membership to that project.
+
+Access control works by registering the Pages daemon as an OAuth application
+with GitLab. Whenever a request to access a private Pages site is made by an
+unauthenticated user, the Pages daemon redirects the user to GitLab. If
+authentication is successful, the user is redirected back to Pages with a token,
+which is persisted in a cookie. The cookies are signed with a secret key, so
+tampering can be detected.
+
+Each request to view a resource in a private site is authenticated by Pages
+using that token. For each request it receives, it makes a request to the GitLab
+API to check that the user is authorized to read that site.
+
+Pages access control is currently disabled by default. To enable it, you must:
+
+1. Modify your `config/gitlab.yml` file:
+ ```yaml
+ pages:
+ access_control: true
+ ```
+1. [Restart GitLab][restart]
+1. Create a new [system OAuth application](../../integration/oauth_provider.md#adding-an-application-through-the-profile)
+ This should be called `GitLab Pages` and have a `Redirect URL` of
+ `https://projects.example.io/auth`. It does not need to be a "trusted"
+ application, but it does need the "api" scope.
+1. Start the Pages daemon with the following additional arguments:
+
+ ```shell
+ -auth-client-secret <OAuth code generated by GitLab> \
+ -auth-redirect-uri http://projects.example.io/auth \
+ -auth-secret <40 random hex characters> \
+ -auth-server <URL of the GitLab instance>
+ ```
+
## Change storage path
Follow the steps below to change the default path where GitLab Pages' contents
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index 83e0fa34ad6..2a179bfbbf0 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -312,7 +312,7 @@ We're always looking for contributions that can mitigate these
If you think that registration token for a Project was revealed, you should
reset them. It's recommended because such token can be used to register another
-Runner to thi Project. It may be next used to obtain the values of secret
+Runner to the Project. It may be next used to obtain the values of secret
variables or clone the project code, that normally may be unavailable for the
attacker.
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 424e1af7ba3..4b2a6ccc7e4 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -2031,3 +2031,5 @@ CI with various languages.
[ce-12909]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12909
[schedules]: ../../user/project/pipelines/schedules.md
[variables-expressions]: ../variables/README.md#variables-expressions
+[ee]: https://about.gitlab.com/gitlab-ee/
+[gitlab-versions]: https://about.gitlab.com/products/ \ No newline at end of file
diff --git a/doc/development/feature_flags.md b/doc/development/feature_flags.md
index 0f1f079bdb4..350593cc813 100644
--- a/doc/development/feature_flags.md
+++ b/doc/development/feature_flags.md
@@ -112,3 +112,8 @@ feature flag. You can stub a feature flag as follows:
```ruby
stub_feature_flags(my_feature_flag: false)
```
+
+## Enabling a feature flag
+
+Check how to [roll out changes using feature flags](rolling_out_changes_using_feature_flags.md).
+
diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md
index 6a67aa7f610..f58d79fccf1 100644
--- a/doc/development/i18n/proofreader.md
+++ b/doc/development/i18n/proofreader.md
@@ -20,6 +20,7 @@ are very appreciative of the work done by translators and proofreaders!
- French
- Davy Defaud - [GitLab](https://gitlab.com/DevDef), [Crowdin](https://crowdin.com/profile/DevDef)
- German
+ - Michael Hahnle - [GitLab](https://gitlab.com/mhah), [Crowdin](https://crowdin.com/profile/mhah)
- Indonesian
- Ahmad Naufal Mukhtar - [GitLab](https://gitlab.com/anaufalm), [Crowdin](https://crowdin.com/profile/anaufalm)
- Italian
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 1210ac58499..37c826ce9e0 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -132,9 +132,9 @@ Remove the old Ruby 1.8 if present:
Download Ruby and compile it:
mkdir /tmp/ruby && cd /tmp/ruby
- curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.4.tar.gz
- echo 'ec82b0d53bd0adad9b19e6b45e44d54e9ec3f10c ruby-2.4.4.tar.gz' | shasum -c - && tar xzf ruby-2.4.4.tar.gz
- cd ruby-2.4.4
+ curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.5.tar.gz
+ echo '4d650f302f1ec00256450b112bb023644b6ab6dd ruby-2.4.5.tar.gz' | shasum -c - && tar xzf ruby-2.4.5.tar.gz
+ cd ruby-2.4.5
./configure --disable-install-rdoc
make
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index c60d25eda1b..4d4832184e2 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -2,14 +2,15 @@
> [Introduced][ce-37115] in GitLab 10.0. Generally available on GitLab 11.0.
-Auto DevOps automatically detects, builds, tests, deploys, and monitors your
-applications.
+Auto DevOps provides pre-defined CI/CD configuration which allows you to automatically detect, build, test,
+deploy, and monitor your applications. Leveraging CI/CD best practices and tools, Auto DevOps aims
+to simplify the setup and execution of a mature & modern software development lifecycle.
## Overview
NOTE: **Enabled by default:**
-Starting with GitLab 11.3, the Auto DevOps pipeline will be enabled by default for all
-projects. If it's not explicitly enabled for the project, Auto DevOps will be automatically
+Starting with GitLab 11.3, the Auto DevOps pipeline is enabled by default for all
+projects. If it has not been explicitly enabled for the project, Auto DevOps will be automatically
disabled on the first pipeline failure. Your project will continue to use an alternative
[CI/CD configuration file](../../ci/yaml/README.md) if one is found. A GitLab
administrator can [change this setting](../../user/admin_area/settings/continuous_integration.html#auto-devops)
@@ -17,33 +18,38 @@ in the admin area.
With Auto DevOps, the software development process becomes easier to set up
as every project can have a complete workflow from verification to monitoring
-without needing to configure anything. Just push your code and GitLab takes
+with minimal configuration. Just push your code and GitLab takes
care of everything else. This makes it easier to start new projects and brings
consistency to how applications are set up throughout a company.
## Quick start
If you are using GitLab.com, see the [quick start guide](quick_start_guide.md)
-for using Auto DevOps with GitLab.com and a Kubernetes cluster on Google Kubernetes
-Engine.
+for how to use Auto DevOps with GitLab.com and a Kubernetes cluster on Google Kubernetes
+Engine (GKE).
+
+If you are using a self-hosted instance of GitLab, you will need to configure the
+[Google OAuth2 OmniAuth Provider](../../integration/google.md) before
+you can configure a cluster on GKE. Once this is set up, you can follow the steps on the
+[quick start guide](quick_start_guide.md) to get started.
## Comparison to application platforms and PaaS
-Auto DevOps provides functionality described by others as an application
-platform or as a Platform as a Service (PaaS). It takes inspiration from the
+Auto DevOps provides functionality that is often included in an application
+platform or a Platform as a Service (PaaS). It takes inspiration from the
innovative work done by [Heroku](https://www.heroku.com/) and goes beyond it
-in a couple of ways:
+in multiple ways:
-1. Auto DevOps works with any Kubernetes cluster, you're not limited to running
- on GitLab's infrastructure (note that many features also work without Kubernetes).
+1. Auto DevOps works with any Kubernetes cluster; you're not limited to running
+ on GitLab's infrastructure. (Note that many features also work without Kubernetes.)
1. There is no additional cost (no markup on the infrastructure costs), and you
can use a self-hosted Kubernetes cluster or Containers as a Service on any
- public cloud (for example [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/)).
+ public cloud (for example, [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/)).
1. Auto DevOps has more features including security testing, performance testing,
and code quality testing.
-1. It offers an incremental graduation path. If you need advanced customizations
+1. Auto DevOps offers an incremental graduation path. If you need advanced customizations,
you can start modifying the templates without having to start over on a
- completely different platform.
+ completely different platform. Review the [customizing](#customizing) section for more information.
## Features
@@ -197,23 +203,37 @@ and verifying that your app is deployed as a review app in the Kubernetes
cluster with the `review/*` environment scope. Similarly, you can check the
other environments.
-## Enabling Auto DevOps
+## Enabling/Disabling Auto DevOps
-If you haven't done already, read the [requirements](#requirements) to make
-full use of Auto DevOps. If this is your fist time, we recommend you follow the
+When first using Auto Devops, review the [requirements](#requirements) to ensure all necessary components to make
+full use of Auto DevOps are available. If this is your fist time, we recommend you follow the
[quick start guide](quick_start_guide.md).
-To enable Auto DevOps to your project:
+GitLab.com users can enable/disable Auto DevOps at the project-level only. Self-managed users
+can enable/disable Auto DevOps at either the project-level or instance-level.
+
+### Enabling/disabling Auto DevOps at the instance-level (Administrators only)
+
+1. Go to **Admin area > Settings > Continuous Integration and Deployment**.
+1. Toggle the checkbox labeled **Default to Auto DevOps pipeline for all projects**.
+1. If enabling, optionally set up the Auto DevOps [base domain](#auto-devops-base-domain) which will be used for Auto Deploy and Auto Review Apps.
+1. Click **Save changes** for the changes to take effect.
+
+NOTE: **Note:**
+Even when disabled at the instance level, project maintainers are still able to enable
+Auto DevOps at the project level.
+
+### Enabling/disabling Auto DevOps at the project-level
-1. Check that your project doesn't have a `.gitlab-ci.yml`, or remove it otherwise
-1. Go to your project's **Settings > CI/CD > Auto DevOps**
-1. Select "Enable Auto DevOps"
+1. Check that your project doesn't have a `.gitlab-ci.yml`, or if one exists, remove it.
+1. Go to your project's **Settings > CI/CD > Auto DevOps**.
+1. Check the **Default to Auto DevOps pipeline** checkbox.
1. Optionally, but recommended, add in the [base domain](#auto-devops-base-domain)
- that will be used by Kubernetes to [deploy your application](#auto-deploy)
- and choose the [deployment strategy](#deployment-strategy)
-1. Hit **Save changes** for the changes to take effect
+ that will be used by Auto DevOps to [deploy your application](#auto-deploy)
+ and choose the [deployment strategy](#deployment-strategy).
+1. Click **Save changes** for the changes to take effect.
-Once saved, an Auto DevOps pipeline will be triggered on the default branch.
+When the feature has been enabled, an Auto DevOps pipeline is triggered on the default branch.
NOTE: **Note:**
For GitLab versions 10.0 - 10.2, when enabling Auto DevOps, a pipeline needs to be
@@ -222,16 +242,16 @@ manually triggered either by pushing a new commit to the repository or by visiti
a new pipeline for your default branch, generally `master`.
NOTE: **Note:**
-If you are a GitLab Administrator, you can
-[enable/disable Auto DevOps instance-wide](../../user/admin_area/settings/continuous_integration.md#auto-devops),
-and all projects that haven't explicitly set an option will have Auto DevOps
-enabled/disabled by default.
-
-NOTE: **Note:**
There is also a feature flag to enable Auto DevOps to a percentage of projects
which can be enabled from the console with
`Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(10)`.
+### Disable Auto DevOps at the project level
+
+1. Go to your project's **Settings > CI/CD > Auto DevOps**.
+1. Uncheck the **Default to Auto DevOps pipeline** checkbox.
+1. Click **Save changes** for the changes to take effect.
+
### Deployment strategy
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/38542) in GitLab 11.0.
@@ -299,8 +319,7 @@ static analysis and other code checks on the current code. The report is
created, and is uploaded as an artifact which you can later download and check
out.
-In GitLab Starter, differences between the source and
-target branches are also
+Any differences between the source and target branches are also
[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html).
### Auto SAST **[ULTIMATE]**
@@ -313,9 +332,12 @@ analysis on the current code and checks for potential security issues. Once the
report is created, it's uploaded as an artifact which you can later download and
check out.
-In GitLab Ultimate, any security warnings are also
+Any security warnings are also
[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/sast.html).
+NOTE: **Note:**
+The Auto SAST stage will be skipped on licenses other than Ultimate.
+
### Auto Dependency Scanning **[ULTIMATE]**
> Introduced in [GitLab Ultimate][ee] 10.7.
@@ -329,6 +351,9 @@ check out.
Any security warnings are also
[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/dependency_scanning.html).
+NOTE: **Note:**
+The Auto Dependency Scanning stage will be skipped on licenses other than Ultimate.
+
### Auto License Management **[ULTIMATE]**
> Introduced in [GitLab Ultimate][ee] 11.0.
@@ -342,6 +367,9 @@ check out.
Any licenses are also
[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/license_management.html).
+NOTE: **Note:**
+The Auto License Management stage will be skipped on licenses other than Ultimate.
+
### Auto Container Scanning
> Introduced in GitLab 10.4.
@@ -352,9 +380,12 @@ Docker image and checks for potential security issues. Once the report is
created, it's uploaded as an artifact which you can later download and
check out.
-In GitLab Ultimate, any security warnings are also
+Any security warnings are also
[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/container_scanning.html).
+NOTE: **Note:**
+The Auto Container Scanning stage will be skipped on licenses other than Ultimate.
+
### Auto Review Apps
NOTE: **Note:**
@@ -374,6 +405,9 @@ branch's code so developers, designers, QA, product managers, and other
reviewers can actually see and interact with code changes as part of the review
process. Auto Review Apps create a Review App for each branch.
+Auto Review Apps will deploy your app to your Kubernetes cluster only. When no cluster
+is available, no deployment will occur.
+
The Review App will have a unique URL based on the project name, the branch
name, and a unique number, combined with the Auto DevOps base domain. For
example, `user-project-branch-1234.example.com`. A link to the Review App shows
@@ -391,9 +425,12 @@ to perform an analysis on the current code and checks for potential security
issues. Once the report is created, it's uploaded as an artifact which you can
later download and check out.
-In GitLab Ultimate, any security warnings are also
+Any security warnings are also
[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/dast.html).
+NOTE: **Note:**
+The Auto DAST stage will be skipped on licenses other than Ultimate.
+
### Auto Browser Performance Testing **[PREMIUM]**
> Introduced in [GitLab Premium][ee] 10.4.
@@ -406,8 +443,8 @@ Auto Browser Performance Testing utilizes the [Sitespeed.io container](https://h
/direction
```
-In GitLab Premium, performance differences between the source
-and target branches are [shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/browser_performance_testing.html).
+Any performance differences between the source and target branches are also
+[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/browser_performance_testing.html).
### Auto Deploy
diff --git a/doc/update/11.3-to-11.4.md b/doc/update/11.3-to-11.4.md
index b50e21f27dd..00dfb19b4b4 100644
--- a/doc/update/11.3-to-11.4.md
+++ b/doc/update/11.3-to-11.4.md
@@ -39,9 +39,9 @@ Download Ruby and compile it:
```bash
mkdir /tmp/ruby && cd /tmp/ruby
-curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.4.tar.gz
-echo 'ec82b0d53bd0adad9b19e6b45e44d54e9ec3f10c ruby-2.4.4.tar.gz' | shasum -c - && tar xzf ruby-2.4.4.tar.gz
-cd ruby-2.4.4
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.5.tar.gz
+echo '4d650f302f1ec00256450b112bb023644b6ab6dd ruby-2.4.5.tar.gz' | shasum -c - && tar xzf ruby-2.4.5.tar.gz
+cd ruby-2.4.5
./configure --disable-install-rdoc
make
diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md
index 632253db94c..3cf46231a9d 100644
--- a/doc/user/project/milestones/index.md
+++ b/doc/user/project/milestones/index.md
@@ -68,7 +68,8 @@ From [project issue boards](../issue_board.md), you can filter by both group mil
When filtering by milestone, in addition to choosing a specific project milestone or group milestone, you can choose a special milestone filter.
-- **No Milestone**: Show issues or merge requests with no assigned milestone.
+- **None**: Show issues or merge requests with no assigned milestone.
+- **Any**: Show issues or merge requests that have an assigned milestone.
- **Upcoming**: Show issues or merge requests that have been assigned the open milestone that has the next upcoming due date (i.e. nearest due date in the future).
- **Started**: Show issues or merge requests that have an assigned milestone with a start date that is before today.
diff --git a/doc/user/project/repository/branches/index.md b/doc/user/project/repository/branches/index.md
index e1d8345f415..783081cec26 100644
--- a/doc/user/project/repository/branches/index.md
+++ b/doc/user/project/repository/branches/index.md
@@ -30,12 +30,12 @@ to learn more.
## Delete merged branches
-> [Introduced][ce-6449] in GitLab 8.14.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6449) in GitLab 8.14.
![Delete merged branches](img/delete_merged_branches.png)
This feature allows merged branches to be deleted in bulk. Only branches that
-have been merged and [are not protected][protected] will be deleted as part of
+have been merged and [are not protected](../../protected_branches.md) will be deleted as part of
this operation.
It's particularly useful to clean up old branches that were not deleted
@@ -44,7 +44,7 @@ automatically when a merge request was merged.
## Branch filter search box
-> [Introduced][https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22166] in GitLab 11.5.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22166) in GitLab 11.5.
![Branch filter search box](img/branch_filter_search_box.png)
@@ -57,6 +57,3 @@ Sometimes when you have hundreds of branches you may want a more flexible matchi
- `^feature` will only match branch names that begin with 'feature'.
- `feature$` will only match branch names that end with 'feature'.
-
-[ce-6449]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6449 "Add button to delete all merged branches"
-[protected]: ../../protected_branches.md
diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb
index 375d8bc1ff5..551d4f4473e 100644
--- a/lib/gitlab/ci/build/artifacts/metadata.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata.rb
@@ -59,9 +59,12 @@ module Gitlab
until gz.eof?
begin
- path = read_string(gz).force_encoding('UTF-8')
- meta = read_string(gz).force_encoding('UTF-8')
+ path = read_string(gz)&.force_encoding('UTF-8')
+ meta = read_string(gz)&.force_encoding('UTF-8')
+ # We might hit an EOF while reading either value, so we should
+ # abort if we don't get any data.
+ next unless path && meta
next unless path.valid_encoding? && meta.valid_encoding?
next unless path =~ match_pattern
next if path =~ INVALID_PATH_PATTERN
diff --git a/lib/gitlab/ci/config/entry/reports.rb b/lib/gitlab/ci/config/entry/reports.rb
index 98f12c226b3..3ac2a6fa777 100644
--- a/lib/gitlab/ci/config/entry/reports.rb
+++ b/lib/gitlab/ci/config/entry/reports.rb
@@ -11,7 +11,7 @@ module Gitlab
include Validatable
include Attributable
- ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast].freeze
+ ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast performance license_management].freeze
attributes ALLOWED_KEYS
@@ -26,6 +26,8 @@ module Gitlab
validates :dependency_scanning, array_of_strings_or_string: true
validates :container_scanning, array_of_strings_or_string: true
validates :dast, array_of_strings_or_string: true
+ validates :performance, array_of_strings_or_string: true
+ validates :license_management, array_of_strings_or_string: true
end
end
diff --git a/lib/gitlab/ci/templates/Android.gitlab-ci.yml b/lib/gitlab/ci/templates/Android.gitlab-ci.yml
index bf7831b937c..6e138639b71 100644
--- a/lib/gitlab/ci/templates/Android.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Android.gitlab-ci.yml
@@ -1,51 +1,45 @@
-# Read more about this script on this blog post https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/, by Greyson Parrelli
+# Read more about this script on this blog post https://about.gitlab.com/2018/10/24/setting-up-gitlab-ci-for-android-projects/, by Jason Lenny
image: openjdk:8-jdk
variables:
ANDROID_COMPILE_SDK: "28"
- ANDROID_BUILD_TOOLS: "28.0.3"
- ANDROID_SDK_TOOLS: "26.1.1"
+ ANDROID_BUILD_TOOLS: "28.0.2"
+ ANDROID_SDK_TOOLS: "4333796"
before_script:
-- apt-get --quiet update --yes
-- apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1
-- wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip
-- unzip android-sdk.zip -d android-sdk-linux
-- echo y | android-sdk-linux/tools/bin/sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" > /dev/null
-- echo y | android-sdk-linux/tools/bin/sdkmanager platform-tools > /dev/null
-- echo y | android-sdk-linux/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" > /dev/null
-- echo y | android-sdk-linux/tools/bin/sdkmanager "extras;google;google_play_services" > /dev/null
-- echo y | android-sdk-linux/tools/bin/sdkmanager "extras;google;m2repository" > /dev/null
-- export ANDROID_HOME=$PWD/android-sdk-linux
-- export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/
-- yes | android-sdk-linux/tools/bin/sdkmanager --licenses &
-- chmod +x ./gradlew
+ - apt-get --quiet update --yes
+ - apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1
+ - wget --quiet --output-document=android-sdk.zip https://dl.google.com/android/repository/sdk-tools-linux-${ANDROID_SDK_TOOLS}.zip
+ - unzip -d android-sdk-linux android-sdk.zip
+ - echo y | android-sdk-linux/tools/bin/sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" >/dev/null
+ - echo y | android-sdk-linux/tools/bin/sdkmanager "platform-tools" >/dev/null
+ - echo y | android-sdk-linux/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" >/dev/null
+ - export ANDROID_HOME=$PWD/android-sdk-linux
+ - export PATH=$PATH:$PWD/android-sdk-linux/platform-tools/
+ - chmod +x ./gradlew
+ # temporarily disable checking for EPIPE error and use yes to accept all licenses
+ - set +o pipefail
+ - yes | android-sdk-linux/tools/bin/sdkmanager --licenses
+ - set -o pipefail
stages:
-- build
-- test
+ - build
+ - test
-build:
+lintDebug:
stage: build
script:
- - ./gradlew assembleDebug
+ - ./gradlew -Pci --console=plain :app:lintDebug -PbuildDir=lint
+
+assembleDebug:
+ stage: build
+ script:
+ - ./gradlew assembleDebug
artifacts:
paths:
- app/build/outputs/
-unitTests:
- stage: test
- script:
- - ./gradlew test
-
-functionalTests:
+debugTests:
stage: test
script:
- - wget --quiet --output-document=android-wait-for-emulator https://raw.githubusercontent.com/travis-ci/travis-cookbooks/0f497eb71291b52a703143c5cd63a217c8766dc9/community-cookbooks/android-sdk/files/default/android-wait-for-emulator
- - chmod +x android-wait-for-emulator
- - echo y | android-sdk-linux/tools/android --silent update sdk --no-ui --all --filter sys-img-x86-google_apis-${ANDROID_COMPILE_SDK}
- - echo no | android-sdk-linux/tools/android create avd -n test -t android-${ANDROID_COMPILE_SDK} --abi google_apis/x86
- - android-sdk-linux/tools/emulator64-x86 -avd test -no-window -no-audio &
- - ./android-wait-for-emulator
- - adb shell input keyevent 82
- - ./gradlew cAT
+ - ./gradlew -Pci --console=plain :app:testDebug
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 30541ee3553..a17f27a3147 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -937,7 +937,7 @@ database (#{dbname}) using a super user and running:
For MySQL you instead need to run:
- GRANT ALL PRIVILEGES ON *.* TO #{user}@'%'
+ GRANT ALL PRIVILEGES ON #{dbname}.* TO #{user}@'%'
Both queries will grant the user super user permissions, ensuring you don't run
into similar problems in the future (e.g. when new tables are created).
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index 6fc86925f81..5d9ecd651a0 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -286,7 +286,7 @@ module Gitlab
end
def patch_name_from_branch(branch_name)
- branch_name.parameterize << '.patch'
+ "#{branch_name.parameterize}.patch"
end
def patch_url
@@ -434,9 +434,11 @@ module Gitlab
end
def conflicting_files_msg
- failed_files.reduce("The conflicts detected were as follows:\n") do |memo, file|
- memo << "\n - #{file}"
- end
+ header = "The conflicts detected were as follows:\n"
+ separator = "\n - "
+ failed_items = failed_files.join(separator)
+
+ "#{header}#{separator}#{failed_items}"
end
end
end
diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb
index 588238de608..f266177bec1 100644
--- a/lib/gitlab/kubernetes/kube_client.rb
+++ b/lib/gitlab/kubernetes/kube_client.rb
@@ -13,11 +13,21 @@ module Gitlab
class KubeClient
include Gitlab::Utils::StrongMemoize
- SUPPORTED_API_GROUPS = [
- 'api',
- 'apis/rbac.authorization.k8s.io',
- 'apis/extensions'
- ].freeze
+ SUPPORTED_API_GROUPS = {
+ core: { group: 'api', version: 'v1' },
+ rbac: { group: 'apis/rbac.authorization.k8s.io', version: 'v1' },
+ extensions: { group: 'apis/extensions', version: 'v1beta1' }
+ }.freeze
+
+ SUPPORTED_API_GROUPS.each do |name, params|
+ client_method_name = "#{name}_client".to_sym
+
+ define_method(client_method_name) do
+ strong_memoize(client_method_name) do
+ build_kubeclient(params[:group], params[:version])
+ end
+ end
+ end
# Core API methods delegates to the core api group client
delegate :get_pods,
@@ -45,6 +55,13 @@ module Gitlab
:update_cluster_role_binding,
to: :rbac_client
+ # RBAC methods delegates to the apis/rbac.authorization.k8s.io api
+ # group client
+ delegate :create_role_binding,
+ :get_role_binding,
+ :update_role_binding,
+ to: :rbac_client
+
# Deployments resource is currently on the apis/extensions api group
delegate :get_deployments,
to: :extensions_client
@@ -55,48 +72,21 @@ module Gitlab
:watch_pod_log,
to: :core_client
- def initialize(api_prefix, api_groups = ['api'], api_version = 'v1', **kubeclient_options)
- raise ArgumentError unless check_api_groups_supported?(api_groups)
+ attr_reader :api_prefix, :kubeclient_options
+ def initialize(api_prefix, **kubeclient_options)
@api_prefix = api_prefix
- @api_groups = api_groups
- @api_version = api_version
@kubeclient_options = kubeclient_options
end
- def discover!
- clients.each(&:discover)
- end
-
- def clients
- hashed_clients.values
- end
-
- def core_client
- hashed_clients['api']
- end
-
- def rbac_client
- hashed_clients['apis/rbac.authorization.k8s.io']
- end
-
- def extensions_client
- hashed_clients['apis/extensions']
- end
-
- def hashed_clients
- strong_memoize(:hashed_clients) do
- @api_groups.map do |api_group|
- api_url = join_api_url(@api_prefix, api_group)
- [api_group, ::Kubeclient::Client.new(api_url, @api_version, **@kubeclient_options)]
- end.to_h
- end
- end
-
private
- def check_api_groups_supported?(api_groups)
- api_groups.all? {|api_group| SUPPORTED_API_GROUPS.include?(api_group) }
+ def build_kubeclient(api_group, api_version)
+ ::Kubeclient::Client.new(
+ join_api_url(api_prefix, api_group),
+ api_version,
+ **kubeclient_options
+ )
end
def join_api_url(api_prefix, api_path)
diff --git a/lib/gitlab/kubernetes/role_binding.rb b/lib/gitlab/kubernetes/role_binding.rb
new file mode 100644
index 00000000000..4f3ee040bf2
--- /dev/null
+++ b/lib/gitlab/kubernetes/role_binding.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Kubernetes
+ class RoleBinding
+ attr_reader :role_name, :namespace, :service_account_name
+
+ def initialize(role_name:, namespace:, service_account_name:)
+ @role_name = role_name
+ @namespace = namespace
+ @service_account_name = service_account_name
+ end
+
+ def generate
+ ::Kubeclient::Resource.new.tap do |resource|
+ resource.metadata = metadata
+ resource.roleRef = role_ref
+ resource.subjects = subjects
+ end
+ end
+
+ private
+
+ def metadata
+ { name: "gitlab-#{namespace}", namespace: namespace }
+ end
+
+ def role_ref
+ {
+ apiGroup: 'rbac.authorization.k8s.io',
+ kind: 'Role',
+ name: role_name
+ }
+ end
+
+ def subjects
+ [
+ {
+ kind: 'ServiceAccount',
+ name: service_account_name,
+ namespace: namespace
+ }
+ ]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb
index 4a745147858..2b7e12639be 100644
--- a/lib/gitlab/setup_helper.rb
+++ b/lib/gitlab/setup_helper.rb
@@ -32,7 +32,10 @@ module Gitlab
end
if Rails.env.test?
- storages << { name: 'test_second_storage', path: Rails.root.join('tmp', 'tests', 'second_storage').to_s }
+ storage_path = Rails.root.join('tmp', 'tests', 'second_storage').to_s
+
+ FileUtils.mkdir(storage_path) unless File.exist?(storage_path)
+ storages << { name: 'test_second_storage', path: storage_path }
end
config = { socket_path: address.sub(/\Aunix:/, ''), storage: storages }
diff --git a/lib/tasks/haml-lint.rake b/lib/tasks/haml-lint.rake
index ad2d034b0b4..786efd14b1a 100644
--- a/lib/tasks/haml-lint.rake
+++ b/lib/tasks/haml-lint.rake
@@ -2,5 +2,16 @@ unless Rails.env.production?
require 'haml_lint/rake_task'
require 'haml_lint/inline_javascript'
+ # Workaround for warnings from parser/current
+ # Keep it even if it no longer emits any warnings,
+ # because we'll still see warnings in console/server anyway,
+ # and we don't need to break static-analysis for this.
+ task :haml_lint do
+ require 'parser'
+ def Parser.warn(*args)
+ puts(*args) # static-analysis ignores stdout if status is 0
+ end
+ end
+
HamlLint::RakeTask.new
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index bb18d4eccd8..26270595c6a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3599,6 +3599,9 @@ msgstr ""
msgid "List available repositories"
msgstr ""
+msgid "List view"
+msgstr ""
+
msgid "List your Bitbucket Server repositories"
msgstr ""
@@ -4027,6 +4030,12 @@ msgstr ""
msgid "No"
msgstr ""
+msgid "No Assignee"
+msgstr ""
+
+msgid "No Label"
+msgstr ""
+
msgid "No assignee"
msgstr ""
@@ -4132,6 +4141,12 @@ msgstr ""
msgid "Notes|Are you sure you want to cancel creating this comment?"
msgstr ""
+msgid "Notes|Show all activity"
+msgstr ""
+
+msgid "Notes|Show comments only"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -5589,6 +5604,9 @@ msgstr ""
msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
+msgid "Something went wrong while fetching comments. Please try again."
+msgstr ""
+
msgid "Something went wrong while fetching the projects."
msgstr ""
@@ -6504,6 +6522,9 @@ msgstr ""
msgid "Track time with quick actions"
msgstr ""
+msgid "Tree view"
+msgstr ""
+
msgid "Trending"
msgstr ""
@@ -6585,6 +6606,9 @@ msgstr ""
msgid "Up to date"
msgstr ""
+msgid "Upcoming"
+msgstr ""
+
msgid "Update"
msgstr ""
diff --git a/package.json b/package.json
index 5b0a92ee7a1..086617dc265 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,7 @@
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-syntax-import-meta": "^7.0.0",
"@babel/preset-env": "^7.1.0",
- "@gitlab-org/gitlab-svgs": "^1.32.0",
+ "@gitlab-org/gitlab-svgs": "^1.33.0",
"@gitlab-org/gitlab-ui": "^1.8.0",
"autosize": "^4.0.0",
"axios": "^0.17.1",
diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh
index 78293464265..d372bcbdab1 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -47,15 +47,23 @@ function create_secret() {
--dry-run -o json | kubectl apply -f -
}
+function deployExists() {
+ local namespace="${1}"
+ local deploy="${2}"
+ helm status --tiller-namespace "${namespace}" "${deploy}" >/dev/null 2>&1
+ return $?
+}
+
function previousDeployFailed() {
set +e
- echo "Checking for previous deployment of $CI_ENVIRONMENT_SLUG"
- deployment_status=$(helm status $CI_ENVIRONMENT_SLUG >/dev/null 2>&1)
+ deploy="${1}"
+ echo "Checking for previous deployment of ${deploy}"
+ deployment_status=$(helm status ${deploy} >/dev/null 2>&1)
status=$?
# if `status` is `0`, deployment exists, has a status
if [ $status -eq 0 ]; then
echo "Previous deployment found, checking status"
- deployment_status=$(helm status $CI_ENVIRONMENT_SLUG | grep ^STATUS | cut -d' ' -f2)
+ deployment_status=$(helm status ${deploy} | grep ^STATUS | cut -d' ' -f2)
echo "Previous deployment state: $deployment_status"
if [[ "$deployment_status" == "FAILED" || "$deployment_status" == "PENDING_UPGRADE" || "$deployment_status" == "PENDING_INSTALL" ]]; then
status=0;
@@ -113,7 +121,7 @@ function deploy() {
fi
# Cleanup and previous installs, as FAILED and PENDING_UPGRADE will cause errors with `upgrade`
- if [ "$CI_ENVIRONMENT_SLUG" != "production" ] && previousDeployFailed ; then
+ if [ "$CI_ENVIRONMENT_SLUG" != "production" ] && previousDeployFailed "$CI_ENVIRONMENT_SLUG" ; then
echo "Deployment in bad state, cleaning up $CI_ENVIRONMENT_SLUG"
delete
cleanup
@@ -149,6 +157,7 @@ HELM_CMD=$(cat << EOF
--set gitlab.gitlab-shell.image.tag="v$GITLAB_SHELL_VERSION" \
--set gitlab.unicorn.workhorse.image="$gitlab_workhorse_image_repository" \
--set gitlab.unicorn.workhorse.tag="$CI_COMMIT_REF_NAME" \
+ --set nginx-ingress.controller.config.ssl-ciphers="ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4" \
--namespace="$KUBE_NAMESPACE" \
--version="$CI_PIPELINE_ID-$CI_JOB_ID" \
"$name" \
@@ -182,3 +191,23 @@ function cleanup() {
| xargs kubectl -n "$KUBE_NAMESPACE" delete \
|| true
}
+
+function install_external_dns() {
+ local release_name="dns-gitlab-review-app"
+ local domain=$(echo "${REVIEW_APPS_DOMAIN}" | awk -F. '{printf "%s.%s", $(NF-1), $NF}')
+
+ if ! deployExists "${KUBE_NAMESPACE}" "${release_name}" || previousDeployFailed "${release_name}" ; then
+ echo "Installing external-dns helm chart"
+ helm repo update
+ helm install stable/external-dns \
+ -n "${release_name}" \
+ --namespace "${KUBE_NAMESPACE}" \
+ --set provider="aws" \
+ --set aws.secretKey="${REVIEW_APPS_AWS_SECRET_KEY}" \
+ --set aws.accessKey="${REVIEW_APPS_AWS_ACCESS_KEY}" \
+ --set aws.zoneType="public" \
+ --set domainFilters[0]="${domain}" \
+ --set txtOwnerId="${KUBE_NAMESPACE}" \
+ --set rbac.create="true"
+ fi
+}
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 9df77560320..80138183c07 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -1028,6 +1028,13 @@ describe Projects::IssuesController do
.not_to exceed_query_limit(control)
end
+ context 'when user is setting notes filters' do
+ let(:issuable) { issue }
+ let!(:discussion_note) { create(:discussion_note_on_issue, :system, noteable: issuable, project: project) }
+
+ it_behaves_like 'issuable notes filter'
+ end
+
context 'with cross-reference system note', :request_store do
let(:new_issue) { create(:issue) }
let(:cross_reference) { "mentioned in #{new_issue.to_reference(issue.project)}" }
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 1484676eea3..2023d4b0bd0 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -297,6 +297,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
expect(response).to match_response_schema('job/job_details')
expect(json_response['runners']['online']).to be false
expect(json_response['runners']['available']).to be false
+ expect(json_response['stuck']).to be true
end
end
@@ -309,6 +310,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
expect(response).to match_response_schema('job/job_details')
expect(json_response['runners']['online']).to be false
expect(json_response['runners']['available']).to be true
+ expect(json_response['stuck']).to be true
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 78581dc37a4..dcfd6c05200 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -87,6 +87,14 @@ describe Projects::MergeRequestsController do
end
end
+ context 'when user is setting notes filters' do
+ let(:issuable) { merge_request }
+ let!(:discussion_note) { create(:discussion_note_on_merge_request, :system, noteable: issuable, project: project) }
+ let!(:discussion_comment) { create(:discussion_note_on_merge_request, noteable: issuable, project: project) }
+
+ it_behaves_like 'issuable notes filter'
+ end
+
describe 'as json' do
context 'with basic serializer param' do
it 'renders basic MR entity as json' do
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index e48c9dea976..9ac7b8ee8a8 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -47,6 +47,37 @@ describe Projects::NotesController do
get :index, request_params
end
+ context 'when user notes_filter is present' do
+ let(:notes_json) { parsed_response[:notes] }
+ let!(:comment) { create(:note, noteable: issue, project: project) }
+ let!(:system_note) { create(:note, noteable: issue, project: project, system: true) }
+
+ it 'filters system notes by comments' do
+ user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_comments], issue)
+
+ get :index, request_params
+
+ expect(notes_json.count).to eq(1)
+ expect(notes_json.first[:id].to_i).to eq(comment.id)
+ end
+
+ it 'returns all notes' do
+ user.set_notes_filter(UserPreference::NOTES_FILTERS[:all_notes], issue)
+
+ get :index, request_params
+
+ expect(notes_json.map { |note| note[:id].to_i }).to contain_exactly(comment.id, system_note.id)
+ end
+
+ it 'does not merge label event notes' do
+ user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_comments], issue)
+
+ expect(ResourceEvents::MergeIntoNotesService).not_to receive(:new)
+
+ get :index, request_params
+ end
+ end
+
context 'for a discussion note' do
let(:project) { create(:project, :repository) }
let!(:note) { create(:discussion_note_on_merge_request, project: project) }
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
index f564e7bee47..24e70913b87 100644
--- a/spec/factories/ci/runners.rb
+++ b/spec/factories/ci/runners.rb
@@ -47,5 +47,15 @@ FactoryBot.define do
trait :ref_protected do
access_level :ref_protected
end
+
+ trait :tagged_only do
+ run_untagged false
+
+ tag_list %w(tag1 tag2)
+ end
+
+ trait :locked do
+ locked true
+ end
end
end
diff --git a/spec/factories/user_preferences.rb b/spec/factories/user_preferences.rb
new file mode 100644
index 00000000000..19059a93625
--- /dev/null
+++ b/spec/factories/user_preferences.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :user_preference do
+ user
+
+ trait :only_comments do
+ issue_notes_filter { UserPreference::NOTES_FILTERS[:only_comments] }
+ merge_request_notes_filter { UserPreference::NOTE_FILTERS[:only_comments] }
+ end
+ end
+end
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index f76d30056da..ef5801e61e8 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -189,13 +189,21 @@ describe 'Dropdown milestone', :js do
end
it 'selects `no milestone`' do
- click_static_milestone('No Milestone')
+ click_static_milestone('None')
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect_tokens([milestone_token('none', false)])
expect_filtered_search_input_empty
end
+ it 'selects `any milestone`' do
+ click_static_milestone('Any')
+
+ expect(page).to have_css(js_dropdown_milestone, visible: false)
+ expect_tokens([milestone_token('any', false)])
+ expect_filtered_search_input_empty
+ end
+
it 'selects `upcoming milestone`' do
click_static_milestone('Upcoming')
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 1ea8a640e17..b3bea92e635 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -151,9 +151,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
it 'renders escaped tooltip name' do
- page.within('aside.right-sidebar') do
- expect(find('.active.build-job a')['data-original-title']).to eq('&lt;img src=x onerror=alert(document.domain)&gt; - passed')
- end
+ page.find('.active.build-job a').hover
+ expect(page).to have_content('<img src=x onerror=alert(document.domain)> - passed')
end
end
@@ -722,6 +721,62 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
expect(page).not_to have_css('.js-job-sidebar.right-sidebar-collpased')
end
end
+
+ context 'stuck', :js do
+ before do
+ visit project_job_path(project, job)
+ wait_for_requests
+ end
+
+ context 'without active runners available' do
+ let(:runner) { create(:ci_runner, :instance, active: false) }
+ let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner) }
+
+ it 'renders message about job being stuck because no runners are active' do
+ expect(page).to have_css('.js-stuck-no-active-runner')
+ expect(page).to have_content("This job is stuck, because you don't have any active runners that can run this job.")
+ end
+ end
+
+ context 'when available runners can not run specified tag' do
+ let(:runner) { create(:ci_runner, :instance, active: false) }
+ let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner, tag_list: %w(docker linux)) }
+
+ it 'renders message about job being stuck because of no runners with the specified tags' do
+ expect(page).to have_css('.js-stuck-with-tags')
+ expect(page).to have_content("This job is stuck, because you don't have any active runners online with any of these tags assigned to them:")
+ end
+ end
+
+ context 'when runners are offline and build has tags' do
+ let(:runner) { create(:ci_runner, :instance, active: true) }
+ let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner, tag_list: %w(docker linux)) }
+
+ it 'renders message about job being stuck because of no runners with the specified tags' do
+ expect(page).to have_css('.js-stuck-with-tags')
+ expect(page).to have_content("This job is stuck, because you don't have any active runners online with any of these tags assigned to them:")
+ end
+ end
+
+ context 'without any runners available' do
+ let(:job) { create(:ci_build, :pending, pipeline: pipeline) }
+
+ it 'renders message about job being stuck because not runners are available' do
+ expect(page).to have_css('.js-stuck-no-active-runner')
+ expect(page).to have_content("This job is stuck, because you don't have any active runners that can run this job.")
+ end
+ end
+
+ context 'without available runners online' do
+ let(:runner) { create(:ci_runner, :instance, active: true) }
+ let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner) }
+
+ it 'renders message about job being stuck because runners are offline' do
+ expect(page).to have_css('.js-stuck-no-runners')
+ expect(page).to have_content("This job is stuck, because the project doesn't have any runners online assigned to it.")
+ end
+ end
+ end
end
describe "POST /:project/jobs/:id/cancel", :js do
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 491c64fc329..cd6c37bf54d 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -68,6 +68,10 @@ describe 'Pipeline', :js do
expect(page).to have_css('#js-tab-pipeline.active')
end
+ it 'shows link to the pipeline ref' do
+ expect(page).to have_link(pipeline.ref)
+ end
+
it_behaves_like 'showing user status' do
let(:user_with_status) { pipeline.user }
@@ -236,6 +240,20 @@ describe 'Pipeline', :js do
it { expect(page).not_to have_content('Cancel running') }
end
end
+
+ context 'when pipeline ref does not exist in repository anymore' do
+ let(:pipeline) do
+ create(:ci_empty_pipeline, project: project,
+ ref: 'non-existent',
+ sha: project.commit.id,
+ user: user)
+ end
+
+ it 'does not render link to the pipeline ref' do
+ expect(page).not_to have_link(pipeline.ref)
+ expect(page).to have_content(pipeline.ref)
+ end
+ end
end
context 'when user does not have access to read jobs' do
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index b776e9d856a..de9974c45e1 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -9,6 +9,27 @@ describe NotesFinder do
end
describe '#execute' do
+ context 'when notes filter is present' do
+ let!(:comment) { create(:note_on_issue, project: project) }
+ let!(:system_note) { create(:note_on_issue, project: project, system: true) }
+
+ it 'filters system notes' do
+ finder = described_class.new(project, user, notes_filter: UserPreference::NOTES_FILTERS[:only_comments])
+
+ notes = finder.execute
+
+ expect(notes).to match_array(comment)
+ end
+
+ it 'gets all notes' do
+ finder = described_class.new(project, user, notes_filter: UserPreference::NOTES_FILTERS[:all_activity])
+
+ notes = finder.execute
+
+ expect(notes).to match_array([comment, system_note])
+ end
+ end
+
it 'finds notes on merge requests' do
create(:note_on_merge_request, project: project)
diff --git a/spec/fixtures/api/schemas/job/job_details.json b/spec/fixtures/api/schemas/job/job_details.json
index 8218474705c..cdf7b049ab6 100644
--- a/spec/fixtures/api/schemas/job/job_details.json
+++ b/spec/fixtures/api/schemas/job/job_details.json
@@ -18,6 +18,7 @@
"runner": { "$ref": "runner.json" },
"runners": { "$ref": "runners.json" },
"has_trace": { "type": "boolean" },
- "stage": { "type": "string" }
+ "stage": { "type": "string" },
+ "stuck": { "type": "boolean" }
}
}
diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js
index bdee85f90b1..dc5737558c0 100644
--- a/spec/javascripts/collapsed_sidebar_todo_spec.js
+++ b/spec/javascripts/collapsed_sidebar_todo_spec.js
@@ -45,8 +45,10 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
expect(document.querySelector('.js-issuable-todo.sidebar-collapsed-icon')).not.toBeNull();
expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .fa-plus-square'),
- ).not.toBeNull();
+ document
+ .querySelector('.js-issuable-todo.sidebar-collapsed-icon svg use')
+ .getAttribute('xlink:href'),
+ ).toContain('todo-add');
expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
@@ -68,8 +70,10 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
).not.toBeNull();
expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .fa-check-square'),
- ).not.toBeNull();
+ document
+ .querySelector('.js-issuable-todo.sidebar-collapsed-icon svg.todo-undone use')
+ .getAttribute('xlink:href'),
+ ).toContain('todo-done');
done();
});
diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js
index 9fedbcc4c25..2821f4d6793 100644
--- a/spec/javascripts/datetime_utility_spec.js
+++ b/spec/javascripts/datetime_utility_spec.js
@@ -192,3 +192,163 @@ describe('formatTime', () => {
});
});
});
+
+describe('datefix', () => {
+ describe('pad', () => {
+ it('should add a 0 when length is smaller than 2', () => {
+ expect(datetimeUtility.pad(2)).toEqual('02');
+ });
+
+ it('should not add a zero when lenght matches the default', () => {
+ expect(datetimeUtility.pad(12)).toEqual('12');
+ });
+
+ it('should add a 0 when lenght is smaller than the provided', () => {
+ expect(datetimeUtility.pad(12, 3)).toEqual('012');
+ });
+ });
+
+ describe('parsePikadayDate', () => {
+ // removed because of https://gitlab.com/gitlab-org/gitlab-ce/issues/39834
+ });
+
+ describe('pikadayToString', () => {
+ it('should format a UTC date into yyyy-mm-dd format', () => {
+ expect(datetimeUtility.pikadayToString(new Date('2020-01-29:00:00'))).toEqual('2020-01-29');
+ });
+ });
+});
+
+describe('prettyTime methods', () => {
+ const assertTimeUnits = (obj, minutes, hours, days, weeks) => {
+ expect(obj.minutes).toBe(minutes);
+ expect(obj.hours).toBe(hours);
+ expect(obj.days).toBe(days);
+ expect(obj.weeks).toBe(weeks);
+ };
+
+ describe('parseSeconds', () => {
+ it('should correctly parse a negative value', () => {
+ const zeroSeconds = datetimeUtility.parseSeconds(-1000);
+
+ assertTimeUnits(zeroSeconds, 16, 0, 0, 0);
+ });
+
+ it('should correctly parse a zero value', () => {
+ const zeroSeconds = datetimeUtility.parseSeconds(0);
+
+ assertTimeUnits(zeroSeconds, 0, 0, 0, 0);
+ });
+
+ it('should correctly parse a small non-zero second values', () => {
+ const subOneMinute = datetimeUtility.parseSeconds(10);
+ const aboveOneMinute = datetimeUtility.parseSeconds(100);
+ const manyMinutes = datetimeUtility.parseSeconds(1000);
+
+ assertTimeUnits(subOneMinute, 0, 0, 0, 0);
+ assertTimeUnits(aboveOneMinute, 1, 0, 0, 0);
+ assertTimeUnits(manyMinutes, 16, 0, 0, 0);
+ });
+
+ it('should correctly parse large second values', () => {
+ const aboveOneHour = datetimeUtility.parseSeconds(4800);
+ const aboveOneDay = datetimeUtility.parseSeconds(110000);
+ const aboveOneWeek = datetimeUtility.parseSeconds(25000000);
+
+ assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
+ assertTimeUnits(aboveOneDay, 33, 6, 3, 0);
+ assertTimeUnits(aboveOneWeek, 26, 0, 3, 173);
+ });
+
+ it('should correctly accept a custom param for hoursPerDay', () => {
+ const config = { hoursPerDay: 24 };
+
+ const aboveOneHour = datetimeUtility.parseSeconds(4800, config);
+ const aboveOneDay = datetimeUtility.parseSeconds(110000, config);
+ const aboveOneWeek = datetimeUtility.parseSeconds(25000000, config);
+
+ assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
+ assertTimeUnits(aboveOneDay, 33, 6, 1, 0);
+ assertTimeUnits(aboveOneWeek, 26, 8, 4, 57);
+ });
+
+ it('should correctly accept a custom param for daysPerWeek', () => {
+ const config = { daysPerWeek: 7 };
+
+ const aboveOneHour = datetimeUtility.parseSeconds(4800, config);
+ const aboveOneDay = datetimeUtility.parseSeconds(110000, config);
+ const aboveOneWeek = datetimeUtility.parseSeconds(25000000, config);
+
+ assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
+ assertTimeUnits(aboveOneDay, 33, 6, 3, 0);
+ assertTimeUnits(aboveOneWeek, 26, 0, 0, 124);
+ });
+
+ it('should correctly accept custom params for daysPerWeek and hoursPerDay', () => {
+ const config = { daysPerWeek: 55, hoursPerDay: 14 };
+
+ const aboveOneHour = datetimeUtility.parseSeconds(4800, config);
+ const aboveOneDay = datetimeUtility.parseSeconds(110000, config);
+ const aboveOneWeek = datetimeUtility.parseSeconds(25000000, config);
+
+ assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
+ assertTimeUnits(aboveOneDay, 33, 2, 2, 0);
+ assertTimeUnits(aboveOneWeek, 26, 0, 1, 9);
+ });
+ });
+
+ describe('stringifyTime', () => {
+ it('should stringify values with all non-zero units', () => {
+ const timeObject = {
+ weeks: 1,
+ days: 4,
+ hours: 7,
+ minutes: 20,
+ };
+
+ const timeString = datetimeUtility.stringifyTime(timeObject);
+
+ expect(timeString).toBe('1w 4d 7h 20m');
+ });
+
+ it('should stringify values with some non-zero units', () => {
+ const timeObject = {
+ weeks: 0,
+ days: 4,
+ hours: 0,
+ minutes: 20,
+ };
+
+ const timeString = datetimeUtility.stringifyTime(timeObject);
+
+ expect(timeString).toBe('4d 20m');
+ });
+
+ it('should stringify values with no non-zero units', () => {
+ const timeObject = {
+ weeks: 0,
+ days: 0,
+ hours: 0,
+ minutes: 0,
+ };
+
+ const timeString = datetimeUtility.stringifyTime(timeObject);
+
+ expect(timeString).toBe('0m');
+ });
+ });
+
+ describe('abbreviateTime', () => {
+ it('should abbreviate stringified times for weeks', () => {
+ const fullTimeString = '1w 3d 4h 5m';
+
+ expect(datetimeUtility.abbreviateTime(fullTimeString)).toBe('1w');
+ });
+
+ it('should abbreviate stringified times for non-weeks', () => {
+ const fullTimeString = '0w 3d 4h 5m';
+
+ expect(datetimeUtility.abbreviateTime(fullTimeString)).toBe('3d');
+ });
+ });
+});
diff --git a/spec/javascripts/diffs/components/tree_list_spec.js b/spec/javascripts/diffs/components/tree_list_spec.js
index 08e25d2004e..fc94d0bab5b 100644
--- a/spec/javascripts/diffs/components/tree_list_spec.js
+++ b/spec/javascripts/diffs/components/tree_list_spec.js
@@ -53,7 +53,7 @@ describe('Diffs tree list component', () => {
fileHash: 'test',
key: 'index.js',
name: 'index.js',
- path: 'index.js',
+ path: 'app/index.js',
removedLines: 0,
tempFile: true,
type: 'blob',
@@ -104,7 +104,55 @@ describe('Diffs tree list component', () => {
vm.$el.querySelector('.file-row').click();
- expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', 'index.js');
+ expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/scrollToFile', 'app/index.js');
+ });
+
+ it('renders as file list when renderTreeList is false', done => {
+ vm.renderTreeList = false;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelectorAll('.file-row').length).toBe(1);
+
+ done();
+ });
+ });
+
+ it('renders file paths when renderTreeList is false', done => {
+ vm.renderTreeList = false;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.file-row').textContent).toContain('app/index.js');
+
+ done();
+ });
+ });
+
+ it('hides render buttons when input is focused', done => {
+ const focusEvent = new Event('focus');
+
+ vm.$el.querySelector('.form-control').dispatchEvent(focusEvent);
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.tree-list-view-toggle').style.display).toBe('none');
+
+ done();
+ });
+ });
+
+ it('shows render buttons when input is blurred', done => {
+ const blurEvent = new Event('blur');
+ vm.focusSearch = true;
+
+ vm.$nextTick()
+ .then(() => {
+ vm.$el.querySelector('.form-control').dispatchEvent(blurEvent);
+ })
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.$el.querySelector('.tree-list-view-toggle').style.display).not.toBe('none');
+ })
+ .then(done)
+ .catch(done.fail);
});
});
@@ -117,4 +165,24 @@ describe('Diffs tree list component', () => {
expect(vm.search).toBe('');
});
});
+
+ describe('toggleRenderTreeList', () => {
+ it('updates renderTreeList', () => {
+ expect(vm.renderTreeList).toBe(true);
+
+ vm.toggleRenderTreeList(false);
+
+ expect(vm.renderTreeList).toBe(false);
+ });
+ });
+
+ describe('toggleFocusSearch', () => {
+ it('updates focusSearch', () => {
+ expect(vm.focusSearch).toBe(false);
+
+ vm.toggleFocusSearch(true);
+
+ expect(vm.focusSearch).toBe(true);
+ });
+ });
});
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index 85c1926fcb1..bb623953710 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -27,7 +27,6 @@ import actions, {
toggleShowTreeList,
} from '~/diffs/store/actions';
import * as types from '~/diffs/store/mutation_types';
-import { reduceDiscussionsToLineCodes } from '~/notes/stores/utils';
import axios from '~/lib/utils/axios_utils';
import testAction from '../../helpers/vuex_action_helper';
@@ -152,7 +151,7 @@ describe('DiffsStoreActions', () => {
original_position: diffPosition,
};
- const discussions = reduceDiscussionsToLineCodes([singleDiscussion]);
+ const discussions = [singleDiscussion];
testAction(
assignDiscussionsToDiff,
@@ -162,8 +161,7 @@ describe('DiffsStoreActions', () => {
{
type: types.SET_LINE_DISCUSSIONS_FOR_FILE,
payload: {
- fileHash: 'ABC',
- discussions: [singleDiscussion],
+ discussion: singleDiscussion,
diffPositionByLineCode: {
ABC_1_1: {
baseSha: 'abc',
@@ -581,7 +579,6 @@ describe('DiffsStoreActions', () => {
describe('saveDiffDiscussion', () => {
beforeEach(() => {
spyOnDependency(actions, 'getNoteFormData').and.returnValue('testData');
- spyOnDependency(actions, 'reduceDiscussionsToLineCodes').and.returnValue('discussions');
});
it('dispatches actions', done => {
@@ -602,7 +599,7 @@ describe('DiffsStoreActions', () => {
.then(() => {
expect(dispatch.calls.argsFor(0)).toEqual(['saveNote', 'testData', { root: true }]);
expect(dispatch.calls.argsFor(1)).toEqual(['updateDiscussion', 'test', { root: true }]);
- expect(dispatch.calls.argsFor(2)).toEqual(['assignDiscussionsToDiff', 'discussions']);
+ expect(dispatch.calls.argsFor(2)).toEqual(['assignDiscussionsToDiff', ['discussion']]);
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js
index b7e28391419..4b6d3d5bcba 100644
--- a/spec/javascripts/diffs/store/mutations_spec.js
+++ b/spec/javascripts/diffs/store/mutations_spec.js
@@ -198,40 +198,32 @@ describe('DiffsStoreMutations', () => {
},
],
};
- const discussions = [
- {
- id: 1,
- line_code: 'ABC_1',
- diff_discussion: true,
- resolvable: true,
- original_position: diffPosition,
- position: diffPosition,
+ const discussion = {
+ id: 1,
+ line_code: 'ABC_1',
+ diff_discussion: true,
+ resolvable: true,
+ original_position: diffPosition,
+ position: diffPosition,
+ diff_file: {
+ file_hash: state.diffFiles[0].fileHash,
},
- {
- id: 2,
- line_code: 'ABC_1',
- diff_discussion: true,
- resolvable: true,
- original_position: diffPosition,
- position: diffPosition,
- },
- ];
+ };
const diffPositionByLineCode = {
ABC_1: diffPosition,
};
mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, {
- fileHash: 'ABC',
- discussions,
+ discussion,
diffPositionByLineCode,
});
- expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(2);
- expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[1].id).toEqual(2);
+ expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(1);
+ expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[0].id).toEqual(1);
- expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(2);
- expect(state.diffFiles[0].highlightedDiffLines[0].discussions[1].id).toEqual(2);
+ expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(1);
+ expect(state.diffFiles[0].highlightedDiffLines[0].discussions[0].id).toEqual(1);
});
it('should add legacy discussions to the given line', () => {
@@ -272,36 +264,30 @@ describe('DiffsStoreMutations', () => {
},
],
};
- const discussions = [
- {
- id: 1,
- line_code: 'ABC_1',
- diff_discussion: true,
- active: true,
+ const discussion = {
+ id: 1,
+ line_code: 'ABC_1',
+ diff_discussion: true,
+ active: true,
+ diff_file: {
+ file_hash: state.diffFiles[0].fileHash,
},
- {
- id: 2,
- line_code: 'ABC_1',
- diff_discussion: true,
- active: true,
- },
- ];
+ };
const diffPositionByLineCode = {
ABC_1: diffPosition,
};
mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, {
- fileHash: 'ABC',
- discussions,
+ discussion,
diffPositionByLineCode,
});
- expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(2);
- expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[1].id).toEqual(2);
+ expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(1);
+ expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[0].id).toEqual(1);
- expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(2);
- expect(state.diffFiles[0].highlightedDiffLines[0].discussions[1].id).toEqual(2);
+ expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(1);
+ expect(state.diffFiles[0].highlightedDiffLines[0].discussions[0].id).toEqual(1);
});
});
diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js
index ef367fc09fa..f49dee3696d 100644
--- a/spec/javascripts/diffs/store/utils_spec.js
+++ b/spec/javascripts/diffs/store/utils_spec.js
@@ -445,6 +445,14 @@ describe('DiffsStoreUtils', () => {
fileHash: 'test',
},
{
+ newPath: 'app/test/filepathneedstruncating.js',
+ deletedFile: false,
+ newFile: true,
+ removedLines: 0,
+ addedLines: 0,
+ fileHash: 'test',
+ },
+ {
newPath: 'package.json',
deletedFile: true,
newFile: false,
@@ -498,6 +506,19 @@ describe('DiffsStoreUtils', () => {
type: 'blob',
tree: [],
},
+ {
+ addedLines: 0,
+ changed: true,
+ deleted: false,
+ fileHash: 'test',
+ key: 'app/test/filepathneedstruncating.js',
+ name: 'filepathneedstruncating.js',
+ path: 'app/test/filepathneedstruncating.js',
+ removedLines: 0,
+ tempFile: true,
+ type: 'blob',
+ tree: [],
+ },
],
},
],
@@ -527,6 +548,7 @@ describe('DiffsStoreUtils', () => {
'app/index.js',
'app/test',
'app/test/index.js',
+ 'app/test/filepathneedstruncating.js',
'package.json',
]);
});
diff --git a/spec/javascripts/flash_spec.js b/spec/javascripts/flash_spec.js
index d7338ee0f66..aecab331ead 100644
--- a/spec/javascripts/flash_spec.js
+++ b/spec/javascripts/flash_spec.js
@@ -172,7 +172,7 @@ describe('Flash', () => {
flash('test');
expect(document.querySelector('.flash-text').className).toBe(
- 'flash-text container-fluid container-limited',
+ 'flash-text container-fluid container-limited limit-container-width',
);
});
@@ -180,7 +180,7 @@ describe('Flash', () => {
document.querySelector('.content-wrapper').className = 'js-content-wrapper';
flash('test');
- expect(document.querySelector('.flash-text').className.trim()).toBe('flash-text');
+ expect(document.querySelector('.flash-text').className.trim()).toContain('flash-text');
});
it('removes element after clicking', () => {
diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js
index e6d403dc826..288c06d6615 100644
--- a/spec/javascripts/jobs/components/job_app_spec.js
+++ b/spec/javascripts/jobs/components/job_app_spec.js
@@ -88,7 +88,9 @@ describe('Job App ', () => {
describe('triggered job', () => {
beforeEach(() => {
- mock.onGet(props.endpoint).replyOnce(200, Object.assign({}, job, { started: '2017-05-24T10:59:52.000+01:00' }));
+ mock
+ .onGet(props.endpoint)
+ .replyOnce(200, Object.assign({}, job, { started: '2017-05-24T10:59:52.000+01:00' }));
vm = mountComponentWithStore(Component, { props, store });
});
@@ -133,57 +135,106 @@ describe('Job App ', () => {
});
describe('stuck block', () => {
- it('renders stuck block when there are no runners', done => {
- mock.onGet(props.endpoint).replyOnce(
- 200,
- Object.assign({}, job, {
- status: {
- group: 'pending',
- icon: 'status_pending',
- label: 'pending',
- text: 'pending',
- details_path: 'path',
- },
- runners: {
- available: false,
- },
- }),
- );
- vm = mountComponentWithStore(Component, { props, store });
-
- setTimeout(() => {
- expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull();
+ describe('without active runners availabl', () => {
+ it('renders stuck block when there are no runners', done => {
+ mock.onGet(props.endpoint).replyOnce(
+ 200,
+ Object.assign({}, job, {
+ status: {
+ group: 'pending',
+ icon: 'status_pending',
+ label: 'pending',
+ text: 'pending',
+ details_path: 'path',
+ },
+ stuck: true,
+ runners: {
+ available: false,
+ online: false,
+ },
+ tags: [],
+ }),
+ );
+ vm = mountComponentWithStore(Component, { props, store });
- done();
- }, 0);
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-job-stuck')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(
+ "This job is stuck, because you don't have any active runners that can run this job.",
+ );
+ done();
+ }, 0);
+ });
});
- it('renders tags in stuck block when there are no runners', done => {
- mock.onGet(props.endpoint).replyOnce(
- 200,
- Object.assign({}, job, {
- status: {
- group: 'pending',
- icon: 'status_pending',
- label: 'pending',
- text: 'pending',
- details_path: 'path',
- },
- runners: {
- available: false,
- },
- }),
- );
+ describe('when available runners can not run specified tag', () => {
+ it('renders tags in stuck block when there are no runners', done => {
+ mock.onGet(props.endpoint).replyOnce(
+ 200,
+ Object.assign({}, job, {
+ status: {
+ group: 'pending',
+ icon: 'status_pending',
+ label: 'pending',
+ text: 'pending',
+ details_path: 'path',
+ },
+ stuck: true,
+ runners: {
+ available: false,
+ online: false,
+ },
+ }),
+ );
- vm = mountComponentWithStore(Component, {
- props,
- store,
+ vm = mountComponentWithStore(Component, {
+ props,
+ store,
+ });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]);
+ expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(
+ "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:",
+ );
+ done();
+ }, 0);
});
+ });
- setTimeout(() => {
- expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]);
- done();
- }, 0);
+ describe('when runners are offline and build has tags', () => {
+ it('renders message about job being stuck because of no runners with the specified tags', done => {
+ mock.onGet(props.endpoint).replyOnce(
+ 200,
+ Object.assign({}, job, {
+ status: {
+ group: 'pending',
+ icon: 'status_pending',
+ label: 'pending',
+ text: 'pending',
+ details_path: 'path',
+ },
+ stuck: true,
+ runners: {
+ available: true,
+ online: true,
+ },
+ }),
+ );
+
+ vm = mountComponentWithStore(Component, {
+ props,
+ store,
+ });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(job.tags[0]);
+ expect(vm.$el.querySelector('.js-job-stuck').textContent).toContain(
+ "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:",
+ );
+ done();
+ }, 0);
+ })
});
it('does not renders stuck block when there are no runners', done => {
@@ -418,10 +469,11 @@ describe('Job App ', () => {
vm.$store.state.trace = 'Update';
setTimeout(() => {
- expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).not.toContain('Update');
- expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).toContain(
- 'Different',
+ expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).not.toContain(
+ 'Update',
);
+
+ expect(vm.$el.querySelector('.js-build-trace').textContent.trim()).toContain('Different');
done();
}, 0);
});
diff --git a/spec/javascripts/jobs/store/getters_spec.js b/spec/javascripts/jobs/store/getters_spec.js
index 46a20122ec8..34e9707eadd 100644
--- a/spec/javascripts/jobs/store/getters_spec.js
+++ b/spec/javascripts/jobs/store/getters_spec.js
@@ -175,43 +175,37 @@ describe('Job Store Getters', () => {
});
});
- describe('isJobStuck', () => {
- describe('when job is pending and runners are not available', () => {
+ describe('hasRunnersForProject', () => {
+ describe('with available and offline runners', () => {
it('returns true', () => {
- localState.job.status = {
- group: 'pending',
- };
localState.job.runners = {
- available: false,
+ available: true,
+ online: false
};
- expect(getters.isJobStuck(localState)).toEqual(true);
+ expect(getters.hasRunnersForProject(localState)).toEqual(true);
});
});
- describe('when job is not pending', () => {
+ describe('with non available runners', () => {
it('returns false', () => {
- localState.job.status = {
- group: 'running',
- };
localState.job.runners = {
available: false,
+ online: false
};
- expect(getters.isJobStuck(localState)).toEqual(false);
+ expect(getters.hasRunnersForProject(localState)).toEqual(false);
});
});
- describe('when runners are available', () => {
+ describe('with online runners', () => {
it('returns false', () => {
- localState.job.status = {
- group: 'pending',
- };
localState.job.runners = {
- available: true,
+ available: false,
+ online: true
};
- expect(getters.isJobStuck(localState)).toEqual(false);
+ expect(getters.hasRunnersForProject(localState)).toEqual(false);
});
});
});
diff --git a/spec/javascripts/lib/utils/datefix_spec.js b/spec/javascripts/lib/utils/datefix_spec.js
deleted file mode 100644
index a9f3abcf2a4..00000000000
--- a/spec/javascripts/lib/utils/datefix_spec.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { pad, pikadayToString } from '~/lib/utils/datefix';
-
-describe('datefix', () => {
- describe('pad', () => {
- it('should add a 0 when length is smaller than 2', () => {
- expect(pad(2)).toEqual('02');
- });
-
- it('should not add a zero when lenght matches the default', () => {
- expect(pad(12)).toEqual('12');
- });
-
- it('should add a 0 when lenght is smaller than the provided', () => {
- expect(pad(12, 3)).toEqual('012');
- });
- });
-
- describe('parsePikadayDate', () => {
- // removed because of https://gitlab.com/gitlab-org/gitlab-ce/issues/39834
- });
-
- describe('pikadayToString', () => {
- it('should format a UTC date into yyyy-mm-dd format', () => {
- expect(pikadayToString(new Date('2020-01-29:00:00'))).toEqual('2020-01-29');
- });
- });
-});
diff --git a/spec/javascripts/notes/components/discussion_filter_spec.js b/spec/javascripts/notes/components/discussion_filter_spec.js
new file mode 100644
index 00000000000..70dd5bb3be5
--- /dev/null
+++ b/spec/javascripts/notes/components/discussion_filter_spec.js
@@ -0,0 +1,60 @@
+import Vue from 'vue';
+import createStore from '~/notes/stores';
+import DiscussionFilter from '~/notes/components/discussion_filter.vue';
+import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper';
+import { discussionFiltersMock, discussionMock } from '../mock_data';
+
+describe('DiscussionFilter component', () => {
+ let vm;
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+
+ const discussions = [{
+ ...discussionMock,
+ id: discussionMock.id,
+ notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }],
+ }];
+ const Component = Vue.extend(DiscussionFilter);
+ const defaultValue = discussionFiltersMock[0].value;
+
+ store.state.discussions = discussions;
+ vm = mountComponentWithStore(Component, {
+ el: null,
+ store,
+ props: {
+ filters: discussionFiltersMock,
+ defaultValue,
+ },
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders the all filters', () => {
+ expect(vm.$el.querySelectorAll('.dropdown-menu li').length).toEqual(discussionFiltersMock.length);
+ });
+
+ it('renders the default selected item', () => {
+ expect(vm.$el.querySelector('#discussion-filter-dropdown').textContent.trim()).toEqual(discussionFiltersMock[0].title);
+ });
+
+ it('updates to the selected item', () => {
+ const filterItem = vm.$el.querySelector('.dropdown-menu li:last-child button');
+ filterItem.click();
+
+ expect(vm.currentFilter.title).toEqual(filterItem.textContent.trim());
+ });
+
+ it('only updates when selected filter changes', () => {
+ const filterItem = vm.$el.querySelector('.dropdown-menu li:first-child button');
+
+ spyOn(vm, 'filterDiscussion');
+ filterItem.click();
+
+ expect(vm.filterDiscussion).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js
index 3e289a6b8e6..06b30375306 100644
--- a/spec/javascripts/notes/components/note_app_spec.js
+++ b/spec/javascripts/notes/components/note_app_spec.js
@@ -97,8 +97,7 @@ describe('note_app', () => {
});
it('should render list of notes', done => {
- const note =
- mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[
+ const note = mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[
'/gitlab-org/gitlab-ce/issues/26/discussions.json'
][0].notes[0];
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index 9a0e7f34a9c..ad0e793b915 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -1244,3 +1244,18 @@ export const discussion3 = {
export const unresolvableDiscussion = {
resolvable: false,
};
+
+export const discussionFiltersMock = [
+ {
+ title: 'Show all activity',
+ value: 0,
+ },
+ {
+ title: 'Show comments only',
+ value: 1,
+ },
+ {
+ title: 'Show system notes only',
+ value: 2,
+ },
+];
diff --git a/spec/javascripts/pretty_time_spec.js b/spec/javascripts/pretty_time_spec.js
deleted file mode 100644
index 158cd76dd13..00000000000
--- a/spec/javascripts/pretty_time_spec.js
+++ /dev/null
@@ -1,135 +0,0 @@
-import { parseSeconds, abbreviateTime, stringifyTime } from '~/lib/utils/pretty_time';
-
-function assertTimeUnits(obj, minutes, hours, days, weeks) {
- expect(obj.minutes).toBe(minutes);
- expect(obj.hours).toBe(hours);
- expect(obj.days).toBe(days);
- expect(obj.weeks).toBe(weeks);
-}
-
-describe('prettyTime methods', () => {
- describe('parseSeconds', () => {
- it('should correctly parse a negative value', () => {
- const zeroSeconds = parseSeconds(-1000);
-
- assertTimeUnits(zeroSeconds, 16, 0, 0, 0);
- });
-
- it('should correctly parse a zero value', () => {
- const zeroSeconds = parseSeconds(0);
-
- assertTimeUnits(zeroSeconds, 0, 0, 0, 0);
- });
-
- it('should correctly parse a small non-zero second values', () => {
- const subOneMinute = parseSeconds(10);
- const aboveOneMinute = parseSeconds(100);
- const manyMinutes = parseSeconds(1000);
-
- assertTimeUnits(subOneMinute, 0, 0, 0, 0);
- assertTimeUnits(aboveOneMinute, 1, 0, 0, 0);
- assertTimeUnits(manyMinutes, 16, 0, 0, 0);
- });
-
- it('should correctly parse large second values', () => {
- const aboveOneHour = parseSeconds(4800);
- const aboveOneDay = parseSeconds(110000);
- const aboveOneWeek = parseSeconds(25000000);
-
- assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
- assertTimeUnits(aboveOneDay, 33, 6, 3, 0);
- assertTimeUnits(aboveOneWeek, 26, 0, 3, 173);
- });
-
- it('should correctly accept a custom param for hoursPerDay', () => {
- const config = { hoursPerDay: 24 };
-
- const aboveOneHour = parseSeconds(4800, config);
- const aboveOneDay = parseSeconds(110000, config);
- const aboveOneWeek = parseSeconds(25000000, config);
-
- assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
- assertTimeUnits(aboveOneDay, 33, 6, 1, 0);
- assertTimeUnits(aboveOneWeek, 26, 8, 4, 57);
- });
-
- it('should correctly accept a custom param for daysPerWeek', () => {
- const config = { daysPerWeek: 7 };
-
- const aboveOneHour = parseSeconds(4800, config);
- const aboveOneDay = parseSeconds(110000, config);
- const aboveOneWeek = parseSeconds(25000000, config);
-
- assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
- assertTimeUnits(aboveOneDay, 33, 6, 3, 0);
- assertTimeUnits(aboveOneWeek, 26, 0, 0, 124);
- });
-
- it('should correctly accept custom params for daysPerWeek and hoursPerDay', () => {
- const config = { daysPerWeek: 55, hoursPerDay: 14 };
-
- const aboveOneHour = parseSeconds(4800, config);
- const aboveOneDay = parseSeconds(110000, config);
- const aboveOneWeek = parseSeconds(25000000, config);
-
- assertTimeUnits(aboveOneHour, 20, 1, 0, 0);
- assertTimeUnits(aboveOneDay, 33, 2, 2, 0);
- assertTimeUnits(aboveOneWeek, 26, 0, 1, 9);
- });
- });
-
- describe('stringifyTime', () => {
- it('should stringify values with all non-zero units', () => {
- const timeObject = {
- weeks: 1,
- days: 4,
- hours: 7,
- minutes: 20,
- };
-
- const timeString = stringifyTime(timeObject);
-
- expect(timeString).toBe('1w 4d 7h 20m');
- });
-
- it('should stringify values with some non-zero units', () => {
- const timeObject = {
- weeks: 0,
- days: 4,
- hours: 0,
- minutes: 20,
- };
-
- const timeString = stringifyTime(timeObject);
-
- expect(timeString).toBe('4d 20m');
- });
-
- it('should stringify values with no non-zero units', () => {
- const timeObject = {
- weeks: 0,
- days: 0,
- hours: 0,
- minutes: 0,
- };
-
- const timeString = stringifyTime(timeObject);
-
- expect(timeString).toBe('0m');
- });
- });
-
- describe('abbreviateTime', () => {
- it('should abbreviate stringified times for weeks', () => {
- const fullTimeString = '1w 3d 4h 5m';
-
- expect(abbreviateTime(fullTimeString)).toBe('1w');
- });
-
- it('should abbreviate stringified times for non-weeks', () => {
- const fullTimeString = '0w 3d 4h 5m';
-
- expect(abbreviateTime(fullTimeString)).toBe('3d');
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/file_row_spec.js b/spec/javascripts/vue_shared/components/file_row_spec.js
index 9914c0b70f3..67752c1c455 100644
--- a/spec/javascripts/vue_shared/components/file_row_spec.js
+++ b/spec/javascripts/vue_shared/components/file_row_spec.js
@@ -71,4 +71,40 @@ describe('RepoFile', () => {
expect(vm.$el.querySelector('.file-row-name').style.marginLeft).toBe('32px');
});
+
+ describe('outputText', () => {
+ beforeEach(done => {
+ createComponent({
+ file: {
+ ...file(),
+ path: 'app/assets/index.js',
+ },
+ level: 0,
+ });
+
+ vm.displayTextKey = 'path';
+
+ vm.$nextTick(done);
+ });
+
+ it('returns text if truncateStart is 0', done => {
+ vm.truncateStart = 0;
+
+ vm.$nextTick(() => {
+ expect(vm.outputText).toBe('app/assets/index.js');
+
+ done();
+ });
+ });
+
+ it('returns text truncated at start', done => {
+ vm.truncateStart = 5;
+
+ vm.$nextTick(() => {
+ expect(vm.outputText).toBe('...ssets/index.js');
+
+ done();
+ });
+ });
+ });
});
diff --git a/spec/lib/gitaly/server_spec.rb b/spec/lib/gitaly/server_spec.rb
index 09bf21b5946..292ab870dad 100644
--- a/spec/lib/gitaly/server_spec.rb
+++ b/spec/lib/gitaly/server_spec.rb
@@ -26,9 +26,7 @@ describe Gitaly::Server do
end
end
- context 'when the storage is not readable' do
- let(:server) { described_class.new('broken') }
-
+ context 'when the storage is not readable', :broken_storage do
it 'returns false' do
expect(server).not_to be_readable
end
@@ -42,9 +40,7 @@ describe Gitaly::Server do
end
end
- context 'when the storage is not writeable' do
- let(:server) { described_class.new('broken') }
-
+ context 'when the storage is not writeable', :broken_storage do
it 'returns false' do
expect(server).not_to be_writeable
end
diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
index e327399d82d..a9a4af1f455 100644
--- a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
+++ b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
@@ -112,4 +112,34 @@ describe Gitlab::Ci::Build::Artifacts::Metadata do
end
end
end
+
+ context 'generated metadata' do
+ let(:tmpfile) { Tempfile.new('test-metadata') }
+ let(:generator) { CiArtifactMetadataGenerator.new(tmpfile) }
+ let(:entry_count) { 5 }
+
+ before do
+ tmpfile.binmode
+
+ (1..entry_count).each do |index|
+ generator.add_entry("public/test-#{index}.txt")
+ end
+
+ generator.write
+ end
+
+ after do
+ File.unlink(tmpfile.path)
+ end
+
+ describe '#find_entries!' do
+ it 'reads expected number of entries' do
+ stream = File.open(tmpfile.path)
+
+ metadata = described_class.new(stream, 'public', { recursive: true })
+
+ expect(metadata.find_entries!.count).to eq entry_count
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
index 7cf541447ce..8095a231cf3 100644
--- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb
@@ -38,6 +38,8 @@ describe Gitlab::Ci::Config::Entry::Reports do
:dependency_scanning | 'gl-dependency-scanning-report.json'
:container_scanning | 'gl-container-scanning-report.json'
:dast | 'gl-dast-report.json'
+ :license_management | 'gl-license-management-report.json'
+ :performance | 'performance.json'
end
with_them do
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index 7ebfc61f5e7..b0570680d5a 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -335,7 +335,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
restored_project_json
- expect(project.lfs_enabled).to be_nil
+ expect(project.lfs_enabled).to be_falsey
end
end
diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
index 53c5a4e7c94..eed4135d8a2 100644
--- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb
+++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
@@ -6,104 +6,63 @@ describe Gitlab::Kubernetes::KubeClient do
include KubernetesHelpers
let(:api_url) { 'https://kubernetes.example.com/prefix' }
- let(:api_groups) { ['api', 'apis/rbac.authorization.k8s.io'] }
- let(:api_version) { 'v1' }
let(:kubeclient_options) { { auth_options: { bearer_token: 'xyz' } } }
- let(:client) { described_class.new(api_url, api_groups, api_version, kubeclient_options) }
+ let(:client) { described_class.new(api_url, kubeclient_options) }
before do
stub_kubeclient_discover(api_url)
end
- describe '#hashed_clients' do
- subject { client.hashed_clients }
-
- it 'has keys from api groups' do
- expect(subject.keys).to match_array api_groups
- end
-
- it 'has values of Kubeclient::Client' do
- expect(subject.values).to all(be_an_instance_of Kubeclient::Client)
- end
- end
-
- describe '#clients' do
- subject { client.clients }
-
- it 'is not empty' do
- is_expected.to be_present
- end
-
- it 'is an array of Kubeclient::Client objects' do
- is_expected.to all(be_an_instance_of Kubeclient::Client)
- end
-
- it 'has each API group url' do
- expected_urls = api_groups.map { |group| "#{api_url}/#{group}" }
-
- expect(subject.map(&:api_endpoint).map(&:to_s)).to match_array(expected_urls)
+ shared_examples 'a Kubeclient' do
+ it 'is a Kubeclient::Client' do
+ is_expected.to be_an_instance_of Kubeclient::Client
end
it 'has the kubeclient options' do
- subject.each do |client|
- expect(client.auth_options).to eq({ bearer_token: 'xyz' })
- end
- end
-
- it 'has the api_version' do
- subject.each do |client|
- expect(client.instance_variable_get(:@api_version)).to eq('v1')
- end
+ expect(subject.auth_options).to eq({ bearer_token: 'xyz' })
end
end
describe '#core_client' do
subject { client.core_client }
- it 'is a Kubeclient::Client' do
- is_expected.to be_an_instance_of Kubeclient::Client
- end
+ it_behaves_like 'a Kubeclient'
it 'has the core API endpoint' do
expect(subject.api_endpoint.to_s).to match(%r{\/api\Z})
end
+
+ it 'has the api_version' do
+ expect(subject.instance_variable_get(:@api_version)).to eq('v1')
+ end
end
describe '#rbac_client' do
subject { client.rbac_client }
- it 'is a Kubeclient::Client' do
- is_expected.to be_an_instance_of Kubeclient::Client
- end
+ it_behaves_like 'a Kubeclient'
it 'has the RBAC API group endpoint' do
expect(subject.api_endpoint.to_s).to match(%r{\/apis\/rbac.authorization.k8s.io\Z})
end
+
+ it 'has the api_version' do
+ expect(subject.instance_variable_get(:@api_version)).to eq('v1')
+ end
end
describe '#extensions_client' do
subject { client.extensions_client }
- let(:api_groups) { ['apis/extensions'] }
-
- it 'is a Kubeclient::Client' do
- is_expected.to be_an_instance_of Kubeclient::Client
- end
+ it_behaves_like 'a Kubeclient'
it 'has the extensions API group endpoint' do
expect(subject.api_endpoint.to_s).to match(%r{\/apis\/extensions\Z})
end
- end
- describe '#discover!' do
- it 'makes a discovery request for each API group' do
- client.discover!
-
- api_groups.each do |api_group|
- discovery_url = api_url + '/' + api_group + '/v1'
- expect(WebMock).to have_requested(:get, discovery_url).once
- end
+ it 'has the api_version' do
+ expect(subject.instance_variable_get(:@api_version)).to eq('v1beta1')
end
end
@@ -156,21 +115,12 @@ describe Gitlab::Kubernetes::KubeClient do
it 'responds to the method' do
expect(client).to respond_to method
end
-
- context 'no rbac client' do
- let(:api_groups) { ['api'] }
-
- it 'throws an error' do
- expect { client.public_send(method) }.to raise_error(Module::DelegationError)
- end
- end
end
end
end
describe 'extensions API group' do
let(:api_groups) { ['apis/extensions'] }
- let(:api_version) { 'v1beta1' }
let(:extensions_client) { client.extensions_client }
describe '#get_deployments' do
@@ -181,22 +131,11 @@ describe Gitlab::Kubernetes::KubeClient do
it 'responds to the method' do
expect(client).to respond_to :get_deployments
end
-
- context 'no extensions client' do
- let(:api_groups) { ['api'] }
- let(:api_version) { 'v1' }
-
- it 'throws an error' do
- expect { client.get_deployments }.to raise_error(Module::DelegationError)
- end
- end
end
end
describe 'non-entity methods' do
it 'does not proxy for non-entity methods' do
- expect(client.clients.first).to respond_to :proxy_url
-
expect(client).not_to respond_to :proxy_url
end
@@ -211,14 +150,6 @@ describe Gitlab::Kubernetes::KubeClient do
it 'is delegated to the core client' do
expect(client).to delegate_method(:get_pod_log).to(:core_client)
end
-
- context 'when no core client' do
- let(:api_groups) { ['apis/extensions'] }
-
- it 'throws an error' do
- expect { client.get_pod_log('pod-name') }.to raise_error(Module::DelegationError)
- end
- end
end
describe '#watch_pod_log' do
@@ -227,14 +158,6 @@ describe Gitlab::Kubernetes::KubeClient do
it 'is delegated to the core client' do
expect(client).to delegate_method(:watch_pod_log).to(:core_client)
end
-
- context 'when no core client' do
- let(:api_groups) { ['apis/extensions'] }
-
- it 'throws an error' do
- expect { client.watch_pod_log('pod-name') }.to raise_error(Module::DelegationError)
- end
- end
end
describe 'methods that do not exist on any client' do
diff --git a/spec/lib/gitlab/kubernetes/role_binding_spec.rb b/spec/lib/gitlab/kubernetes/role_binding_spec.rb
new file mode 100644
index 00000000000..da3f5d27b25
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/role_binding_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Kubernetes::RoleBinding, '#generate' do
+ let(:role_name) { 'edit' }
+ let(:namespace) { 'my-namespace' }
+ let(:service_account_name) { 'my-service-account' }
+
+ let(:subjects) do
+ [
+ {
+ kind: 'ServiceAccount',
+ name: service_account_name,
+ namespace: namespace
+ }
+ ]
+ end
+
+ let(:role_ref) do
+ {
+ apiGroup: 'rbac.authorization.k8s.io',
+ kind: 'Role',
+ name: role_name
+ }
+ end
+
+ let(:resource) do
+ ::Kubeclient::Resource.new(
+ metadata: { name: "gitlab-#{namespace}", namespace: namespace },
+ roleRef: role_ref,
+ subjects: subjects
+ )
+ end
+
+ subject do
+ described_class.new(
+ role_name: role_name,
+ namespace: namespace,
+ service_account_name: service_account_name
+ ).generate
+ end
+
+ it 'should build a Kubeclient Resource' do
+ is_expected.to eq(resource)
+ end
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 3b01b39ecab..153244b2159 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -779,6 +779,41 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe 'ref_exists?' do
+ context 'when repository exists' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:project) { create(:project, :repository) }
+
+ where(:tag, :ref, :result) do
+ false | 'master' | true
+ false | 'non-existent-branch' | false
+ true | 'v1.1.0' | true
+ true | 'non-existent-tag' | false
+ end
+
+ with_them do
+ let(:pipeline) do
+ create(:ci_empty_pipeline, project: project, tag: tag, ref: ref)
+ end
+
+ it "correctly detects ref" do
+ expect(pipeline.ref_exists?).to be result
+ end
+ end
+ end
+
+ context 'when repository does not exist' do
+ let(:pipeline) do
+ create(:ci_empty_pipeline, project: project, ref: 'master')
+ end
+
+ it 'always returns false' do
+ expect(pipeline.ref_exists?).to eq false
+ end
+ end
+ end
+
context 'with non-empty project' do
let(:project) { create(:project, :repository) }
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index 23643d1c4d2..d5fb1a9d010 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -17,7 +17,7 @@ describe Clusters::Applications::Runner do
let(:application) { create(:clusters_applications_runner, :scheduled, version: '0.1.30') }
it 'updates the application version' do
- expect(application.reload.version).to eq('0.1.34')
+ expect(application.reload.version).to eq('0.1.35')
end
end
end
@@ -45,7 +45,7 @@ describe Clusters::Applications::Runner do
it 'should be initialized with 4 arguments' do
expect(subject.name).to eq('runner')
expect(subject.chart).to eq('runner/gitlab-runner')
- expect(subject.version).to eq('0.1.34')
+ expect(subject.version).to eq('0.1.35')
expect(subject).not_to be_rbac
expect(subject.repository).to eq('https://charts.gitlab.io')
expect(subject.files).to eq(gitlab_runner.files)
@@ -63,7 +63,7 @@ describe Clusters::Applications::Runner do
let(:gitlab_runner) { create(:clusters_applications_runner, :errored, runner: ci_runner, version: '0.1.13') }
it 'should be initialized with the locked version' do
- expect(subject.version).to eq('0.1.34')
+ expect(subject.version).to eq('0.1.35')
end
end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 1783dd3206b..f9be61e4768 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -865,5 +865,29 @@ describe Note do
note.save!
end
end
+
+ describe '#with_notes_filter' do
+ let!(:comment) { create(:note) }
+ let!(:system_note) { create(:note, system: true) }
+
+ context 'when notes filter is nil' do
+ subject { described_class.with_notes_filter(nil) }
+
+ it { is_expected.to include(comment, system_note) }
+ end
+
+ context 'when notes filter is set to all notes' do
+ subject { described_class.with_notes_filter(UserPreference::NOTES_FILTERS[:all_notes]) }
+
+ it { is_expected.to include(comment, system_note) }
+ end
+
+ context 'when notes filter is set to only comments' do
+ subject { described_class.with_notes_filter(UserPreference::NOTES_FILTERS[:only_comments]) }
+
+ it { is_expected.to include(comment) }
+ it { is_expected.not_to include(system_note) }
+ end
+ end
end
end
diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb
new file mode 100644
index 00000000000..64d9d9a78b4
--- /dev/null
+++ b/spec/models/user_preference_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe UserPreference do
+ describe '#set_notes_filter' do
+ let(:issuable) { build_stubbed(:issue) }
+ let(:user_preference) { create(:user_preference) }
+ let(:only_comments) { described_class::NOTES_FILTERS[:only_comments] }
+
+ it 'returns updated discussion filter' do
+ filter_name =
+ user_preference.set_notes_filter(only_comments, issuable)
+
+ expect(filter_name).to eq(only_comments)
+ end
+
+ it 'updates discussion filter for issuable class' do
+ user_preference.set_notes_filter(only_comments, issuable)
+
+ expect(user_preference.reload.issue_notes_filter).to eq(only_comments)
+ end
+
+ context 'when notes_filter parameter is invalid' do
+ it 'returns the current notes filter' do
+ user_preference.set_notes_filter(only_comments, issuable)
+
+ expect(user_preference.set_notes_filter(9999, issuable)).to eq(only_comments)
+ end
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 99d17f563d9..b3474e74aa4 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -715,6 +715,15 @@ describe User do
end
end
+ describe 'ensure user preference' do
+ it 'has user preference upon user initialization' do
+ user = build(:user)
+
+ expect(user.user_preference).to be_present
+ expect(user.user_preference).not_to be_persisted
+ end
+ end
+
describe 'ensure incoming email token' do
it 'has incoming email token' do
user = create(:user)
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 642de81ed52..368abded448 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -27,6 +27,7 @@ describe Ci::RetryBuildService do
job_artifacts_metadata job_artifacts_trace job_artifacts_junit
job_artifacts_sast job_artifacts_dependency_scanning
job_artifacts_container_scanning job_artifacts_dast
+ job_artifacts_license_management job_artifacts_performance
job_artifacts_codequality scheduled_at].freeze
IGNORE_ACCESSORS =
diff --git a/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb b/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb
index 065d021db5e..b096f1fa4fb 100644
--- a/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb
+++ b/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb
@@ -16,7 +16,6 @@ describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do
let(:kubeclient) do
Gitlab::Kubernetes::KubeClient.new(
api_url,
- ['api', 'apis/rbac.authorization.k8s.io'],
auth_options: { username: username, password: password }
)
end
diff --git a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb b/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb
index c543de21d5b..2355827fa5a 100644
--- a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb
+++ b/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb
@@ -11,7 +11,6 @@ describe Clusters::Gcp::Kubernetes::FetchKubernetesTokenService do
let(:kubeclient) do
Gitlab::Kubernetes::KubeClient.new(
api_url,
- ['api', 'apis/rbac.authorization.k8s.io'],
auth_options: { username: username, password: password }
)
end
diff --git a/spec/support/helpers/ci_artifact_metadata_generator.rb b/spec/support/helpers/ci_artifact_metadata_generator.rb
new file mode 100644
index 00000000000..ef638d59d2d
--- /dev/null
+++ b/spec/support/helpers/ci_artifact_metadata_generator.rb
@@ -0,0 +1,48 @@
+# frozen_sting_literal: true
+
+# This generates fake CI metadata .gz for testing
+# Based off https://gitlab.com/gitlab-org/gitlab-workhorse/blob/master/internal/zipartifacts/metadata.go
+class CiArtifactMetadataGenerator
+ attr_accessor :entries, :output
+
+ ARTIFACT_METADATA = "GitLab Build Artifacts Metadata 0.0.2\n".freeze
+
+ def initialize(stream)
+ @entries = {}
+ @output = Zlib::GzipWriter.new(stream)
+ end
+
+ def add_entry(filename)
+ @entries[filename] = { CRC: rand(0xfffffff), Comment: FFaker::Lorem.sentence(10) }
+ end
+
+ def write
+ write_version
+ write_errors
+ write_entries
+ output.close
+ end
+
+ private
+
+ def write_version
+ write_string(ARTIFACT_METADATA)
+ end
+
+ def write_errors
+ write_string('{}')
+ end
+
+ def write_entries
+ entries.each do |filename, metadata|
+ write_string(filename)
+ write_string(metadata.to_json + "\n")
+ end
+ end
+
+ def write_string(data)
+ bytes = [data.length].pack('L>')
+ output.write(bytes)
+ output.write(data)
+ end
+end
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 1a9aa252511..71d72ff27e9 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -70,7 +70,6 @@ module TestEnv
TMP_TEST_PATH = Rails.root.join('tmp', 'tests', '**')
REPOS_STORAGE = 'default'.freeze
- BROKEN_STORAGE = 'broken'.freeze
# Test environment
#
@@ -159,10 +158,6 @@ module TestEnv
version: Gitlab::GitalyClient.expected_server_version,
task: "gitlab:gitaly:install[#{gitaly_dir},#{repos_path}]") do
- # Re-create config, to specify the broken storage path
- storage_paths = { 'default' => repos_path, 'broken' => broken_path }
- Gitlab::SetupHelper.create_gitaly_configuration(gitaly_dir, storage_paths, force: true)
-
start_gitaly(gitaly_dir)
end
end
@@ -173,6 +168,8 @@ module TestEnv
return
end
+ FileUtils.mkdir_p("tmp/tests/second_storage") unless File.exist?("tmp/tests/second_storage")
+
spawn_script = Rails.root.join('scripts/gitaly-test-spawn').to_s
Bundler.with_original_env do
raise "gitaly spawn failed" unless system(spawn_script)
@@ -257,10 +254,6 @@ module TestEnv
@repos_path ||= Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path
end
- def broken_path
- @broken_path ||= Gitlab.config.repositories.storages[BROKEN_STORAGE].legacy_disk_path
- end
-
def backup_path
Gitlab.config.backup.path
end
diff --git a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb
new file mode 100644
index 00000000000..9c9d7ad781e
--- /dev/null
+++ b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb
@@ -0,0 +1,54 @@
+shared_examples 'issuable notes filter' do
+ it 'sets discussion filter' do
+ notes_filter = UserPreference::NOTES_FILTERS[:only_comments]
+
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid, notes_filter: notes_filter
+
+ expect(user.reload.notes_filter_for(issuable)).to eq(notes_filter)
+ expect(UserPreference.count).to eq(1)
+ end
+
+ it 'expires notes e-tag cache for issuable if filter changed' do
+ notes_filter = UserPreference::NOTES_FILTERS[:only_comments]
+
+ expect_any_instance_of(issuable.class).to receive(:expire_note_etag_cache)
+
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid, notes_filter: notes_filter
+ end
+
+ it 'does not expires notes e-tag cache for issuable if filter did not change' do
+ notes_filter = UserPreference::NOTES_FILTERS[:only_comments]
+ user.set_notes_filter(notes_filter, issuable)
+
+ expect_any_instance_of(issuable.class).not_to receive(:expire_note_etag_cache)
+
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid, notes_filter: notes_filter
+ end
+
+ it 'does not set notes filter when database is in read only mode' do
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+ notes_filter = UserPreference::NOTES_FILTERS[:only_comments]
+
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid, notes_filter: notes_filter
+
+ expect(user.reload.notes_filter_for(issuable)).to eq(0)
+ end
+
+ it 'returns no system note' do
+ user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_comments], issuable)
+
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid
+
+ expect(JSON.parse(response.body).count).to eq(1)
+ end
+
+ context 'when filter is set to "only_comments"' do
+ it 'does not merge label event notes' do
+ user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_comments], issuable)
+
+ expect(ResourceEvents::MergeIntoNotesService).not_to receive(:new)
+
+ get :discussions, namespace_id: project.namespace, project_id: project, id: issuable.iid
+ end
+ end
+end
diff --git a/spec/support/stored_repositories.rb b/spec/support/stored_repositories.rb
index 6a9ad43941d..55212355daa 100644
--- a/spec/support/stored_repositories.rb
+++ b/spec/support/stored_repositories.rb
@@ -1,8 +1,4 @@
RSpec.configure do |config|
- config.before(:all, :broken_storage) do
- FileUtils.rm_rf Gitlab.config.repositories.storages.broken.legacy_disk_path
- end
-
config.before(:each, :broken_storage) do
allow(Gitlab::GitalyClient).to receive(:call) do
raise GRPC::Unavailable.new('Gitaly broken in this spec')
diff --git a/spec/views/shared/runners/show.html.haml_spec.rb b/spec/views/shared/runners/show.html.haml_spec.rb
new file mode 100644
index 00000000000..5e92928b143
--- /dev/null
+++ b/spec/views/shared/runners/show.html.haml_spec.rb
@@ -0,0 +1,155 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'shared/runners/show.html.haml' do
+ include PageLayoutHelper
+
+ let(:runner) do
+ create(:ci_runner, name: 'test runner',
+ version: '11.4.0',
+ ip_address: '127.1.2.3',
+ revision: 'abcd1234',
+ architecture: 'amd64' )
+ end
+
+ before do
+ assign(:runner, runner)
+ end
+
+ subject do
+ render
+ rendered
+ end
+
+ describe 'Page title' do
+ before do
+ expect_any_instance_of(PageLayoutHelper).to receive(:page_title).with("#{runner.description} ##{runner.id}", 'Runners')
+ end
+
+ it 'sets proper page title' do
+ render
+ end
+ end
+
+ describe 'Runner id and type' do
+ context 'when runner is of type instance' do
+ it { is_expected.to have_content("Runner ##{runner.id} Shared") }
+ end
+
+ context 'when runner is of type group' do
+ let(:runner) { create(:ci_runner, :group) }
+
+ it { is_expected.to have_content("Runner ##{runner.id} Group") }
+ end
+
+ context 'when runner is of type project' do
+ let(:runner) { create(:ci_runner, :project) }
+
+ it { is_expected.to have_content("Runner ##{runner.id} Specific") }
+ end
+ end
+
+ describe 'Active value' do
+ context 'when runner is active' do
+ it { is_expected.to have_content('Active Yes') }
+ end
+
+ context 'when runner is inactive' do
+ let(:runner) { create(:ci_runner, :inactive) }
+
+ it { is_expected.to have_content('Active No') }
+ end
+ end
+
+ describe 'Protected value' do
+ context 'when runner is not protected' do
+ it { is_expected.to have_content('Protected No') }
+ end
+
+ context 'when runner is protected' do
+ let(:runner) { create(:ci_runner, :ref_protected) }
+
+ it { is_expected.to have_content('Protected Yes') }
+ end
+ end
+
+ describe 'Can run untagged jobs value' do
+ context 'when runner run untagged job is set' do
+ it { is_expected.to have_content('Can run untagged jobs Yes') }
+ end
+
+ context 'when runner run untagged job is unset' do
+ let(:runner) { create(:ci_runner, :tagged_only) }
+
+ it { is_expected.to have_content('Can run untagged jobs No') }
+ end
+ end
+
+ describe 'Locked to this project value' do
+ context 'when runner locked is not set' do
+ it { is_expected.to have_content('Locked to this project No') }
+
+ context 'when runner is of type group' do
+ let(:runner) { create(:ci_runner, :group) }
+
+ it { is_expected.not_to have_content('Locked to this project') }
+ end
+ end
+
+ context 'when runner locked is set' do
+ let(:runner) { create(:ci_runner, :locked) }
+
+ it { is_expected.to have_content('Locked to this project Yes') }
+
+ context 'when runner is of type group' do
+ let(:runner) { create(:ci_runner, :group, :locked) }
+
+ it { is_expected.not_to have_content('Locked to this project') }
+ end
+ end
+ end
+
+ describe 'Tags value' do
+ context 'when runner does not have tags' do
+ it { is_expected.to have_content('Tags') }
+ it { is_expected.not_to have_selector('span.badge.badge-primary')}
+ end
+
+ context 'when runner have tags' do
+ let(:runner) { create(:ci_runner, tag_list: %w(tag2 tag3 tag1)) }
+
+ it { is_expected.to have_content('Tags tag1 tag2 tag3') }
+ it { is_expected.to have_selector('span.badge.badge-primary')}
+ end
+ end
+
+ describe 'Metadata values' do
+ it { is_expected.to have_content("Name #{runner.name}") }
+ it { is_expected.to have_content("Version #{runner.version}") }
+ it { is_expected.to have_content("IP Address #{runner.ip_address}") }
+ it { is_expected.to have_content("Revision #{runner.revision}") }
+ it { is_expected.to have_content("Platform #{runner.platform}") }
+ it { is_expected.to have_content("Architecture #{runner.architecture}") }
+ it { is_expected.to have_content("Description #{runner.description}") }
+ end
+
+ describe 'Maximum job timeout value' do
+ let(:runner) { create(:ci_runner, maximum_timeout: 5400) }
+
+ it { is_expected.to have_content('Maximum job timeout 1h 30m') }
+ end
+
+ describe 'Last contact value' do
+ context 'when runner have not contacted yet' do
+ it { is_expected.to have_content('Last contact Never') }
+ end
+
+ context 'when runner have already contacted' do
+ let(:runner) { create(:ci_runner, contacted_at: DateTime.now - 6.days) }
+ let(:expected_contacted_at) { I18n.localize(runner.contacted_at, format: "%b %d, %Y") }
+
+ it { is_expected.to have_content("Last contact #{expected_contacted_at}") }
+ end
+ end
+end
diff --git a/spec/workers/repository_check/batch_worker_spec.rb b/spec/workers/repository_check/batch_worker_spec.rb
index ede271b2cdd..50b93fce2dc 100644
--- a/spec/workers/repository_check/batch_worker_spec.rb
+++ b/spec/workers/repository_check/batch_worker_spec.rb
@@ -51,7 +51,7 @@ describe RepositoryCheck::BatchWorker do
it 'does nothing when shard is unhealthy' do
shard_name = 'broken'
- create(:project, created_at: 1.week.ago, repository_storage: shard_name)
+ create(:project, :broken_storage, created_at: 1.week.ago)
expect(subject.perform(shard_name)).to eq(nil)
end
diff --git a/yarn.lock b/yarn.lock
index 544bd4a05bd..5da401c1d43 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -616,11 +616,16 @@
lodash "^4.17.10"
to-fast-properties "^2.0.0"
-"@gitlab-org/gitlab-svgs@^1.23.0", "@gitlab-org/gitlab-svgs@^1.32.0":
+"@gitlab-org/gitlab-svgs@^1.23.0":
version "1.32.0"
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.32.0.tgz#a65ab7724fa7d55be8e5cc9b2dbe3f0757432fd3"
integrity sha512-L3o8dFUd2nSkVZBwh2hCJWzNzADJ3dTBZxamND8NLosZK9/ohNhccmsQOZGyMCUHaOzm4vifaaXkAXh04UtMKA==
+"@gitlab-org/gitlab-svgs@^1.33.0":
+ version "1.33.0"
+ resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.33.0.tgz#068566e8ee00795f6f09f58236f08e1716f9f04a"
+ integrity sha512-8ajtUHk6gQ1xosL/CO5IzHSFM/t18hx5pfzQ3cd0VuQXcyR6QKGuXTLwbYdmJDYOw1Etoo5DqDWxPEClHyZpiA==
+
"@gitlab-org/gitlab-ui@^1.8.0":
version "1.8.0"
resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-ui/-/gitlab-ui-1.8.0.tgz#dee33d78f68c91644273dbd51734b796108263ee"