summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimon Knox <psimyn@gmail.com>2018-07-10 13:13:42 +1000
committerSimon Knox <psimyn@gmail.com>2018-07-10 13:13:42 +1000
commitcc40dc852147e12292a1c942b12d01fdefc9e4d1 (patch)
tree8daa619c803f2aa163e07b56198e2173842ed40f
parent51aa6098580829a6f6b0623a1559460d99be55be (diff)
parent255db3d59792e9bac92f4327b4e324e2bd32810a (diff)
downloadgitlab-ce-backport-3772-total-weight.tar.gz
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into backport-3772-total-weightbackport-3772-total-weight
-rw-r--r--.gitlab-ci.yml2
-rw-r--r--.rubocop_todo.yml6
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--Gemfile8
-rw-r--r--Gemfile.lock28
-rw-r--r--Gemfile.rails5.lock36
-rw-r--r--README.md6
-rwxr-xr-xRakefile4
-rw-r--r--app/assets/javascripts/api.js4
-rw-r--r--app/assets/javascripts/diffs/components/app.vue5
-rw-r--r--app/assets/javascripts/diffs/components/changed_files.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue6
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussions.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue9
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue1
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_gutter_content.vue3
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue7
-rw-r--r--app/assets/javascripts/diffs/components/diff_table_cell.vue8
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_comment_row.vue15
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_table_row.vue13
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_view.vue24
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue10
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_table_row.vue30
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_view.vue38
-rw-r--r--app/assets/javascripts/diffs/store/getters.js24
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js18
-rw-r--r--app/assets/javascripts/diffs/store/modules/index.js22
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js15
-rw-r--r--app/assets/javascripts/due_date_select.js4
-rw-r--r--app/assets/javascripts/frequent_items/components/app.vue122
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list.vue78
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue117
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_mixin.js23
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue55
-rw-r--r--app/assets/javascripts/frequent_items/constants.js38
-rw-r--r--app/assets/javascripts/frequent_items/event_hub.js (renamed from app/assets/javascripts/projects_dropdown/event_hub.js)0
-rw-r--r--app/assets/javascripts/frequent_items/index.js69
-rw-r--r--app/assets/javascripts/frequent_items/store/actions.js81
-rw-r--r--app/assets/javascripts/frequent_items/store/getters.js4
-rw-r--r--app/assets/javascripts/frequent_items/store/index.js16
-rw-r--r--app/assets/javascripts/frequent_items/store/mutation_types.js9
-rw-r--r--app/assets/javascripts/frequent_items/store/mutations.js71
-rw-r--r--app/assets/javascripts/frequent_items/store/state.js8
-rw-r--r--app/assets/javascripts/frequent_items/utils.js49
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/info.vue43
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue27
-rw-r--r--app/assets/javascripts/ide/constants.js1
-rw-r--r--app/assets/javascripts/ide/lib/themes/gl_theme.js1
-rw-r--r--app/assets/javascripts/ide/services/index.js4
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js2
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue6
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description.vue6
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue6
-rw-r--r--app/assets/javascripts/issue_show/components/title.vue112
-rw-r--r--app/assets/javascripts/main.js2
-rw-r--r--app/assets/javascripts/notes.js4
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue6
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue1
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue6
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue6
-rw-r--r--app/assets/javascripts/notes/index.js3
-rw-r--r--app/assets/javascripts/notes/stores/actions.js9
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js1
-rw-r--r--app/assets/javascripts/pages/profiles/keys/index.js16
-rw-r--r--app/assets/javascripts/preview_markdown.js14
-rw-r--r--app/assets/javascripts/profile/add_ssh_key_validation.js43
-rw-r--r--app/assets/javascripts/projects_dropdown/components/app.vue158
-rw-r--r--app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue57
-rw-r--r--app/assets/javascripts/projects_dropdown/components/projects_list_item.vue116
-rw-r--r--app/assets/javascripts/projects_dropdown/components/projects_list_search.vue63
-rw-r--r--app/assets/javascripts/projects_dropdown/components/search.vue65
-rw-r--r--app/assets/javascripts/projects_dropdown/constants.js10
-rw-r--r--app/assets/javascripts/projects_dropdown/index.js66
-rw-r--r--app/assets/javascripts/projects_dropdown/service/projects_service.js137
-rw-r--r--app/assets/javascripts/projects_dropdown/store/projects_store.js33
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue92
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue190
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue104
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue22
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss69
-rw-r--r--app/assets/stylesheets/framework/filters.scss5
-rw-r--r--app/assets/stylesheets/framework/forms.scss5
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss21
-rw-r--r--app/assets/stylesheets/framework/header.scss24
-rw-r--r--app/assets/stylesheets/framework/icons.scss24
-rw-r--r--app/assets/stylesheets/framework/mixins.scss7
-rw-r--r--app/assets/stylesheets/framework/typography.scss3
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/boards.scss11
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss182
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss15
-rw-r--r--app/assets/stylesheets/pages/repo.scss11
-rw-r--r--app/controllers/admin/deploy_keys_controller.rb4
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/admin/hooks_controller.rb4
-rw-r--r--app/controllers/admin/identities_controller.rb2
-rw-r--r--app/controllers/admin/impersonations_controller.rb2
-rw-r--r--app/controllers/admin/jobs_controller.rb2
-rw-r--r--app/controllers/admin/runner_projects_controller.rb2
-rw-r--r--app/controllers/admin/runners_controller.rb2
-rw-r--r--app/controllers/admin/services_controller.rb2
-rw-r--r--app/controllers/admin/users_controller.rb2
-rw-r--r--app/controllers/concerns/group_tree.rb40
-rw-r--r--app/controllers/concerns/issuable_actions.rb2
-rw-r--r--app/controllers/concerns/lfs_request.rb2
-rw-r--r--app/controllers/concerns/preview_markdown.rb3
-rw-r--r--app/controllers/dashboard/projects_controller.rb5
-rw-r--r--app/controllers/groups/avatars_controller.rb2
-rw-r--r--app/controllers/groups/runners_controller.rb2
-rw-r--r--app/controllers/jwt_controller.rb4
-rw-r--r--app/controllers/notification_settings_controller.rb4
-rw-r--r--app/controllers/profiles/active_sessions_controller.rb2
-rw-r--r--app/controllers/profiles/avatars_controller.rb2
-rw-r--r--app/controllers/profiles/chat_names_controller.rb2
-rw-r--r--app/controllers/profiles/emails_controller.rb2
-rw-r--r--app/controllers/profiles/gpg_keys_controller.rb4
-rw-r--r--app/controllers/profiles/keys_controller.rb2
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb2
-rw-r--r--app/controllers/projects/application_controller.rb2
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb2
-rw-r--r--app/controllers/projects/avatars_controller.rb2
-rw-r--r--app/controllers/projects/branches_controller.rb2
-rw-r--r--app/controllers/projects/clusters_controller.rb2
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb2
-rw-r--r--app/controllers/projects/environments_controller.rb2
-rw-r--r--app/controllers/projects/git_http_client_controller.rb4
-rw-r--r--app/controllers/projects/group_links_controller.rb4
-rw-r--r--app/controllers/projects/hooks_controller.rb4
-rw-r--r--app/controllers/projects/labels_controller.rb4
-rw-r--r--app/controllers/projects/lfs_api_controller.rb2
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb2
-rw-r--r--app/controllers/projects/milestones_controller.rb2
-rw-r--r--app/controllers/projects/mirrors_controller.rb2
-rw-r--r--app/controllers/projects/pipeline_schedules_controller.rb2
-rw-r--r--app/controllers/projects/releases_controller.rb2
-rw-r--r--app/controllers/projects/repositories_controller.rb2
-rw-r--r--app/controllers/projects/runner_projects_controller.rb2
-rw-r--r--app/controllers/projects/runners_controller.rb2
-rw-r--r--app/controllers/projects/services_controller.rb2
-rw-r--r--app/controllers/projects/snippets_controller.rb2
-rw-r--r--app/controllers/projects/tags_controller.rb2
-rw-r--r--app/controllers/projects/templates_controller.rb2
-rw-r--r--app/controllers/projects/triggers_controller.rb2
-rw-r--r--app/controllers/projects/wikis_controller.rb4
-rw-r--r--app/controllers/projects_controller.rb19
-rw-r--r--app/controllers/sessions_controller.rb4
-rw-r--r--app/controllers/sherlock/transactions_controller.rb2
-rw-r--r--app/controllers/snippets_controller.rb2
-rw-r--r--app/helpers/groups_helper.rb6
-rw-r--r--app/helpers/issuables_helper.rb1
-rw-r--r--app/helpers/markup_helper.rb4
-rw-r--r--app/helpers/notes_helper.rb1
-rw-r--r--app/helpers/projects_helper.rb1
-rw-r--r--app/helpers/users_helper.rb8
-rw-r--r--app/mailers/previews/devise_mailer_preview.rb (renamed from spec/mailers/previews/devise_mailer_preview.rb)0
-rw-r--r--app/mailers/previews/email_rejection_mailer_preview.rb (renamed from spec/mailers/previews/email_rejection_mailer_preview.rb)0
-rw-r--r--app/mailers/previews/notify_preview.rb (renamed from spec/mailers/previews/notify_preview.rb)2
-rw-r--r--app/mailers/previews/repository_check_mailer_preview.rb (renamed from spec/mailers/previews/repository_check_mailer_preview.rb)0
-rw-r--r--app/models/ci/build.rb8
-rw-r--r--app/models/ci/build_trace_chunk.rb149
-rw-r--r--app/models/ci/build_trace_chunks/database.rb29
-rw-r--r--app/models/ci/build_trace_chunks/fog.rb59
-rw-r--r--app/models/ci/build_trace_chunks/redis.rb51
-rw-r--r--app/models/concerns/cache_markdown_field.rb22
-rw-r--r--app/models/concerns/cacheable_attributes.rb4
-rw-r--r--app/models/concerns/group_descendant.rb4
-rw-r--r--app/models/concerns/protected_ref.rb2
-rw-r--r--app/models/import_export_upload.rb13
-rw-r--r--app/models/milestone.rb3
-rw-r--r--app/models/network/commit.rb4
-rw-r--r--app/models/project.rb20
-rw-r--r--app/models/project_wiki.rb2
-rw-r--r--app/models/remote_mirror.rb2
-rw-r--r--app/models/repository.rb16
-rw-r--r--app/models/user.rb8
-rw-r--r--app/models/wiki_page.rb1
-rw-r--r--app/policies/group_policy.rb13
-rw-r--r--app/serializers/note_entity.rb2
-rw-r--r--app/services/badges/update_service.rb2
-rw-r--r--app/services/commits/change_service.rb2
-rw-r--r--app/services/import_export_clean_up_service.rb11
-rw-r--r--app/services/issuable_base_service.rb2
-rw-r--r--app/services/members/update_service.rb2
-rw-r--r--app/services/merge_requests/rebase_service.rb2
-rw-r--r--app/services/milestones/update_service.rb2
-rw-r--r--app/services/notes/update_service.rb2
-rw-r--r--app/services/notification_recipient_service.rb13
-rw-r--r--app/services/notification_service.rb6
-rw-r--r--app/services/preview_markdown_service.rb7
-rw-r--r--app/services/projects/autocomplete_service.rb34
-rw-r--r--app/services/projects/destroy_service.rb2
-rw-r--r--app/services/projects/fork_service.rb2
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_service.rb2
-rw-r--r--app/services/projects/update_service.rb2
-rw-r--r--app/services/update_release_service.rb2
-rw-r--r--app/uploaders/import_export_uploader.rb15
-rw-r--r--app/views/import/_githubish_status.html.haml4
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml6
-rw-r--r--app/views/layouts/header/_default.html.haml4
-rw-r--r--app/views/layouts/header/_new_dropdown.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml16
-rw-r--r--app/views/layouts/nav/groups_dropdown/_show.html.haml12
-rw-r--r--app/views/layouts/nav/projects_dropdown/_show.html.haml6
-rw-r--r--app/views/profiles/keys/_form.html.haml13
-rw-r--r--app/views/projects/_export.html.haml2
-rw-r--r--app/views/projects/issues/_form.html.haml4
-rw-r--r--app/views/projects/merge_requests/_form.html.haml4
-rw-r--r--app/views/projects/milestones/_form.html.haml4
-rw-r--r--app/views/projects/releases/edit.html.haml4
-rw-r--r--app/views/projects/wikis/_form.html.haml4
-rw-r--r--app/views/shared/_user_dropdown_contributing_link.html.haml5
-rw-r--r--app/views/shared/hook_logs/_content.html.haml4
-rw-r--r--app/views/shared/notes/_note.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/views/shared/snippets/_form.html.haml4
-rw-r--r--app/workers/all_queues.yml1
-rw-r--r--app/workers/archive_trace_worker.rb2
-rw-r--r--app/workers/ci/archive_traces_cron_worker.rb1
-rw-r--r--app/workers/ci/build_trace_chunk_flush_worker.rb2
-rw-r--r--app/workers/email_receiver_worker.rb8
-rw-r--r--app/workers/object_storage/migrate_uploads_worker.rb2
-rw-r--r--app/workers/object_storage_upload_worker.rb23
-rw-r--r--app/workers/repository_check/batch_worker.rb20
-rw-r--r--app/workers/repository_check/dispatch_worker.rb13
-rw-r--r--changelogs/unreleased/31583-osw-gfm-complete-status-indication.yml5
-rw-r--r--changelogs/unreleased/36234-nav-add-groups-dropdown.yml5
-rw-r--r--changelogs/unreleased/41671-fixing-milestone-date-change-when-editing.yml5
-rw-r--r--changelogs/unreleased/46246-gitlab-project-export-should-use-object-storage.yml5
-rw-r--r--changelogs/unreleased/46396-recognise-when-a-user-is-trying-to-validate-a-private-ssh-key.yml5
-rw-r--r--changelogs/unreleased/48661-node-6-and-7-compatibility-broken-by-recent-monaco-editor-upgrade.yml5
-rw-r--r--changelogs/unreleased/48670-application-settings-may-not-be-invalidated-if-migrations-are-run.yml6
-rw-r--r--changelogs/unreleased/48677-also-check-auto_sign_in_with_provider.yml5
-rw-r--r--changelogs/unreleased/48976-fix-sti-background-migration.yml6
-rw-r--r--changelogs/unreleased/48978-fix-helm-installation-on-cluster.yml5
-rw-r--r--changelogs/unreleased/blackst0ne-rails5-activerecord-statementinvalid-mysql2-error-expression-1-of-select-list-is-not-in-group-by-clause.yml5
-rw-r--r--changelogs/unreleased/build-chunks-on-object-storage.yml6
-rw-r--r--changelogs/unreleased/bvl-preload-parents-after-pagination.yml5
-rw-r--r--changelogs/unreleased/fix-search-bar.yml5
-rw-r--r--changelogs/unreleased/fix-trace-archive-cron-worker-race-condition.yml5
-rw-r--r--changelogs/unreleased/fj-43565-wrong-role-displayed.yml5
-rw-r--r--changelogs/unreleased/fl-mr-refactor-performance-improvements.yml5
-rw-r--r--changelogs/unreleased/ide-merge-request-info.yml5
-rw-r--r--changelogs/unreleased/jprovazn-delete-upload-worker.yml5
-rw-r--r--changelogs/unreleased/jprovazn-label-links-update.yml5
-rw-r--r--changelogs/unreleased/jprovazn-upload-symlink.yml5
-rw-r--r--changelogs/unreleased/perf-wiki-pattern-once.yml5
-rw-r--r--changelogs/unreleased/rails5-fix-48977.yml5
-rw-r--r--changelogs/unreleased/rails5-mysql-fix-pr-importer-spec.yml5
-rw-r--r--changelogs/unreleased/rails5-update-gemfile-lock.yml5
-rw-r--r--changelogs/unreleased/remove-trace-efficiently.yml5
-rw-r--r--changelogs/unreleased/rosulk-patch-12.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-47797-ce.yml5
-rw-r--r--changelogs/unreleased/upgrade-hamlit-for-ruby25.yml5
-rw-r--r--config/application.rb4
-rw-r--r--config/environment.rb2
-rw-r--r--config/environments/development.rb2
-rw-r--r--config/gitlab.yml.example5
-rw-r--r--config/initializers/active_record_locking.rb4
-rw-r--r--config/initializers/devise.rb2
-rw-r--r--config/initializers/omniauth.rb2
-rw-r--r--db/migrate/20180625113853_create_import_export_uploads.rb16
-rw-r--r--db/post_migrate/20180702120647_enqueue_fix_cross_project_label_links.rb30
-rw-r--r--db/schema.rb13
-rw-r--r--doc/administration/job_traces.md10
-rw-r--r--doc/administration/pages/index.md4
-rw-r--r--doc/administration/raketasks/project_import_export.md7
-rw-r--r--doc/api/merge_requests.md1
-rw-r--r--doc/ci/variables/README.md4
-rw-r--r--doc/development/emails.md4
-rw-r--r--doc/development/fe_guide/development_process.md6
-rw-r--r--doc/development/i18n/externalization.md12
-rw-r--r--doc/development/i18n/proofreader.md1
-rw-r--r--doc/development/licensing.md22
-rw-r--r--doc/development/testing_guide/frontend_testing.md4
-rw-r--r--doc/gitlab-basics/create-project.md3
-rw-r--r--doc/gitlab-basics/img/create_new_project_info.pngbin75470 -> 77490 bytes
-rw-r--r--doc/integration/bitbucket.md21
-rw-r--r--doc/integration/saml.md172
-rw-r--r--doc/public_access/public_access.md6
-rw-r--r--doc/ssh/README.md2
-rw-r--r--doc/user/markdown.md4
-rw-r--r--doc/user/project/clusters/index.md9
-rw-r--r--lib/api/deploy_keys.rb6
-rw-r--r--lib/api/entities.rb6
-rw-r--r--lib/api/merge_requests.rb3
-rw-r--r--lib/api/project_export.rb10
-rw-r--r--lib/api/project_hooks.rb2
-rw-r--r--lib/api/services.rb4
-rw-r--r--lib/api/users.rb2
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb11
-rw-r--r--lib/banzai/filter/markdown_engines/common_mark.rb2
-rw-r--r--lib/declarative_policy/base.rb10
-rw-r--r--lib/declarative_policy/delegate_dsl.rb6
-rw-r--r--lib/declarative_policy/policy_dsl.rb14
-rw-r--r--lib/declarative_policy/preferred_scope.rb10
-rw-r--r--lib/declarative_policy/rule.rb4
-rw-r--r--lib/declarative_policy/rule_dsl.rb10
-rw-r--r--lib/declarative_policy/runner.rb2
-rw-r--r--lib/extracts_path.rb5
-rw-r--r--lib/gitlab.rb4
-rw-r--r--lib/gitlab/auth/o_auth/user.rb2
-rw-r--r--lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb1
-rw-r--r--lib/gitlab/background_migration/archive_legacy_traces.rb1
-rw-r--r--lib/gitlab/background_migration/create_fork_network_memberships_range.rb1
-rw-r--r--lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb1
-rw-r--r--lib/gitlab/background_migration/delete_diff_files.rb1
-rw-r--r--lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb1
-rw-r--r--lib/gitlab/background_migration/fill_file_store_job_artifact.rb1
-rw-r--r--lib/gitlab/background_migration/fill_file_store_lfs_object.rb1
-rw-r--r--lib/gitlab/background_migration/fill_store_upload.rb1
-rw-r--r--lib/gitlab/background_migration/fix_cross_project_label_links.rb140
-rw-r--r--lib/gitlab/background_migration/migrate_build_stage.rb1
-rw-r--r--lib/gitlab/background_migration/migrate_events_to_push_event_payloads.rb1
-rw-r--r--lib/gitlab/background_migration/migrate_system_uploads_to_new_folder.rb1
-rw-r--r--lib/gitlab/background_migration/move_personal_snippet_files.rb1
-rw-r--r--lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb1
-rw-r--r--lib/gitlab/background_migration/populate_fork_networks_range.rb2
-rw-r--r--lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data.rb3
-rw-r--r--lib/gitlab/background_migration/populate_untracked_uploads.rb2
-rw-r--r--lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb4
-rw-r--r--lib/gitlab/background_migration/prepare_untracked_uploads.rb2
-rw-r--r--lib/gitlab/ci/ansi2html.rb118
-rw-r--r--lib/gitlab/ci/config/entry/configurable.rb2
-rw-r--r--lib/gitlab/ci/trace.rb26
-rw-r--r--lib/gitlab/ci/trace/section_parser.rb12
-rw-r--r--lib/gitlab/current_settings.rb2
-rw-r--r--lib/gitlab/database.rb15
-rw-r--r--lib/gitlab/diff/image_point.rb6
-rw-r--r--lib/gitlab/diff/inline_diff.rb4
-rw-r--r--lib/gitlab/encoding_helper.rb12
-rw-r--r--lib/gitlab/exclusive_lease_helpers.rb29
-rw-r--r--lib/gitlab/fogbugz_import/importer.rb22
-rw-r--r--lib/gitlab/git/blob.rb19
-rw-r--r--lib/gitlab/git/commit.rb32
-rw-r--r--lib/gitlab/git/conflict/resolver.rb69
-rw-r--r--lib/gitlab/git/diff_collection.rb12
-rw-r--r--lib/gitlab/git/repository.rb175
-rwxr-xr-xlib/gitlab/git/support/format-git-cat-file-input21
-rw-r--r--lib/gitlab/gitaly_client.rb4
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb4
-rw-r--r--lib/gitlab/gitaly_client/operation_service.rb16
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb49
-rw-r--r--lib/gitlab/gitaly_client/storage_settings.rb4
-rw-r--r--lib/gitlab/google_code_import/importer.rb22
-rw-r--r--lib/gitlab/gpg/commit.rb2
-rw-r--r--lib/gitlab/import_export.rb4
-rw-r--r--lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb17
-rw-r--r--lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb26
-rw-r--r--lib/gitlab/import_export/saver.rb31
-rw-r--r--lib/gitlab/kubernetes/helm/base_command.rb2
-rw-r--r--lib/gitlab/mail_room.rb2
-rw-r--r--lib/gitlab/metrics/influx_db.rb2
-rw-r--r--lib/gitlab/metrics/method_call.rb2
-rw-r--r--lib/gitlab/middleware/multipart.rb2
-rw-r--r--lib/gitlab/middleware/read_only/controller.rb1
-rw-r--r--lib/gitlab/omniauth_initializer.rb5
-rw-r--r--lib/gitlab/shell.rb21
-rw-r--r--lib/gitlab/workhorse.rb39
-rw-r--r--lib/rouge/formatters/html_gitlab.rb2
-rw-r--r--lib/tasks/gettext.rake30
-rw-r--r--lib/tasks/gitlab/uploads/migrate.rake2
-rw-r--r--lib/uploaded_file.rb5
-rw-r--r--locale/gitlab.pot147
-rw-r--r--package.json4
-rw-r--r--qa/qa.rb11
-rw-r--r--qa/qa/factory/resource/group.rb2
-rw-r--r--qa/qa/factory/resource/project.rb18
-rw-r--r--qa/qa/factory/resource/project_imported_from_github.rb37
-rw-r--r--qa/qa/factory/resource/sandbox.rb4
-rw-r--r--qa/qa/page/component/select2.rb11
-rw-r--r--qa/qa/page/issuable/sidebar.rb17
-rw-r--r--qa/qa/page/menu/main.rb10
-rw-r--r--qa/qa/page/menu/side.rb8
-rw-r--r--qa/qa/page/project/import/github.rb66
-rw-r--r--qa/qa/page/project/new.rb20
-rw-r--r--qa/qa/page/project/show.rb10
-rw-r--r--qa/qa/runtime/env.rb11
-rw-r--r--qa/qa/runtime/namespace.rb2
-rw-r--r--qa/qa/runtime/release.rb2
-rw-r--r--qa/qa/scenario/test/integration/github.rb18
-rw-r--r--qa/qa/specs/features/project/import_from_github_spec.rb106
-rw-r--r--qa/spec/runtime/env_spec.rb23
-rw-r--r--rubocop/cop/line_break_around_conditional_block.rb2
-rw-r--r--scripts/frontend/prettier.js4
-rw-r--r--spec/controllers/concerns/group_tree_spec.rb11
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb2
-rw-r--r--spec/controllers/projects/group_links_controller_spec.rb2
-rw-r--r--spec/controllers/projects/imports_controller_spec.rb8
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb2
-rw-r--r--spec/controllers/projects_controller_spec.rb83
-rw-r--r--spec/factories/ci/build_trace_chunks.rb48
-rw-r--r--spec/factories/import_export_uploads.rb5
-rw-r--r--spec/factories/issues.rb2
-rw-r--r--spec/factories/merge_requests.rb2
-rw-r--r--spec/factories/projects.rb16
-rw-r--r--spec/features/commits_spec.rb6
-rw-r--r--spec/features/dashboard/projects_spec.rb34
-rw-r--r--spec/features/groups/members/request_access_spec.rb2
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb2
-rw-r--r--spec/features/merge_request/user_posts_diff_notes_spec.rb2
-rw-r--r--spec/features/milestones/user_edits_milestone_spec.rb22
-rw-r--r--spec/features/profiles/keys_spec.rb14
-rw-r--r--spec/features/profiles/password_spec.rb2
-rw-r--r--spec/features/projects/files/user_browses_files_spec.rb3
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb1
-rw-r--r--spec/features/projects/import_export/namespace_export_file_spec.rb1
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin343136 -> 3368 bytes
-rw-r--r--spec/features/projects/jobs/permissions_spec.rb2
-rw-r--r--spec/features/projects/jobs_spec.rb10
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb2
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb2
-rw-r--r--spec/features/projects/remote_mirror_spec.rb4
-rw-r--r--spec/features/projects/services/user_activates_slack_notifications_spec.rb2
-rw-r--r--spec/features/projects/show/user_sees_deletion_failure_message_spec.rb2
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb7
-rw-r--r--spec/features/signed_commits_spec.rb2
-rw-r--r--spec/features/snippets/show_spec.rb20
-rw-r--r--spec/features/users/show_spec.rb24
-rw-r--r--spec/finders/group_projects_finder_spec.rb4
-rw-r--r--spec/finders/joined_groups_finder_spec.rb2
-rw-r--r--spec/finders/move_to_project_finder_spec.rb2
-rw-r--r--spec/finders/personal_projects_finder_spec.rb2
-rw-r--r--spec/fixtures/project_export.tar.gzbin0 -> 343091 bytes
-rw-r--r--spec/helpers/blob_helper_spec.rb4
-rw-r--r--spec/helpers/groups_helper_spec.rb8
-rw-r--r--spec/helpers/issuables_helper_spec.rb1
-rw-r--r--spec/helpers/markup_helper_spec.rb21
-rw-r--r--spec/helpers/projects_helper_spec.rb5
-rw-r--r--spec/javascripts/diffs/components/diff_file_spec.js4
-rw-r--r--spec/javascripts/diffs/components/inline_diff_view_spec.js2
-rw-r--r--spec/javascripts/diffs/store/getters_spec.js52
-rw-r--r--spec/javascripts/frequent_items/components/app_spec.js251
-rw-r--r--spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js (renamed from spec/javascripts/projects_dropdown/components/projects_list_item_spec.js)34
-rw-r--r--spec/javascripts/frequent_items/components/frequent_items_list_spec.js84
-rw-r--r--spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js77
-rw-r--r--spec/javascripts/frequent_items/mock_data.js168
-rw-r--r--spec/javascripts/frequent_items/store/actions_spec.js225
-rw-r--r--spec/javascripts/frequent_items/store/getters_spec.js24
-rw-r--r--spec/javascripts/frequent_items/store/mutations_spec.js117
-rw-r--r--spec/javascripts/frequent_items/utils_spec.js89
-rw-r--r--spec/javascripts/ide/components/merge_requests/info_spec.js51
-rw-r--r--spec/javascripts/ide/components/panes/right_spec.js72
-rw-r--r--spec/javascripts/ide/stores/actions/merge_request_spec.js4
-rw-r--r--spec/javascripts/notes/mock_data.js1
-rw-r--r--spec/javascripts/profile/add_ssh_key_validation_spec.js69
-rw-r--r--spec/javascripts/projects_dropdown/components/app_spec.js349
-rw-r--r--spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js72
-rw-r--r--spec/javascripts/projects_dropdown/components/projects_list_search_spec.js84
-rw-r--r--spec/javascripts/projects_dropdown/components/search_spec.js100
-rw-r--r--spec/javascripts/projects_dropdown/mock_data.js96
-rw-r--r--spec/javascripts/projects_dropdown/service/projects_service_spec.js179
-rw-r--r--spec/javascripts/projects_dropdown/store/projects_store_spec.js41
-rw-r--r--spec/javascripts/vue_mr_widget/components/deployment_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js8
-rw-r--r--spec/lib/banzai/filter/markdown_filter_spec.rb58
-rw-r--r--spec/lib/extracts_path_spec.rb26
-rw-r--r--spec/lib/gitlab/background_migration/fix_cross_project_label_links_spec.rb109
-rw-r--r--spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb2
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb2
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb2
-rw-r--r--spec/lib/gitlab/database_spec.rb29
-rw-r--r--spec/lib/gitlab/exclusive_lease_helpers_spec.rb76
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb7
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb48
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb41
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb6
-rw-r--r--spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_object_storage_spec.rb105
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb1
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb31
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/import_export/saver_spec.rb43
-rw-r--r--spec/lib/gitlab/middleware/multipart_spec.rb27
-rw-r--r--spec/lib/gitlab/middleware/read_only_spec.rb44
-rw-r--r--spec/lib/gitlab/sanitizers/svg_spec.rb4
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb86
-rw-r--r--spec/migrations/issues_moved_to_id_foreign_key_spec.rb2
-rw-r--r--spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb6
-rw-r--r--spec/models/ci/build_metadata_spec.rb2
-rw-r--r--spec/models/ci/build_spec.rb60
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb446
-rw-r--r--spec/models/ci/build_trace_chunks/database_spec.rb105
-rw-r--r--spec/models/ci/build_trace_chunks/fog_spec.rb146
-rw-r--r--spec/models/ci/build_trace_chunks/redis_spec.rb132
-rw-r--r--spec/models/ci/job_artifact_spec.rb4
-rw-r--r--spec/models/concerns/batch_destroy_dependent_associations_spec.rb4
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb16
-rw-r--r--spec/models/concerns/cacheable_attributes_spec.rb2
-rw-r--r--spec/models/concerns/mentionable_spec.rb6
-rw-r--r--spec/models/concerns/routable_spec.rb2
-rw-r--r--spec/models/environment_spec.rb2
-rw-r--r--spec/models/group_spec.rb4
-rw-r--r--spec/models/import_export_upload_spec.rb25
-rw-r--r--spec/models/merge_request_spec.rb2
-rw-r--r--spec/models/namespace_spec.rb4
-rw-r--r--spec/models/notification_setting_spec.rb2
-rw-r--r--spec/models/project_feature_spec.rb2
-rw-r--r--spec/models/project_spec.rb28
-rw-r--r--spec/models/remote_mirror_spec.rb8
-rw-r--r--spec/models/route_spec.rb14
-rw-r--r--spec/models/service_spec.rb2
-rw-r--r--spec/models/user_spec.rb40
-rw-r--r--spec/policies/group_policy_spec.rb6
-rw-r--r--spec/requests/api/access_requests_spec.rb2
-rw-r--r--spec/requests/api/helpers_spec.rb2
-rw-r--r--spec/requests/api/merge_requests_spec.rb8
-rw-r--r--spec/requests/api/notes_spec.rb4
-rw-r--r--spec/requests/api/pipeline_schedules_spec.rb2
-rw-r--r--spec/requests/api/project_export_spec.rb23
-rw-r--r--spec/requests/api/project_import_spec.rb2
-rw-r--r--spec/requests/api/projects_spec.rb6
-rw-r--r--spec/requests/api/runner_spec.rb1
-rw-r--r--spec/requests/api/tags_spec.rb6
-rw-r--r--spec/services/ci/retry_build_service_spec.rb14
-rw-r--r--spec/services/import_export_clean_up_service_spec.rb19
-rw-r--r--spec/services/issues/update_service_spec.rb4
-rw-r--r--spec/services/members/destroy_service_spec.rb4
-rw-r--r--spec/services/merge_requests/conflicts/list_service_spec.rb20
-rw-r--r--spec/services/merge_requests/conflicts/resolve_service_spec.rb11
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb2
-rw-r--r--spec/services/notification_service_spec.rb2
-rw-r--r--spec/services/preview_markdown_service_spec.rb12
-rw-r--r--spec/services/projects/autocomplete_service_spec.rb54
-rw-r--r--spec/services/projects/fork_service_spec.rb2
-rw-r--r--spec/services/projects/update_pages_service_spec.rb16
-rw-r--r--spec/services/reset_project_cache_service_spec.rb2
-rw-r--r--spec/services/system_hooks_service_spec.rb2
-rw-r--r--spec/spec_helper.rb13
-rw-r--r--spec/support/api/repositories_shared_context.rb2
-rw-r--r--spec/support/api/time_tracking_shared_examples.rb8
-rwxr-xr-xspec/support/generate-seed-repo-rb2
-rw-r--r--spec/support/helpers/jira_service_helper.rb2
-rw-r--r--spec/support/helpers/key_generator_helper.rb2
-rw-r--r--spec/support/helpers/stub_object_storage.rb5
-rw-r--r--spec/support/matchers/disallow_request_matchers.rb15
-rw-r--r--spec/support/shared_examples/ci_trace_shared_examples.rb106
-rw-r--r--spec/support/shared_examples/helm_generated_script.rb2
-rw-r--r--spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb18
-rw-r--r--spec/uploaders/import_export_uploader_spec.rb20
-rw-r--r--spec/views/projects/imports/new.html.haml_spec.rb2
-rw-r--r--spec/views/projects/merge_requests/show.html.haml_spec.rb2
-rw-r--r--spec/workers/ci/archive_traces_cron_worker_spec.rb24
-rw-r--r--spec/workers/concerns/waitable_worker_spec.rb4
-rw-r--r--spec/workers/object_storage_upload_worker_spec.rb108
-rw-r--r--spec/workers/repository_check/batch_worker_spec.rb8
-rw-r--r--spec/workers/repository_check/dispatch_worker_spec.rb8
-rw-r--r--spec/workers/repository_import_worker_spec.rb4
-rw-r--r--spec/workers/repository_update_remote_mirror_worker_spec.rb10
-rw-r--r--spec/workers/stuck_import_jobs_worker_spec.rb4
-rw-r--r--vendor/gitignore/Autotools.gitignore3
-rw-r--r--vendor/gitignore/CraftCMS.gitignore5
-rw-r--r--vendor/gitignore/Delphi.gitignore2
-rw-r--r--vendor/gitignore/Eagle.gitignore1
-rw-r--r--vendor/gitignore/GWT.gitignore3
-rw-r--r--vendor/gitignore/Global/Backup.gitignore5
-rw-r--r--vendor/gitignore/Global/CodeKit.gitignore1
-rw-r--r--vendor/gitignore/Global/Eclipse.gitignore5
-rw-r--r--vendor/gitignore/Global/JetBrains.gitignore12
-rw-r--r--vendor/gitignore/Global/Matlab.gitignore15
-rw-r--r--vendor/gitignore/Global/Patch.gitignore2
-rw-r--r--vendor/gitignore/Global/SynopsysVCS.gitignore8
-rw-r--r--vendor/gitignore/Global/Vim.gitignore3
-rw-r--r--vendor/gitignore/LabVIEW.gitignore1
-rw-r--r--vendor/gitignore/Maven.gitignore4
-rw-r--r--vendor/gitignore/Node.gitignore6
-rw-r--r--vendor/gitignore/Objective-C.gitignore3
-rw-r--r--vendor/gitignore/Perl6.gitignore7
-rw-r--r--vendor/gitignore/Swift.gitignore3
-rw-r--r--vendor/gitignore/TeX.gitignore6
-rw-r--r--vendor/gitignore/Typo3.gitignore5
-rw-r--r--vendor/gitignore/Umbraco.gitignore2
-rw-r--r--vendor/gitignore/UnrealEngine.gitignore3
-rw-r--r--vendor/gitignore/VisualStudio.gitignore6
-rw-r--r--vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml12
-rw-r--r--vendor/licenses.csv84
-rw-r--r--yarn.lock12
580 files changed, 7435 insertions, 3929 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 49c4b059dbc..610a5ecba6d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -327,7 +327,7 @@ cloud-native-image:
cache: {}
script:
- gem install gitlab --no-ri --no-rdoc
- - ./trigger-build cng
+ - ./scripts/trigger-build cng
only:
- tags@gitlab-org/gitlab-ce
- tags@gitlab-org/gitlab-ee
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 3e1713f845e..8a1ca6747a8 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -199,12 +199,6 @@ Naming/HeredocDelimiterCase:
Naming/HeredocDelimiterNaming:
Enabled: false
-# Offense count: 27
-# Cop supports --auto-correct.
-# Configuration parameters: AutoCorrect.
-Performance/HashEachMethods:
- Enabled: false
-
# Offense count: 1
Performance/UnfreezeString:
Exclude:
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 068bced3785..5af665a17e0 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.110.0
+0.111.0
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index f374f6662e9..3eefcb9dd5b 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-0.9.1
+1.0.0
diff --git a/Gemfile b/Gemfile
index 052d0d7e0d4..d575568adaa 100644
--- a/Gemfile
+++ b/Gemfile
@@ -104,7 +104,7 @@ gem 'hashie-forbidden_attributes'
gem 'kaminari', '~> 1.0'
# HAML
-gem 'hamlit', '~> 2.6.1'
+gem 'hamlit', '~> 2.8.8'
# Files attachments
gem 'carrierwave', '~> 1.2'
@@ -351,9 +351,9 @@ group :development, :test do
gem 'spring', '~> 2.0.0'
gem 'spring-commands-rspec', '~> 1.0.4'
- gem 'gitlab-styles', '~> 2.3', require: false
+ gem 'gitlab-styles', '~> 2.4', require: false
# Pin these dependencies, otherwise a new rule could break the CI pipelines
- gem 'rubocop', '~> 0.52.1'
+ gem 'rubocop', '~> 0.54.0'
gem 'rubocop-rspec', '~> 1.22.1'
gem 'scss_lint', '~> 0.56.0', require: false
@@ -418,7 +418,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.103.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.105.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
diff --git a/Gemfile.lock b/Gemfile.lock
index 79682559522..1b8a777ceb4 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -282,7 +282,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (0.103.0)
+ gitaly-proto (0.105.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
@@ -312,8 +312,8 @@ GEM
mime-types (>= 1.16)
posix-spawn (~> 0.3)
gitlab-markup (1.6.4)
- gitlab-styles (2.3.2)
- rubocop (~> 0.51)
+ gitlab-styles (2.4.1)
+ rubocop (~> 0.54.0)
rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.19)
gitlab_omniauth-ldap (2.0.4)
@@ -381,8 +381,8 @@ GEM
rake (>= 10, < 13)
rubocop (>= 0.49.0)
sysexits (~> 1.1)
- hamlit (2.6.1)
- temple (~> 0.7.6)
+ hamlit (2.8.8)
+ temple (>= 0.8.0)
thor
tilt
hashdiff (0.3.4)
@@ -776,16 +776,16 @@ GEM
pg
rails
sqlite3
- rubocop (0.52.1)
+ rubocop (0.54.0)
parallel (~> 1.10)
- parser (>= 2.4.0.2, < 3.0)
+ parser (>= 2.5)
powerpack (~> 0.1)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-gitlab-security (0.1.1)
rubocop (>= 0.51)
- rubocop-rspec (1.22.1)
+ rubocop-rspec (1.22.2)
rubocop (>= 0.52.1)
ruby-enum (0.7.2)
i18n
@@ -889,7 +889,7 @@ GEM
sys-filesystem (1.1.6)
ffi
sysexits (1.2.0)
- temple (0.7.7)
+ temple (0.8.0)
test-prof (0.2.5)
test_after_commit (1.1.0)
activerecord (>= 3.2)
@@ -900,7 +900,7 @@ GEM
rack (>= 1, < 3)
thor (0.19.4)
thread_safe (0.3.6)
- tilt (2.0.6)
+ tilt (2.0.8)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
toml (0.1.2)
@@ -1037,13 +1037,13 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.103.0)
+ gitaly-proto (~> 0.105.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
gitlab-gollum-rugged_adapter (~> 0.4.4)
gitlab-markup (~> 1.6.4)
- gitlab-styles (~> 2.3)
+ gitlab-styles (~> 2.4)
gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2)
google-api-client (~> 0.19.8)
@@ -1057,7 +1057,7 @@ DEPENDENCIES
graphql (~> 1.8.0)
grpc (~> 1.11.0)
haml_lint (~> 0.26.0)
- hamlit (~> 2.6.1)
+ hamlit (~> 2.8.8)
hashie-forbidden_attributes
health_check (~> 2.6.0)
hipchat (~> 1.5.0)
@@ -1143,7 +1143,7 @@ DEPENDENCIES
rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5)
- rubocop (~> 0.52.1)
+ rubocop (~> 0.54.0)
rubocop-rspec (~> 1.22.1)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.17.0)
diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock
index 0d153a526e7..766f2479ea5 100644
--- a/Gemfile.rails5.lock
+++ b/Gemfile.rails5.lock
@@ -79,7 +79,7 @@ GEM
babosa (1.0.2)
base32 (0.3.2)
batch-loader (1.2.1)
- bcrypt (3.1.11)
+ bcrypt (3.1.12)
bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0)
better_errors (2.1.1)
@@ -111,7 +111,7 @@ GEM
capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
- carrierwave (1.2.1)
+ carrierwave (1.2.3)
activemodel (>= 4.0.0)
activesupport (>= 4.0.0)
mime-types (>= 1.16)
@@ -285,7 +285,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (0.103.0)
+ gitaly-proto (0.105.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
@@ -315,8 +315,8 @@ GEM
mime-types (>= 1.16)
posix-spawn (~> 0.3)
gitlab-markup (1.6.4)
- gitlab-styles (2.3.2)
- rubocop (~> 0.51)
+ gitlab-styles (2.4.1)
+ rubocop (~> 0.54.0)
rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.19)
gitlab_omniauth-ldap (2.0.4)
@@ -384,8 +384,8 @@ GEM
rake (>= 10, < 13)
rubocop (>= 0.49.0)
sysexits (~> 1.1)
- hamlit (2.6.1)
- temple (~> 0.7.6)
+ hamlit (2.8.8)
+ temple (>= 0.8.0)
thor
tilt
hashdiff (0.3.4)
@@ -514,7 +514,7 @@ GEM
net-ssh (5.0.1)
netrc (0.11.0)
nio4r (2.3.1)
- nokogiri (1.8.2)
+ nokogiri (1.8.3)
mini_portile2 (~> 2.3.0)
nokogumbo (1.5.0)
nokogiri
@@ -785,16 +785,16 @@ GEM
pg
rails
sqlite3
- rubocop (0.52.1)
+ rubocop (0.54.0)
parallel (~> 1.10)
- parser (>= 2.4.0.2, < 3.0)
+ parser (>= 2.5)
powerpack (~> 0.1)
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-gitlab-security (0.1.1)
rubocop (>= 0.51)
- rubocop-rspec (1.22.1)
+ rubocop-rspec (1.22.2)
rubocop (>= 0.52.1)
ruby-enum (0.7.2)
i18n
@@ -811,7 +811,7 @@ GEM
rubyzip (1.2.1)
rufus-scheduler (3.4.0)
et-orbi (~> 1.0)
- rugged (0.27.1)
+ rugged (0.27.2)
safe_yaml (1.0.4)
sanitize (4.6.5)
crass (~> 1.0.2)
@@ -877,7 +877,7 @@ GEM
activesupport (>= 4.2)
spring-commands-rspec (1.0.4)
spring (>= 0.9.1)
- sprockets (3.7.1)
+ sprockets (3.7.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)
@@ -898,7 +898,7 @@ GEM
sys-filesystem (1.1.6)
ffi
sysexits (1.2.0)
- temple (0.7.7)
+ temple (0.8.0)
test-prof (0.2.5)
text (1.3.1)
thin (1.7.0)
@@ -1047,13 +1047,13 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.103.0)
+ gitaly-proto (~> 0.105.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
gitlab-gollum-rugged_adapter (~> 0.4.4)
gitlab-markup (~> 1.6.4)
- gitlab-styles (~> 2.3)
+ gitlab-styles (~> 2.4)
gitlab_omniauth-ldap (~> 2.0.4)
gon (~> 6.2)
google-api-client (~> 0.19.8)
@@ -1067,7 +1067,7 @@ DEPENDENCIES
graphql (~> 1.8.0)
grpc (~> 1.11.0)
haml_lint (~> 0.26.0)
- hamlit (~> 2.6.1)
+ hamlit (~> 2.8.8)
hashie-forbidden_attributes
health_check (~> 2.6.0)
hipchat (~> 1.5.0)
@@ -1154,7 +1154,7 @@ DEPENDENCIES
rspec-retry (~> 0.4.5)
rspec-set (~> 0.1.3)
rspec_profiling (~> 0.0.5)
- rubocop (~> 0.52.1)
+ rubocop (~> 0.54.0)
rubocop-rspec (~> 1.22.1)
ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.17.0)
diff --git a/README.md b/README.md
index 77f03b791f2..b6e1cc9a432 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,5 @@
# GitLab
-[![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
-[![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines)
-[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
-[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
-[![Gitter](https://badges.gitter.im/gitlabhq/gitlabhq.svg)](https://gitter.im/gitlabhq/gitlabhq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
-
## Test coverage
- [![Ruby coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) Ruby
diff --git a/Rakefile b/Rakefile
index 85fff2d51eb..de0d6695c7b 100755
--- a/Rakefile
+++ b/Rakefile
@@ -2,9 +2,9 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
-require File.expand_path('../config/application', __FILE__)
+require File.expand_path('config/application', __dir__)
-relative_url_conf = File.expand_path('../config/initializers/relative_url', __FILE__)
+relative_url_conf = File.expand_path('config/initializers/relative_url', __dir__)
require relative_url_conf if File.exist?("#{relative_url_conf}.rb")
Gitlab::Application.load_tasks
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 0ca0e8f35dd..422becb7db8 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -100,12 +100,12 @@ const Api = {
},
// Return Merge Request for project
- mergeRequest(projectPath, mergeRequestId) {
+ mergeRequest(projectPath, mergeRequestId, params = {}) {
const url = Api.buildUrl(Api.mergeRequestPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':mrid', mergeRequestId);
- return axios.get(url);
+ return axios.get(url, { params });
},
mergeRequests(params = {}) {
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index eb0985e5603..0327fceb38d 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -63,7 +63,8 @@ export default {
plainDiffPath: state => state.diffs.plainDiffPath,
emailPatchPath: state => state.diffs.emailPatchPath,
}),
- ...mapGetters(['isParallelView', 'isNotesFetched']),
+ ...mapGetters('diffs', ['isParallelView']),
+ ...mapGetters(['isNotesFetched']),
targetBranch() {
return {
branchName: this.targetBranchName,
@@ -115,7 +116,7 @@ export default {
this.adjustView();
},
methods: {
- ...mapActions(['setBaseConfig', 'fetchDiffFiles']),
+ ...mapActions('diffs', ['setBaseConfig', 'fetchDiffFiles']),
fetchData() {
this.fetchDiffFiles().catch(() => {
createFlash(__('Something went wrong on our end. Please try again!'));
diff --git a/app/assets/javascripts/diffs/components/changed_files.vue b/app/assets/javascripts/diffs/components/changed_files.vue
index c5ef9fefc2f..9d29357d800 100644
--- a/app/assets/javascripts/diffs/components/changed_files.vue
+++ b/app/assets/javascripts/diffs/components/changed_files.vue
@@ -31,7 +31,7 @@ export default {
};
},
computed: {
- ...mapGetters(['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
+ ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']),
sumAddedLines() {
return this.sumValues('addedLines');
},
@@ -66,7 +66,7 @@ export default {
document.removeEventListener('scroll', this.handleScroll);
},
methods: {
- ...mapActions(['setInlineDiffViewType', 'setParallelDiffViewType', 'expandAllFiles']),
+ ...mapActions('diffs', ['setInlineDiffViewType', 'setParallelDiffViewType', 'expandAllFiles']),
pluralize,
handleScroll() {
if (!this.updating) {
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index b6af49c7e2e..02d5be1821b 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -22,7 +22,7 @@ export default {
projectPath: state => state.diffs.projectPath,
endpoint: state => state.diffs.endpoint,
}),
- ...mapGetters(['isInlineView', 'isParallelView']),
+ ...mapGetters('diffs', ['isInlineView', 'isParallelView']),
diffMode() {
const diffModeKey = Object.keys(diffModes).find(key => this.diffFile[`${key}File`]);
return diffModes[diffModeKey] || diffModes.replaced;
@@ -39,12 +39,12 @@ export default {
<div class="diff-viewer">
<template v-if="isTextFile">
<inline-diff-view
- v-show="isInlineView"
+ v-if="isInlineView"
:diff-file="diffFile"
:diff-lines="diffFile.highlightedDiffLines || []"
/>
<parallel-diff-view
- v-show="isParallelView"
+ v-if="isParallelView"
:diff-file="diffFile"
:diff-lines="diffFile.parallelDiffLines || []"
/>
diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue
index 39d535036f6..20483161033 100644
--- a/app/assets/javascripts/diffs/components/diff_discussions.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussions.vue
@@ -15,9 +15,7 @@ export default {
</script>
<template>
- <div
- v-if="discussions.length"
- >
+ <div>
<div
v-for="discussion in discussions"
:key="discussion.id"
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 108eefdac5f..060386c3ecb 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -47,6 +47,9 @@ export default {
false,
);
},
+ showExpandMessage() {
+ return this.isCollapsed && !this.isLoadingCollapsedDiff && !this.file.tooLarge;
+ },
},
mounted() {
document.addEventListener('scroll', this.handleScroll);
@@ -55,7 +58,7 @@ export default {
document.removeEventListener('scroll', this.handleScroll);
},
methods: {
- ...mapActions(['loadCollapsedDiff']),
+ ...mapActions('diffs', ['loadCollapsedDiff']),
handleToggle() {
const { collapsed, highlightedDiffLines, parallelDiffLines } = this.file;
@@ -159,7 +162,7 @@ export default {
</div>
<diff-content
- v-show="!isCollapsed"
+ v-if="!isCollapsed"
:class="{ hidden: isCollapsed || file.tooLarge }"
:diff-file="file"
/>
@@ -168,7 +171,7 @@ export default {
class="diff-content loading"
/>
<div
- v-show="isCollapsed && !isLoadingCollapsedDiff && !file.tooLarge"
+ v-if="showExpandMessage"
class="nothing-here-block diff-collapsed"
>
{{ __('This diff is collapsed.') }}
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index a8e8732053b..1957698c6c1 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -145,6 +145,7 @@ export default {
@click.stop="handleToggle"
/>
<a
+ v-once
ref="titleWrapper"
:href="titleLink"
class="append-right-4"
diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
index a74ea4bfaaf..ad838a32518 100644
--- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue
@@ -108,7 +108,7 @@ export default {
},
},
methods: {
- ...mapActions(['loadMoreLines', 'showCommentForm']),
+ ...mapActions('diffs', ['loadMoreLines', 'showCommentForm']),
handleCommentButton() {
this.showCommentForm({ lineCode: this.lineCode });
},
@@ -189,6 +189,7 @@ export default {
</button>
<a
v-if="lineNumber"
+ v-once
:data-linenumber="lineNumber"
:href="lineHref"
>
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index 6943b462e86..db380e68bd1 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -59,7 +59,8 @@ export default {
}
},
methods: {
- ...mapActions(['cancelCommentForm', 'saveNote', 'fetchDiscussions']),
+ ...mapActions('diffs', ['cancelCommentForm']),
+ ...mapActions(['saveNote', 'refetchDiscussionById']),
handleCancelCommentForm() {
this.autosave.reset();
this.cancelCommentForm({
@@ -78,10 +79,10 @@ export default {
});
this.saveNote(postData)
- .then(() => {
+ .then(result => {
const endpoint = this.getNotesDataByProp('discussionsPath');
- this.fetchDiscussions(endpoint)
+ this.refetchDiscussionById({ path: endpoint, discussionId: result.discussion_id })
.then(() => {
this.handleCancelCommentForm();
})
diff --git a/app/assets/javascripts/diffs/components/diff_table_cell.vue b/app/assets/javascripts/diffs/components/diff_table_cell.vue
index 5b08b161114..bd02b45a63c 100644
--- a/app/assets/javascripts/diffs/components/diff_table_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue
@@ -117,14 +117,6 @@ export default {
<template>
<td
- v-if="isContentLine"
- :class="lineType"
- class="line_content"
- v-html="normalizedLine.richText"
- >
- </td>
- <td
- v-else
:class="classNameMap"
>
<diff-line-gutter-content
diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
index 0e935f1d68e..1e8f2eecd76 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue
@@ -31,22 +31,9 @@ export default {
diffLineCommentForms: state => state.diffs.diffLineCommentForms,
}),
...mapGetters(['discussionsByLineCode']),
- isDiscussionExpanded() {
- if (!this.discussions.length) {
- return false;
- }
-
- return this.discussions.every(discussion => discussion.expanded);
- },
- hasCommentForm() {
- return this.diffLineCommentForms[this.line.lineCode];
- },
discussions() {
return this.discussionsByLineCode[this.line.lineCode] || [];
},
- shouldRender() {
- return this.isDiscussionExpanded || this.hasCommentForm;
- },
className() {
return this.discussions.length ? '' : 'js-temp-notes-holder';
},
@@ -56,7 +43,6 @@ export default {
<template>
<tr
- v-if="shouldRender"
:class="className"
class="notes_holder"
>
@@ -67,6 +53,7 @@ export default {
<td class="notes_content">
<div class="content">
<diff-discussions
+ v-if="discussions.length"
:discussions="discussions"
/>
<diff-line-note-form
diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
index a2470843ca6..8e4715c9862 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
@@ -36,7 +36,7 @@ export default {
};
},
computed: {
- ...mapGetters(['isInlineView']),
+ ...mapGetters('diffs', ['isInlineView']),
isContextLine() {
return this.line.type === CONTEXT_LINE_TYPE;
},
@@ -94,11 +94,12 @@ export default {
:is-hover="isHover"
class="diff-line-num new_line"
/>
- <diff-table-cell
+ <td
+ v-once
:class="line.type"
- :diff-file="diffFile"
- :line="line"
- :is-content-line="true"
- />
+ class="line_content"
+ v-html="line.richText"
+ >
+ </td>
</tr>
</template>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue
index b884230fb63..9c1359f7c89 100644
--- a/app/assets/javascripts/diffs/components/inline_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue
@@ -1,5 +1,5 @@
<script>
-import { mapGetters } from 'vuex';
+import { mapGetters, mapState } from 'vuex';
import inlineDiffTableRow from './inline_diff_table_row.vue';
import inlineDiffCommentRow from './inline_diff_comment_row.vue';
import { trimFirstCharOfLineContent } from '../store/utils';
@@ -20,20 +20,33 @@ export default {
},
},
computed: {
- ...mapGetters(['commit']),
+ ...mapGetters('diffs', ['commitId']),
+ ...mapGetters(['discussionsByLineCode']),
+ ...mapState({
+ diffLineCommentForms: state => state.diffs.diffLineCommentForms,
+ }),
normalizedDiffLines() {
return this.diffLines.map(line => (line.richText ? trimFirstCharOfLineContent(line) : line));
},
diffLinesLength() {
return this.normalizedDiffLines.length;
},
- commitId() {
- return this.commit && this.commit.id;
- },
userColorScheme() {
return window.gon.user_color_scheme;
},
},
+ methods: {
+ shouldRenderCommentRow(line) {
+ if (this.diffLineCommentForms[line.lineCode]) return true;
+
+ const lineDiscussions = this.discussionsByLineCode[line.lineCode];
+ if (lineDiscussions === undefined) {
+ return false;
+ }
+
+ return lineDiscussions.every(discussion => discussion.expanded);
+ },
+ },
};
</script>
@@ -53,6 +66,7 @@ export default {
:key="line.lineCode"
/>
<inline-diff-comment-row
+ v-if="shouldRenderCommentRow(line)"
:diff-file="diffFile"
:diff-lines="normalizedDiffLines"
:line="line"
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
index 5f33ec7a3c2..1e20792b647 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue
@@ -55,13 +55,6 @@ export default {
hasAnyExpandedDiscussion() {
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
},
- shouldRenderDiscussionsRow() {
- const hasDiscussion = this.hasDiscussion && this.hasAnyExpandedDiscussion;
- const hasCommentFormOnLeft = this.diffLineCommentForms[this.leftLineCode];
- const hasCommentFormOnRight = this.diffLineCommentForms[this.rightLineCode];
-
- return hasDiscussion || hasCommentFormOnLeft || hasCommentFormOnRight;
- },
shouldRenderDiscussionsOnLeft() {
return this.discussionsByLineCode[this.leftLineCode] && this.hasExpandedDiscussionOnLeft;
},
@@ -81,7 +74,6 @@ export default {
<template>
<tr
- v-if="shouldRenderDiscussionsRow"
:class="className"
class="notes_holder"
>
@@ -92,6 +84,7 @@ export default {
class="content"
>
<diff-discussions
+ v-if="discussionsByLineCode[leftLineCode].length"
:discussions="discussionsByLineCode[leftLineCode]"
/>
</div>
@@ -112,6 +105,7 @@ export default {
class="content"
>
<diff-discussions
+ v-if="discussionsByLineCode[rightLineCode].length"
:discussions="discussionsByLineCode[rightLineCode]"
/>
</div>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
index eb769584d74..b76fc63205b 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
@@ -40,7 +40,7 @@ export default {
};
},
computed: {
- ...mapGetters(['isParallelView']),
+ ...mapGetters('diffs', ['isParallelView']),
isContextLine() {
return this.line.left.type === CONTEXT_LINE_TYPE;
},
@@ -113,17 +113,15 @@ export default {
:diff-view-type="parallelDiffViewType"
class="diff-line-num old_line"
/>
- <diff-table-cell
+ <td
+ v-once
:id="line.left.lineCode"
- :diff-file="diffFile"
- :line="line"
- :is-content-line="true"
- :line-position="linePositionLeft"
- :line-type="parallelViewLeftLineType"
- :diff-view-type="parallelDiffViewType"
+ :class="parallelViewLeftLineType"
class="line_content parallel left-side"
@mousedown.native="handleParallelLineMouseDown"
- />
+ v-html="line.left.richText"
+ >
+ </td>
<diff-table-cell
:diff-file="diffFile"
:line="line"
@@ -135,16 +133,14 @@ export default {
:diff-view-type="parallelDiffViewType"
class="diff-line-num new_line"
/>
- <diff-table-cell
+ <td
+ v-once
:id="line.right.lineCode"
- :diff-file="diffFile"
- :line="line"
- :is-content-line="true"
- :line-position="linePositionRight"
- :line-type="line.right.type"
- :diff-view-type="parallelDiffViewType"
+ :class="line.right.type"
class="line_content parallel right-side"
@mousedown.native="handleParallelLineMouseDown"
- />
+ v-html="line.right.richText"
+ >
+ </td>
</tr>
</template>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
index 52561e197e6..216865474a6 100644
--- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue
+++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
@@ -1,5 +1,5 @@
<script>
-import { mapGetters } from 'vuex';
+import { mapState, mapGetters } from 'vuex';
import parallelDiffTableRow from './parallel_diff_table_row.vue';
import parallelDiffCommentRow from './parallel_diff_comment_row.vue';
import { EMPTY_CELL_TYPE } from '../constants';
@@ -21,7 +21,11 @@ export default {
},
},
computed: {
- ...mapGetters(['commit']),
+ ...mapGetters('diffs', ['commitId']),
+ ...mapGetters(['discussionsByLineCode']),
+ ...mapState({
+ diffLineCommentForms: state => state.diffs.diffLineCommentForms,
+ }),
parallelDiffLines() {
return this.diffLines.map(line => {
const parallelLine = Object.assign({}, line);
@@ -44,13 +48,36 @@ export default {
diffLinesLength() {
return this.parallelDiffLines.length;
},
- commitId() {
- return this.commit && this.commit.id;
- },
userColorScheme() {
return window.gon.user_color_scheme;
},
},
+ methods: {
+ shouldRenderCommentRow(line) {
+ const leftLineCode = line.left.lineCode;
+ const rightLineCode = line.right.lineCode;
+ const discussions = this.discussionsByLineCode;
+ const leftDiscussions = discussions[leftLineCode];
+ const rightDiscussions = discussions[rightLineCode];
+ const hasDiscussion = leftDiscussions || rightDiscussions;
+
+ const hasExpandedDiscussionOnLeft = leftDiscussions
+ ? leftDiscussions.every(discussion => discussion.expanded)
+ : false;
+ const hasExpandedDiscussionOnRight = rightDiscussions
+ ? rightDiscussions.every(discussion => discussion.expanded)
+ : false;
+
+ if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) {
+ return true;
+ }
+
+ const hasCommentFormOnLeft = this.diffLineCommentForms[leftLineCode];
+ const hasCommentFormOnRight = this.diffLineCommentForms[rightLineCode];
+
+ return hasCommentFormOnLeft || hasCommentFormOnRight;
+ },
+ },
};
</script>
@@ -72,6 +99,7 @@ export default {
:key="index"
/>
<parallel-diff-comment-row
+ v-if="shouldRenderCommentRow(line)"
:key="line.left.lineCode || line.right.lineCode"
:line="line"
:diff-file="diffFile"
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index 66d0f47d102..f3c2d7427e7 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -1,16 +1,12 @@
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants';
-export default {
- isParallelView(state) {
- return state.diffViewType === PARALLEL_DIFF_VIEW_TYPE;
- },
- isInlineView(state) {
- return state.diffViewType === INLINE_DIFF_VIEW_TYPE;
- },
- areAllFilesCollapsed(state) {
- return state.diffFiles.every(file => file.collapsed);
- },
- commit(state) {
- return state.commit;
- },
-};
+export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE;
+
+export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE;
+
+export const areAllFilesCollapsed = state => state.diffFiles.every(file => file.collapsed);
+
+export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null);
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
new file mode 100644
index 00000000000..39d90a64aab
--- /dev/null
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -0,0 +1,18 @@
+import Cookies from 'js-cookie';
+import { getParameterValues } from '~/lib/utils/url_utility';
+import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME } from '../../constants';
+
+const viewTypeFromQueryString = getParameterValues('view')[0];
+const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME);
+const defaultViewType = INLINE_DIFF_VIEW_TYPE;
+
+export default () => ({
+ isLoading: true,
+ endpoint: '',
+ basePath: '',
+ commit: null,
+ diffFiles: [],
+ mergeRequestDiffs: [],
+ diffLineCommentForms: {},
+ diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType,
+});
diff --git a/app/assets/javascripts/diffs/store/modules/index.js b/app/assets/javascripts/diffs/store/modules/index.js
index 94caa131506..90505f83b60 100644
--- a/app/assets/javascripts/diffs/store/modules/index.js
+++ b/app/assets/javascripts/diffs/store/modules/index.js
@@ -1,25 +1,11 @@
-import Cookies from 'js-cookie';
-import { getParameterValues } from '~/lib/utils/url_utility';
import actions from '../actions';
-import getters from '../getters';
+import * as getters from '../getters';
import mutations from '../mutations';
-import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME } from '../../constants';
-
-const viewTypeFromQueryString = getParameterValues('view')[0];
-const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME);
-const defaultViewType = INLINE_DIFF_VIEW_TYPE;
+import createState from './diff_state';
export default {
- state: {
- isLoading: true,
- endpoint: '',
- basePath: '',
- commit: null,
- diffFiles: [],
- mergeRequestDiffs: [],
- diffLineCommentForms: {},
- diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType,
- },
+ namespaced: true,
+ state: createState(),
getters,
actions,
mutations,
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 8aa8a114c6f..a98b2be89a3 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -66,15 +66,10 @@ export default {
},
[types.EXPAND_ALL_FILES](state) {
- const diffFiles = [];
-
- state.diffFiles.forEach(file => {
- diffFiles.push({
- ...file,
- collapsed: false,
- });
- });
-
- Object.assign(state, { diffFiles });
+ // eslint-disable-next-line no-param-reassign
+ state.diffFiles = state.diffFiles.map(file => ({
+ ...file,
+ collapsed: false,
+ }));
},
};
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index 17ea3bdb179..8abd8bc581a 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -171,6 +171,8 @@ export default class DueDateSelectors {
initMilestoneDatePicker() {
$('.datepicker').each(function initPikadayMilestone() {
const $datePicker = $(this);
+ const datePickerVal = $datePicker.val();
+
const calendar = new Pikaday({
field: $datePicker.get(0),
theme: 'gitlab-theme animate-picker',
@@ -183,7 +185,7 @@ export default class DueDateSelectors {
},
});
- calendar.setDate(parsePikadayDate($datePicker.val()));
+ calendar.setDate(parsePikadayDate(datePickerVal));
$datePicker.data('pikaday', calendar);
});
diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue
new file mode 100644
index 00000000000..2f030de8967
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/components/app.vue
@@ -0,0 +1,122 @@
+<script>
+import { mapState, mapActions, mapGetters } from 'vuex';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+import AccessorUtilities from '~/lib/utils/accessor';
+import eventHub from '../event_hub';
+import store from '../store/';
+import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants';
+import { isMobile, updateExistingFrequentItem } from '../utils';
+import FrequentItemsSearchInput from './frequent_items_search_input.vue';
+import FrequentItemsList from './frequent_items_list.vue';
+import frequentItemsMixin from './frequent_items_mixin';
+
+export default {
+ store,
+ components: {
+ LoadingIcon,
+ FrequentItemsSearchInput,
+ FrequentItemsList,
+ },
+ mixins: [frequentItemsMixin],
+ props: {
+ currentUserName: {
+ type: String,
+ required: true,
+ },
+ currentItem: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['searchQuery', 'isLoadingItems', 'isFetchFailed', 'items']),
+ ...mapGetters(['hasSearchQuery']),
+ translations() {
+ return this.getTranslations(['loadingMessage', 'header']);
+ },
+ },
+ created() {
+ const { namespace, currentUserName, currentItem } = this;
+ const storageKey = `${currentUserName}/${STORAGE_KEY[namespace]}`;
+
+ this.setNamespace(namespace);
+ this.setStorageKey(storageKey);
+
+ if (currentItem.id) {
+ this.logItemAccess(storageKey, currentItem);
+ }
+
+ eventHub.$on(`${this.namespace}-dropdownOpen`, this.dropdownOpenHandler);
+ },
+ beforeDestroy() {
+ eventHub.$off(`${this.namespace}-dropdownOpen`, this.dropdownOpenHandler);
+ },
+ methods: {
+ ...mapActions(['setNamespace', 'setStorageKey', 'fetchFrequentItems']),
+ dropdownOpenHandler() {
+ if (this.searchQuery === '' || isMobile()) {
+ this.fetchFrequentItems();
+ }
+ },
+ logItemAccess(storageKey, item) {
+ if (!AccessorUtilities.isLocalStorageAccessSafe()) {
+ return false;
+ }
+
+ // Check if there's any frequent items list set
+ const storedRawItems = localStorage.getItem(storageKey);
+ const storedFrequentItems = storedRawItems
+ ? JSON.parse(storedRawItems)
+ : [{ ...item, frequency: 1 }]; // No frequent items list set, set one up.
+
+ // Check if item already exists in list
+ const itemMatchIndex = storedFrequentItems.findIndex(
+ frequentItem => frequentItem.id === item.id,
+ );
+
+ if (itemMatchIndex > -1) {
+ storedFrequentItems[itemMatchIndex] = updateExistingFrequentItem(
+ storedFrequentItems[itemMatchIndex],
+ item,
+ );
+ } else {
+ if (storedFrequentItems.length === FREQUENT_ITEMS.MAX_COUNT) {
+ storedFrequentItems.shift();
+ }
+
+ storedFrequentItems.push({ ...item, frequency: 1 });
+ }
+
+ return localStorage.setItem(storageKey, JSON.stringify(storedFrequentItems));
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <frequent-items-search-input
+ :namespace="namespace"
+ />
+ <loading-icon
+ v-if="isLoadingItems"
+ :label="translations.loadingMessage"
+ class="loading-animation prepend-top-20"
+ size="2"
+ />
+ <div
+ v-if="!isLoadingItems && !hasSearchQuery"
+ class="section-header"
+ >
+ {{ translations.header }}
+ </div>
+ <frequent-items-list
+ v-if="!isLoadingItems"
+ :items="items"
+ :namespace="namespace"
+ :has-search-query="hasSearchQuery"
+ :is-fetch-failed="isFetchFailed"
+ :matcher="searchQuery"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
new file mode 100644
index 00000000000..8e511aa2a36
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
@@ -0,0 +1,78 @@
+<script>
+import FrequentItemsListItem from './frequent_items_list_item.vue';
+import frequentItemsMixin from './frequent_items_mixin';
+
+export default {
+ components: {
+ FrequentItemsListItem,
+ },
+ mixins: [frequentItemsMixin],
+ props: {
+ items: {
+ type: Array,
+ required: true,
+ },
+ hasSearchQuery: {
+ type: Boolean,
+ required: true,
+ },
+ isFetchFailed: {
+ type: Boolean,
+ required: true,
+ },
+ matcher: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ translations() {
+ return this.getTranslations([
+ 'itemListEmptyMessage',
+ 'itemListErrorMessage',
+ 'searchListEmptyMessage',
+ 'searchListErrorMessage',
+ ]);
+ },
+ isListEmpty() {
+ return this.items.length === 0;
+ },
+ listEmptyMessage() {
+ if (this.hasSearchQuery) {
+ return this.isFetchFailed
+ ? this.translations.searchListErrorMessage
+ : this.translations.searchListEmptyMessage;
+ }
+
+ return this.isFetchFailed
+ ? this.translations.itemListErrorMessage
+ : this.translations.itemListEmptyMessage;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="frequent-items-list-container">
+ <ul class="list-unstyled">
+ <li
+ v-if="isListEmpty"
+ :class="{ 'section-failure': isFetchFailed }"
+ class="section-empty"
+ >
+ {{ listEmptyMessage }}
+ </li>
+ <frequent-items-list-item
+ v-for="item in items"
+ v-else
+ :key="item.id"
+ :item-id="item.id"
+ :item-name="item.name"
+ :namespace="item.namespace"
+ :web-url="item.webUrl"
+ :avatar-url="item.avatarUrl"
+ :matcher="matcher"
+ />
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
new file mode 100644
index 00000000000..1f1665ff7fe
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
@@ -0,0 +1,117 @@
+<script>
+/* eslint-disable vue/require-default-prop, vue/require-prop-types */
+import Identicon from '../../vue_shared/components/identicon.vue';
+
+export default {
+ components: {
+ Identicon,
+ },
+ props: {
+ matcher: {
+ type: String,
+ required: false,
+ },
+ itemId: {
+ type: Number,
+ required: true,
+ },
+ itemName: {
+ type: String,
+ required: true,
+ },
+ namespace: {
+ type: String,
+ required: false,
+ },
+ webUrl: {
+ type: String,
+ required: true,
+ },
+ avatarUrl: {
+ required: true,
+ validator(value) {
+ return value === null || typeof value === 'string';
+ },
+ },
+ },
+ computed: {
+ hasAvatar() {
+ return this.avatarUrl !== null;
+ },
+ highlightedItemName() {
+ if (this.matcher) {
+ const matcherRegEx = new RegExp(this.matcher, 'gi');
+ const matches = this.itemName.match(matcherRegEx);
+
+ if (matches && matches.length > 0) {
+ return this.itemName.replace(matches[0], `<b>${matches[0]}</b>`);
+ }
+ }
+ return this.itemName;
+ },
+ /**
+ * Smartly truncates item namespace by doing two things;
+ * 1. Only include Group names in path by removing item name
+ * 2. Only include first and last group names in the path
+ * when namespace has more than 2 groups present
+ *
+ * First part (removal of item name from namespace) can be
+ * done from backend but doing so involves migration of
+ * existing item namespaces which is not wise thing to do.
+ */
+ truncatedNamespace() {
+ if (!this.namespace) {
+ return null;
+ }
+ const namespaceArr = this.namespace.split(' / ');
+
+ namespaceArr.splice(-1, 1);
+ let namespace = namespaceArr.join(' / ');
+
+ if (namespaceArr.length > 2) {
+ namespace = `${namespaceArr[0]} / ... / ${namespaceArr.pop()}`;
+ }
+
+ return namespace;
+ },
+ },
+};
+</script>
+
+<template>
+ <li class="frequent-items-list-item-container">
+ <a
+ :href="webUrl"
+ class="clearfix"
+ >
+ <div class="frequent-items-item-avatar-container">
+ <img
+ v-if="hasAvatar"
+ :src="avatarUrl"
+ class="avatar s32"
+ />
+ <identicon
+ v-else
+ :entity-id="itemId"
+ :entity-name="itemName"
+ size-class="s32"
+ />
+ </div>
+ <div class="frequent-items-item-metadata-container">
+ <div
+ :title="itemName"
+ class="frequent-items-item-title"
+ v-html="highlightedItemName"
+ >
+ </div>
+ <div
+ v-if="truncatedNamespace"
+ :title="namespace"
+ class="frequent-items-item-namespace"
+ >
+ {{ truncatedNamespace }}
+ </div>
+ </div>
+ </a>
+ </li>
+</template>
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_mixin.js b/app/assets/javascripts/frequent_items/components/frequent_items_mixin.js
new file mode 100644
index 00000000000..704dc83ca8e
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_mixin.js
@@ -0,0 +1,23 @@
+import { TRANSLATION_KEYS } from '../constants';
+
+export default {
+ props: {
+ namespace: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ getTranslations(keys) {
+ const translationStrings = keys.reduce(
+ (acc, key) => ({
+ ...acc,
+ [key]: TRANSLATION_KEYS[this.namespace][key],
+ }),
+ {},
+ );
+
+ return translationStrings;
+ },
+ },
+};
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
new file mode 100644
index 00000000000..a6a265eb3fd
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
@@ -0,0 +1,55 @@
+<script>
+import _ from 'underscore';
+import { mapActions } from 'vuex';
+import eventHub from '../event_hub';
+import frequentItemsMixin from './frequent_items_mixin';
+
+export default {
+ mixins: [frequentItemsMixin],
+ data() {
+ return {
+ searchQuery: '',
+ };
+ },
+ computed: {
+ translations() {
+ return this.getTranslations(['searchInputPlaceholder']);
+ },
+ },
+ watch: {
+ searchQuery: _.debounce(function debounceSearchQuery() {
+ this.setSearchQuery(this.searchQuery);
+ }, 500),
+ },
+ mounted() {
+ eventHub.$on(`${this.namespace}-dropdownOpen`, this.setFocus);
+ },
+ beforeDestroy() {
+ eventHub.$off(`${this.namespace}-dropdownOpen`, this.setFocus);
+ },
+ methods: {
+ ...mapActions(['setSearchQuery']),
+ setFocus() {
+ this.$refs.search.focus();
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="search-input-container d-none d-sm-block">
+ <input
+ ref="search"
+ v-model="searchQuery"
+ :placeholder="translations.searchInputPlaceholder"
+ type="search"
+ class="form-control"
+ />
+ <i
+ v-if="!searchQuery"
+ class="search-icon fa fa-fw fa-search"
+ aria-hidden="true"
+ >
+ </i>
+ </div>
+</template>
diff --git a/app/assets/javascripts/frequent_items/constants.js b/app/assets/javascripts/frequent_items/constants.js
new file mode 100644
index 00000000000..9bc17f5ef4f
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/constants.js
@@ -0,0 +1,38 @@
+import { s__ } from '~/locale';
+
+export const FREQUENT_ITEMS = {
+ MAX_COUNT: 20,
+ LIST_COUNT_DESKTOP: 5,
+ LIST_COUNT_MOBILE: 3,
+ ELIGIBLE_FREQUENCY: 3,
+};
+
+export const HOUR_IN_MS = 3600000;
+
+export const STORAGE_KEY = {
+ projects: 'frequent-projects',
+ groups: 'frequent-groups',
+};
+
+export const TRANSLATION_KEYS = {
+ projects: {
+ loadingMessage: s__('ProjectsDropdown|Loading projects'),
+ header: s__('ProjectsDropdown|Frequently visited'),
+ itemListErrorMessage: s__(
+ 'ProjectsDropdown|This feature requires browser localStorage support',
+ ),
+ itemListEmptyMessage: s__('ProjectsDropdown|Projects you visit often will appear here'),
+ searchListErrorMessage: s__('ProjectsDropdown|Something went wrong on our end.'),
+ searchListEmptyMessage: s__('ProjectsDropdown|Sorry, no projects matched your search'),
+ searchInputPlaceholder: s__('ProjectsDropdown|Search your projects'),
+ },
+ groups: {
+ loadingMessage: s__('GroupsDropdown|Loading groups'),
+ header: s__('GroupsDropdown|Frequently visited'),
+ itemListErrorMessage: s__('GroupsDropdown|This feature requires browser localStorage support'),
+ itemListEmptyMessage: s__('GroupsDropdown|Groups you visit often will appear here'),
+ searchListErrorMessage: s__('GroupsDropdown|Something went wrong on our end.'),
+ searchListEmptyMessage: s__('GroupsDropdown|Sorry, no groups matched your search'),
+ searchInputPlaceholder: s__('GroupsDropdown|Search your groups'),
+ },
+};
diff --git a/app/assets/javascripts/projects_dropdown/event_hub.js b/app/assets/javascripts/frequent_items/event_hub.js
index 0948c2e5352..0948c2e5352 100644
--- a/app/assets/javascripts/projects_dropdown/event_hub.js
+++ b/app/assets/javascripts/frequent_items/event_hub.js
diff --git a/app/assets/javascripts/frequent_items/index.js b/app/assets/javascripts/frequent_items/index.js
new file mode 100644
index 00000000000..5157ff211dc
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/index.js
@@ -0,0 +1,69 @@
+import $ from 'jquery';
+import Vue from 'vue';
+import Translate from '~/vue_shared/translate';
+import eventHub from '~/frequent_items/event_hub';
+import frequentItems from './components/app.vue';
+
+Vue.use(Translate);
+
+const frequentItemDropdowns = [
+ {
+ namespace: 'projects',
+ key: 'project',
+ },
+ {
+ namespace: 'groups',
+ key: 'group',
+ },
+];
+
+document.addEventListener('DOMContentLoaded', () => {
+ frequentItemDropdowns.forEach(dropdown => {
+ const { namespace, key } = dropdown;
+ const el = document.getElementById(`js-${namespace}-dropdown`);
+ const navEl = document.getElementById(`nav-${namespace}-dropdown`);
+
+ // Don't do anything if element doesn't exist (No groups dropdown)
+ // This is for when the user accesses GitLab without logging in
+ if (!el || !navEl) {
+ return;
+ }
+
+ $(navEl).on('shown.bs.dropdown', () => {
+ eventHub.$emit(`${namespace}-dropdownOpen`);
+ });
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ components: {
+ frequentItems,
+ },
+ data() {
+ const { dataset } = this.$options.el;
+ const item = {
+ id: Number(dataset[`${key}Id`]),
+ name: dataset[`${key}Name`],
+ namespace: dataset[`${key}Namespace`],
+ webUrl: dataset[`${key}WebUrl`],
+ avatarUrl: dataset[`${key}AvatarUrl`] || null,
+ lastAccessedOn: Date.now(),
+ };
+
+ return {
+ currentUserName: dataset.userName,
+ currentItem: item,
+ };
+ },
+ render(createElement) {
+ return createElement('frequent-items', {
+ props: {
+ namespace,
+ currentUserName: this.currentUserName,
+ currentItem: this.currentItem,
+ },
+ });
+ },
+ });
+ });
+});
diff --git a/app/assets/javascripts/frequent_items/store/actions.js b/app/assets/javascripts/frequent_items/store/actions.js
new file mode 100644
index 00000000000..3dd89a82a42
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/store/actions.js
@@ -0,0 +1,81 @@
+import Api from '~/api';
+import AccessorUtilities from '~/lib/utils/accessor';
+import * as types from './mutation_types';
+import { getTopFrequentItems } from '../utils';
+
+export const setNamespace = ({ commit }, namespace) => {
+ commit(types.SET_NAMESPACE, namespace);
+};
+
+export const setStorageKey = ({ commit }, key) => {
+ commit(types.SET_STORAGE_KEY, key);
+};
+
+export const requestFrequentItems = ({ commit }) => {
+ commit(types.REQUEST_FREQUENT_ITEMS);
+};
+export const receiveFrequentItemsSuccess = ({ commit }, data) => {
+ commit(types.RECEIVE_FREQUENT_ITEMS_SUCCESS, data);
+};
+export const receiveFrequentItemsError = ({ commit }) => {
+ commit(types.RECEIVE_FREQUENT_ITEMS_ERROR);
+};
+
+export const fetchFrequentItems = ({ state, dispatch }) => {
+ dispatch('requestFrequentItems');
+
+ if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ const storedFrequentItems = JSON.parse(localStorage.getItem(state.storageKey));
+
+ dispatch(
+ 'receiveFrequentItemsSuccess',
+ !storedFrequentItems ? [] : getTopFrequentItems(storedFrequentItems),
+ );
+ } else {
+ dispatch('receiveFrequentItemsError');
+ }
+};
+
+export const requestSearchedItems = ({ commit }) => {
+ commit(types.REQUEST_SEARCHED_ITEMS);
+};
+export const receiveSearchedItemsSuccess = ({ commit }, data) => {
+ commit(types.RECEIVE_SEARCHED_ITEMS_SUCCESS, data);
+};
+export const receiveSearchedItemsError = ({ commit }) => {
+ commit(types.RECEIVE_SEARCHED_ITEMS_ERROR);
+};
+export const fetchSearchedItems = ({ state, dispatch }, searchQuery) => {
+ dispatch('requestSearchedItems');
+
+ const params = {
+ simple: true,
+ per_page: 20,
+ membership: !!gon.current_user_id,
+ };
+
+ if (state.namespace === 'projects') {
+ params.order_by = 'last_activity_at';
+ }
+
+ return Api[state.namespace](searchQuery, params)
+ .then(results => {
+ dispatch('receiveSearchedItemsSuccess', results);
+ })
+ .catch(() => {
+ dispatch('receiveSearchedItemsError');
+ });
+};
+
+export const setSearchQuery = ({ commit, dispatch }, query) => {
+ commit(types.SET_SEARCH_QUERY, query);
+
+ if (query) {
+ dispatch('fetchSearchedItems', query);
+ } else {
+ dispatch('fetchFrequentItems');
+ }
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/frequent_items/store/getters.js b/app/assets/javascripts/frequent_items/store/getters.js
new file mode 100644
index 00000000000..00165db6684
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/store/getters.js
@@ -0,0 +1,4 @@
+export const hasSearchQuery = state => state.searchQuery !== '';
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/frequent_items/store/index.js b/app/assets/javascripts/frequent_items/store/index.js
new file mode 100644
index 00000000000..ece9e6419dd
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/store/index.js
@@ -0,0 +1,16 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import * as actions from './actions';
+import * as getters from './getters';
+import mutations from './mutations';
+import state from './state';
+
+Vue.use(Vuex);
+
+export default () =>
+ new Vuex.Store({
+ actions,
+ getters,
+ mutations,
+ state: state(),
+ });
diff --git a/app/assets/javascripts/frequent_items/store/mutation_types.js b/app/assets/javascripts/frequent_items/store/mutation_types.js
new file mode 100644
index 00000000000..cbe2c9401ad
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/store/mutation_types.js
@@ -0,0 +1,9 @@
+export const SET_NAMESPACE = 'SET_NAMESPACE';
+export const SET_STORAGE_KEY = 'SET_STORAGE_KEY';
+export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
+export const REQUEST_FREQUENT_ITEMS = 'REQUEST_FREQUENT_ITEMS';
+export const RECEIVE_FREQUENT_ITEMS_SUCCESS = 'RECEIVE_FREQUENT_ITEMS_SUCCESS';
+export const RECEIVE_FREQUENT_ITEMS_ERROR = 'RECEIVE_FREQUENT_ITEMS_ERROR';
+export const REQUEST_SEARCHED_ITEMS = 'REQUEST_SEARCHED_ITEMS';
+export const RECEIVE_SEARCHED_ITEMS_SUCCESS = 'RECEIVE_SEARCHED_ITEMS_SUCCESS';
+export const RECEIVE_SEARCHED_ITEMS_ERROR = 'RECEIVE_SEARCHED_ITEMS_ERROR';
diff --git a/app/assets/javascripts/frequent_items/store/mutations.js b/app/assets/javascripts/frequent_items/store/mutations.js
new file mode 100644
index 00000000000..41b660a243f
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/store/mutations.js
@@ -0,0 +1,71 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_NAMESPACE](state, namespace) {
+ Object.assign(state, {
+ namespace,
+ });
+ },
+ [types.SET_STORAGE_KEY](state, storageKey) {
+ Object.assign(state, {
+ storageKey,
+ });
+ },
+ [types.SET_SEARCH_QUERY](state, searchQuery) {
+ const hasSearchQuery = searchQuery !== '';
+
+ Object.assign(state, {
+ searchQuery,
+ isLoadingItems: true,
+ hasSearchQuery,
+ });
+ },
+ [types.REQUEST_FREQUENT_ITEMS](state) {
+ Object.assign(state, {
+ isLoadingItems: true,
+ hasSearchQuery: false,
+ });
+ },
+ [types.RECEIVE_FREQUENT_ITEMS_SUCCESS](state, rawItems) {
+ Object.assign(state, {
+ items: rawItems,
+ isLoadingItems: false,
+ hasSearchQuery: false,
+ isFetchFailed: false,
+ });
+ },
+ [types.RECEIVE_FREQUENT_ITEMS_ERROR](state) {
+ Object.assign(state, {
+ isLoadingItems: false,
+ hasSearchQuery: false,
+ isFetchFailed: true,
+ });
+ },
+ [types.REQUEST_SEARCHED_ITEMS](state) {
+ Object.assign(state, {
+ isLoadingItems: true,
+ hasSearchQuery: true,
+ });
+ },
+ [types.RECEIVE_SEARCHED_ITEMS_SUCCESS](state, rawItems) {
+ Object.assign(state, {
+ items: rawItems.map(rawItem => ({
+ id: rawItem.id,
+ name: rawItem.name,
+ namespace: rawItem.name_with_namespace || rawItem.full_name,
+ webUrl: rawItem.web_url,
+ avatarUrl: rawItem.avatar_url,
+ })),
+ isLoadingItems: false,
+ hasSearchQuery: true,
+ isFetchFailed: false,
+ });
+ },
+ [types.RECEIVE_SEARCHED_ITEMS_ERROR](state) {
+ Object.assign(state, {
+ isLoadingItems: false,
+ hasSearchQuery: true,
+ isFetchFailed: true,
+ });
+ },
+};
diff --git a/app/assets/javascripts/frequent_items/store/state.js b/app/assets/javascripts/frequent_items/store/state.js
new file mode 100644
index 00000000000..75b04febee4
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/store/state.js
@@ -0,0 +1,8 @@
+export default () => ({
+ namespace: '',
+ storageKey: '',
+ searchQuery: '',
+ isLoadingItems: false,
+ isFetchFailed: false,
+ items: [],
+});
diff --git a/app/assets/javascripts/frequent_items/utils.js b/app/assets/javascripts/frequent_items/utils.js
new file mode 100644
index 00000000000..aba692e4b99
--- /dev/null
+++ b/app/assets/javascripts/frequent_items/utils.js
@@ -0,0 +1,49 @@
+import _ from 'underscore';
+import bp from '~/breakpoints';
+import { FREQUENT_ITEMS, HOUR_IN_MS } from './constants';
+
+export const isMobile = () => {
+ const screenSize = bp.getBreakpointSize();
+
+ return screenSize === 'sm' || screenSize === 'xs';
+};
+
+export const getTopFrequentItems = items => {
+ if (!items) {
+ return [];
+ }
+ const frequentItemsCount = isMobile()
+ ? FREQUENT_ITEMS.LIST_COUNT_MOBILE
+ : FREQUENT_ITEMS.LIST_COUNT_DESKTOP;
+
+ const frequentItems = items.filter(item => item.frequency >= FREQUENT_ITEMS.ELIGIBLE_FREQUENCY);
+
+ if (!frequentItems || frequentItems.length === 0) {
+ return [];
+ }
+
+ frequentItems.sort((itemA, itemB) => {
+ // Sort all frequent items in decending order of frequency
+ // and then by lastAccessedOn with recent most first
+ if (itemA.frequency !== itemB.frequency) {
+ return itemB.frequency - itemA.frequency;
+ } else if (itemA.lastAccessedOn !== itemB.lastAccessedOn) {
+ return itemB.lastAccessedOn - itemA.lastAccessedOn;
+ }
+
+ return 0;
+ });
+
+ return _.first(frequentItems, frequentItemsCount);
+};
+
+export const updateExistingFrequentItem = (frequentItem, item) => {
+ const accessedOverHourAgo =
+ Math.abs(item.lastAccessedOn - frequentItem.lastAccessedOn) / HOUR_IN_MS > 1;
+
+ return {
+ ...item,
+ frequency: accessedOverHourAgo ? frequentItem.frequency + 1 : frequentItem.frequency,
+ lastAccessedOn: accessedOverHourAgo ? Date.now() : frequentItem.lastAccessedOn,
+ };
+};
diff --git a/app/assets/javascripts/ide/components/merge_requests/info.vue b/app/assets/javascripts/ide/components/merge_requests/info.vue
new file mode 100644
index 00000000000..199d2e74971
--- /dev/null
+++ b/app/assets/javascripts/ide/components/merge_requests/info.vue
@@ -0,0 +1,43 @@
+<script>
+import { mapGetters } from 'vuex';
+import Icon from '../../../vue_shared/components/icon.vue';
+import TitleComponent from '../../../issue_show/components/title.vue';
+import DescriptionComponent from '../../../issue_show/components/description.vue';
+
+export default {
+ components: {
+ Icon,
+ TitleComponent,
+ DescriptionComponent,
+ },
+ computed: {
+ ...mapGetters(['currentMergeRequest']),
+ },
+};
+</script>
+
+<template>
+ <div class="ide-merge-request-info h-100 d-flex flex-column">
+ <div class="detail-page-header">
+ <icon
+ name="git-merge"
+ class="align-self-center append-right-8"
+ />
+ <strong>
+ !{{ currentMergeRequest.iid }}
+ </strong>
+ </div>
+ <div class="issuable-details">
+ <title-component
+ :issuable-ref="currentMergeRequest.iid"
+ :title-html="currentMergeRequest.title_html"
+ :title-text="currentMergeRequest.title"
+ />
+ <description-component
+ :description-html="currentMergeRequest.description_html"
+ :description-text="currentMergeRequest.description"
+ :can-update="false"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
index 5cd2c9ce188..e4a5fcc67c4 100644
--- a/app/assets/javascripts/ide/components/panes/right.vue
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -5,6 +5,7 @@ import Icon from '../../../vue_shared/components/icon.vue';
import { rightSidebarViews } from '../../constants';
import PipelinesList from '../pipelines/list.vue';
import JobsDetail from '../jobs/detail.vue';
+import MergeRequestInfo from '../merge_requests/info.vue';
import ResizablePanel from '../resizable_panel.vue';
export default {
@@ -16,9 +17,10 @@ export default {
PipelinesList,
JobsDetail,
ResizablePanel,
+ MergeRequestInfo,
},
computed: {
- ...mapState(['rightPane']),
+ ...mapState(['rightPane', 'currentMergeRequestId']),
pipelinesActive() {
return (
this.rightPane === rightSidebarViews.pipelines ||
@@ -54,10 +56,33 @@ export default {
</resizable-panel>
<nav class="ide-activity-bar">
<ul class="list-unstyled">
+ <li
+ v-if="currentMergeRequestId"
+ >
+ <button
+ v-tooltip
+ :title="__('Merge Request')"
+ :aria-label="__('Merge Request')"
+ :class="{
+ active: rightPane === $options.rightSidebarViews.mergeRequestInfo
+ }"
+ data-container="body"
+ data-placement="left"
+ class="ide-sidebar-link is-right"
+ type="button"
+ @click="clickTab($event, $options.rightSidebarViews.mergeRequestInfo)"
+ >
+ <icon
+ :size="16"
+ name="text-description"
+ />
+ </button>
+ </li>
<li>
<button
v-tooltip
:title="__('Pipelines')"
+ :aria-label="__('Pipelines')"
:class="{
active: pipelinesActive
}"
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index 12e0c3aeef0..45d36f6f42c 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -31,6 +31,7 @@ export const diffModes = {
export const rightSidebarViews = {
pipelines: 'pipelines-list',
jobsDetail: 'jobs-detail',
+ mergeRequestInfo: 'merge-request-info',
};
export const stageKeys = {
diff --git a/app/assets/javascripts/ide/lib/themes/gl_theme.js b/app/assets/javascripts/ide/lib/themes/gl_theme.js
index 2fc96250c7d..439ae50448a 100644
--- a/app/assets/javascripts/ide/lib/themes/gl_theme.js
+++ b/app/assets/javascripts/ide/lib/themes/gl_theme.js
@@ -9,6 +9,7 @@ export default {
'diffEditor.insertedTextBackground': '#ddfbe6',
'diffEditor.removedTextBackground': '#f9d7dc',
'editor.selectionBackground': '#aad6f8',
+ 'editorIndentGuide.activeBackground': '#cccccc',
},
},
};
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 3e939f0c1a3..49a481f25d5 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -40,8 +40,8 @@ export default {
getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`);
},
- getProjectMergeRequestData(projectId, mergeRequestId) {
- return Api.mergeRequest(projectId, mergeRequestId);
+ getProjectMergeRequestData(projectId, mergeRequestId, params = {}) {
+ return Api.mergeRequest(projectId, mergeRequestId, params);
},
getProjectMergeRequestChanges(projectId, mergeRequestId) {
return Api.mergeRequestChanges(projectId, mergeRequestId);
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
index 4aa151abcb7..6bdf9dc3028 100644
--- a/app/assets/javascripts/ide/stores/actions/merge_request.js
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -9,7 +9,7 @@ export const getMergeRequestData = (
new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) {
service
- .getProjectMergeRequestData(projectId, mergeRequestId)
+ .getProjectMergeRequestData(projectId, mergeRequestId, { render_html: true })
.then(({ data }) => {
commit(types.SET_MERGE_REQUEST, {
projectPath: projectId,
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index b6364318537..ad928484952 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -108,6 +108,11 @@
type: String,
required: true,
},
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
projectPath: {
type: String,
required: true,
@@ -282,6 +287,7 @@
:issuable-templates="issuableTemplates"
:markdown-docs-path="markdownDocsPath"
:markdown-preview-path="markdownPreviewPath"
+ :markdown-version="markdownVersion"
:project-path="projectPath"
:project-namespace="projectNamespace"
:show-delete-button="showDeleteButton"
diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue
index 5f58f671c73..97acc5ba385 100644
--- a/app/assets/javascripts/issue_show/components/fields/description.vue
+++ b/app/assets/javascripts/issue_show/components/fields/description.vue
@@ -20,6 +20,11 @@
type: String,
required: true,
},
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
canAttachFile: {
type: Boolean,
required: false,
@@ -47,6 +52,7 @@
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
+ :markdown-version="markdownVersion"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
>
diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue
index 5bfc072e3da..e509bb52f7d 100644
--- a/app/assets/javascripts/issue_show/components/form.vue
+++ b/app/assets/javascripts/issue_show/components/form.vue
@@ -35,6 +35,11 @@
type: String,
required: true,
},
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
projectPath: {
type: String,
required: true,
@@ -97,6 +102,7 @@
:form-state="formState"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
+ :markdown-version="markdownVersion"
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
/>
diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue
index 12101c0daa5..b5e8e0ea44b 100644
--- a/app/assets/javascripts/issue_show/components/title.vue
+++ b/app/assets/javascripts/issue_show/components/title.vue
@@ -1,67 +1,67 @@
<script>
- import animateMixin from '../mixins/animate';
- import eventHub from '../event_hub';
- import tooltip from '../../vue_shared/directives/tooltip';
- import { spriteIcon } from '../../lib/utils/common_utils';
+import animateMixin from '../mixins/animate';
+import eventHub from '../event_hub';
+import tooltip from '../../vue_shared/directives/tooltip';
+import { spriteIcon } from '../../lib/utils/common_utils';
- export default {
- directives: {
- tooltip,
+export default {
+ directives: {
+ tooltip,
+ },
+ mixins: [animateMixin],
+ props: {
+ issuableRef: {
+ type: [String, Number],
+ required: true,
},
- mixins: [animateMixin],
- props: {
- issuableRef: {
- type: String,
- required: true,
- },
- canUpdate: {
- required: false,
- type: Boolean,
- default: false,
- },
- titleHtml: {
- type: String,
- required: true,
- },
- titleText: {
- type: String,
- required: true,
- },
- showInlineEditButton: {
- type: Boolean,
- required: false,
- default: false,
- },
+ canUpdate: {
+ required: false,
+ type: Boolean,
+ default: false,
},
- data() {
- return {
- preAnimation: false,
- pulseAnimation: false,
- titleEl: document.querySelector('title'),
- };
+ titleHtml: {
+ type: String,
+ required: true,
},
- computed: {
- pencilIcon() {
- return spriteIcon('pencil', 'link-highlight');
- },
+ titleText: {
+ type: String,
+ required: true,
},
- watch: {
- titleHtml() {
- this.setPageTitle();
- this.animateChange();
- },
+ showInlineEditButton: {
+ type: Boolean,
+ required: false,
+ default: false,
},
- methods: {
- setPageTitle() {
- const currentPageTitleScope = this.titleEl.innerText.split('·');
- currentPageTitleScope[0] = `${this.titleText} (${this.issuableRef}) `;
- this.titleEl.textContent = currentPageTitleScope.join('·');
- },
- edit() {
- eventHub.$emit('open.form');
- },
+ },
+ data() {
+ return {
+ preAnimation: false,
+ pulseAnimation: false,
+ titleEl: document.querySelector('title'),
+ };
+ },
+ computed: {
+ pencilIcon() {
+ return spriteIcon('pencil', 'link-highlight');
},
- };
+ },
+ watch: {
+ titleHtml() {
+ this.setPageTitle();
+ this.animateChange();
+ },
+ },
+ methods: {
+ setPageTitle() {
+ const currentPageTitleScope = this.titleEl.innerText.split('·');
+ currentPageTitleScope[0] = `${this.titleText} (${this.issuableRef}) `;
+ this.titleEl.textContent = currentPageTitleScope.join('·');
+ },
+ edit() {
+ eventHub.$emit('open.form');
+ },
+ },
+};
</script>
<template>
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index c9ce838cd48..2718f73a830 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -26,7 +26,7 @@ import './feature_highlight/feature_highlight_options';
import LazyLoader from './lazy_loader';
import initLogoAnimation from './logo';
import './milestone_select';
-import './projects_dropdown';
+import './frequent_items';
import initBreadcrumbs from './breadcrumb';
import initDispatcher from './dispatcher';
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 48cda28a1ae..8124ae6201f 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1251,13 +1251,15 @@ export default class Notes {
var postUrl = $originalContentEl.data('postUrl');
var targetId = $originalContentEl.data('targetId');
var targetType = $originalContentEl.data('targetType');
+ var markdownVersion = $originalContentEl.data('markdownVersion');
this.glForm = new GLForm($editForm.find('form'), this.enableGFM);
$editForm
.find('form')
.attr('action', `${postUrl}?html=true`)
- .attr('data-remote', 'true');
+ .attr('data-remote', 'true')
+ .attr('data-markdown-version', markdownVersion);
$editForm.find('.js-form-target-id').val(targetId);
$editForm.find('.js-form-target-type').val(targetType);
$editForm
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index c6a524f68cb..6612bc44e0b 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -34,6 +34,11 @@ export default {
type: String,
required: true,
},
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
data() {
return {
@@ -344,6 +349,7 @@ Please check your network connection and try again.`;
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:quick-actions-docs-path="quickActionsDocsPath"
+ :markdown-version="markdownVersion"
:add-spacing-classes="false">
<textarea
id="note-body"
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index d2db68df98e..6f4a0709825 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -92,6 +92,7 @@ export default {
:is-editing="isEditing"
:note-body="noteBody"
:note-id="note.id"
+ :markdown-version="note.cached_markdown_version"
@handleFormUpdate="handleFormUpdate"
@cancelForm="formCancelHandler"
/>
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index a4e3faa5d75..963e3a37b39 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -24,6 +24,11 @@ export default {
required: false,
default: 0,
},
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
saveButtonTitle: {
type: String,
required: false,
@@ -156,6 +161,7 @@ export default {
<markdown-field
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
+ :markdown-version="markdownVersion"
:quick-actions-docs-path="quickActionsDocsPath"
:add-spacing-classes="false">
<textarea
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index a8995021699..9b8713b40fb 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -43,6 +43,11 @@ export default {
required: false,
default: true,
},
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
data() {
return {
@@ -192,6 +197,7 @@ export default {
<comment-form
:noteable-type="noteableType"
+ :markdown-version="markdownVersion"
/>
</div>
</template>
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index eed3a82854d..6dd4c9d66ac 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -15,6 +15,7 @@ document.addEventListener('DOMContentLoaded', () => {
const notesDataset = document.getElementById('js-vue-notes').dataset;
const parsedUserData = JSON.parse(notesDataset.currentUserData);
const noteableData = JSON.parse(notesDataset.noteableData);
+ const { markdownVersion } = notesDataset;
let currentUserData = {};
noteableData.noteableType = notesDataset.noteableType;
@@ -33,6 +34,7 @@ document.addEventListener('DOMContentLoaded', () => {
return {
noteableData,
currentUserData,
+ markdownVersion,
notesData: JSON.parse(notesDataset.notesData),
};
},
@@ -42,6 +44,7 @@ document.addEventListener('DOMContentLoaded', () => {
noteableData: this.noteableData,
notesData: this.notesData,
userData: this.currentUserData,
+ markdownVersion: this.markdownVersion,
},
});
},
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 671fa4d7d22..b2bf86eea56 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -41,6 +41,15 @@ export const fetchDiscussions = ({ commit }, path) =>
commit(types.SET_INITIAL_DISCUSSIONS, discussions);
});
+export const refetchDiscussionById = ({ commit }, { path, discussionId }) =>
+ service
+ .fetchDiscussions(path)
+ .then(res => res.json())
+ .then(discussions => {
+ const selectedDiscussion = discussions.find(discussion => discussion.id === discussionId);
+ if (selectedDiscussion) commit(types.UPDATE_DISCUSSION, selectedDiscussion);
+ });
+
export const deleteNote = ({ commit }, note) =>
service.deleteNote(note.path).then(() => {
commit(types.DELETE_NOTE, note);
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index e5e40ce07fa..a1849269010 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -114,7 +114,6 @@ export default {
Object.assign(state, { discussions });
},
-
[types.SET_LAST_FETCHED_AT](state, fetchedAt) {
Object.assign(state, { lastFetchedAt: fetchedAt });
},
diff --git a/app/assets/javascripts/pages/profiles/keys/index.js b/app/assets/javascripts/pages/profiles/keys/index.js
new file mode 100644
index 00000000000..1cd3ee1dfdb
--- /dev/null
+++ b/app/assets/javascripts/pages/profiles/keys/index.js
@@ -0,0 +1,16 @@
+import AddSshKeyValidation from '~/profile/add_ssh_key_validation';
+
+document.addEventListener('DOMContentLoaded', () => {
+ const input = document.querySelector('.js-add-ssh-key-validation-input');
+ const warning = document.querySelector('.js-add-ssh-key-validation-warning');
+ const originalSubmit = input.form.querySelector('.js-add-ssh-key-validation-original-submit');
+ const confirmSubmit = warning.querySelector('.js-add-ssh-key-validation-confirm-submit');
+
+ const addSshKeyValidation = new AddSshKeyValidation(
+ input,
+ warning,
+ originalSubmit,
+ confirmSubmit,
+ );
+ addSshKeyValidation.register();
+});
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index 0e973cab4d2..0964baf8954 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -28,12 +28,16 @@ MarkdownPreview.prototype.ajaxCache = {};
MarkdownPreview.prototype.showPreview = function ($form) {
var mdText;
+ var markdownVersion;
+ var url;
var preview = $form.find('.js-md-preview');
- var url = preview.data('url');
if (preview.hasClass('md-preview-loading')) {
return;
}
+
mdText = $form.find('textarea.markdown-area').val();
+ markdownVersion = $form.attr('data-markdown-version');
+ url = this.versionedPreviewPath(preview.data('url'), markdownVersion);
if (mdText.trim().length === 0) {
preview.text(this.emptyMessage);
@@ -59,6 +63,14 @@ MarkdownPreview.prototype.showPreview = function ($form) {
}
};
+MarkdownPreview.prototype.versionedPreviewPath = function (markdownPreviewPath, markdownVersion) {
+ if (typeof markdownVersion === 'undefined') {
+ return markdownPreviewPath;
+ }
+
+ return `${markdownPreviewPath}${markdownPreviewPath.indexOf('?') === -1 ? '?' : '&'}markdown_version=${markdownVersion}`;
+};
+
MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) {
if (!url) {
return;
diff --git a/app/assets/javascripts/profile/add_ssh_key_validation.js b/app/assets/javascripts/profile/add_ssh_key_validation.js
new file mode 100644
index 00000000000..ab6a6c1896c
--- /dev/null
+++ b/app/assets/javascripts/profile/add_ssh_key_validation.js
@@ -0,0 +1,43 @@
+export default class AddSshKeyValidation {
+ constructor(inputElement, warningElement, originalSubmitElement, confirmSubmitElement) {
+ this.inputElement = inputElement;
+ this.form = inputElement.form;
+
+ this.warningElement = warningElement;
+
+ this.originalSubmitElement = originalSubmitElement;
+ this.confirmSubmitElement = confirmSubmitElement;
+
+ this.isValid = false;
+ }
+
+ register() {
+ this.form.addEventListener('submit', event => this.submit(event));
+
+ this.confirmSubmitElement.addEventListener('click', () => {
+ this.isValid = true;
+ this.form.submit();
+ });
+
+ this.inputElement.addEventListener('input', () => this.toggleWarning(false));
+ }
+
+ submit(event) {
+ this.isValid = AddSshKeyValidation.isPublicKey(this.inputElement.value);
+
+ if (this.isValid) return true;
+
+ event.preventDefault();
+ this.toggleWarning(true);
+ return false;
+ }
+
+ toggleWarning(isVisible) {
+ this.warningElement.classList.toggle('hide', !isVisible);
+ this.originalSubmitElement.classList.toggle('hide', isVisible);
+ }
+
+ static isPublicKey(value) {
+ return /^(ssh|ecdsa-sha2)-/.test(value);
+ }
+}
diff --git a/app/assets/javascripts/projects_dropdown/components/app.vue b/app/assets/javascripts/projects_dropdown/components/app.vue
deleted file mode 100644
index 73d49488299..00000000000
--- a/app/assets/javascripts/projects_dropdown/components/app.vue
+++ /dev/null
@@ -1,158 +0,0 @@
-<script>
-import bs from '../../breakpoints';
-import eventHub from '../event_hub';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
-
-import projectsListFrequent from './projects_list_frequent.vue';
-import projectsListSearch from './projects_list_search.vue';
-
-import search from './search.vue';
-
-export default {
- components: {
- search,
- loadingIcon,
- projectsListFrequent,
- projectsListSearch,
- },
- props: {
- currentProject: {
- type: Object,
- required: true,
- },
- store: {
- type: Object,
- required: true,
- },
- service: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- isLoadingProjects: false,
- isFrequentsListVisible: false,
- isSearchListVisible: false,
- isLocalStorageFailed: false,
- isSearchFailed: false,
- searchQuery: '',
- };
- },
- computed: {
- frequentProjects() {
- return this.store.getFrequentProjects();
- },
- searchProjects() {
- return this.store.getSearchedProjects();
- },
- },
- created() {
- if (this.currentProject.id) {
- this.logCurrentProjectAccess();
- }
-
- eventHub.$on('dropdownOpen', this.fetchFrequentProjects);
- eventHub.$on('searchProjects', this.fetchSearchedProjects);
- eventHub.$on('searchCleared', this.handleSearchClear);
- eventHub.$on('searchFailed', this.handleSearchFailure);
- },
- beforeDestroy() {
- eventHub.$off('dropdownOpen', this.fetchFrequentProjects);
- eventHub.$off('searchProjects', this.fetchSearchedProjects);
- eventHub.$off('searchCleared', this.handleSearchClear);
- eventHub.$off('searchFailed', this.handleSearchFailure);
- },
- methods: {
- toggleFrequentProjectsList(state) {
- this.isLoadingProjects = !state;
- this.isSearchListVisible = !state;
- this.isFrequentsListVisible = state;
- },
- toggleSearchProjectsList(state) {
- this.isLoadingProjects = !state;
- this.isFrequentsListVisible = !state;
- this.isSearchListVisible = state;
- },
- toggleLoader(state) {
- this.isFrequentsListVisible = !state;
- this.isSearchListVisible = !state;
- this.isLoadingProjects = state;
- },
- fetchFrequentProjects() {
- const screenSize = bs.getBreakpointSize();
- if (this.searchQuery && (screenSize !== 'sm' && screenSize !== 'xs')) {
- this.toggleSearchProjectsList(true);
- } else {
- this.toggleLoader(true);
- this.isLocalStorageFailed = false;
- const projects = this.service.getFrequentProjects();
- if (projects) {
- this.toggleFrequentProjectsList(true);
- this.store.setFrequentProjects(projects);
- } else {
- this.isLocalStorageFailed = true;
- this.toggleFrequentProjectsList(true);
- this.store.setFrequentProjects([]);
- }
- }
- },
- fetchSearchedProjects(searchQuery) {
- this.searchQuery = searchQuery;
- this.toggleLoader(true);
- this.service
- .getSearchedProjects(this.searchQuery)
- .then(res => res.json())
- .then(results => {
- this.toggleSearchProjectsList(true);
- this.store.setSearchedProjects(results);
- })
- .catch(() => {
- this.isSearchFailed = true;
- this.toggleSearchProjectsList(true);
- });
- },
- logCurrentProjectAccess() {
- this.service.logProjectAccess(this.currentProject);
- },
- handleSearchClear() {
- this.searchQuery = '';
- this.toggleFrequentProjectsList(true);
- this.store.clearSearchedProjects();
- },
- handleSearchFailure() {
- this.isSearchFailed = true;
- this.toggleSearchProjectsList(true);
- },
- },
-};
-</script>
-
-<template>
- <div>
- <search/>
- <loading-icon
- v-if="isLoadingProjects"
- :label="s__('ProjectsDropdown|Loading projects')"
- class="loading-animation prepend-top-20"
- size="2"
- />
- <div
- v-if="isFrequentsListVisible"
- class="section-header"
- >
- {{ s__('ProjectsDropdown|Frequently visited') }}
- </div>
- <projects-list-frequent
- v-if="isFrequentsListVisible"
- :local-storage-failed="isLocalStorageFailed"
- :projects="frequentProjects"
- />
- <projects-list-search
- v-if="isSearchListVisible"
- :search-failed="isSearchFailed"
- :matcher="searchQuery"
- :projects="searchProjects"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue
deleted file mode 100644
index 625e0aa548c..00000000000
--- a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue
+++ /dev/null
@@ -1,57 +0,0 @@
-<script>
- import { s__ } from '../../locale';
- import projectsListItem from './projects_list_item.vue';
-
- export default {
- components: {
- projectsListItem,
- },
- props: {
- projects: {
- type: Array,
- required: true,
- },
- localStorageFailed: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- isListEmpty() {
- return this.projects.length === 0;
- },
- listEmptyMessage() {
- return this.localStorageFailed ?
- s__('ProjectsDropdown|This feature requires browser localStorage support') :
- s__('ProjectsDropdown|Projects you visit often will appear here');
- },
- },
- };
-</script>
-
-<template>
- <div
- class="projects-list-frequent-container"
- >
- <ul
- class="list-unstyled"
- >
- <li
- v-if="isListEmpty"
- class="section-empty"
- >
- {{ listEmptyMessage }}
- </li>
- <projects-list-item
- v-for="(project, index) in projects"
- v-else
- :key="index"
- :project-id="project.id"
- :project-name="project.name"
- :namespace="project.namespace"
- :web-url="project.webUrl"
- :avatar-url="project.avatarUrl"
- />
- </ul>
- </div>
-</template>
diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
deleted file mode 100644
index eafbf6c99e2..00000000000
--- a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue
+++ /dev/null
@@ -1,116 +0,0 @@
-<script>
- /* eslint-disable vue/require-default-prop, vue/require-prop-types */
- import identicon from '../../vue_shared/components/identicon.vue';
-
- export default {
- components: {
- identicon,
- },
- props: {
- matcher: {
- type: String,
- required: false,
- },
- projectId: {
- type: Number,
- required: true,
- },
- projectName: {
- type: String,
- required: true,
- },
- namespace: {
- type: String,
- required: true,
- },
- webUrl: {
- type: String,
- required: true,
- },
- avatarUrl: {
- required: true,
- validator(value) {
- return value === null || typeof value === 'string';
- },
- },
- },
- computed: {
- hasAvatar() {
- return this.avatarUrl !== null;
- },
- highlightedProjectName() {
- if (this.matcher) {
- const matcherRegEx = new RegExp(this.matcher, 'gi');
- const matches = this.projectName.match(matcherRegEx);
-
- if (matches && matches.length > 0) {
- return this.projectName.replace(matches[0], `<b>${matches[0]}</b>`);
- }
- }
- return this.projectName;
- },
- /**
- * Smartly truncates project namespace by doing two things;
- * 1. Only include Group names in path by removing project name
- * 2. Only include first and last group names in the path
- * when namespace has more than 2 groups present
- *
- * First part (removal of project name from namespace) can be
- * done from backend but doing so involves migration of
- * existing project namespaces which is not wise thing to do.
- */
- truncatedNamespace() {
- const namespaceArr = this.namespace.split(' / ');
- namespaceArr.splice(-1, 1);
- let namespace = namespaceArr.join(' / ');
-
- if (namespaceArr.length > 2) {
- namespace = `${namespaceArr[0]} / ... / ${namespaceArr.pop()}`;
- }
-
- return namespace;
- },
- },
- };
-</script>
-
-<template>
- <li
- class="projects-list-item-container"
- >
- <a
- :href="webUrl"
- class="clearfix"
- >
- <div
- class="project-item-avatar-container"
- >
- <img
- v-if="hasAvatar"
- :src="avatarUrl"
- class="avatar s32"
- />
- <identicon
- v-else
- :entity-id="projectId"
- :entity-name="projectName"
- size-class="s32"
- />
- </div>
- <div
- class="project-item-metadata-container"
- >
- <div
- :title="projectName"
- class="project-title"
- v-html="highlightedProjectName"
- >
- </div>
- <div
- :title="namespace"
- class="project-namespace"
- >{{ truncatedNamespace }}</div>
- </div>
- </a>
- </li>
-</template>
diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue
deleted file mode 100644
index 76e9cb9e53f..00000000000
--- a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-<script>
-import { s__ } from '../../locale';
-import projectsListItem from './projects_list_item.vue';
-
-export default {
- components: {
- projectsListItem,
- },
- props: {
- matcher: {
- type: String,
- required: true,
- },
- projects: {
- type: Array,
- required: true,
- },
- searchFailed: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- isListEmpty() {
- return this.projects.length === 0;
- },
- listEmptyMessage() {
- return this.searchFailed ?
- s__('ProjectsDropdown|Something went wrong on our end.') :
- s__('ProjectsDropdown|Sorry, no projects matched your search');
- },
- },
-};
-</script>
-
-<template>
- <div
- class="projects-list-search-container"
- >
- <ul
- class="list-unstyled"
- >
- <li
- v-if="isListEmpty"
- :class="{ 'section-failure': searchFailed }"
- class="section-empty"
- >
- {{ listEmptyMessage }}
- </li>
- <projects-list-item
- v-for="(project, index) in projects"
- v-else
- :key="index"
- :project-id="project.id"
- :project-name="project.name"
- :namespace="project.namespace"
- :web-url="project.webUrl"
- :avatar-url="project.avatarUrl"
- :matcher="matcher"
- />
- </ul>
- </div>
-</template>
diff --git a/app/assets/javascripts/projects_dropdown/components/search.vue b/app/assets/javascripts/projects_dropdown/components/search.vue
deleted file mode 100644
index 28f2a18f2a6..00000000000
--- a/app/assets/javascripts/projects_dropdown/components/search.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<script>
- import _ from 'underscore';
- import eventHub from '../event_hub';
-
- export default {
- data() {
- return {
- searchQuery: '',
- };
- },
- watch: {
- searchQuery() {
- this.handleInput();
- },
- },
- mounted() {
- eventHub.$on('dropdownOpen', this.setFocus);
- },
- beforeDestroy() {
- eventHub.$off('dropdownOpen', this.setFocus);
- },
- methods: {
- setFocus() {
- this.$refs.search.focus();
- },
- emitSearchEvents() {
- if (this.searchQuery) {
- eventHub.$emit('searchProjects', this.searchQuery);
- } else {
- eventHub.$emit('searchCleared');
- }
- },
- /**
- * Callback function within _.debounce is intentionally
- * kept as ES5 `function() {}` instead of ES6 `() => {}`
- * as it otherwise messes up function context
- * and component reference is no longer accessible via `this`
- */
- // eslint-disable-next-line func-names
- handleInput: _.debounce(function () {
- this.emitSearchEvents();
- }, 500),
- },
- };
-</script>
-
-<template>
- <div
- class="search-input-container d-none d-sm-block"
- >
- <input
- ref="search"
- v-model="searchQuery"
- :placeholder="s__('ProjectsDropdown|Search your projects')"
- type="search"
- class="form-control"
- />
- <i
- v-if="!searchQuery"
- class="search-icon fa fa-fw fa-search"
- aria-hidden="true"
- >
- </i>
- </div>
-</template>
diff --git a/app/assets/javascripts/projects_dropdown/constants.js b/app/assets/javascripts/projects_dropdown/constants.js
deleted file mode 100644
index 8937097184c..00000000000
--- a/app/assets/javascripts/projects_dropdown/constants.js
+++ /dev/null
@@ -1,10 +0,0 @@
-export const FREQUENT_PROJECTS = {
- MAX_COUNT: 20,
- LIST_COUNT_DESKTOP: 5,
- LIST_COUNT_MOBILE: 3,
- ELIGIBLE_FREQUENCY: 3,
-};
-
-export const HOUR_IN_MS = 3600000;
-
-export const STORAGE_KEY = 'frequent-projects';
diff --git a/app/assets/javascripts/projects_dropdown/index.js b/app/assets/javascripts/projects_dropdown/index.js
deleted file mode 100644
index 6056f12aa4f..00000000000
--- a/app/assets/javascripts/projects_dropdown/index.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import $ from 'jquery';
-import Vue from 'vue';
-
-import Translate from '../vue_shared/translate';
-import eventHub from './event_hub';
-import ProjectsService from './service/projects_service';
-import ProjectsStore from './store/projects_store';
-
-import projectsDropdownApp from './components/app.vue';
-
-Vue.use(Translate);
-
-document.addEventListener('DOMContentLoaded', () => {
- const el = document.getElementById('js-projects-dropdown');
- const navEl = document.getElementById('nav-projects-dropdown');
-
- // Don't do anything if element doesn't exist (No projects dropdown)
- // This is for when the user accesses GitLab without logging in
- if (!el || !navEl) {
- return;
- }
-
- $(navEl).on('shown.bs.dropdown', () => {
- eventHub.$emit('dropdownOpen');
- });
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- components: {
- projectsDropdownApp,
- },
- data() {
- const { dataset } = this.$options.el;
- const store = new ProjectsStore();
- const service = new ProjectsService(dataset.userName);
-
- const project = {
- id: Number(dataset.projectId),
- name: dataset.projectName,
- namespace: dataset.projectNamespace,
- webUrl: dataset.projectWebUrl,
- avatarUrl: dataset.projectAvatarUrl || null,
- lastAccessedOn: Date.now(),
- };
-
- return {
- store,
- service,
- state: store.state,
- currentUserName: dataset.userName,
- currentProject: project,
- };
- },
- render(createElement) {
- return createElement('projects-dropdown-app', {
- props: {
- currentUserName: this.currentUserName,
- currentProject: this.currentProject,
- store: this.store,
- service: this.service,
- },
- });
- },
- });
-});
diff --git a/app/assets/javascripts/projects_dropdown/service/projects_service.js b/app/assets/javascripts/projects_dropdown/service/projects_service.js
deleted file mode 100644
index ed1c3deead2..00000000000
--- a/app/assets/javascripts/projects_dropdown/service/projects_service.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import _ from 'underscore';
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-import bp from '../../breakpoints';
-import Api from '../../api';
-import AccessorUtilities from '../../lib/utils/accessor';
-
-import { FREQUENT_PROJECTS, HOUR_IN_MS, STORAGE_KEY } from '../constants';
-
-Vue.use(VueResource);
-
-export default class ProjectsService {
- constructor(currentUserName) {
- this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
- this.currentUserName = currentUserName;
- this.storageKey = `${this.currentUserName}/${STORAGE_KEY}`;
- this.projectsPath = Vue.resource(Api.buildUrl(Api.projectsPath));
- }
-
- getSearchedProjects(searchQuery) {
- return this.projectsPath.get({
- simple: true,
- per_page: 20,
- membership: !!gon.current_user_id,
- order_by: 'last_activity_at',
- search: searchQuery,
- });
- }
-
- getFrequentProjects() {
- if (this.isLocalStorageAvailable) {
- return this.getTopFrequentProjects();
- }
- return null;
- }
-
- logProjectAccess(project) {
- let matchFound = false;
- let storedFrequentProjects;
-
- if (this.isLocalStorageAvailable) {
- const storedRawProjects = localStorage.getItem(this.storageKey);
-
- // Check if there's any frequent projects list set
- if (!storedRawProjects) {
- // No frequent projects list set, set one up.
- storedFrequentProjects = [];
- storedFrequentProjects.push({ ...project, frequency: 1 });
- } else {
- // Check if project is already present in frequents list
- // When found, update metadata of it.
- storedFrequentProjects = JSON.parse(storedRawProjects).map(projectItem => {
- if (projectItem.id === project.id) {
- matchFound = true;
- const diff = Math.abs(project.lastAccessedOn - projectItem.lastAccessedOn) / HOUR_IN_MS;
- const updatedProject = {
- ...project,
- frequency: projectItem.frequency,
- lastAccessedOn: projectItem.lastAccessedOn,
- };
-
- // Check if duration since last access of this project
- // is over an hour
- if (diff > 1) {
- return {
- ...updatedProject,
- frequency: updatedProject.frequency + 1,
- lastAccessedOn: Date.now(),
- };
- }
-
- return {
- ...updatedProject,
- };
- }
-
- return projectItem;
- });
-
- // Check whether currently logged project is present in frequents list
- if (!matchFound) {
- // We always keep size of frequents collection to 20 projects
- // out of which only 5 projects with
- // highest value of `frequency` and most recent `lastAccessedOn`
- // are shown in projects dropdown
- if (storedFrequentProjects.length === FREQUENT_PROJECTS.MAX_COUNT) {
- storedFrequentProjects.shift(); // Remove an item from head of array
- }
-
- storedFrequentProjects.push({ ...project, frequency: 1 });
- }
- }
-
- localStorage.setItem(this.storageKey, JSON.stringify(storedFrequentProjects));
- }
- }
-
- getTopFrequentProjects() {
- const storedFrequentProjects = JSON.parse(localStorage.getItem(this.storageKey));
- let frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_DESKTOP;
-
- if (!storedFrequentProjects) {
- return [];
- }
-
- if (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'xs') {
- frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_MOBILE;
- }
-
- const frequentProjects = storedFrequentProjects.filter(
- project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY,
- );
-
- if (!frequentProjects || frequentProjects.length === 0) {
- return [];
- }
-
- // Sort all frequent projects in decending order of frequency
- // and then by lastAccessedOn with recent most first
- frequentProjects.sort((projectA, projectB) => {
- if (projectA.frequency < projectB.frequency) {
- return 1;
- } else if (projectA.frequency > projectB.frequency) {
- return -1;
- } else if (projectA.lastAccessedOn < projectB.lastAccessedOn) {
- return 1;
- } else if (projectA.lastAccessedOn > projectB.lastAccessedOn) {
- return -1;
- }
-
- return 0;
- });
-
- return _.first(frequentProjects, frequentProjectsCount);
- }
-}
diff --git a/app/assets/javascripts/projects_dropdown/store/projects_store.js b/app/assets/javascripts/projects_dropdown/store/projects_store.js
deleted file mode 100644
index ffefbe693f4..00000000000
--- a/app/assets/javascripts/projects_dropdown/store/projects_store.js
+++ /dev/null
@@ -1,33 +0,0 @@
-export default class ProjectsStore {
- constructor() {
- this.state = {};
- this.state.frequentProjects = [];
- this.state.searchedProjects = [];
- }
-
- setFrequentProjects(rawProjects) {
- this.state.frequentProjects = rawProjects;
- }
-
- getFrequentProjects() {
- return this.state.frequentProjects;
- }
-
- setSearchedProjects(rawProjects) {
- this.state.searchedProjects = rawProjects.map(rawProject => ({
- id: rawProject.id,
- name: rawProject.name,
- namespace: rawProject.name_with_namespace,
- webUrl: rawProject.web_url,
- avatarUrl: rawProject.avatar_url,
- }));
- }
-
- getSearchedProjects() {
- return this.state.searchedProjects;
- }
-
- clearSearchedProjects() {
- this.state.searchedProjects = [];
- }
-}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
index 5e464f8a0e2..21f21232596 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue
@@ -79,66 +79,62 @@ export default {
</script>
<template>
- <div class="mr-widget-heading deploy-heading">
+ <div class="mr-widget-heading deploy-heading append-bottom-default">
<div class="ci-widget media">
- <div class="ci-status-icon ci-status-icon-success">
- <span class="js-icon-link icon-link">
- <status-icon status="success" />
- </span>
- </div>
<div class="media-body">
<div class="deploy-body">
- <template v-if="hasDeploymentMeta">
- <span>
- Deployed to
- </span>
- <a
- :href="deployment.url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="deploy-link js-deploy-meta"
+ <div class="deployment-info">
+ <template v-if="hasDeploymentMeta">
+ <span>
+ Deployed to
+ </span>
+ <a
+ :href="deployment.url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="deploy-link js-deploy-meta"
+ >
+ {{ deployment.name }}
+ </a>
+ </template>
+ <span
+ v-tooltip
+ v-if="hasDeploymentTime"
+ :title="deployment.deployed_at_formatted"
+ class="js-deploy-time"
>
- {{ deployment.name }}
- </a>
- </template>
- <template v-if="hasExternalUrls">
- <span>
- on
+ {{ deployTimeago }}
</span>
+ <memory-usage
+ v-if="hasMetrics"
+ :metrics-url="deployment.metrics_url"
+ :metrics-monitoring-url="deployment.metrics_monitoring_url"
+ />
+ </div>
+ <div>
<a
+ v-if="hasExternalUrls"
:href="deployment.external_url"
target="_blank"
rel="noopener noreferrer nofollow"
- class="deploy-link js-deploy-url"
+ class="deploy-link js-deploy-url btn btn-default btn-sm inline"
>
- {{ deployment.external_url_formatted }}
- <icon
- :size="16"
- name="external-link"
- />
+ <span>
+ View app
+ <icon name="external-link" />
+ </span>
</a>
- </template>
- <span
- v-tooltip
- v-if="hasDeploymentTime"
- :title="deployment.deployed_at_formatted"
- class="js-deploy-time"
- >
- {{ deployTimeago }}
- </span>
- <loading-button
- v-if="deployment.stop_url"
- :loading="isStopping"
- container-class="btn btn-default btn-sm prepend-left-default"
- label="Stop environment"
- @click="stopEnvironment"
- />
+ <loading-button
+ v-if="deployment.stop_url"
+ :loading="isStopping"
+ container-class="btn btn-default btn-sm inline prepend-left-4"
+ title="Stop environment"
+ @click="stopEnvironment"
+ >
+ <icon name="stop" />
+ </loading-button>
+ </div>
</div>
- <memory-usage
- v-if="hasMetrics"
- :metrics-url="deployment.metrics_url"
- :metrics-monitoring-url="deployment.metrics_monitoring_url"
- />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 3ce9d8dc26a..c18b74743e4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -2,7 +2,7 @@
import tooltip from '~/vue_shared/directives/tooltip';
import { n__ } from '~/locale';
import { webIDEUrl } from '~/lib/utils/url_utility';
-import icon from '~/vue_shared/components/icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
@@ -11,7 +11,7 @@ export default {
tooltip,
},
components: {
- icon,
+ Icon,
clipboardButton,
},
props: {
@@ -54,104 +54,114 @@ export default {
};
</script>
<template>
- <div class="mr-source-target">
- <div class="normal">
- <strong>
- {{ s__("mrWidget|Request to merge") }}
- <span
- :class="{ 'label-truncated': isSourceBranchLong }"
- :title="isSourceBranchLong ? mr.sourceBranch : ''"
- :v-tooltip="isSourceBranchLong"
- class="label-branch js-source-branch"
- data-placement="bottom"
- v-html="mr.sourceBranchLink"
- >
- </span>
+ <div class="mr-source-target append-bottom-default">
+ <div class="git-merge-icon-container append-right-default">
+ <icon name="git-merge" />
+ </div>
+ <div class="git-merge-container d-flex">
+ <div class="normal">
+ <strong>
+ {{ s__("mrWidget|Request to merge") }}
+ <span
+ :class="{ 'label-truncated': isSourceBranchLong }"
+ :title="isSourceBranchLong ? mr.sourceBranch : ''"
+ :v-tooltip="isSourceBranchLong"
+ class="label-branch js-source-branch"
+ data-placement="bottom"
+ v-html="mr.sourceBranchLink"
+ >
+ </span>
- <clipboard-button
- :text="branchNameClipboardData"
- :title="__('Copy branch name to clipboard')"
- css-class="btn-default btn-transparent btn-clipboard"
- />
+ <clipboard-button
+ :text="branchNameClipboardData"
+ :title="__('Copy branch name to clipboard')"
+ css-class="btn-default btn-transparent btn-clipboard"
+ />
- {{ s__("mrWidget|into") }}
+ {{ s__("mrWidget|into") }}
- <span
- :v-tooltip="isTargetBranchLong"
- :class="{ 'label-truncatedtooltip': isTargetBranchLong }"
- :title="isTargetBranchLong ? mr.targetBranch : ''"
- class="label-branch"
- data-placement="bottom"
- >
- <a
- :href="mr.targetBranchTreePath"
- class="js-target-branch"
+ <span
+ :v-tooltip="isTargetBranchLong"
+ :class="{ 'label-truncatedtooltip': isTargetBranchLong }"
+ :title="isTargetBranchLong ? mr.targetBranch : ''"
+ class="label-branch"
+ data-placement="bottom"
>
- {{ mr.targetBranch }}
- </a>
- </span>
- </strong>
- <span
- v-if="shouldShowCommitsBehindText"
- class="diverged-commits-count"
- >
- (<a :href="mr.targetBranchPath">{{ commitsText }}</a>)
- </span>
- </div>
+ <a
+ :href="mr.targetBranchTreePath"
+ class="js-target-branch"
+ >
+ {{ mr.targetBranch }}
+ </a>
+ </span>
+ </strong>
+ <div
+ v-if="shouldShowCommitsBehindText"
+ class="diverged-commits-count"
+ >
+ <span class="monospace">{{ mr.sourceBranch }}</span>
+ is {{ commitsText }}
+ <span class="monospace">{{ mr.targetBranch }}</span>
+ </div>
+ </div>
- <div v-if="mr.isOpen">
- <a
- v-if="!mr.sourceBranchRemoved"
- :href="webIdePath"
- class="btn btn-sm btn-default inline js-web-ide"
- >
- {{ s__("mrWidget|Web IDE") }}
- </a>
- <button
- :disabled="mr.sourceBranchRemoved"
- data-target="#modal_merge_info"
- data-toggle="modal"
- class="btn btn-sm btn-default inline js-check-out-branch"
- type="button"
+ <div
+ v-if="mr.isOpen"
+ class="branch-actions"
>
- {{ s__("mrWidget|Check out branch") }}
- </button>
- <span class="dropdown prepend-left-10">
+ <a
+ v-if="!mr.sourceBranchRemoved"
+ :href="webIdePath"
+ class="btn btn-default inline js-web-ide d-none d-md-inline-block"
+ >
+ {{ s__("mrWidget|Open in Web IDE") }}
+ </a>
<button
+ :disabled="mr.sourceBranchRemoved"
+ data-target="#modal_merge_info"
+ data-toggle="modal"
+ class="btn btn-default inline js-check-out-branch"
type="button"
- class="btn btn-sm inline dropdown-toggle"
- data-toggle="dropdown"
- aria-label="Download as"
- aria-haspopup="true"
- aria-expanded="false"
>
- <icon name="download" />
- <i
- class="fa fa-caret-down"
- aria-hidden="true">
- </i>
+ {{ s__("mrWidget|Check out branch") }}
</button>
- <ul class="dropdown-menu dropdown-menu-right">
- <li>
- <a
- :href="mr.emailPatchesPath"
- class="js-download-email-patches"
- download
- >
- {{ s__("mrWidget|Email patches") }}
- </a>
- </li>
- <li>
- <a
- :href="mr.plainDiffPath"
- class="js-download-plain-diff"
- download
- >
- {{ s__("mrWidget|Plain diff") }}
- </a>
- </li>
- </ul>
- </span>
+ <span class="dropdown prepend-left-10">
+ <button
+ type="button"
+ class="btn inline dropdown-toggle"
+ data-toggle="dropdown"
+ aria-label="Download as"
+ aria-haspopup="true"
+ aria-expanded="false"
+ >
+ <icon name="download" />
+ <i
+ class="fa fa-caret-down"
+ aria-hidden="true">
+ </i>
+ </button>
+ <ul class="dropdown-menu dropdown-menu-right">
+ <li>
+ <a
+ :href="mr.emailPatchesPath"
+ class="js-download-email-patches"
+ download
+ >
+ {{ s__("mrWidget|Email patches") }}
+ </a>
+ </li>
+ <li>
+ <a
+ :href="mr.plainDiffPath"
+ class="js-download-plain-diff"
+ download
+ >
+ {{ s__("mrWidget|Plain diff") }}
+ </a>
+ </li>
+ </ul>
+ </span>
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 2f0b5e12c12..4a3fd01fa39 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -26,6 +26,10 @@ export default {
type: String,
required: false,
},
+ sourceBranchLink: {
+ type: String,
+ required: false,
+ },
},
computed: {
hasPipeline() {
@@ -54,12 +58,18 @@ export default {
<template>
<div
v-if="hasPipeline || hasCIError"
- class="mr-widget-heading"
+ class="mr-widget-heading append-bottom-default"
>
<div class="ci-widget media">
<template v-if="hasCIError">
- <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
- <icon name="status_failed" />
+ <div
+ class="add-border ci-status-icon ci-status-icon-failed ci-error
+ js-ci-error append-right-default"
+ >
+ <icon
+ :size="32"
+ name="status_failed_borderless"
+ />
</div>
<div class="media-body">
Could not connect to the CI server. Please check your settings and try again
@@ -68,50 +78,66 @@ export default {
<template v-else-if="hasPipeline">
<a
:href="status.details_path"
- class="append-right-10"
+ class="align-self-start append-right-default"
>
- <ci-icon :status="status" />
+ <ci-icon
+ :status="status"
+ :size="32"
+ :borderless="true"
+ class="add-border"
+ />
</a>
+ <div class="ci-widget-container d-flex">
+ <div class="ci-widget-content">
+ <div class="media-body">
+ <div class="font-weight-bold">
+ Pipeline
+ <a
+ :href="pipeline.path"
+ class="pipeline-id font-weight-normal pipeline-number"
+ >#{{ pipeline.id }}</a>
- <div class="media-body">
- Pipeline
- <a
- :href="pipeline.path"
- class="pipeline-id"
- >
- #{{ pipeline.id }}
- </a>
-
- {{ pipeline.details.status.label }}
+ {{ pipeline.details.status.label }}
- <template v-if="hasCommitInfo">
- for
-
- <a
- :href="pipeline.commit.commit_path"
- class="commit-sha js-commit-link"
- >
- {{ pipeline.commit.short_id }}</a>.
- </template>
-
- <span class="mr-widget-pipeline-graph">
- <span
- v-if="hasStages"
- class="stage-cell"
- >
+ <template v-if="hasCommitInfo">
+ for
+ <a
+ :href="pipeline.commit.commit_path"
+ class="commit-sha js-commit-link font-weight-normal"
+ >
+ {{ pipeline.commit.short_id }}</a>
+ on
+ <span
+ class="label-branch"
+ v-html="sourceBranchLink"
+ >
+ </span>
+ </template>
+ </div>
<div
- v-for="(stage, i) in pipeline.details.stages"
- :key="i"
- class="stage-container dropdown js-mini-pipeline-graph"
+ v-if="pipeline.coverage"
+ class="coverage"
>
- <pipeline-stage :stage="stage" />
+ Coverage {{ pipeline.coverage }}%
</div>
+ </div>
+ </div>
+ <div>
+ <span class="mr-widget-pipeline-graph">
+ <span
+ v-if="hasStages"
+ class="stage-cell"
+ >
+ <div
+ v-for="(stage, i) in pipeline.details.stages"
+ :key="i"
+ class="stage-container dropdown js-mini-pipeline-graph mr-widget-pipeline-stages"
+ >
+ <pipeline-stage :stage="stage" />
+ </div>
+ </span>
</span>
- </span>
-
- <template v-if="pipeline.coverage">
- Coverage {{ pipeline.coverage }}%
- </template>
+ </div>
</div>
</template>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
index 53c4dc8c8f4..55b87f3a8ec 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
@@ -43,6 +43,7 @@
<ci-icon
v-else
:status="statusObj"
+ :size="24"
/>
<button
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 09477da40b5..b5de3dd6d73 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -252,41 +252,44 @@ export default {
:pipeline="mr.pipeline"
:ci-status="mr.ciStatus"
:has-ci="mr.hasCI"
+ :source-branch-link="mr.sourceBranchLink"
/>
<deployment
v-for="deployment in mr.deployments"
:key="deployment.id"
:deployment="deployment"
/>
- <div class="mr-widget-section">
- <component
- :is="componentName"
- :mr="mr"
- :service="service"
- />
+ <div class="mr-section-container">
+ <div class="mr-widget-section">
+ <component
+ :is="componentName"
+ :mr="mr"
+ :service="service"
+ />
- <section
- v-if="mr.allowCollaboration"
- class="mr-info-list mr-links"
- >
- {{ s__("mrWidget|Allows commits from members who can merge to the target branch") }}
- </section>
+ <section
+ v-if="mr.allowCollaboration"
+ class="mr-info-list mr-links"
+ >
+ {{ s__("mrWidget|Allows commits from members who can merge to the target branch") }}
+ </section>
- <mr-widget-related-links
- v-if="shouldRenderRelatedLinks"
- :state="mr.state"
- :related-links="mr.relatedLinks"
- />
+ <mr-widget-related-links
+ v-if="shouldRenderRelatedLinks"
+ :state="mr.state"
+ :related-links="mr.relatedLinks"
+ />
- <source-branch-removal-status
- v-if="shouldRenderSourceBranchRemovalStatus"
- />
- </div>
- <div
- v-if="shouldRenderMergeHelp"
- class="mr-widget-footer"
- >
- <mr-widget-merge-help />
+ <source-branch-removal-status
+ v-if="shouldRenderSourceBranchRemovalStatus"
+ />
+ </div>
+ <div
+ v-if="shouldRenderMergeHelp"
+ class="mr-widget-footer"
+ >
+ <mr-widget-merge-help />
+ </div>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 298971a36b2..d62537021ca 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -1,5 +1,6 @@
<script>
import $ from 'jquery';
+ import { s__ } from '~/locale';
import Flash from '../../../flash';
import GLForm from '../../../gl_form';
import markdownHeader from './header.vue';
@@ -22,6 +23,11 @@
type: String,
required: true,
},
+ markdownVersion: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
addSpacingClasses: {
type: Boolean,
required: false,
@@ -92,10 +98,11 @@
if (text) {
this.markdownPreviewLoading = true;
- this.$http.post(this.markdownPreviewPath, { text })
- .then(resp => resp.json())
- .then(data => this.renderMarkdown(data))
- .catch(() => new Flash('Error loading markdown preview'));
+ this.$http
+ .post(this.versionedPreviewPath(), { text })
+ .then(resp => resp.json())
+ .then(data => this.renderMarkdown(data))
+ .catch(() => new Flash(s__('Error loading markdown preview')));
} else {
this.renderMarkdown();
}
@@ -119,6 +126,13 @@
$(this.$refs['markdown-preview']).renderGFM();
});
},
+
+ versionedPreviewPath() {
+ const { markdownPreviewPath, markdownVersion } = this;
+ return `${markdownPreviewPath}${
+ markdownPreviewPath.indexOf('?') === -1 ? '?' : '&'
+ }markdown_version=${markdownVersion}`;
+ },
},
};
</script>
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 74475daae14..c7b5e22c33d 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -36,7 +36,7 @@
width: 100%;
}
- &.projects-dropdown-menu {
+ &.frequent-items-dropdown-menu {
padding: 0;
overflow-y: initial;
max-height: initial;
@@ -790,6 +790,7 @@
@include media-breakpoint-down(xs) {
.navbar-gitlab {
li.header-projects,
+ li.header-groups,
li.header-more,
li.header-new,
li.header-user {
@@ -813,18 +814,18 @@
}
}
-header.header-content .dropdown-menu.projects-dropdown-menu {
+header.header-content .dropdown-menu.frequent-items-dropdown-menu {
padding: 0;
}
-.projects-dropdown-container {
+.frequent-items-dropdown-container {
display: flex;
flex-direction: row;
width: 500px;
height: 334px;
- .project-dropdown-sidebar,
- .project-dropdown-content {
+ .frequent-items-dropdown-sidebar,
+ .frequent-items-dropdown-content {
padding: 8px 0;
}
@@ -832,12 +833,12 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
color: $almost-black;
}
- .project-dropdown-sidebar {
+ .frequent-items-dropdown-sidebar {
width: 30%;
border-right: 1px solid $border-color;
}
- .project-dropdown-content {
+ .frequent-items-dropdown-content {
position: relative;
width: 70%;
}
@@ -848,33 +849,35 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
height: auto;
flex: 1;
- .project-dropdown-sidebar,
- .project-dropdown-content {
+ .frequent-items-dropdown-sidebar,
+ .frequent-items-dropdown-content {
width: 100%;
}
- .project-dropdown-sidebar {
+ .frequent-items-dropdown-sidebar {
border-bottom: 1px solid $border-color;
border-right: 0;
}
}
- .projects-list-frequent-container,
- .projects-list-search-container {
+ .section-header,
+ .frequent-items-list-container li.section-empty {
+ padding: 0 $gl-padding;
+ color: $gl-text-color-secondary;
+ font-size: $gl-font-size;
+ }
+
+ .frequent-items-list-container {
padding: 8px 0;
overflow-y: auto;
li.section-empty.section-failure {
color: $callout-danger-color;
}
- }
- .section-header,
- .projects-list-frequent-container li.section-empty,
- .projects-list-search-container li.section-empty {
- padding: 0 15px;
- color: $gl-text-color-secondary;
- font-size: $gl-font-size;
+ .frequent-items-list-item-container a {
+ display: flex;
+ }
}
.search-input-container {
@@ -894,12 +897,12 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
margin-top: 8px;
}
- .projects-list-search-container {
+ .frequent-items-search-container {
height: 284px;
}
@include media-breakpoint-down(xs) {
- .projects-list-frequent-container {
+ .frequent-items-list-container {
width: auto;
height: auto;
padding-bottom: 0;
@@ -907,32 +910,38 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
}
}
-.projects-list-item-container {
- .project-item-avatar-container .project-item-metadata-container {
+.frequent-items-list-item-container {
+ .frequent-items-item-avatar-container,
+ .frequent-items-item-metadata-container {
float: left;
}
- .project-title,
- .project-namespace {
+ .frequent-items-item-metadata-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
+
+ .frequent-items-item-title,
+ .frequent-items-item-namespace {
max-width: 250px;
- overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&:hover {
- .project-item-avatar-container .avatar {
+ .frequent-items-item-avatar-container .avatar {
border-color: $md-area-border;
}
}
- .project-title {
+ .frequent-items-item-title {
font-size: $gl-font-size;
font-weight: 400;
line-height: 16px;
}
- .project-namespace {
+ .frequent-items-item-namespace {
margin-top: 4px;
font-size: 12px;
line-height: 12px;
@@ -940,7 +949,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
}
@include media-breakpoint-down(xs) {
- .project-item-metadata-container {
+ .frequent-items-item-metadata-container {
float: none;
}
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 551a7e852ae..5d79610b21e 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -224,7 +224,10 @@
.form-control {
position: relative;
min-width: 200px;
- padding: 5px 25px 6px 0;
+ padding-right: 25px;
+ padding-left: 0;
+ height: $input-height;
+ line-height: inherit;
border-color: transparent;
&:focus,
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 282e424fc38..a22454c24e2 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -255,3 +255,8 @@ label {
color: $theme-gray-600;
}
}
+
+.input-lg {
+ max-width: 320px;
+ width: 100%;
+}
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index aaa8bed3df0..dff6bce370f 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -29,15 +29,21 @@
.navbar-sub-nav,
.navbar-nav {
> li {
- > a:hover,
- > a:focus {
- background-color: rgba($search-and-nav-links, 0.2);
+ > a,
+ > button {
+ &:hover,
+ &:focus {
+ background-color: rgba($search-and-nav-links, 0.2);
+ }
}
- &.active > a,
- &.dropdown.show > a {
- color: $nav-svg-color;
- background-color: $color-alternate;
+ &.active,
+ &.dropdown.show {
+ > a,
+ > button {
+ color: $nav-svg-color;
+ background-color: $color-alternate;
+ }
}
&.line-separator {
@@ -147,7 +153,6 @@
}
}
-
// Sidebar
.nav-sidebar li.active {
box-shadow: inset 4px 0 0 $border-and-box-shadow;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 8bcaf5eb6ac..2097bcebf69 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -269,14 +269,8 @@
.navbar-sub-nav,
.navbar-nav {
> li {
- > a:hover,
- > a:focus {
- text-decoration: none;
- outline: 0;
- color: $white-light;
- }
-
- > a {
+ > a,
+ > button {
display: -webkit-flex;
display: flex;
align-items: center;
@@ -288,6 +282,18 @@
border-radius: $border-radius-default;
height: 32px;
font-weight: $gl-font-weight-bold;
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ outline: 0;
+ color: $white-light;
+ }
+ }
+
+ > button {
+ background: transparent;
+ border: 0;
}
&.line-separator {
@@ -311,7 +317,7 @@
font-size: 10px;
}
- .project-item-select-holder {
+ .frequent-items-item-select-holder {
display: inline;
}
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index 30314f3d6cb..d1f7ff4438b 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -3,12 +3,20 @@
svg {
fill: $green-500;
}
+
+ &.add-border {
+ @include borderless-status-icon($green-500);
+ }
}
.ci-status-icon-failed {
svg {
fill: $gl-danger;
}
+
+ &.add-border {
+ @include borderless-status-icon($red-500);
+ }
}
.ci-status-icon-pending,
@@ -17,12 +25,20 @@
svg {
fill: $orange-500;
}
+
+ &.add-border {
+ @include borderless-status-icon($orange-500);
+ }
}
.ci-status-icon-running {
svg {
fill: $blue-400;
}
+
+ &.add-border {
+ @include borderless-status-icon($blue-400);
+ }
}
.ci-status-icon-canceled,
@@ -30,6 +46,10 @@
svg {
fill: $gl-text-color;
}
+
+ &.add-border {
+ @include borderless-status-icon($gl-text-color);
+ }
}
.ci-status-icon-created,
@@ -38,6 +58,10 @@
svg {
fill: $gray-darkest;
}
+
+ &.add-border {
+ @include borderless-status-icon($gray-darkest);
+ }
}
.ci-status-icon-manual {
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 0b645eb811b..76ebfc22ef7 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -232,3 +232,10 @@
word-break: break-word;
max-width: 100%;
}
+
+@mixin borderless-status-icon($color) {
+ svg {
+ border: 1px solid $color;
+ border-radius: 50%;
+ }
+}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 54799593727..c1836228a49 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -350,7 +350,8 @@ code {
}
.commit-sha,
-.ref-name {
+.ref-name,
+.pipeline-number {
@extend .monospace;
font-size: 95%;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 7808f6d3a25..6cfa09b56a7 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -743,6 +743,7 @@ Pipeline Graph
*/
$stage-hover-bg: $gray-darker;
$ci-action-icon-size: 22px;
+$ci-action-icon-size-lg: 24px;
$pipeline-dropdown-line-height: 20px;
$pipeline-dropdown-status-icon-size: 18px;
$ci-action-dropdown-button-size: 24px;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index ca63a8ae916..202377d6d9e 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -80,7 +80,6 @@
overflow-x: scroll;
white-space: nowrap;
min-height: 200px;
- display: flex;
@include media-breakpoint-only(sm) {
height: calc(100vh - #{$issue-board-list-difference-sm});
@@ -111,15 +110,17 @@
.board {
display: inline-block;
- flex: 1;
- min-width: 300px;
- max-width: 400px;
+ width: calc(85vw - 15px);
height: 100%;
padding-right: ($gl-padding / 2);
padding-left: ($gl-padding / 2);
white-space: normal;
vertical-align: top;
+ @include media-breakpoint-up(sm) {
+ width: 400px;
+ }
+
&.is-expandable {
.board-header {
cursor: pointer;
@@ -127,8 +128,6 @@
}
&.is-collapsed {
- flex: none;
- min-width: 0;
width: 50px;
.board-header {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index efd730af558..c32049e1b33 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -15,16 +15,38 @@
}
}
+.mr-widget-heading {
+ position: relative;
+ border: 1px solid $border-color;
+ border-radius: 4px;
+
+ &:not(.deploy-heading)::before {
+ content: '';
+ border-left: 1px solid $theme-gray-200;
+ position: absolute;
+ left: 32px;
+ top: -17px;
+ height: 16px;
+ }
+}
+
+.mr-section-container {
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+ border-top: 0;
+}
+
+.mr-widget-heading,
+.mr-widget-section,
+.mr-widget-footer {
+ padding: $gl-padding;
+}
+
.mr-state-widget {
color: $gl-text-color;
- border: 1px solid $border-color;
- border-radius: 2px;
- line-height: 28px;
- .mr-widget-heading,
.mr-widget-section,
.mr-widget-footer {
- padding: $gl-padding;
border-top: solid 1px $border-color;
}
@@ -124,10 +146,17 @@
.ci-widget {
color: $gl-text-color;
display: flex;
+ align-items: center;
+ justify-content: space-between;
@include media-breakpoint-down(xs) {
flex-wrap: wrap;
}
+
+ .ci-widget-content {
+ display: flex;
+ align-items: center;
+ }
}
.mr-widget-icon {
@@ -136,8 +165,6 @@
}
.ci-status-icon svg {
- width: $status-icon-size;
- height: $status-icon-size;
margin: 3px 0;
position: relative;
overflow: visible;
@@ -145,8 +172,6 @@
}
.mr-widget-pipeline-graph {
- padding: 0 4px;
-
.dropdown-menu {
z-index: 300;
}
@@ -157,7 +182,7 @@
}
.normal {
- line-height: 28px;
+ flex: 1;
}
.capitalize {
@@ -168,7 +193,7 @@
@extend .ref-name;
color: $gl-text-color;
- font-weight: $gl-font-weight-bold;
+ font-weight: normal;
overflow: hidden;
word-break: break-all;
@@ -192,6 +217,8 @@
}
.mr-widget-body {
+ line-height: 28px;
+
@include clearfix;
&.media > *:first-child {
@@ -474,18 +501,66 @@
}
}
+.merge-request-details .content-block {
+ border-bottom: 0;
+}
+
.mr-source-target {
display: flex;
flex-wrap: wrap;
- justify-content: space-between;
- align-items: center;
- background-color: $gray-light;
- border-radius: $border-radius-default $border-radius-default 0 0;
- padding: $gl-padding / 2 $gl-padding;
+ border-radius: $border-radius-default;
+ padding: $gl-padding;
+ border: 1px solid $border-color;
+ min-height: 69px;
+
+ @include media-breakpoint-up(md) {
+ align-items: center;
+ }
.dropdown-toggle .fa {
color: $gl-text-color;
}
+
+ .git-merge-icon-container {
+ border: 1px solid $theme-gray-400;
+ border-radius: 50%;
+ height: 32px;
+ width: 32px;
+ color: $theme-gray-700;
+ line-height: 28px;
+
+ .ic-git-merge {
+ vertical-align: middle;
+ width: 31px;
+ }
+ }
+
+ .git-merge-container {
+ justify-content: space-between;
+ flex: 1;
+ flex-direction: row;
+ align-items: center;
+
+ @include media-breakpoint-down(md) {
+ flex-direction: column;
+ align-items: flex-start;
+
+ .branch-actions {
+ margin-top: 16px;
+ }
+ }
+
+ @include media-breakpoint-up(lg) {
+ .branch-actions {
+ align-self: center;
+ }
+ }
+ }
+
+ .diverged-commits-count {
+ color: $gl-text-color-secondary;
+ font-size: 12px;
+ }
}
.card-new-merge-request {
@@ -720,13 +795,25 @@
}
.deploy-heading {
+ margin-top: -19px;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ background-color: $gray-light;
+
+ @include media-breakpoint-up(md) {
+ padding: $gl-padding-8 $gl-padding;
+ }
+
.media-body {
min-width: 0;
+ font-size: 12px;
+ margin-left: 48px;
}
}
.deploy-body {
display: flex;
+ align-items: center;
flex-wrap: wrap;
@include media-breakpoint-up(xs) {
@@ -734,6 +821,15 @@
white-space: nowrap;
}
+ @include media-breakpoint-down(md) {
+ flex-direction: column;
+ align-items: flex-start;
+
+ .deployment-info {
+ margin-bottom: $gl-padding;
+ }
+ }
+
> *:not(:last-child) {
margin-right: .3em;
}
@@ -741,18 +837,22 @@
svg {
vertical-align: text-top;
}
-}
-.deploy-link {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- min-width: 100px;
- max-width: 150px;
+ .deployment-info {
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ min-width: 100px;
- @include media-breakpoint-up(xs) {
- min-width: 0;
- max-width: 100%;
+ @include media-breakpoint-up(xs) {
+ min-width: 0;
+ max-width: 100%;
+ }
+ }
+
+ .btn svg {
+ fill: $theme-gray-700;
}
}
@@ -772,3 +872,33 @@
}
}
}
+
+.ci-widget-container {
+ justify-content: space-between;
+ flex: 1;
+ flex-direction: row;
+
+ @include media-breakpoint-down(md) {
+ flex-direction: column;
+
+ .stage-cell .stage-container {
+ margin-top: 16px;
+ }
+
+ .dropdown .mini-pipeline-graph-dropdown-menu.dropdown-menu {
+ transform: initial;
+ }
+ }
+
+ .coverage {
+ font-size: 12px;
+ color: $theme-gray-700;
+ line-height: initial;
+ }
+
+ .mini-pipeline-graph-dropdown-toggle,
+ .stage-cell .mini-pipeline-graph-dropdown-toggle svg {
+ height: $ci-action-icon-size-lg;
+ width: $ci-action-icon-size-lg;
+ }
+}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 52332ac97dd..b68c89c25d8 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -301,6 +301,21 @@
border-bottom: 2px solid $border-color;
}
}
+
+ //delete when all pipelines are updated to new size
+ &.mr-widget-pipeline-stages {
+ + .stage-container {
+ margin-left: 4px;
+ }
+
+ &:not(:last-child) {
+ &::after {
+ width: 4px;
+ right: -4px;
+ top: 11px;
+ }
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 3c24aaa65e8..6e2b285285a 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -1329,3 +1329,14 @@
line-height: 16px;
color: $gl-text-color-secondary;
}
+
+.ide-merge-request-info {
+ .detail-page-header {
+ line-height: initial;
+ min-height: 38px;
+ }
+
+ .issuable-details {
+ overflow: auto;
+ }
+}
diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb
index b0c4c31cffc..5c2025c1988 100644
--- a/app/controllers/admin/deploy_keys_controller.rb
+++ b/app/controllers/admin/deploy_keys_controller.rb
@@ -22,7 +22,7 @@ class Admin::DeployKeysController < Admin::ApplicationController
end
def update
- if deploy_key.update_attributes(update_params)
+ if deploy_key.update(update_params)
flash[:notice] = 'Deploy key was successfully updated.'
redirect_to admin_deploy_keys_path
else
@@ -34,7 +34,7 @@ class Admin::DeployKeysController < Admin::ApplicationController
deploy_key.destroy
respond_to do |format|
- format.html { redirect_to admin_deploy_keys_path, status: 302 }
+ format.html { redirect_to admin_deploy_keys_path, status: :found }
format.json { head :ok }
end
end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 96b7bc65ac9..d7a5b745d3f 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -39,7 +39,7 @@ class Admin::GroupsController < Admin::ApplicationController
end
def update
- if @group.update_attributes(group_params)
+ if @group.update(group_params)
redirect_to [:admin, @group], notice: 'Group was successfully updated.'
else
render "edit"
diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb
index 6944857bd33..a98c355c7ba 100644
--- a/app/controllers/admin/hooks_controller.rb
+++ b/app/controllers/admin/hooks_controller.rb
@@ -23,7 +23,7 @@ class Admin::HooksController < Admin::ApplicationController
end
def update
- if hook.update_attributes(hook_params)
+ if hook.update(hook_params)
flash[:notice] = 'System hook was successfully updated.'
redirect_to admin_hooks_path
else
@@ -34,7 +34,7 @@ class Admin::HooksController < Admin::ApplicationController
def destroy
hook.destroy
- redirect_to admin_hooks_path, status: 302
+ redirect_to admin_hooks_path, status: :found
end
def test
diff --git a/app/controllers/admin/identities_controller.rb b/app/controllers/admin/identities_controller.rb
index 43b4e3a2cc3..ceb45865804 100644
--- a/app/controllers/admin/identities_controller.rb
+++ b/app/controllers/admin/identities_controller.rb
@@ -25,7 +25,7 @@ class Admin::IdentitiesController < Admin::ApplicationController
end
def update
- if @identity.update_attributes(identity_params)
+ if @identity.update(identity_params)
RepairLdapBlockedUserService.new(@user).execute
redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully updated.'
else
diff --git a/app/controllers/admin/impersonations_controller.rb b/app/controllers/admin/impersonations_controller.rb
index 39dbf85f6c0..d2f947d2c66 100644
--- a/app/controllers/admin/impersonations_controller.rb
+++ b/app/controllers/admin/impersonations_controller.rb
@@ -11,7 +11,7 @@ class Admin::ImpersonationsController < Admin::ApplicationController
session[:impersonator_id] = nil
- redirect_to admin_user_path(original_user), status: 302
+ redirect_to admin_user_path(original_user), status: :found
end
private
diff --git a/app/controllers/admin/jobs_controller.rb b/app/controllers/admin/jobs_controller.rb
index ae7a7f6279c..ac1ae0f16b3 100644
--- a/app/controllers/admin/jobs_controller.rb
+++ b/app/controllers/admin/jobs_controller.rb
@@ -20,6 +20,6 @@ class Admin::JobsController < Admin::ApplicationController
def cancel_all
Ci::Build.running_or_pending.each(&:cancel)
- redirect_to admin_jobs_path, status: 303
+ redirect_to admin_jobs_path, status: :see_other
end
end
diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb
index 7aba77d8129..51d5799cd89 100644
--- a/app/controllers/admin/runner_projects_controller.rb
+++ b/app/controllers/admin/runner_projects_controller.rb
@@ -16,7 +16,7 @@ class Admin::RunnerProjectsController < Admin::ApplicationController
runner = rp.runner
rp.destroy
- redirect_to admin_runner_path(runner), status: 302
+ redirect_to admin_runner_path(runner), status: :found
end
private
diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb
index 4b01904f2a1..6c76c55a9d4 100644
--- a/app/controllers/admin/runners_controller.rb
+++ b/app/controllers/admin/runners_controller.rb
@@ -28,7 +28,7 @@ class Admin::RunnersController < Admin::ApplicationController
def destroy
@runner.destroy
- redirect_to admin_runners_path, status: 302
+ redirect_to admin_runners_path, status: :found
end
def resume
diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb
index a7025b62ad7..e70aa549140 100644
--- a/app/controllers/admin/services_controller.rb
+++ b/app/controllers/admin/services_controller.rb
@@ -16,7 +16,7 @@ class Admin::ServicesController < Admin::ApplicationController
end
def update
- if service.update_attributes(service_params[:service])
+ if service.update(service_params[:service])
PropagateServiceTemplateWorker.perform_async(service.id) if service.active?
redirect_to admin_application_settings_services_path,
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 653f3dfffc4..a51a8c3ed4a 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -163,7 +163,7 @@ class Admin::UsersController < Admin::ApplicationController
format.json { head :ok }
else
format.html { redirect_back_or_admin_user(alert: 'There was an error removing the e-mail.') }
- format.json { render json: 'There was an error removing the e-mail.', status: 400 }
+ format.json { render json: 'There was an error removing the e-mail.', status: :bad_request }
end
end
end
diff --git a/app/controllers/concerns/group_tree.rb b/app/controllers/concerns/group_tree.rb
index 56770a17406..6ec6897e707 100644
--- a/app/controllers/concerns/group_tree.rb
+++ b/app/controllers/concerns/group_tree.rb
@@ -1,21 +1,16 @@
module GroupTree
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def render_group_tree(groups)
- @groups = if params[:filter].present?
- # We find the ancestors by ID of the search results here.
- # Otherwise the ancestors would also have filters applied,
- # which would cause them not to be preloaded.
- group_ids = groups.search(params[:filter]).select(:id)
- Gitlab::GroupHierarchy.new(Group.where(id: group_ids))
- .base_and_ancestors
- else
- # Only show root groups if no parent-id is given
- groups.where(parent_id: params[:parent_id])
- end
+ groups = groups.sort_by_attribute(@sort = params[:sort])
- @groups = @groups.with_selects_for_list(archived: params[:archived])
- .sort_by_attribute(@sort = params[:sort])
- .page(params[:page])
+ groups = if params[:filter].present?
+ filtered_groups_with_ancestors(groups)
+ else
+ # If `params[:parent_id]` is `nil`, we will only show root-groups
+ groups.where(parent_id: params[:parent_id]).page(params[:page])
+ end
+
+ @groups = groups.with_selects_for_list(archived: params[:archived])
respond_to do |format|
format.html
@@ -28,4 +23,21 @@ module GroupTree
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
+
+ def filtered_groups_with_ancestors(groups)
+ filtered_groups = groups.search(params[:filter]).page(params[:page])
+
+ if Group.supports_nested_groups?
+ # We find the ancestors by ID of the search results here.
+ # Otherwise the ancestors would also have filters applied,
+ # which would cause them not to be preloaded.
+ #
+ # Pagination needs to be applied before loading the ancestors to
+ # make sure ancestors are not cut off by pagination.
+ Gitlab::GroupHierarchy.new(Group.where(id: filtered_groups.select(:id)))
+ .base_and_ancestors
+ else
+ filtered_groups
+ end
+ end
end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index ba510968684..37e03d70b6f 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -127,7 +127,7 @@ module IssuableActions
errors: [
"Someone edited this #{issuable.human_class_name} at the same time you did. Please refresh your browser and make sure your changes will not unintentionally remove theirs."
]
- }, status: 409
+ }, status: :conflict
end
end
end
diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb
index 5e4e8a87153..79ee5b2f91e 100644
--- a/app/controllers/concerns/lfs_request.rb
+++ b/app/controllers/concerns/lfs_request.rb
@@ -27,7 +27,7 @@ module LfsRequest
message: 'Git LFS is not enabled on this GitLab server, contact your admin.',
documentation_url: help_url
},
- status: 501
+ status: :not_implemented
)
end
diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb
index 90bb7a87b45..99123fcb3b0 100644
--- a/app/controllers/concerns/preview_markdown.rb
+++ b/app/controllers/concerns/preview_markdown.rb
@@ -10,9 +10,12 @@ module PreviewMarkdown
when 'wikis' then { pipeline: :wiki, project_wiki: @project_wiki, page_slug: params[:id] }
when 'snippets' then { skip_project_check: true }
when 'groups' then { group: group }
+ when 'projects' then { issuable_state_filter_enabled: true }
else {}
end
+ markdown_params[:markdown_engine] = result[:markdown_engine]
+
render json: {
body: view_context.markdown(result[:text], markdown_params),
references: {
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 4d4ac025f8c..ccfcbbdc776 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -7,7 +7,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
skip_cross_project_access_check :index, :starred
def index
- @projects = load_projects(params.merge(non_public: true)).page(params[:page])
+ @projects = load_projects(params.merge(non_public: true))
respond_to do |format|
format.html
@@ -25,7 +25,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
def starred
@projects = load_projects(params.merge(starred: true))
- .includes(:forked_from_project, :tags).page(params[:page])
+ .includes(:forked_from_project, :tags)
@groups = []
@@ -51,6 +51,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
.new(params: finder_params, current_user: current_user)
.execute
.includes(:route, :creator, namespace: [:route, :owner])
+ .page(finder_params[:page])
prepare_projects_for_rendering(projects)
end
diff --git a/app/controllers/groups/avatars_controller.rb b/app/controllers/groups/avatars_controller.rb
index cc5ba5878f8..35a61b359c8 100644
--- a/app/controllers/groups/avatars_controller.rb
+++ b/app/controllers/groups/avatars_controller.rb
@@ -7,6 +7,6 @@ class Groups::AvatarsController < Groups::ApplicationController
@group.remove_avatar!
@group.save
- redirect_to edit_group_path(@group), status: 302
+ redirect_to edit_group_path(@group), status: :found
end
end
diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb
index 78992ec7f46..1036b4e6ed3 100644
--- a/app/controllers/groups/runners_controller.rb
+++ b/app/controllers/groups/runners_controller.rb
@@ -23,7 +23,7 @@ class Groups::RunnersController < Groups::ApplicationController
def destroy
@runner.destroy
- redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), status: 302
+ redirect_to group_settings_ci_cd_path(@group, anchor: 'runners-settings'), status: :found
end
def resume
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 67057b5b126..3cb9e46b548 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -41,7 +41,7 @@ class JwtController < ApplicationController
"You must use a personal access token with 'api' scope for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}" }
]
- }, status: 401
+ }, status: :unauthorized
end
def render_unauthorized
@@ -50,7 +50,7 @@ class JwtController < ApplicationController
{ code: 'UNAUTHORIZED',
message: 'HTTP Basic: Access denied' }
]
- }, status: 401
+ }, status: :unauthorized
end
def auth_params
diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb
index 8ec4bb1233f..ed20302487c 100644
--- a/app/controllers/notification_settings_controller.rb
+++ b/app/controllers/notification_settings_controller.rb
@@ -5,14 +5,14 @@ class NotificationSettingsController < ApplicationController
return render_404 unless can_read?(resource)
@notification_setting = current_user.notification_settings_for(resource)
- @saved = @notification_setting.update_attributes(notification_setting_params)
+ @saved = @notification_setting.update(notification_setting_params)
render_response
end
def update
@notification_setting = current_user.notification_settings.find(params[:id])
- @saved = @notification_setting.update_attributes(notification_setting_params)
+ @saved = @notification_setting.update(notification_setting_params)
render_response
end
diff --git a/app/controllers/profiles/active_sessions_controller.rb b/app/controllers/profiles/active_sessions_controller.rb
index f0cdc228366..f1e77d68acd 100644
--- a/app/controllers/profiles/active_sessions_controller.rb
+++ b/app/controllers/profiles/active_sessions_controller.rb
@@ -7,7 +7,7 @@ class Profiles::ActiveSessionsController < Profiles::ApplicationController
ActiveSession.destroy(current_user, params[:id])
respond_to do |format|
- format.html { redirect_to profile_active_sessions_url, status: 302 }
+ format.html { redirect_to profile_active_sessions_url, status: :found }
format.js { head :ok }
end
end
diff --git a/app/controllers/profiles/avatars_controller.rb b/app/controllers/profiles/avatars_controller.rb
index 39b9f8a84d1..4f030ded80f 100644
--- a/app/controllers/profiles/avatars_controller.rb
+++ b/app/controllers/profiles/avatars_controller.rb
@@ -4,6 +4,6 @@ class Profiles::AvatarsController < Profiles::ApplicationController
Users::UpdateService.new(current_user, user: @user).execute { |user| user.remove_avatar! }
- redirect_to profile_path, status: 302
+ redirect_to profile_path, status: :found
end
end
diff --git a/app/controllers/profiles/chat_names_controller.rb b/app/controllers/profiles/chat_names_controller.rb
index 2353f0840d6..a186c5f36a8 100644
--- a/app/controllers/profiles/chat_names_controller.rb
+++ b/app/controllers/profiles/chat_names_controller.rb
@@ -39,7 +39,7 @@ class Profiles::ChatNamesController < Profiles::ApplicationController
flash[:alert] = "Could not delete chat nickname #{@chat_name.chat_name}."
end
- redirect_to profile_chat_names_path, status: 302
+ redirect_to profile_chat_names_path, status: :found
end
private
diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb
index bbd7ba49d77..a39824ec9c8 100644
--- a/app/controllers/profiles/emails_controller.rb
+++ b/app/controllers/profiles/emails_controller.rb
@@ -19,7 +19,7 @@ class Profiles::EmailsController < Profiles::ApplicationController
Emails::DestroyService.new(current_user, user: current_user).execute(@email)
respond_to do |format|
- format.html { redirect_to profile_emails_url, status: 302 }
+ format.html { redirect_to profile_emails_url, status: :found }
format.js { head :ok }
end
end
diff --git a/app/controllers/profiles/gpg_keys_controller.rb b/app/controllers/profiles/gpg_keys_controller.rb
index 38e3eacd229..c32507756e8 100644
--- a/app/controllers/profiles/gpg_keys_controller.rb
+++ b/app/controllers/profiles/gpg_keys_controller.rb
@@ -21,7 +21,7 @@ class Profiles::GpgKeysController < Profiles::ApplicationController
@gpg_key.destroy
respond_to do |format|
- format.html { redirect_to profile_gpg_keys_url, status: 302 }
+ format.html { redirect_to profile_gpg_keys_url, status: :found }
format.js { head :ok }
end
end
@@ -30,7 +30,7 @@ class Profiles::GpgKeysController < Profiles::ApplicationController
@gpg_key.revoke
respond_to do |format|
- format.html { redirect_to profile_gpg_keys_url, status: 302 }
+ format.html { redirect_to profile_gpg_keys_url, status: :found }
format.js { head :ok }
end
end
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index 12a6cd11f80..6035258667e 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -26,7 +26,7 @@ class Profiles::KeysController < Profiles::ApplicationController
Keys::DestroyService.new(current_user).execute(@key)
respond_to do |format|
- format.html { redirect_to profile_keys_url, status: 302 }
+ format.html { redirect_to profile_keys_url, status: :found }
format.js { head :ok }
end
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index aa9789f8a0f..29ff18a1219 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -78,7 +78,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def destroy
current_user.disable_two_factor!
- redirect_to profile_account_path, status: 302
+ redirect_to profile_account_path, status: :found
end
def skip
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 5ab6d103c89..b4f814fd3a4 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -61,7 +61,7 @@ class Projects::ApplicationController < ApplicationController
def require_non_empty_project
# Be sure to return status code 303 to avoid a double DELETE:
# http://api.rubyonrails.org/classes/ActionController/Redirecting.html
- redirect_to project_path(@project), status: 303 if @project.empty_repo?
+ redirect_to project_path(@project), status: :see_other if @project.empty_repo?
end
def require_branch_head
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
index 992c8ea6992..07627ffb69f 100644
--- a/app/controllers/projects/autocomplete_sources_controller.rb
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -14,7 +14,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
end
def labels
- render json: @autocomplete_service.labels(target)
+ render json: @autocomplete_service.labels_as_hash(target)
end
def milestones
diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb
index 21a403f3765..a13d552dbd8 100644
--- a/app/controllers/projects/avatars_controller.rb
+++ b/app/controllers/projects/avatars_controller.rb
@@ -21,6 +21,6 @@ class Projects::AvatarsController < Projects::ApplicationController
@project.save
- redirect_to edit_project_path(@project), status: 302
+ redirect_to edit_project_path(@project), status: :found
end
end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index cd7250b10fc..d1dc9fe9600 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -98,7 +98,7 @@ class Projects::BranchesController < Projects::ApplicationController
flash_type = result[:status] == :error ? :alert : :notice
flash[flash_type] = result[:message]
- redirect_to project_branches_path(@project), status: 303
+ redirect_to project_branches_path(@project), status: :see_other
end
format.js { render nothing: true, status: result[:return_code] }
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 62193257940..358fe59618b 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -62,7 +62,7 @@ class Projects::ClustersController < Projects::ApplicationController
def destroy
if cluster.destroy
flash[:notice] = _('Kubernetes cluster integration was successfully removed.')
- redirect_to project_clusters_path(project), status: 302
+ redirect_to project_clusters_path(project), status: :found
else
flash[:notice] = _('Kubernetes cluster integration was not removed.')
render :show
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index f43ef2e5f2f..06739d8fd4a 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -35,7 +35,7 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
def update
- if deploy_key.update_attributes(update_params)
+ if deploy_key.update(update_params)
flash[:notice] = 'Deploy key was successfully updated.'
redirect_to_repository_settings(@project)
else
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 27b7425b965..395c5336ad5 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -116,7 +116,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.terminal_websocket(terminal)
else
- render text: 'Not found', status: 404
+ render text: 'Not found', status: :not_found
end
end
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index 07249fe3182..a52814e6e52 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -53,7 +53,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
end
send_challenges
- render plain: "HTTP Basic: Access denied\n", status: 401
+ render plain: "HTTP Basic: Access denied\n", status: :unauthorized
rescue Gitlab::Auth::MissingPersonalAccessTokenError
render_missing_personal_access_token
end
@@ -83,7 +83,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
render plain: "HTTP Basic: Access denied\n" \
"You must use a personal access token with 'api' scope for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}",
- status: 401
+ status: :unauthorized
end
def repository
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index f58ee3e9109..bc5f38f3c2b 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -24,7 +24,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
def update
@group_link = @project.project_group_links.find(params[:id])
- @group_link.update_attributes(group_link_params)
+ @group_link.update(group_link_params)
end
def destroy
@@ -34,7 +34,7 @@ class Projects::GroupLinksController < Projects::ApplicationController
respond_to do |format|
format.html do
- redirect_to project_project_members_path(project), status: 302
+ redirect_to project_project_members_path(project), status: :found
end
format.js { head :ok }
end
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index 6800d742b0a..2da2aad9b33 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -29,7 +29,7 @@ class Projects::HooksController < Projects::ApplicationController
end
def update
- if hook.update_attributes(hook_params)
+ if hook.update(hook_params)
flash[:notice] = 'Hook was successfully updated.'
redirect_to project_settings_integrations_path(@project)
else
@@ -48,7 +48,7 @@ class Projects::HooksController < Projects::ApplicationController
def destroy
hook.destroy
- redirect_to project_settings_integrations_path(@project), status: 302
+ redirect_to project_settings_integrations_path(@project), status: :found
end
private
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 91016f6494e..21d3c918581 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -39,7 +39,7 @@ class Projects::LabelsController < Projects::ApplicationController
else
respond_to do |format|
format.html { render :new }
- format.json { render json: { message: @label.errors.messages }, status: 400 }
+ format.json { render json: { message: @label.errors.messages }, status: :bad_request }
end
end
end
@@ -115,7 +115,7 @@ class Projects::LabelsController < Projects::ApplicationController
flash[:notice] = "#{@label.title} promoted to <a href=\"#{group_labels_path(@project.group)}\">group label</a>.".html_safe
respond_to do |format|
format.html do
- redirect_to(project_labels_path(@project), status: 303)
+ redirect_to(project_labels_path(@project), status: :see_other)
end
format.json do
render json: { url: project_labels_path(@project) }
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index 3f4962b543d..c64ccc3d473 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -25,7 +25,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
message: 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.',
documentation_url: "#{Gitlab.config.gitlab.url}/help"
},
- status: 501
+ status: :not_implemented
)
end
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index 45c98d60822..dd7e673ec75 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -28,7 +28,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
if store_file!(oid, size)
head 200
else
- render plain: 'Unprocessable entity', status: 422
+ render plain: 'Unprocessable entity', status: :unprocessable_entity
end
rescue ActiveRecord::RecordInvalid
render_lfs_forbidden
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index a7c5f858c42..1ad2e93c85f 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -227,7 +227,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def rebase
RebaseWorker.perform_async(@merge_request.id, current_user.id)
- render nothing: true, status: 200
+ render nothing: true, status: :ok
end
protected
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 594563d1f6f..5e86ec93f34 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -96,7 +96,7 @@ class Projects::MilestonesController < Projects::ApplicationController
Milestones::DestroyService.new(project, current_user).execute(milestone)
respond_to do |format|
- format.html { redirect_to namespace_project_milestones_path, status: 303 }
+ format.html { redirect_to namespace_project_milestones_path, status: :see_other }
format.js { head :ok }
end
end
diff --git a/app/controllers/projects/mirrors_controller.rb b/app/controllers/projects/mirrors_controller.rb
index 5698ff4e706..3b24d231f3d 100644
--- a/app/controllers/projects/mirrors_controller.rb
+++ b/app/controllers/projects/mirrors_controller.rb
@@ -13,7 +13,7 @@ class Projects::MirrorsController < Projects::ApplicationController
end
def update
- if project.update_attributes(mirror_params)
+ if project.update(mirror_params)
flash[:notice] = 'Mirroring settings were successfully updated.'
else
flash[:alert] = project.errors.full_messages.join(', ').html_safe
diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb
index fa258f3d9af..aeda7b3edf5 100644
--- a/app/controllers/projects/pipeline_schedules_controller.rb
+++ b/app/controllers/projects/pipeline_schedules_controller.rb
@@ -64,7 +64,7 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController
def destroy
if schedule.destroy
- redirect_to pipeline_schedules_path(@project), status: 302
+ redirect_to pipeline_schedules_path(@project), status: :found
else
redirect_to pipeline_schedules_path(@project),
status: :forbidden,
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
index 3e0a530fdb9..19e09b3af6f 100644
--- a/app/controllers/projects/releases_controller.rb
+++ b/app/controllers/projects/releases_controller.rb
@@ -14,7 +14,7 @@ class Projects::ReleasesController < Projects::ApplicationController
# it exists only to save a description to each Tag.
# If description is empty we should destroy the existing record.
if release_params[:description].present?
- release.update_attributes(release_params)
+ release.update(release_params)
else
release.destroy
end
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index d01f324e6fd..ecb2ece7532 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -24,7 +24,7 @@ class Projects::RepositoriesController < Projects::ApplicationController
send_git_archive @repository, ref: @ref, format: params[:format], append_sha: append_sha
rescue => ex
logger.error("#{self.class.name}: #{ex}")
- return git_not_found!
+ git_not_found!
end
def assign_archive_vars
diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb
index a080724634b..c098c82081e 100644
--- a/app/controllers/projects/runner_projects_controller.rb
+++ b/app/controllers/projects/runner_projects_controller.rb
@@ -21,6 +21,6 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
runner_project = project.runner_projects.find(params[:id])
runner_project.destroy
- redirect_to project_runners_path(project), status: 302
+ redirect_to project_runners_path(project), status: :found
end
end
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index bef94cea989..cc7cce887bf 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -24,7 +24,7 @@ class Projects::RunnersController < Projects::ApplicationController
@runner.destroy
end
- redirect_to project_runners_path(@project), status: 302
+ redirect_to project_runners_path(@project), status: :found
end
def resume
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 690596b12db..d55046047ae 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -34,7 +34,7 @@ class Projects::ServicesController < Projects::ApplicationController
private
def service_test_response
- if @service.update_attributes(service_params[:service])
+ if @service.update(service_params[:service])
data = @service.test_data(project, current_user)
outcome = @service.test(data)
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 208a1d19862..f742d7edf83 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -82,7 +82,7 @@ class Projects::SnippetsController < Projects::ApplicationController
@snippet.destroy
- redirect_to project_snippets_path(@project), status: 302
+ redirect_to project_snippets_path(@project), status: :found
end
protected
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index b62d7d9b7c5..b17753222a0 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -50,7 +50,7 @@ class Projects::TagsController < Projects::ApplicationController
respond_to do |format|
if result[:status] == :success
format.html do
- redirect_to project_tags_path(@project), status: 303
+ redirect_to project_tags_path(@project), status: :see_other
end
format.js
diff --git a/app/controllers/projects/templates_controller.rb b/app/controllers/projects/templates_controller.rb
index 694b468c8d3..52d6fb82093 100644
--- a/app/controllers/projects/templates_controller.rb
+++ b/app/controllers/projects/templates_controller.rb
@@ -14,6 +14,6 @@ class Projects::TemplatesController < Projects::ApplicationController
def get_template_class
template_types = { issue: Gitlab::Template::IssueTemplate, merge_request: Gitlab::Template::MergeRequestTemplate }.with_indifferent_access
@template_type = template_types[params[:template_type]]
- render json: [], status: 404 unless @template_type
+ render json: [], status: :not_found unless @template_type
end
end
diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb
index e04145dd0b3..6f3de43f85a 100644
--- a/app/controllers/projects/triggers_controller.rb
+++ b/app/controllers/projects/triggers_controller.rb
@@ -50,7 +50,7 @@ class Projects::TriggersController < Projects::ApplicationController
flash[:alert] = "Could not remove the trigger."
end
- redirect_to project_settings_ci_cd_path(@project), status: 302
+ redirect_to project_settings_ci_cd_path(@project), status: :found
end
private
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index aa844e94d89..c01066c688a 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -120,7 +120,7 @@ class Projects::WikisController < Projects::ApplicationController
rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
redirect_to project_path(@project)
- return false
+ false
end
def wiki_params
@@ -129,7 +129,7 @@ class Projects::WikisController < Projects::ApplicationController
def build_page(args)
WikiPage.new(@project_wiki).tap do |page|
- page.update_attributes(args)
+ page.update_attributes(args) # rubocop:disable Rails/ActiveRecordAliases
end
end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index ec3a5788ba1..9d1c44db137 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -2,6 +2,7 @@ class ProjectsController < Projects::ApplicationController
include IssuableCollections
include ExtractsPath
include PreviewMarkdown
+ include SendFileUpload
before_action :whitelist_query_limiting, only: [:create]
before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
@@ -132,7 +133,7 @@ class ProjectsController < Projects::ApplicationController
::Projects::DestroyService.new(@project, current_user, {}).async_execute
flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.full_name }
- redirect_to dashboard_projects_path, status: 302
+ redirect_to dashboard_projects_path, status: :found
rescue Projects::DestroyService::DestroyError => ex
redirect_to edit_project_path(@project), status: 302, alert: ex.message
end
@@ -188,9 +189,9 @@ class ProjectsController < Projects::ApplicationController
end
def download_export
- export_project_path = @project.export_project_path
-
- if export_project_path
+ if export_project_object_storage?
+ send_upload(@project.import_export_upload.export_file)
+ elsif export_project_path
send_file export_project_path, disposition: 'attachment'
else
redirect_to(
@@ -265,8 +266,6 @@ class ProjectsController < Projects::ApplicationController
render json: options.to_json
end
- private
-
# Render project landing depending of which features are available
# So if page is not availble in the list it renders the next page
#
@@ -424,4 +423,12 @@ class ProjectsController < Projects::ApplicationController
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42440')
end
+
+ def export_project_path
+ @export_project_path ||= @project.export_project_path
+ end
+
+ def export_project_object_storage?
+ @project.export_project_object_exists?
+ end
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 1de6ae24622..9dd652206fe 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -32,8 +32,8 @@ class SessionsController < Devise::SessionsController
super do |resource|
# User has successfully signed in, so clear any unused reset token
if resource.reset_password_token.present?
- resource.update_attributes(reset_password_token: nil,
- reset_password_sent_at: nil)
+ resource.update(reset_password_token: nil,
+ reset_password_sent_at: nil)
end
# hide the signed-in notification
diff --git a/app/controllers/sherlock/transactions_controller.rb b/app/controllers/sherlock/transactions_controller.rb
index cb6c3a7cd98..ae4953c3259 100644
--- a/app/controllers/sherlock/transactions_controller.rb
+++ b/app/controllers/sherlock/transactions_controller.rb
@@ -13,7 +13,7 @@ module Sherlock
def destroy_all
Gitlab::Sherlock.collection.clear
- redirect_to :back, status: 302
+ redirect_to :back, status: :found
end
end
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 3d51520ddf4..1d6d0943674 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -89,7 +89,7 @@ class SnippetsController < ApplicationController
@snippet.destroy
- redirect_to snippets_path, status: 302
+ redirect_to snippets_path, status: :found
end
protected
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 95fea2f18d1..3c5c8bbd71b 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -128,8 +128,10 @@ module GroupsHelper
def get_group_sidebar_links
links = [:overview, :group_members]
- if can?(current_user, :read_cross_project)
- links += [:activity, :issues, :boards, :labels, :milestones, :merge_requests]
+ resources = [:activity, :issues, :boards, :labels, :milestones,
+ :merge_requests]
+ links += resources.select do |resource|
+ can?(current_user, "read_group_#{resource}".to_sym, @group)
end
if can?(current_user, :admin_group, @group)
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 353479776b8..7bbdc798ddd 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -249,6 +249,7 @@ module IssuablesHelper
issuableRef: issuable.to_reference,
markdownPreviewPath: preview_markdown_path(parent),
markdownDocsPath: help_page_path('user/markdown'),
+ markdownVersion: issuable.cached_markdown_version,
issuableTemplates: issuable_templates(issuable),
initialTitleHtml: markdown_field(issuable, :title),
initialTitleText: issuable.title,
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 39e7a7fd396..cbb971cf8b7 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -107,6 +107,7 @@ module MarkupHelper
def markup(file_name, text, context = {})
context[:project] ||= @project
+ context[:markdown_engine] ||= :redcarpet
html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
prepare_for_rendering(html, context)
end
@@ -120,7 +121,8 @@ module MarkupHelper
project: @project,
project_wiki: @project_wiki,
page_slug: wiki_page.slug,
- issuable_state_filter_enabled: true
+ issuable_state_filter_enabled: true,
+ markdown_engine: :redcarpet
}
html =
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 3fa2e5452c8..5404ead44f3 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -169,6 +169,7 @@ module NotesHelper
registerPath: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'),
newSessionPath: new_session_path(:user, redirect_to_referer: 'yes'),
markdownDocsPath: help_page_path('user/markdown'),
+ markdownVersion: issuable.cached_markdown_version,
quickActionsDocsPath: help_page_path('user/project/quick_actions'),
closePath: close_issuable_path(issuable),
reopenPath: reopen_issuable_path(issuable),
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index c7a434ea092..b0f381db5ab 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -177,6 +177,7 @@ module ProjectsHelper
controller.action_name,
Gitlab::CurrentSettings.cache_key,
"cross-project:#{can?(current_user, :read_cross_project)}",
+ max_project_member_access_cache_key(project),
'v2.6'
]
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index ce9373f5883..4d17b22a4a1 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -31,6 +31,14 @@ module UsersHelper
current_user_menu_items.include?(item)
end
+ def max_project_member_access(project)
+ current_user&.max_member_access_for_project(project.id) || Gitlab::Access::NO_ACCESS
+ end
+
+ def max_project_member_access_cache_key(project)
+ "access:#{max_project_member_access(project)}"
+ end
+
private
def get_profile_tabs
diff --git a/spec/mailers/previews/devise_mailer_preview.rb b/app/mailers/previews/devise_mailer_preview.rb
index d6588efc486..d6588efc486 100644
--- a/spec/mailers/previews/devise_mailer_preview.rb
+++ b/app/mailers/previews/devise_mailer_preview.rb
diff --git a/spec/mailers/previews/email_rejection_mailer_preview.rb b/app/mailers/previews/email_rejection_mailer_preview.rb
index 639e8471232..639e8471232 100644
--- a/spec/mailers/previews/email_rejection_mailer_preview.rb
+++ b/app/mailers/previews/email_rejection_mailer_preview.rb
diff --git a/spec/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index e32fd0bd120..3615cde8026 100644
--- a/spec/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -153,7 +153,7 @@ class NotifyPreview < ActionMailer::Preview
cleanup do
note = yield
- Notify.public_send(method, user.id, note)
+ Notify.public_send(method, user.id, note) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/spec/mailers/previews/repository_check_mailer_preview.rb b/app/mailers/previews/repository_check_mailer_preview.rb
index 19d4eab1805..19d4eab1805 100644
--- a/spec/mailers/previews/repository_check_mailer_preview.rb
+++ b/app/mailers/previews/repository_check_mailer_preview.rb
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index bf93a2caf72..44103e3bc4f 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -371,7 +371,7 @@ module Ci
def update_coverage
coverage = trace.extract_coverage(coverage_regex)
- update_attributes(coverage: coverage) if coverage.present?
+ update(coverage: coverage) if coverage.present?
end
def parse_trace_sections!
@@ -386,6 +386,10 @@ module Ci
trace.exist?
end
+ def has_old_trace?
+ old_trace.present?
+ end
+
def trace=(data)
raise NotImplementedError
end
@@ -395,6 +399,8 @@ module Ci
end
def erase_old_trace!
+ return unless has_old_trace?
+
update_column(:trace, nil)
end
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 4856f10846c..b442de34061 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -1,54 +1,58 @@
module Ci
class BuildTraceChunk < ActiveRecord::Base
include FastDestroyAll
+ include ::Gitlab::ExclusiveLeaseHelpers
extend Gitlab::Ci::Model
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
default_value_for :data_store, :redis
- WriteError = Class.new(StandardError)
-
CHUNK_SIZE = 128.kilobytes
- CHUNK_REDIS_TTL = 1.week
WRITE_LOCK_RETRY = 10
WRITE_LOCK_SLEEP = 0.01.seconds
WRITE_LOCK_TTL = 1.minute
+ # Note: The ordering of this enum is related to the precedence of persist store.
+ # The bottom item takes the higest precedence, and the top item takes the lowest precedence.
enum data_store: {
redis: 1,
- db: 2
+ database: 2,
+ fog: 3
}
class << self
- def redis_data_key(build_id, chunk_index)
- "gitlab:ci:trace:#{build_id}:chunks:#{chunk_index}"
+ def all_stores
+ @all_stores ||= self.data_stores.keys
end
- def redis_data_keys
- redis.pluck(:build_id, :chunk_index).map do |data|
- redis_data_key(data.first, data.second)
- end
+ def persistable_store
+ # get first available store from the back of the list
+ all_stores.reverse.find { |store| get_store_class(store).available? }
end
- def redis_delete_data(keys)
- return if keys.empty?
-
- Gitlab::Redis::SharedState.with do |redis|
- redis.del(keys)
- end
+ def get_store_class(store)
+ @stores ||= {}
+ @stores[store] ||= "Ci::BuildTraceChunks::#{store.capitalize}".constantize.new
end
##
# FastDestroyAll concerns
def begin_fast_destroy
- redis_data_keys
+ all_stores.each_with_object({}) do |store, result|
+ relation = public_send(store) # rubocop:disable GitlabSecurity/PublicSend
+ keys = get_store_class(store).keys(relation)
+
+ result[store] = keys if keys.present?
+ end
end
##
# FastDestroyAll concerns
def finalize_fast_destroy(keys)
- redis_delete_data(keys)
+ keys.each do |store, value|
+ get_store_class(store).delete_keys(value)
+ end
end
end
@@ -66,10 +70,15 @@ module Ci
end
def append(new_data, offset)
+ raise ArgumentError, 'New data is missing' unless new_data
raise ArgumentError, 'Offset is out of range' if offset > size || offset < 0
raise ArgumentError, 'Chunk size overflow' if CHUNK_SIZE < (offset + new_data.bytesize)
- set_data(data.byteslice(0, offset) + new_data)
+ in_lock(*lock_params) do # Write opetation is atomic
+ unsafe_set_data!(data.byteslice(0, offset) + new_data)
+ end
+
+ schedule_to_persist if full?
end
def size
@@ -88,93 +97,63 @@ module Ci
(start_offset...end_offset)
end
- def use_database!
- in_lock do
- break if db?
- break unless size > 0
-
- self.update!(raw_data: data, data_store: :db)
- self.class.redis_delete_data([redis_data_key])
+ def persist_data!
+ in_lock(*lock_params) do # Write opetation is atomic
+ unsafe_persist_to!(self.class.persistable_store)
end
end
private
- def get_data
- if redis?
- redis_data
- elsif db?
- raw_data
- else
- raise 'Unsupported data store'
- end&.force_encoding(Encoding::BINARY) # Redis/Database return UTF-8 string as default
- end
-
- def set_data(value)
- raise ArgumentError, 'too much data' if value.bytesize > CHUNK_SIZE
-
- in_lock do
- if redis?
- redis_set_data(value)
- elsif db?
- self.raw_data = value
- else
- raise 'Unsupported data store'
- end
+ def unsafe_persist_to!(new_store)
+ return if data_store == new_store.to_s
+ raise ArgumentError, 'Can not persist empty data' unless size > 0
- @data = value
+ old_store_class = self.class.get_store_class(data_store)
- save! if changed?
+ get_data.tap do |the_data|
+ self.raw_data = nil
+ self.data_store = new_store
+ unsafe_set_data!(the_data)
end
- schedule_to_db if full?
- end
-
- def schedule_to_db
- return if db?
-
- Ci::BuildTraceChunkFlushWorker.perform_async(id)
+ old_store_class.delete_data(self)
end
- def full?
- size == CHUNK_SIZE
+ def get_data
+ self.class.get_store_class(data_store).data(self)&.force_encoding(Encoding::BINARY) # Redis/Database return UTF-8 string as default
+ rescue Excon::Error::NotFound
+ # If the data store is :fog and the file does not exist in the object storage, this method returns nil.
end
- def redis_data
- Gitlab::Redis::SharedState.with do |redis|
- redis.get(redis_data_key)
- end
- end
+ def unsafe_set_data!(value)
+ raise ArgumentError, 'New data size exceeds chunk size' if value.bytesize > CHUNK_SIZE
- def redis_set_data(data)
- Gitlab::Redis::SharedState.with do |redis|
- redis.set(redis_data_key, data, ex: CHUNK_REDIS_TTL)
- end
- end
+ self.class.get_store_class(data_store).set_data(self, value)
+ @data = value
- def redis_data_key
- self.class.redis_data_key(build_id, chunk_index)
+ save! if changed?
end
- def in_lock
- write_lock_key = "trace_write:#{build_id}:chunks:#{chunk_index}"
+ def schedule_to_persist
+ return if data_persisted?
- lease = Gitlab::ExclusiveLease.new(write_lock_key, timeout: WRITE_LOCK_TTL)
- retry_count = 0
+ Ci::BuildTraceChunkFlushWorker.perform_async(id)
+ end
- until uuid = lease.try_obtain
- # Keep trying until we obtain the lease. To prevent hammering Redis too
- # much we'll wait for a bit between retries.
- sleep(WRITE_LOCK_SLEEP)
- break if WRITE_LOCK_RETRY < (retry_count += 1)
- end
+ def data_persisted?
+ !redis?
+ end
- raise WriteError, 'Failed to obtain write lock' unless uuid
+ def full?
+ size == CHUNK_SIZE
+ end
- self.reload if self.persisted?
- return yield
- ensure
- Gitlab::ExclusiveLease.cancel(write_lock_key, uuid)
+ def lock_params
+ ["trace_write:#{build_id}:chunks:#{chunk_index}",
+ { ttl: WRITE_LOCK_TTL,
+ retries: WRITE_LOCK_RETRY,
+ sleep_sec: WRITE_LOCK_SLEEP }]
end
end
end
diff --git a/app/models/ci/build_trace_chunks/database.rb b/app/models/ci/build_trace_chunks/database.rb
new file mode 100644
index 00000000000..3666d77c790
--- /dev/null
+++ b/app/models/ci/build_trace_chunks/database.rb
@@ -0,0 +1,29 @@
+module Ci
+ module BuildTraceChunks
+ class Database
+ def available?
+ true
+ end
+
+ def keys(relation)
+ []
+ end
+
+ def delete_keys(keys)
+ # no-op
+ end
+
+ def data(model)
+ model.raw_data
+ end
+
+ def set_data(model, data)
+ model.raw_data = data
+ end
+
+ def delete_data(model)
+ model.update_columns(raw_data: nil) unless model.raw_data.nil?
+ end
+ end
+ end
+end
diff --git a/app/models/ci/build_trace_chunks/fog.rb b/app/models/ci/build_trace_chunks/fog.rb
new file mode 100644
index 00000000000..7506c40a39d
--- /dev/null
+++ b/app/models/ci/build_trace_chunks/fog.rb
@@ -0,0 +1,59 @@
+module Ci
+ module BuildTraceChunks
+ class Fog
+ def available?
+ object_store.enabled
+ end
+
+ def data(model)
+ connection.get_object(bucket_name, key(model))[:body]
+ end
+
+ def set_data(model, data)
+ connection.put_object(bucket_name, key(model), data)
+ end
+
+ def delete_data(model)
+ delete_keys([[model.build_id, model.chunk_index]])
+ end
+
+ def keys(relation)
+ return [] unless available?
+
+ relation.pluck(:build_id, :chunk_index)
+ end
+
+ def delete_keys(keys)
+ keys.each do |key|
+ connection.delete_object(bucket_name, key_raw(*key))
+ end
+ end
+
+ private
+
+ def key(model)
+ key_raw(model.build_id, model.chunk_index)
+ end
+
+ def key_raw(build_id, chunk_index)
+ "tmp/builds/#{build_id.to_i}/chunks/#{chunk_index.to_i}.log"
+ end
+
+ def bucket_name
+ return unless available?
+
+ object_store.remote_directory
+ end
+
+ def connection
+ return unless available?
+
+ @connection ||= ::Fog::Storage.new(object_store.connection.to_hash.deep_symbolize_keys)
+ end
+
+ def object_store
+ Gitlab.config.artifacts.object_store
+ end
+ end
+ end
+end
diff --git a/app/models/ci/build_trace_chunks/redis.rb b/app/models/ci/build_trace_chunks/redis.rb
new file mode 100644
index 00000000000..fdb6065e2a0
--- /dev/null
+++ b/app/models/ci/build_trace_chunks/redis.rb
@@ -0,0 +1,51 @@
+module Ci
+ module BuildTraceChunks
+ class Redis
+ CHUNK_REDIS_TTL = 1.week
+
+ def available?
+ true
+ end
+
+ def data(model)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.get(key(model))
+ end
+ end
+
+ def set_data(model, data)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(key(model), data, ex: CHUNK_REDIS_TTL)
+ end
+ end
+
+ def delete_data(model)
+ delete_keys([[model.build_id, model.chunk_index]])
+ end
+
+ def keys(relation)
+ relation.pluck(:build_id, :chunk_index)
+ end
+
+ def delete_keys(keys)
+ return if keys.empty?
+
+ keys = keys.map { |key| key_raw(*key) }
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.del(keys)
+ end
+ end
+
+ private
+
+ def key(model)
+ key_raw(model.build_id, model.chunk_index)
+ end
+
+ def key_raw(build_id, chunk_index)
+ "gitlab:ci:trace:#{build_id.to_i}:chunks:#{chunk_index.to_i}"
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 9f6358cecbe..b05bf909058 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -40,6 +40,18 @@ module CacheMarkdownField
end
end
+ class MarkdownEngine
+ def self.from_version(version = nil)
+ return :common_mark if version.nil? || version == 0
+
+ if version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
+ :redcarpet
+ else
+ :common_mark
+ end
+ end
+ end
+
def skip_project_check?
false
end
@@ -57,7 +69,7 @@ module CacheMarkdownField
# Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author)
- context[:markdown_engine] = markdown_engine
+ context[:markdown_engine] = MarkdownEngine.from_version(latest_cached_markdown_version)
context
end
@@ -123,14 +135,6 @@ module CacheMarkdownField
end
end
- def markdown_engine
- if latest_cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
- :redcarpet
- else
- :common_mark
- end
- end
-
included do
cattr_reader :cached_markdown_fields do
FieldData.new
diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb
index d58d7165969..606549b947f 100644
--- a/app/models/concerns/cacheable_attributes.rb
+++ b/app/models/concerns/cacheable_attributes.rb
@@ -7,7 +7,7 @@ module CacheableAttributes
class_methods do
def cache_key
- "#{name}:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:#{Rails.version}".freeze
+ "#{name}:#{Gitlab::VERSION}:#{Rails.version}".freeze
end
# Can be overriden
@@ -69,6 +69,6 @@ module CacheableAttributes
end
def cache!
- Rails.cache.write(self.class.cache_key, self)
+ Rails.cache.write(self.class.cache_key, self, expires_in: 1.minute)
end
end
diff --git a/app/models/concerns/group_descendant.rb b/app/models/concerns/group_descendant.rb
index 261ace57a17..5e9a95c3282 100644
--- a/app/models/concerns/group_descendant.rb
+++ b/app/models/concerns/group_descendant.rb
@@ -44,8 +44,8 @@ module GroupDescendant
This error is not user facing, but causes a +1 query.
MSG
extras = {
- parent: parent,
- child: child,
+ parent: parent.inspect,
+ child: child.inspect,
preloaded: preloaded.map(&:full_path)
}
issue_url = 'https://gitlab.com/gitlab-org/gitlab-ce/issues/40785'
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index 94eef4ff7cd..dbe8d31de37 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -23,7 +23,7 @@ module ProtectedRef
# If we don't `protected_branch` or `protected_tag` would be empty and
# `project` cannot be delegated to it, which in turn would cause validations
# to fail.
- has_many :"#{type}_access_levels", inverse_of: self.model_name.singular # rubocop:disable Cop/ActiveRecordDependent
+ has_many :"#{type}_access_levels", inverse_of: self.model_name.singular
validates :"#{type}_access_levels", length: { is: 1, message: "are restricted to a single instance per #{self.model_name.human}." }
diff --git a/app/models/import_export_upload.rb b/app/models/import_export_upload.rb
new file mode 100644
index 00000000000..60d53d6c2c8
--- /dev/null
+++ b/app/models/import_export_upload.rb
@@ -0,0 +1,13 @@
+class ImportExportUpload < ActiveRecord::Base
+ include WithUploads
+ include ObjectStorage::BackgroundMove
+
+ belongs_to :project
+
+ mount_uploader :import_file, ImportExportUploader
+ mount_uploader :export_file, ImportExportUploader
+
+ def retrieve_upload(_identifier, paths)
+ Upload.find_by(model: self, path: paths)
+ end
+end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index d05dcfd083a..14cc12b38a5 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -131,9 +131,10 @@ class Milestone < ActiveRecord::Base
rel.order(:project_id, :due_date).select('DISTINCT ON (project_id) id')
else
rel
- .group(:project_id)
+ .group(:project_id, :due_date, :id)
.having('due_date = MIN(due_date)')
.pluck(:id, :project_id, :due_date)
+ .uniq(&:second)
.map(&:first)
end
end
diff --git a/app/models/network/commit.rb b/app/models/network/commit.rb
index 22d48c9e661..d667948deae 100644
--- a/app/models/network/commit.rb
+++ b/app/models/network/commit.rb
@@ -11,8 +11,8 @@ module Network
@parent_spaces = []
end
- def method_missing(m, *args, &block)
- @commit.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(msg, *args, &block)
+ @commit.__send__(msg, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
def space
diff --git a/app/models/project.rb b/app/models/project.rb
index 8f40470de82..770262f6193 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -171,6 +171,7 @@ class Project < ActiveRecord::Base
has_one :fork_network, through: :fork_network_member
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
+ has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
# Merge Requests for target project should be removed with it
has_many :merge_requests, foreign_key: 'target_project_id'
@@ -1712,7 +1713,7 @@ class Project < ActiveRecord::Base
:started
elsif after_export_in_progress?
:after_export_action
- elsif export_project_path
+ elsif export_project_path || export_project_object_exists?
:finished
else
:none
@@ -1727,16 +1728,21 @@ class Project < ActiveRecord::Base
import_export_shared.after_export_in_progress?
end
- def remove_exports
- return nil unless export_path.present?
-
- FileUtils.rm_rf(export_path)
+ def remove_exports(path = export_path)
+ if path.present?
+ FileUtils.rm_rf(path)
+ elsif export_project_object_exists?
+ import_export_upload.remove_export_file!
+ import_export_upload.save
+ end
end
def remove_exported_project_file
- return unless export_project_path.present?
+ remove_exports(export_project_path)
+ end
- FileUtils.rm_f(export_project_path)
+ def export_project_object_exists?
+ Gitlab::ImportExport.object_storage? && import_export_upload&.export_file&.file
end
def full_path_slug
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index a6f94b3e3b0..fc868c3ebb7 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -107,7 +107,7 @@ class ProjectWiki
update_project_activity
rescue Gitlab::Git::Wiki::DuplicatePageError => e
@error_message = "Duplicate page: #{e.message}"
- return false
+ false
end
def update_page(page, content:, title: nil, format: :markdown, message: nil)
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index c4b5dd2dc96..976b501e297 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -57,7 +57,7 @@ class RemoteMirror < ActiveRecord::Base
Gitlab::Metrics.add_event(:remote_mirrors_finished, path: remote_mirror.project.full_path)
timestamp = Time.now
- remote_mirror.update_attributes!(
+ remote_mirror.update!(
last_update_at: timestamp, last_successful_update_at: timestamp, last_error: nil
)
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 5f9894f1168..5ed2a7b4068 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -83,7 +83,7 @@ class Repository
@raw_repository&.cleanup
end
- # Return absolute path to repository
+ # Don't use this! It's going away. Use Gitaly to read or write from repos.
def path_to_repo
@path_to_repo ||=
begin
@@ -250,7 +250,7 @@ class Repository
# This will still fail if the file is corrupted (e.g. 0 bytes)
raw_repository.write_ref(keep_around_ref_name(sha), sha, shell: false)
rescue Gitlab::Git::CommandError => ex
- Rails.logger.error "Unable to create keep-around reference for repository #{path}: #{ex}"
+ Rails.logger.error "Unable to create keep-around reference for repository #{disk_path}: #{ex}"
end
def kept_around?(sha)
@@ -462,12 +462,12 @@ class Repository
expire_branches_cache
end
- def method_missing(m, *args, &block)
- if m == :lookup && !block_given?
- lookup_cache[m] ||= {}
- lookup_cache[m][args.join(":")] ||= raw_repository.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(msg, *args, &block)
+ if msg == :lookup && !block_given?
+ lookup_cache[msg] ||= {}
+ lookup_cache[msg][args.join(":")] ||= raw_repository.__send__(msg, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
else
- raw_repository.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
+ raw_repository.__send__(msg, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
end
@@ -564,7 +564,7 @@ class Repository
end
def rendered_readme
- MarkupHelper.markup_unsafe(readme.name, readme.data, project: project) if readme
+ MarkupHelper.markup_unsafe(readme.name, readme.data, project: project, markdown_engine: :redcarpet) if readme
end
cache_method :rendered_readme
diff --git a/app/models/user.rb b/app/models/user.rb
index 27a5d0278b7..1c5d39db118 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -496,7 +496,7 @@ class User < ActiveRecord::Base
def disable_two_factor!
transaction do
- update_attributes(
+ update(
otp_required_for_login: false,
encrypted_otp_secret: nil,
encrypted_otp_secret_iv: nil,
@@ -1053,7 +1053,7 @@ class User < ActiveRecord::Base
return @global_notification_setting if defined?(@global_notification_setting)
@global_notification_setting = notification_settings.find_or_initialize_by(source: nil)
- @global_notification_setting.update_attributes(level: NotificationSetting.levels[DEFAULT_NOTIFICATION_LEVEL]) unless @global_notification_setting.persisted?
+ @global_notification_setting.update(level: NotificationSetting.levels[DEFAULT_NOTIFICATION_LEVEL]) unless @global_notification_setting.persisted?
@global_notification_setting
end
@@ -1333,8 +1333,8 @@ class User < ActiveRecord::Base
end
end
- def self.unique_internal(scope, username, email_pattern, &b)
- scope.first || create_unique_internal(scope, username, email_pattern, &b)
+ def self.unique_internal(scope, username, email_pattern, &block)
+ scope.first || create_unique_internal(scope, username, email_pattern, &block)
end
def self.create_unique_internal(scope, username, email_pattern, &creation_block)
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index cde79b95062..4b49edb01a5 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -1,3 +1,4 @@
+# rubocop:disable Rails/ActiveRecordAliases
class WikiPage
PageChangedError = Class.new(StandardError)
PageRenameError = Class.new(StandardError)
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 520710b757d..ded9fe30eff 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -72,6 +72,19 @@ class GroupPolicy < BasePolicy
enable :change_visibility_level
end
+ rule { can?(:read_nested_project_resources) }.policy do
+ enable :read_group_activity
+ enable :read_group_issues
+ enable :read_group_boards
+ enable :read_group_labels
+ enable :read_group_milestones
+ enable :read_group_merge_requests
+ end
+
+ rule { can?(:read_cross_project) & can?(:read_group) }.policy do
+ enable :read_nested_project_resources
+ end
+
rule { owner & nested_groups_supported }.enable :create_subgroup
rule { public_group | logged_in_viewable }.enable :view_globally
diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb
index ce0c31b5806..0e1f94a9f61 100644
--- a/app/serializers/note_entity.rb
+++ b/app/serializers/note_entity.rb
@@ -62,6 +62,8 @@ class NoteEntity < API::Entities::Note
expose :attachment, using: NoteAttachmentEntity, if: -> (note, _) { note.attachment? }
+ expose :cached_markdown_version
+
private
def current_user
diff --git a/app/services/badges/update_service.rb b/app/services/badges/update_service.rb
index 7ca84b5df31..495a4a2c99d 100644
--- a/app/services/badges/update_service.rb
+++ b/app/services/badges/update_service.rb
@@ -3,7 +3,7 @@ module Badges
# returns the updated badge
def execute(badge)
if params.present?
- badge.update_attributes(params)
+ badge.update(params)
end
badge
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
index b9d0173a2d0..1ce6ab36cbf 100644
--- a/app/services/commits/change_service.rb
+++ b/app/services/commits/change_service.rb
@@ -13,8 +13,6 @@ module Commits
# rubocop:disable GitlabSecurity/PublicSend
message = @commit.public_send(:"#{action}_message", current_user)
-
- # rubocop:disable GitlabSecurity/PublicSend
repository.public_send(
action,
current_user,
diff --git a/app/services/import_export_clean_up_service.rb b/app/services/import_export_clean_up_service.rb
index 74088b970c9..3702c3742ef 100644
--- a/app/services/import_export_clean_up_service.rb
+++ b/app/services/import_export_clean_up_service.rb
@@ -10,7 +10,9 @@ class ImportExportCleanUpService
def execute
Gitlab::Metrics.measure(:import_export_clean_up) do
- next unless File.directory?(path)
+ clean_up_export_object_files
+
+ break unless File.directory?(path)
clean_up_export_files
end
@@ -21,4 +23,11 @@ class ImportExportCleanUpService
def clean_up_export_files
Gitlab::Popen.popen(%W(find #{path} -not -path #{path} -mmin +#{mmin} -delete))
end
+
+ def clean_up_export_object_files
+ ImportExportUpload.where('updated_at < ?', mmin.minutes.ago).each do |upload|
+ upload.remove_export_file!
+ upload.save!
+ end
+ end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 683f64e82ad..5e06e0c61cf 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -130,7 +130,7 @@ class IssuableBaseService < BaseService
def create_issuable(issuable, attributes, label_ids:)
issuable.with_transaction_returning_status do
if issuable.save
- issuable.update_attributes(label_ids: label_ids)
+ issuable.update(label_ids: label_ids)
end
end
end
diff --git a/app/services/members/update_service.rb b/app/services/members/update_service.rb
index 48b3d59f7bd..cb19cf01dd7 100644
--- a/app/services/members/update_service.rb
+++ b/app/services/members/update_service.rb
@@ -6,7 +6,7 @@ module Members
old_access_level = member.human_access
- if member.update_attributes(params)
+ if member.update(params)
after_execute(action: permission, old_access_level: old_access_level, member: member)
end
diff --git a/app/services/merge_requests/rebase_service.rb b/app/services/merge_requests/rebase_service.rb
index 5b4bc86b9ba..c741e913860 100644
--- a/app/services/merge_requests/rebase_service.rb
+++ b/app/services/merge_requests/rebase_service.rb
@@ -26,7 +26,7 @@ module MergeRequests
Gitlab::GitLogger.info("#{log_prefix} rebased to #{rebase_sha}")
- merge_request.update_attributes(rebase_commit_sha: rebase_sha)
+ merge_request.update(rebase_commit_sha: rebase_sha)
Gitlab::GitLogger.info("#{log_prefix} rebase SHA saved: #{rebase_sha}")
diff --git a/app/services/milestones/update_service.rb b/app/services/milestones/update_service.rb
index 31b441ed476..74edbf9b41d 100644
--- a/app/services/milestones/update_service.rb
+++ b/app/services/milestones/update_service.rb
@@ -11,7 +11,7 @@ module Milestones
end
if params.present?
- milestone.update_attributes(params.except(:state_event))
+ milestone.update(params.except(:state_event))
end
milestone
diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb
index 75fd08ea0a9..e16ef398184 100644
--- a/app/services/notes/update_service.rb
+++ b/app/services/notes/update_service.rb
@@ -5,7 +5,7 @@ module Notes
old_mentioned_users = note.mentioned_users.to_a
- note.update_attributes(params.merge(updated_by: current_user))
+ note.update(params.merge(updated_by: current_user))
note.create_new_cross_references!(current_user)
if note.previous_changes.include?('note')
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index 4fa38665abc..d9834fd0ccc 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -10,16 +10,16 @@ module NotificationRecipientService
NotificationRecipient.new(user, *args).notifiable?
end
- def self.build_recipients(*a)
- Builder::Default.new(*a).notification_recipients
+ def self.build_recipients(*args)
+ Builder::Default.new(*args).notification_recipients
end
- def self.build_new_note_recipients(*a)
- Builder::NewNote.new(*a).notification_recipients
+ def self.build_new_note_recipients(*args)
+ Builder::NewNote.new(*args).notification_recipients
end
- def self.build_merge_request_unmergeable_recipients(*a)
- Builder::MergeRequestUnmergeable.new(*a).notification_recipients
+ def self.build_merge_request_unmergeable_recipients(*args)
+ Builder::MergeRequestUnmergeable.new(*args).notification_recipients
end
module Builder
@@ -44,7 +44,6 @@ module NotificationRecipientService
raise 'abstract'
end
- # rubocop:disable Rails/Delegate
def project
target.project
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 636cfbf5b45..8c6221af788 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -135,6 +135,8 @@ class NotificationService
# * watchers of the mr's labels
# * users with custom level checked with "new merge request"
#
+ # In EE, approvers of the merge request are also included
+ #
def new_merge_request(merge_request, current_user)
new_resource_email(merge_request, :new_merge_request_email)
end
@@ -256,6 +258,10 @@ class NotificationService
# ignore gitlab service messages
return true if note.cross_reference? && note.system?
+ send_new_note_notifications(note)
+ end
+
+ def send_new_note_notifications(note)
notify_method = "note_#{note.to_ability_name}_email".to_sym
recipients = NotificationRecipientService.build_new_note_recipients(note)
diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb
index 4ee2c1796bd..6da4d9523cf 100644
--- a/app/services/preview_markdown_service.rb
+++ b/app/services/preview_markdown_service.rb
@@ -6,7 +6,8 @@ class PreviewMarkdownService < BaseService
success(
text: text,
users: users,
- commands: commands.join(' ')
+ commands: commands.join(' '),
+ markdown_engine: markdown_engine
)
end
@@ -42,4 +43,8 @@ class PreviewMarkdownService < BaseService
def commands_target_id
params[:quick_actions_target_id]
end
+
+ def markdown_engine
+ CacheMarkdownField::MarkdownEngine.from_version(params[:markdown_version].to_i)
+ end
end
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index aa60661f7f2..9d0eaaf3152 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -20,24 +20,28 @@ module Projects
MergeRequestsFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
end
- def labels(target = nil)
- labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true)
- .execute.select([:color, :title])
-
- return labels unless target&.respond_to?(:labels)
-
- issuable_label_titles = target.labels.pluck(:title)
-
- if issuable_label_titles
- labels = labels.as_json(only: [:title, :color])
-
- issuable_label_titles.each do |issuable_label_title|
- found_label = labels.find { |label| label['title'] == issuable_label_title }
- found_label[:set] = true if found_label
+ def labels_as_hash(target = nil)
+ available_labels = LabelsFinder.new(
+ current_user,
+ project_id: project.id,
+ include_ancestor_groups: true
+ ).execute
+
+ label_hashes = available_labels.as_json(only: [:title, :color])
+
+ if target&.respond_to?(:labels)
+ already_set_labels = available_labels & target.labels
+ if already_set_labels.present?
+ titles = already_set_labels.map(&:title)
+ label_hashes.each do |hash|
+ if titles.include?(hash['title'])
+ hash[:set] = true
+ end
+ end
end
end
- labels
+ label_hashes
end
def commands(noteable, type)
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 02769e72229..87173cc79ec 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -124,7 +124,7 @@ module Projects
# It's possible that the project was destroyed, but some after_commit
# hook failed and caused us to end up here. A destroyed model will be a frozen hash,
# which cannot be altered.
- project.update_attributes(delete_error: message, pending_delete: false) unless project.destroyed?
+ project.update(delete_error: message, pending_delete: false) unless project.destroyed?
log_error("Deletion failed on #{project.full_path} with the following message: #{message}")
end
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index 348eb0bf8d8..a8aafa9fb4f 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -37,7 +37,7 @@ module Projects
return new_project unless new_project.persisted?
builds_access_level = @project.project_feature.builds_access_level
- new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
+ new_project.project_feature.update(builds_access_level: builds_access_level)
link_fork_network(new_project)
diff --git a/app/services/projects/lfs_pointers/lfs_download_service.rb b/app/services/projects/lfs_pointers/lfs_download_service.rb
index 6ea43561d61..618c30b971f 100644
--- a/app/services/projects/lfs_pointers/lfs_download_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_service.rb
@@ -22,7 +22,7 @@ module Projects
private
def download_and_save_file(file, sanitized_uri)
- IO.copy_stream(open(sanitized_uri.sanitized_url, headers(sanitized_uri)), file)
+ IO.copy_stream(open(sanitized_uri.sanitized_url, headers(sanitized_uri)), file) # rubocop:disable Security/Open
end
def headers(sanitized_uri)
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index d8250cd8102..f4fbaacc08b 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -22,7 +22,7 @@ module Projects
# If the block added errors, don't try to save the project
return validation_failed! if project.errors.any?
- if project.update_attributes(params.except(:default_branch))
+ if project.update(params.except(:default_branch))
if project.previous_changes.include?('path')
project.rename_repo
else
diff --git a/app/services/update_release_service.rb b/app/services/update_release_service.rb
index b7c36651968..dc696e9c440 100644
--- a/app/services/update_release_service.rb
+++ b/app/services/update_release_service.rb
@@ -7,7 +7,7 @@ class UpdateReleaseService < BaseService
release = project.releases.find_by(tag: tag_name)
if release
- release.update_attributes(description: release_description)
+ release.update(description: release_description)
success(release)
else
diff --git a/app/uploaders/import_export_uploader.rb b/app/uploaders/import_export_uploader.rb
new file mode 100644
index 00000000000..213ac5c8011
--- /dev/null
+++ b/app/uploaders/import_export_uploader.rb
@@ -0,0 +1,15 @@
+class ImportExportUploader < AttachmentUploader
+ EXTENSION_WHITELIST = %w[tar.gz].freeze
+
+ def extension_whitelist
+ EXTENSION_WHITELIST
+ end
+
+ def move_to_store
+ true
+ end
+
+ def move_to_cache
+ false
+ end
+end
diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml
index f7094375023..5e7be5cd37b 100644
--- a/app/views/import/_githubish_status.html.haml
+++ b/app/views/import/_githubish_status.html.haml
@@ -40,7 +40,7 @@
= project.human_import_status_name
- @repos.each do |repo|
- %tr{ id: "repo_#{repo.id}" }
+ %tr{ id: "repo_#{repo.id}", data: { qa: { repo_path: repo.full_name } } }
%td
= provider_project_link(provider, repo.full_name)
%td.import-target
@@ -50,7 +50,7 @@
- if current_user.can_select_namespace?
- selected = params[:namespace_id] || :current_user
- opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner.login, path: repo.owner.login) } : {}
- = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'input-group-text select2 js-select-namespace', tabindex: 1 }
+ = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'input-group-text select2 js-select-namespace qa-project-namespace-select', tabindex: 1 }
- else
= text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true
%span.input-group-prepend
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index a74ea246eaf..9ed05d6e3d0 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -17,11 +17,7 @@
= link_to _("Help"), help_path
- if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
%li.divider
- %li
- = link_to "https://about.gitlab.com/contributing", target: '_blank', class: 'text-nowrap' do
- = _("Contribute to GitLab")
- = sprite_icon('external-link', size: 16)
- %li.divider
+ = render 'shared/user_dropdown_contributing_link'
- if current_user_menu?(:sign_out)
%li
= link_to _("Sign out"), destroy_user_session_path, class: "sign-out-link"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index d8e32651b36..3aa8eb18bf3 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -61,7 +61,9 @@
- if header_link?(:sign_in)
%li.nav-item
%div
- = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in'
+ - sign_in_text = allow_signup? ? 'Sign in / Register' : 'Sign in'
+ = link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in'
+
%button.navbar-toggler.d-block.d-sm-none{ type: 'button' }
%span.sr-only Toggle navigation
diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml
index d35df706036..792291bde75 100644
--- a/app/views/layouts/header/_new_dropdown.haml
+++ b/app/views/layouts/header/_new_dropdown.haml
@@ -37,7 +37,7 @@
%li.dropdown-bold-header GitLab
- if current_user.can_create_project?
%li
- = link_to 'New project', new_project_path
+ = link_to 'New project', new_project_path, class: 'qa-global-new-project-link'
- if current_user.can_create_group?
%li
= link_to 'New group', new_group_path
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 7647e25e804..4029287fc0e 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,16 +1,19 @@
%ul.list-unstyled.navbar-sub-nav
- if dashboard_nav_link?(:projects)
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown" }) do
- %a{ href: "#", data: { toggle: "dropdown" } }
+ %button{ type: 'button', data: { toggle: "dropdown" } }
Projects
= sprite_icon('angle-down', css_class: 'caret-down')
- .dropdown-menu.projects-dropdown-menu
+ .dropdown-menu.frequent-items-dropdown-menu
= render "layouts/nav/projects_dropdown/show"
- if dashboard_nav_link?(:groups)
- = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "d-none d-sm-block" }) do
- = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups qa-groups-link', title: 'Groups' do
+ = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { id: 'nav-groups-dropdown', class: "home dropdown header-groups qa-groups-dropdown" }) do
+ %button{ type: 'button', data: { toggle: "dropdown" } }
Groups
+ = sprite_icon('angle-down', css_class: 'caret-down')
+ .dropdown-menu.frequent-items-dropdown-menu
+ = render "layouts/nav/groups_dropdown/show"
- if dashboard_nav_link?(:activity)
= nav_link(path: 'dashboard#activity', html_options: { class: "d-none d-lg-block d-xl-block" }) do
@@ -34,11 +37,6 @@
= sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu
%ul
- - if dashboard_nav_link?(:groups)
- = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { class: "d-block d-sm-none" }) do
- = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
- Groups
-
- if dashboard_nav_link?(:activity)
= nav_link(path: 'dashboard#activity') do
= link_to activity_dashboard_path, title: 'Activity' do
diff --git a/app/views/layouts/nav/groups_dropdown/_show.html.haml b/app/views/layouts/nav/groups_dropdown/_show.html.haml
new file mode 100644
index 00000000000..3ce1fa6bcca
--- /dev/null
+++ b/app/views/layouts/nav/groups_dropdown/_show.html.haml
@@ -0,0 +1,12 @@
+- group_meta = { id: @group.id, name: @group.name, namespace: @group.full_name, web_url: group_path(@group), avatar_url: @group.avatar_url } if @group&.persisted?
+.frequent-items-dropdown-container
+ .frequent-items-dropdown-sidebar.qa-groups-dropdown-sidebar
+ %ul
+ = nav_link(path: 'dashboard/groups#index') do
+ = link_to dashboard_groups_path, class: 'qa-your-groups-link' do
+ = _('Your groups')
+ = nav_link(path: 'groups#explore') do
+ = link_to explore_groups_path do
+ = _('Explore groups')
+ .frequent-items-dropdown-content
+ #js-groups-dropdown{ data: { user_name: current_user.username, group: group_meta } }
diff --git a/app/views/layouts/nav/projects_dropdown/_show.html.haml b/app/views/layouts/nav/projects_dropdown/_show.html.haml
index 5809d6f7fea..f2170f71532 100644
--- a/app/views/layouts/nav/projects_dropdown/_show.html.haml
+++ b/app/views/layouts/nav/projects_dropdown/_show.html.haml
@@ -1,6 +1,6 @@
- project_meta = { id: @project.id, name: @project.name, namespace: @project.full_name, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted?
-.projects-dropdown-container
- .project-dropdown-sidebar.qa-projects-dropdown-sidebar
+.frequent-items-dropdown-container
+ .frequent-items-dropdown-sidebar.qa-projects-dropdown-sidebar
%ul
= nav_link(path: 'dashboard/projects#index') do
= link_to dashboard_projects_path, class: 'qa-your-projects-link' do
@@ -11,5 +11,5 @@
= nav_link(path: 'projects#trending') do
= link_to explore_root_path do
= _('Explore projects')
- .project-dropdown-content
+ .frequent-items-dropdown-content
#js-projects-dropdown{ data: { user_name: current_user.username, project: project_meta } }
diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml
index c14700794ce..43a2d53b84d 100644
--- a/app/views/profiles/keys/_form.html.haml
+++ b/app/views/profiles/keys/_form.html.haml
@@ -5,11 +5,18 @@
.form-group
= f.label :key, class: 'label-light'
%p= _("Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key.")
- = f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: 'Typically starts with "ssh-rsa …"'
+ = f.text_area :key, class: "form-control js-add-ssh-key-validation-input", rows: 8, required: true, placeholder: s_('Profiles|Typically starts with "ssh-rsa …"')
.form-group
= f.label :title, class: 'label-light'
- = f.text_field :title, class: "form-control", required: true, placeholder: 'e.g. My MacBook key'
+ = f.text_field :title, class: "form-control input-lg", required: true, placeholder: s_('Profiles|e.g. My MacBook key')
%p.form-text.text-muted= _('Name your individual key via a title')
+ .js-add-ssh-key-validation-warning.hide
+ .bs-callout.bs-callout-warning{ role: 'alert', aria_live: 'assertive' }
+ %strong= _('Oops, are you sure?')
+ %p= s_("Profiles|This doesn't look like a public SSH key, are you sure you want to add it?")
+
+ %button.btn.btn-create.js-add-ssh-key-validation-confirm-submit= _("Yes, add it")
+
.prepend-top-default
- = f.submit 'Add key', class: "btn btn-create"
+ = f.submit s_('Profiles|Add key'), class: "btn btn-create js-add-ssh-key-validation-original-submit"
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index f4d4888bd15..aa980da7e95 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -31,7 +31,7 @@
%li Any encrypted tokens
%p
Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page.
- - if project.export_project_path
+ - if project.export_status == :finished
= link_to 'Download export', download_export_project_path(project),
rel: 'nofollow', download: '', method: :get, class: "btn btn-default"
= link_to 'Generate new export', generate_new_export_project_path(project),
diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml
index 2c5ffd85372..1e4e9450ffa 100644
--- a/app/views/projects/issues/_form.html.haml
+++ b/app/views/projects/issues/_form.html.haml
@@ -1,2 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'issue-form common-note-form js-quick-submit js-requires-input' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @issue],
+ html: { class: 'issue-form common-note-form js-quick-submit js-requires-input' },
+ data: { markdown_version: @issue.cached_markdown_version } do |f|
= render 'shared/issuable/form', f: f, issuable: @issue
diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml
index 179c1fcc684..5a59f956cb5 100644
--- a/app/views/projects/merge_requests/_form.html.haml
+++ b/app/views/projects/merge_requests/_form.html.haml
@@ -1,2 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @merge_request],
+ html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' },
+ data: { markdown_version: @merge_request.cached_markdown_version } do |f|
= render 'shared/issuable/form', f: f, issuable: @merge_request
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index 4cc59718715..ace094a671a 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -1,4 +1,6 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'milestone-form common-note-form js-quick-submit js-requires-input'} do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @milestone],
+ html: {class: 'milestone-form common-note-form js-quick-submit js-requires-input'},
+ data: { markdown_version: @milestone.cached_markdown_version } do |f|
= form_errors(@milestone)
.row
.col-md-6
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
index d6f758608a0..8093cc2c2d7 100644
--- a/app/views/projects/releases/edit.html.haml
+++ b/app/views/projects/releases/edit.html.haml
@@ -11,7 +11,9 @@
%strong= @tag.name
- = form_for(@release, method: :put, url: project_tag_release_path(@project, @tag.name), html: { class: 'common-note-form release-form js-quick-submit' }) do |f|
+ = form_for(@release, method: :put, url: project_tag_release_path(@project, @tag.name),
+ html: { class: 'common-note-form release-form js-quick-submit' },
+ data: { markdown_version: @release.cached_markdown_version }) do |f|
= render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here…"
= render 'shared/notes/hints'
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 26fe1de31fe..de692466fe5 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -1,7 +1,9 @@
- commit_message = @page.persisted? ? s_("WikiPageEdit|Update %{page_title}") : s_("WikiPageCreate|Create %{page_title}")
- commit_message = commit_message % { page_title: @page.title }
-= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'wiki-form common-note-form prepend-top-default js-quick-submit' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post,
+ html: { class: 'wiki-form common-note-form prepend-top-default js-quick-submit' },
+ data: { markdown_version: CacheMarkdownField::CACHE_REDCARPET_VERSION } do |f|
= form_errors(@page)
- if @page.persisted?
diff --git a/app/views/shared/_user_dropdown_contributing_link.html.haml b/app/views/shared/_user_dropdown_contributing_link.html.haml
new file mode 100644
index 00000000000..333d6fa3489
--- /dev/null
+++ b/app/views/shared/_user_dropdown_contributing_link.html.haml
@@ -0,0 +1,5 @@
+%li
+ = link_to "https://about.gitlab.com/contributing", target: '_blank', class: 'text-nowrap' do
+ = _("Contribute to GitLab")
+ = sprite_icon('external-link', size: 16)
+%li.divider
diff --git a/app/views/shared/hook_logs/_content.html.haml b/app/views/shared/hook_logs/_content.html.haml
index 532712ee6d1..f3b56df0c96 100644
--- a/app/views/shared/hook_logs/_content.html.haml
+++ b/app/views/shared/hook_logs/_content.html.haml
@@ -30,7 +30,7 @@
%h5 Request body:
%pre
- :plain
+ :escaped
#{JSON.pretty_generate(hook_log.request_data)}
%h5 Response headers:
%pre
@@ -40,5 +40,5 @@
%h5 Response body:
%pre
- :plain
+ :escaped
#{hook_log.response_body}
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
index d4e8f30e458..f5464058bc0 100644
--- a/app/views/shared/notes/_note.html.haml
+++ b/app/views/shared/notes/_note.html.haml
@@ -52,7 +52,7 @@
.note-text.md
= markdown_field(note, :note)
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago')
- .original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
+ .original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore, markdown_version: note.cached_markdown_version } }
#{note.note}
- if note_editable
= render 'shared/notes/edit', note: note
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 88f0675f795..6be1fb485a4 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -4,7 +4,7 @@
- ci = false unless local_assigns[:ci] == true
- skip_namespace = false unless local_assigns[:skip_namespace] == true
- user = local_assigns[:user]
-- access = user&.max_member_access_for_project(project.id) unless user.nil?
+- access = max_project_member_access(project)
- css_class = '' unless local_assigns[:css_class]
- show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && can_show_last_commit_in_list?(project)
- css_class += " no-description" if project.description.blank? && !show_last_commit_as_description
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 858adc8be37..5e5c050d5c3 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -2,7 +2,9 @@
= page_specific_javascript_tag('lib/ace.js')
.snippet-form-holder
- = form_for @snippet, url: url, html: { class: "snippet-form js-requires-input js-quick-submit common-note-form" } do |f|
+ = form_for @snippet, url: url,
+ html: { class: "snippet-form js-requires-input js-quick-submit common-note-form" },
+ data: { markdown_version: @snippet.cached_markdown_version } do |f|
= form_errors(@snippet)
.form-group.row
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index b8b854853b7..d4be1ccfcfa 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -46,7 +46,6 @@
- mail_scheduler:mail_scheduler_issue_due
- mail_scheduler:mail_scheduler_notification_service
-- object_storage_upload
- object_storage:object_storage_background_move
- object_storage:object_storage_migrate_uploads
diff --git a/app/workers/archive_trace_worker.rb b/app/workers/archive_trace_worker.rb
index 9169f21af2a..c6f89a17729 100644
--- a/app/workers/archive_trace_worker.rb
+++ b/app/workers/archive_trace_worker.rb
@@ -5,7 +5,7 @@ class ArchiveTraceWorker
include PipelineBackgroundQueue
def perform(job_id)
- Ci::Build.find_by(id: job_id).try do |job|
+ Ci::Build.without_archived_trace.find_by(id: job_id).try do |job|
job.trace.archive!
end
end
diff --git a/app/workers/ci/archive_traces_cron_worker.rb b/app/workers/ci/archive_traces_cron_worker.rb
index 7016edde698..7d4e9660a4e 100644
--- a/app/workers/ci/archive_traces_cron_worker.rb
+++ b/app/workers/ci/archive_traces_cron_worker.rb
@@ -12,6 +12,7 @@ module Ci
Ci::Build.finished.with_live_trace.find_each(batch_size: 100) do |build|
begin
build.trace.archive!
+ rescue ::Gitlab::Ci::Trace::AlreadyArchivedError
rescue => e
failed_archive_counter.increment
Rails.logger.error "Failed to archive stale live trace. id: #{build.id} message: #{e.message}"
diff --git a/app/workers/ci/build_trace_chunk_flush_worker.rb b/app/workers/ci/build_trace_chunk_flush_worker.rb
index 6376c6d32cf..9dbf2e5e1ac 100644
--- a/app/workers/ci/build_trace_chunk_flush_worker.rb
+++ b/app/workers/ci/build_trace_chunk_flush_worker.rb
@@ -7,7 +7,7 @@ module Ci
def perform(build_trace_chunk_id)
::Ci::BuildTraceChunk.find_by(id: build_trace_chunk_id).try do |build_trace_chunk|
- build_trace_chunk.use_database!
+ build_trace_chunk.persist_data!
end
end
end
diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index f9f0efb302a..12706613ac2 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -15,14 +15,14 @@ class EmailReceiverWorker
private
- def handle_failure(raw, e)
- Rails.logger.warn("Email can not be processed: #{e}\n\n#{raw}")
+ def handle_failure(raw, error)
+ Rails.logger.warn("Email can not be processed: #{error}\n\n#{raw}")
return unless raw.present?
can_retry = false
reason =
- case e
+ case error
when Gitlab::Email::UnknownIncomingEmail
"We couldn't figure out what the email is for. Please create your issue or comment through the web interface."
when Gitlab::Email::SentNotificationNotFoundError
@@ -42,7 +42,7 @@ class EmailReceiverWorker
"The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member."
when Gitlab::Email::InvalidRecordError
can_retry = true
- e.message
+ error.message
end
if reason
diff --git a/app/workers/object_storage/migrate_uploads_worker.rb b/app/workers/object_storage/migrate_uploads_worker.rb
index a3ecfa8e711..01d03ec7888 100644
--- a/app/workers/object_storage/migrate_uploads_worker.rb
+++ b/app/workers/object_storage/migrate_uploads_worker.rb
@@ -1,6 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/LineLength
-# rubocop:disable Style/Documentation
module ObjectStorage
class MigrateUploadsWorker
diff --git a/app/workers/object_storage_upload_worker.rb b/app/workers/object_storage_upload_worker.rb
deleted file mode 100644
index f17980a83d8..00000000000
--- a/app/workers/object_storage_upload_worker.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-# @Deprecated - remove once the `object_storage_upload` queue is empty
-# The queue has been renamed `object_storage:object_storage_background_upload`
-#
-class ObjectStorageUploadWorker
- include ApplicationWorker
-
- sidekiq_options retry: 5
-
- def perform(uploader_class_name, subject_class_name, file_field, subject_id)
- uploader_class = uploader_class_name.constantize
- subject_class = subject_class_name.constantize
-
- return unless uploader_class < ObjectStorage::Concern
- return unless uploader_class.object_store_enabled?
- return unless uploader_class.background_upload_enabled?
-
- subject = subject_class.find(subject_id)
- uploader = subject.public_send(file_field) # rubocop:disable GitlabSecurity/PublicSend
- uploader.migrate!(ObjectStorage::Store::REMOTE)
- end
-end
diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb
index 051382a08a9..07559ea479b 100644
--- a/app/workers/repository_check/batch_worker.rb
+++ b/app/workers/repository_check/batch_worker.rb
@@ -4,9 +4,11 @@ module RepositoryCheck
class BatchWorker
include ApplicationWorker
include RepositoryCheckQueue
+ include ExclusiveLeaseGuard
RUN_TIME = 3600
BATCH_SIZE = 10_000
+ LEASE_TIMEOUT = 1.hour
attr_reader :shard_name
@@ -16,6 +18,20 @@ module RepositoryCheck
return unless Gitlab::CurrentSettings.repository_checks_enabled
return unless Gitlab::ShardHealthCache.healthy_shard?(shard_name)
+ try_obtain_lease do
+ perform_repository_checks
+ end
+ end
+
+ def lease_timeout
+ LEASE_TIMEOUT
+ end
+
+ def lease_key
+ "repository_check_batch_worker:#{shard_name}"
+ end
+
+ def perform_repository_checks
start = Time.now
# This loop will break after a little more than one hour ('a little
@@ -26,7 +42,7 @@ module RepositoryCheck
project_ids.each do |project_id|
break if Time.now - start >= RUN_TIME
- next unless try_obtain_lease(project_id)
+ next unless try_obtain_lease_for_project(project_id)
SingleRepositoryWorker.new.perform(project_id)
end
@@ -60,7 +76,7 @@ module RepositoryCheck
Project.where(repository_storage: shard_name)
end
- def try_obtain_lease(id)
+ def try_obtain_lease_for_project(id)
# Use a 24-hour timeout because on servers/projects where 'git fsck' is
# super slow we definitely do not want to run it twice in parallel.
Gitlab::ExclusiveLease.new(
diff --git a/app/workers/repository_check/dispatch_worker.rb b/app/workers/repository_check/dispatch_worker.rb
index 891a273afd7..96634f09a15 100644
--- a/app/workers/repository_check/dispatch_worker.rb
+++ b/app/workers/repository_check/dispatch_worker.rb
@@ -3,13 +3,22 @@ module RepositoryCheck
include ApplicationWorker
include CronjobQueue
include ::EachShardWorker
+ include ExclusiveLeaseGuard
+
+ LEASE_TIMEOUT = 1.hour
def perform
return unless Gitlab::CurrentSettings.repository_checks_enabled
- each_eligible_shard do |shard_name|
- RepositoryCheck::BatchWorker.perform_async(shard_name)
+ try_obtain_lease do
+ each_eligible_shard do |shard_name|
+ RepositoryCheck::BatchWorker.perform_async(shard_name)
+ end
end
end
+
+ def lease_timeout
+ LEASE_TIMEOUT
+ end
end
end
diff --git a/changelogs/unreleased/31583-osw-gfm-complete-status-indication.yml b/changelogs/unreleased/31583-osw-gfm-complete-status-indication.yml
new file mode 100644
index 00000000000..6f2cf275592
--- /dev/null
+++ b/changelogs/unreleased/31583-osw-gfm-complete-status-indication.yml
@@ -0,0 +1,5 @@
+---
+title: Present state indication on GFM preview
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/36234-nav-add-groups-dropdown.yml b/changelogs/unreleased/36234-nav-add-groups-dropdown.yml
new file mode 100644
index 00000000000..86a24102665
--- /dev/null
+++ b/changelogs/unreleased/36234-nav-add-groups-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: Add dropdown to Groups link in top bar
+merge_request: 18280
+author:
+type: added
diff --git a/changelogs/unreleased/41671-fixing-milestone-date-change-when-editing.yml b/changelogs/unreleased/41671-fixing-milestone-date-change-when-editing.yml
new file mode 100644
index 00000000000..c6a0dc4f129
--- /dev/null
+++ b/changelogs/unreleased/41671-fixing-milestone-date-change-when-editing.yml
@@ -0,0 +1,5 @@
+---
+title: "Fixing milestone date change when editing"
+merge_request: 20279
+author: Orlando Del Aguila
+type: fixed \ No newline at end of file
diff --git a/changelogs/unreleased/46246-gitlab-project-export-should-use-object-storage.yml b/changelogs/unreleased/46246-gitlab-project-export-should-use-object-storage.yml
new file mode 100644
index 00000000000..908c7a238fd
--- /dev/null
+++ b/changelogs/unreleased/46246-gitlab-project-export-should-use-object-storage.yml
@@ -0,0 +1,5 @@
+---
+title: Add Object Storage to project export
+merge_request: 20105
+author:
+type: added
diff --git a/changelogs/unreleased/46396-recognise-when-a-user-is-trying-to-validate-a-private-ssh-key.yml b/changelogs/unreleased/46396-recognise-when-a-user-is-trying-to-validate-a-private-ssh-key.yml
new file mode 100644
index 00000000000..64bbecf3405
--- /dev/null
+++ b/changelogs/unreleased/46396-recognise-when-a-user-is-trying-to-validate-a-private-ssh-key.yml
@@ -0,0 +1,5 @@
+---
+title: Update new SSH key page to improve key input validation
+merge_request: 19997
+author:
+type: other
diff --git a/changelogs/unreleased/48661-node-6-and-7-compatibility-broken-by-recent-monaco-editor-upgrade.yml b/changelogs/unreleased/48661-node-6-and-7-compatibility-broken-by-recent-monaco-editor-upgrade.yml
new file mode 100644
index 00000000000..36a4b5f754d
--- /dev/null
+++ b/changelogs/unreleased/48661-node-6-and-7-compatibility-broken-by-recent-monaco-editor-upgrade.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve compatibility issues with node 6
+merge_request: 20461
+author:
+type: fixed
diff --git a/changelogs/unreleased/48670-application-settings-may-not-be-invalidated-if-migrations-are-run.yml b/changelogs/unreleased/48670-application-settings-may-not-be-invalidated-if-migrations-are-run.yml
new file mode 100644
index 00000000000..f4267582f89
--- /dev/null
+++ b/changelogs/unreleased/48670-application-settings-may-not-be-invalidated-if-migrations-are-run.yml
@@ -0,0 +1,6 @@
+---
+title: Stop relying on migrations in the CacheableAttributes cache key and cache attributes
+ for 1 minute instead
+merge_request: 20389
+author:
+type: fixed
diff --git a/changelogs/unreleased/48677-also-check-auto_sign_in_with_provider.yml b/changelogs/unreleased/48677-also-check-auto_sign_in_with_provider.yml
new file mode 100644
index 00000000000..3021fe6b9c8
--- /dev/null
+++ b/changelogs/unreleased/48677-also-check-auto_sign_in_with_provider.yml
@@ -0,0 +1,5 @@
+---
+title: Load Devise with Omniauth when auto_sign_in_with_provider is configured
+merge_request: 20302
+author:
+type: fixed
diff --git a/changelogs/unreleased/48976-fix-sti-background-migration.yml b/changelogs/unreleased/48976-fix-sti-background-migration.yml
new file mode 100644
index 00000000000..e95536b213c
--- /dev/null
+++ b/changelogs/unreleased/48976-fix-sti-background-migration.yml
@@ -0,0 +1,6 @@
+---
+title: "[Rails5] Fix 'Invalid single-table inheritance type: Group is not a subclass
+ of Gitlab::BackgroundMigration::FixCrossProjectLabelLinks::Namespace'"
+merge_request: 20462
+author: "@blackst0ne"
+type: fixed
diff --git a/changelogs/unreleased/48978-fix-helm-installation-on-cluster.yml b/changelogs/unreleased/48978-fix-helm-installation-on-cluster.yml
new file mode 100644
index 00000000000..f786d9e2235
--- /dev/null
+++ b/changelogs/unreleased/48978-fix-helm-installation-on-cluster.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes base command used in Helm installations
+merge_request: 20471
+author:
+type: fixed
diff --git a/changelogs/unreleased/blackst0ne-rails5-activerecord-statementinvalid-mysql2-error-expression-1-of-select-list-is-not-in-group-by-clause.yml b/changelogs/unreleased/blackst0ne-rails5-activerecord-statementinvalid-mysql2-error-expression-1-of-select-list-is-not-in-group-by-clause.yml
new file mode 100644
index 00000000000..d9cccc49830
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-rails5-activerecord-statementinvalid-mysql2-error-expression-1-of-select-list-is-not-in-group-by-clause.yml
@@ -0,0 +1,5 @@
+---
+title: "[Rails5] Fix milestone GROUP BY query"
+merge_request: 20256
+author: "@blackst0ne"
+type: fixed
diff --git a/changelogs/unreleased/build-chunks-on-object-storage.yml b/changelogs/unreleased/build-chunks-on-object-storage.yml
new file mode 100644
index 00000000000..9f36dfee378
--- /dev/null
+++ b/changelogs/unreleased/build-chunks-on-object-storage.yml
@@ -0,0 +1,6 @@
+---
+title: Use object storage as the first class persistable store for new live trace
+ architecture
+merge_request: 19515
+author:
+type: changed
diff --git a/changelogs/unreleased/bvl-preload-parents-after-pagination.yml b/changelogs/unreleased/bvl-preload-parents-after-pagination.yml
new file mode 100644
index 00000000000..ff3d4716d34
--- /dev/null
+++ b/changelogs/unreleased/bvl-preload-parents-after-pagination.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce the number of queries when searching for groups
+merge_request: 20398
+author:
+type: performance
diff --git a/changelogs/unreleased/fix-search-bar.yml b/changelogs/unreleased/fix-search-bar.yml
new file mode 100644
index 00000000000..d4c0c1efddf
--- /dev/null
+++ b/changelogs/unreleased/fix-search-bar.yml
@@ -0,0 +1,5 @@
+---
+title: Fix search bar text input alignment
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-trace-archive-cron-worker-race-condition.yml b/changelogs/unreleased/fix-trace-archive-cron-worker-race-condition.yml
new file mode 100644
index 00000000000..ca8f4252008
--- /dev/null
+++ b/changelogs/unreleased/fix-trace-archive-cron-worker-race-condition.yml
@@ -0,0 +1,5 @@
+---
+title: Check if archived trace exist before archive it
+merge_request: 20297
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-43565-wrong-role-displayed.yml b/changelogs/unreleased/fj-43565-wrong-role-displayed.yml
new file mode 100644
index 00000000000..67ff25bc50c
--- /dev/null
+++ b/changelogs/unreleased/fj-43565-wrong-role-displayed.yml
@@ -0,0 +1,5 @@
+---
+title: Fix wrong role badge displayed in projects dashboard
+merge_request: 20374
+author:
+type: fixed
diff --git a/changelogs/unreleased/fl-mr-refactor-performance-improvements.yml b/changelogs/unreleased/fl-mr-refactor-performance-improvements.yml
new file mode 100644
index 00000000000..649d1b5da67
--- /dev/null
+++ b/changelogs/unreleased/fl-mr-refactor-performance-improvements.yml
@@ -0,0 +1,5 @@
+---
+title: Structure getters for diff Store properly and adds specs
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/ide-merge-request-info.yml b/changelogs/unreleased/ide-merge-request-info.yml
new file mode 100644
index 00000000000..104f48ae309
--- /dev/null
+++ b/changelogs/unreleased/ide-merge-request-info.yml
@@ -0,0 +1,5 @@
+---
+title: Display merge request title & description in Web IDE
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/jprovazn-delete-upload-worker.yml b/changelogs/unreleased/jprovazn-delete-upload-worker.yml
new file mode 100644
index 00000000000..52916482d32
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-delete-upload-worker.yml
@@ -0,0 +1,5 @@
+---
+title: Remove deprecated object_storage_upload queue.
+merge_request:
+author:
+type: removed
diff --git a/changelogs/unreleased/jprovazn-label-links-update.yml b/changelogs/unreleased/jprovazn-label-links-update.yml
new file mode 100644
index 00000000000..75fb46ede6b
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-label-links-update.yml
@@ -0,0 +1,5 @@
+---
+title: Fix cross-project label references.
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/jprovazn-upload-symlink.yml b/changelogs/unreleased/jprovazn-upload-symlink.yml
new file mode 100644
index 00000000000..265791d332f
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-upload-symlink.yml
@@ -0,0 +1,5 @@
+---
+title: Add /uploads subdirectory to allowed upload paths.
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/perf-wiki-pattern-once.yml b/changelogs/unreleased/perf-wiki-pattern-once.yml
new file mode 100644
index 00000000000..fb4085a06ae
--- /dev/null
+++ b/changelogs/unreleased/perf-wiki-pattern-once.yml
@@ -0,0 +1,5 @@
+---
+title: Improve render performance of large wiki pages
+merge_request: 20465
+author: Peter Leitzen
+type: performance
diff --git a/changelogs/unreleased/rails5-fix-48977.yml b/changelogs/unreleased/rails5-fix-48977.yml
new file mode 100644
index 00000000000..bfd86f20e24
--- /dev/null
+++ b/changelogs/unreleased/rails5-fix-48977.yml
@@ -0,0 +1,5 @@
+---
+title: Rails5 fix mysql milliseconds problem in specs
+merge_request: 20464
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-mysql-fix-pr-importer-spec.yml b/changelogs/unreleased/rails5-mysql-fix-pr-importer-spec.yml
new file mode 100644
index 00000000000..afd9865ee45
--- /dev/null
+++ b/changelogs/unreleased/rails5-mysql-fix-pr-importer-spec.yml
@@ -0,0 +1,5 @@
+---
+title: Rails5 mysql fix milliseconds problem in pull request importer spec
+merge_request: 20475
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/rails5-update-gemfile-lock.yml b/changelogs/unreleased/rails5-update-gemfile-lock.yml
new file mode 100644
index 00000000000..58931587fff
--- /dev/null
+++ b/changelogs/unreleased/rails5-update-gemfile-lock.yml
@@ -0,0 +1,5 @@
+---
+title: Update Gemfile.rails5.lock with latest Gemfile.lock changes
+merge_request: 20466
+author: Jasper Maes
+type: fixed
diff --git a/changelogs/unreleased/remove-trace-efficiently.yml b/changelogs/unreleased/remove-trace-efficiently.yml
new file mode 100644
index 00000000000..a6ba6d28dce
--- /dev/null
+++ b/changelogs/unreleased/remove-trace-efficiently.yml
@@ -0,0 +1,5 @@
+---
+title: Remove redundant query when removing trace
+merge_request: 20324
+author:
+type: performance
diff --git a/changelogs/unreleased/rosulk-patch-12.yml b/changelogs/unreleased/rosulk-patch-12.yml
deleted file mode 100644
index 9637c88d1a4..00000000000
--- a/changelogs/unreleased/rosulk-patch-12.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Flex issue board columns
-merge_request: 19250
-author: Roman Rosluk
-type: changed
diff --git a/changelogs/unreleased/sh-fix-issue-47797-ce.yml b/changelogs/unreleased/sh-fix-issue-47797-ce.yml
new file mode 100644
index 00000000000..456d96acacb
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-issue-47797-ce.yml
@@ -0,0 +1,5 @@
+---
+title: Fix handling of annotated tags when Gitaly is not in use
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/upgrade-hamlit-for-ruby25.yml b/changelogs/unreleased/upgrade-hamlit-for-ruby25.yml
new file mode 100644
index 00000000000..39e10121507
--- /dev/null
+++ b/changelogs/unreleased/upgrade-hamlit-for-ruby25.yml
@@ -0,0 +1,5 @@
+---
+title: 'Update hamlit to fix ruby 2.5 incompatibilities, fixes #42045'
+merge_request:
+author: Matthew Dawson
+type: fixed
diff --git a/config/application.rb b/config/application.rb
index 97bc86b3e3a..0304f466734 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -1,4 +1,4 @@
-require File.expand_path('../boot', __FILE__)
+require File.expand_path('boot', __dir__)
require 'rails/all'
@@ -211,7 +211,7 @@ module Gitlab
next unless name.include?('namespace_project')
define_method(name.sub('namespace_project', 'project')) do |project, *args|
- send(name, project&.namespace, project, *args) # rubocop:disable GitlabSecurity/PublicSend
+ send(name, project&.namespace, project, *args)
end
end
end
diff --git a/config/environment.rb b/config/environment.rb
index 487a4564b47..5d35937f7c6 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -4,7 +4,7 @@
if %w[1 true].include?(ENV["RAILS5"])
require_relative 'application'
else
- require File.expand_path('../application', __FILE__)
+ require File.expand_path('application', __dir__)
end
# Initialize the rails application
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 45a8c1add3e..23790b84e3c 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -39,7 +39,7 @@ Rails.application.configure do
config.action_mailer.delivery_method = :letter_opener_web
# Don't make a mess when bootstrapping a development environment
config.action_mailer.perform_deliveries = (ENV['BOOTSTRAP'] != '1')
- config.action_mailer.preview_path = 'spec/mailers/previews'
+ config.action_mailer.preview_path = 'app/mailers/previews'
config.eager_load = false
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index e0779112850..ab109f5d04f 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -160,6 +160,9 @@ production: &base
# aws_access_key_id: AWS_ACCESS_KEY_ID
# aws_secret_access_key: AWS_SECRET_ACCESS_KEY
# region: us-east-1
+ # aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
+ # endpoint: 'https://s3.amazonaws.com' # default: nil - Useful for S3 compliant services such as DigitalOcean Spaces
+
## Git LFS
lfs:
@@ -180,6 +183,7 @@ production: &base
# Use the following options to configure an AWS compatible host
# host: 'localhost' # default: s3.amazonaws.com
# endpoint: 'http://127.0.0.1:9000' # default: nil
+ # aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
# path_style: true # Use 'host/bucket_name/object' instead of 'bucket_name.host/object'
## Uploads (attachments, avatars, etc...)
@@ -197,6 +201,7 @@ production: &base
provider: AWS
aws_access_key_id: AWS_ACCESS_KEY_ID
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
+ aws_signature_version: 4 # For creation of signed URLs. Set to 2 if provider does not support v4.
region: us-east-1
# host: 'localhost' # default: s3.amazonaws.com
# endpoint: 'http://127.0.0.1:9000' # default: nil
diff --git a/config/initializers/active_record_locking.rb b/config/initializers/active_record_locking.rb
index 0861544c5a4..21ff323927b 100644
--- a/config/initializers/active_record_locking.rb
+++ b/config/initializers/active_record_locking.rb
@@ -17,7 +17,7 @@ module ActiveRecord
lock_col = self.class.locking_column
- previous_lock_value = send(lock_col).to_i # rubocop:disable GitlabSecurity/PublicSend
+ previous_lock_value = send(lock_col).to_i
# This line is added as a patch
previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0
@@ -47,7 +47,7 @@ module ActiveRecord
# If something went wrong, revert the version.
rescue Exception
- send(lock_col + '=', previous_lock_value) # rubocop:disable GitlabSecurity/PublicSend
+ send(lock_col + '=', previous_lock_value)
raise
end
end
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index d051b699102..e5772c33307 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -219,7 +219,7 @@ Devise.setup do |config|
end
end
- if Gitlab.config.omniauth.enabled
+ if Gitlab::OmniauthInitializer.enabled?
Gitlab::OmniauthInitializer.new(config).execute(Gitlab.config.omniauth.providers)
end
end
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index a7fa926a853..c558eb28ced 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -17,7 +17,7 @@ OmniAuth.config.before_request_phase do |env|
Gitlab::RequestForgeryProtection.call(env)
end
-if Gitlab.config.omniauth.enabled
+if Gitlab::OmniauthInitializer.enabled?
provider_names = Gitlab.config.omniauth.providers.map(&:name)
Gitlab::Auth.omniauth_setup_providers(provider_names)
end
diff --git a/db/migrate/20180625113853_create_import_export_uploads.rb b/db/migrate/20180625113853_create_import_export_uploads.rb
new file mode 100644
index 00000000000..be42304b0ae
--- /dev/null
+++ b/db/migrate/20180625113853_create_import_export_uploads.rb
@@ -0,0 +1,16 @@
+class CreateImportExportUploads < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :import_export_uploads do |t|
+ t.datetime_with_timezone :updated_at, null: false
+
+ t.references :project, index: true, foreign_key: { on_delete: :cascade }, unique: true
+
+ t.text :import_file
+ t.text :export_file
+ end
+
+ add_index :import_export_uploads, :updated_at
+ end
+end
diff --git a/db/post_migrate/20180702120647_enqueue_fix_cross_project_label_links.rb b/db/post_migrate/20180702120647_enqueue_fix_cross_project_label_links.rb
new file mode 100644
index 00000000000..59aa41adede
--- /dev/null
+++ b/db/post_migrate/20180702120647_enqueue_fix_cross_project_label_links.rb
@@ -0,0 +1,30 @@
+class EnqueueFixCrossProjectLabelLinks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ BATCH_SIZE = 100
+ MIGRATION = 'FixCrossProjectLabelLinks'
+ DELAY_INTERVAL = 5.minutes
+
+ disable_ddl_transaction!
+
+ class Label < ActiveRecord::Base
+ self.table_name = 'labels'
+ end
+
+ class Namespace < ActiveRecord::Base
+ self.table_name = 'namespaces'
+
+ include ::EachBatch
+
+ default_scope { where(type: 'Group', id: Label.where(type: 'GroupLabel').select('distinct group_id')) }
+ end
+
+ def up
+ queue_background_migration_jobs_by_range_at_intervals(Namespace, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c9aaf80f059..1898dfc6022 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20180629191052) do
+ActiveRecord::Schema.define(version: 20180702120647) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -949,6 +949,16 @@ ActiveRecord::Schema.define(version: 20180629191052) do
add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
+ create_table "import_export_uploads", force: :cascade do |t|
+ t.datetime_with_timezone "updated_at", null: false
+ t.integer "project_id"
+ t.text "import_file"
+ t.text "export_file"
+ end
+
+ add_index "import_export_uploads", ["project_id"], name: "index_import_export_uploads_on_project_id", using: :btree
+ add_index "import_export_uploads", ["updated_at"], name: "index_import_export_uploads_on_updated_at", using: :btree
+
create_table "internal_ids", id: :bigserial, force: :cascade do |t|
t.integer "project_id"
t.integer "usage", null: false
@@ -2252,6 +2262,7 @@ ActiveRecord::Schema.define(version: 20180629191052) do
add_foreign_key "gpg_signatures", "gpg_keys", on_delete: :nullify
add_foreign_key "gpg_signatures", "projects", on_delete: :cascade
add_foreign_key "group_custom_attributes", "namespaces", column: "group_id", on_delete: :cascade
+ add_foreign_key "import_export_uploads", "projects", on_delete: :cascade
add_foreign_key "internal_ids", "namespaces", name: "fk_162941d509", on_delete: :cascade
add_foreign_key "internal_ids", "projects", on_delete: :cascade
add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade
diff --git a/doc/administration/job_traces.md b/doc/administration/job_traces.md
index f1c5b194f4c..24d1a3fd151 100644
--- a/doc/administration/job_traces.md
+++ b/doc/administration/job_traces.md
@@ -77,10 +77,10 @@ cloud-native, for example on Kubernetes.
The data flow is the same as described in the [data flow section](#data-flow)
with one change: _the stored path of the first two phases is different_. This new live
-trace architecture stores chunks of traces in Redis and the database instead of
+trace architecture stores chunks of traces in Redis and a persistent store (object storage or database) instead of
file storage. Redis is used as first-class storage, and it stores up-to 128KB
-of data. Once the full chunk is sent, it is flushed to database. After a while,
-the data in Redis and database will be archived to [object storage](#uploading-traces-to-object-storage).
+of data. Once the full chunk is sent, it is flushed a persistent store, either object storage(temporary directory) or database.
+After a while, the data in Redis and a persitent store will be archived to [object storage](#uploading-traces-to-object-storage).
The data are stored in the following Redis namespace: `Gitlab::Redis::SharedState`.
@@ -89,11 +89,11 @@ Here is the detailed data flow:
1. GitLab Runner picks a job from GitLab
1. GitLab Runner sends a piece of trace to GitLab
1. GitLab appends the data to Redis
-1. Once the data in Redis reach 128KB, the data is flushed to the database.
+1. Once the data in Redis reach 128KB, the data is flushed to a persistent store (object storage or the database).
1. The above steps are repeated until the job is finished.
1. Once the job is finished, GitLab schedules a Sidekiq worker to archive the trace.
1. The Sidekiq worker archives the trace to object storage and cleans up the trace
- in Redis and the database.
+ in Redis and a persistent store (object storage or the database).
### Enabling live trace
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 9b1297ca4ba..c0a281382a5 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -49,8 +49,8 @@ supporting custom domains a secondary IP is not needed.
Before proceeding with the Pages configuration, you will need to:
-1. Have a separate domain under which the GitLab Pages will be served. In this
- document we assume that to be `example.io`.
+1. Have an exclusive root domain for serving GitLab Pages. Note that you cannot
+ use a subdomain of your GitLab's instance domain.
1. Configure a **wildcard DNS record**.
1. (Optional) Have a **wildcard certificate** for that domain if you decide to
serve Pages under HTTPS.
diff --git a/doc/administration/raketasks/project_import_export.md b/doc/administration/raketasks/project_import_export.md
index ecc4ac6b29b..7bd765a35e0 100644
--- a/doc/administration/raketasks/project_import_export.md
+++ b/doc/administration/raketasks/project_import_export.md
@@ -30,5 +30,12 @@ sudo gitlab-rake gitlab:import_export:data
bundle exec rake gitlab:import_export:data RAILS_ENV=production
```
+In order to enable Object Storage on the Export, you can use the [feature flag][feature-flags]:
+
+```
+import_export_object_storage
+```
+
[ce-3050]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3050
+[feature-flags]: https://docs.gitlab.com/ee/api/features.html
[tmp]: ../../development/shared_files.md
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 2057ed3588a..34c2dd7b34d 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -358,6 +358,7 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `merge_request_iid` (required) - The internal ID of the merge request
+- `render_html` (optional) - If `true` response includes rendered HTML for title and description
```json
{
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 2f991d86614..9f6476edc34 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -64,7 +64,7 @@ future GitLab releases.**
| **CI_JOB_NAME** | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` |
| **CI_JOB_STAGE** | 9.0 | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` |
| **CI_JOB_TOKEN** | 9.0 | 1.2 | Token used for authenticating with the GitLab Container Registry |
-| **CI_JOB_URL** | 11.0 | 0.5 | Job details URL |
+| **CI_JOB_URL** | 11.1 | 0.5 | Job details URL |
| **CI_REPOSITORY_URL** | 9.0 | all | The URL to clone the Git repository |
| **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab |
| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used |
@@ -82,7 +82,7 @@ future GitLab releases.**
| **CI_PROJECT_NAMESPACE** | 8.10 | 0.5 | The project namespace (username or groupname) that is currently being built |
| **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name |
| **CI_PROJECT_PATH_SLUG** | 9.3 | all | `$CI_PROJECT_PATH` lowercased and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
-| **CI_PIPELINE_URL** | 11.0 | 0.5 | Pipeline details URL |
+| **CI_PIPELINE_URL** | 11.1 | 0.5 | Pipeline details URL |
| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project |
| **CI_PROJECT_VISIBILITY** | 10.3 | all | The project visibility (internal, private, public) |
| **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry |
diff --git a/doc/development/emails.md b/doc/development/emails.md
index 73cac82caf0..35ada35babe 100644
--- a/doc/development/emails.md
+++ b/doc/development/emails.md
@@ -10,12 +10,12 @@ To view rendered emails "sent" in your development instance, visit
Rails provides a way to preview our mailer templates in HTML and plaintext using
dummy data.
-The previews live in [`spec/mailers/previews`][previews] and can be viewed at
+The previews live in [`app/mailers/previews`][previews] and can be viewed at
[`/rails/mailers`](http://localhost:3000/rails/mailers).
See the [Rails guides] for more info.
-[previews]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/spec/mailers/previews
+[previews]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/mailers/previews
[Rails guides]: http://guides.rubyonrails.org/action_mailer_basics.html#previewing-emails
## Incoming email
diff --git a/doc/development/fe_guide/development_process.md b/doc/development/fe_guide/development_process.md
index d240dbe8b02..905668eef58 100644
--- a/doc/development/fe_guide/development_process.md
+++ b/doc/development/fe_guide/development_process.md
@@ -1,6 +1,6 @@
# Frontend Development Process
-You can find more about the organization of the frontend team in the [handbook](https://about.gitlab.com/handbook/frontend/).
+You can find more about the organization of the frontend team in the [handbook](https://about.gitlab.com/handbook/engineering/frontend/).
## Development Checklist
@@ -34,7 +34,7 @@ Please use your best judgement when to use it and please contribute new points t
- [ ] **Cookie Mode** Think about hiding the feature behind a cookie flag if the implementation is on top of existing features
- [ ] **New route** Are you refactoring something big then you might consider adding a new route where you implement the new feature and when finished delete the current route and rename the new one. (for example 'merge_request' and 'new_merge_request')
- [ ] **Setup** Is there any specific setup needed for your implementation (for example a kubernetes cluster)? Then let everyone know if it is not already mentioned where they can find documentation (if it doesn't exist - create it)
-- [ ] **Security** Are there any new security relevant implementations? Then please contact the security team for an app security review. If you are not sure ask our [domain expert](https://about.gitlab.com/handbook/frontend/#frontend-domain-experts)
+- [ ] **Security** Are there any new security relevant implementations? Then please contact the security team for an app security review. If you are not sure ask our [domain expert](https://about.gitlab.com/handbook/engineering/frontend/#frontend-domain-experts)
#### During development
@@ -51,7 +51,7 @@ Please use your best judgement when to use it and please contribute new points t
- [ ] **Performance** Have you checked performance? For example do the same thing with 500 comments instead of 1. Document the tests and possible findings in the MR so a reviewer can directly see it.
- [ ] Have you tested with a variety of our [supported browsers](../../install/requirements.md#supported-web-browsers)? You can use [browserstack](https://www.browserstack.com/) to be able to access a wide variety of browsers and operating systems.
- [ ] Did you check the mobile view?
-- [ ] Check the built webpack bundle (For the report run `WEBPACK_REPORT=true gdk run`, then open `webpack-report/index.html`) if we have unnecessary bloat due to wrong references, including libraries multiple times, etc.. If you need help contact the webpack [domain expert](https://about.gitlab.com/handbook/frontend/#frontend-domain-experts)
+- [ ] Check the built webpack bundle (For the report run `WEBPACK_REPORT=true gdk run`, then open `webpack-report/index.html`) if we have unnecessary bloat due to wrong references, including libraries multiple times, etc.. If you need help contact the webpack [domain expert](https://about.gitlab.com/handbook/engineering/frontend/#frontend-domain-experts)
- [ ] **Tests** Not only greenfield tests - Test also all bad cases that come to your mind.
- [ ] If you have multiple MR's then also smoke test against the final merge.
- [ ] Are there any big changes on how and especially how frequently we use the API then let production know about it
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index f7d703b8f0b..6323275426f 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -281,7 +281,7 @@ Now that the new content is marked for translation, we need to update the PO
files with the following command:
```sh
-bin/rake gettext:find
+bin/rake gettext:regenerate
```
This command will update the `locale/gitlab.pot` file with the newly externalized
@@ -292,16 +292,6 @@ file in. Once the changes are on master, they will be picked up by
If there are merge conflicts in the `gitlab.pot` file, you can delete the file
and regenerate it using the same command. Confirm that you are not deleting any strings accidentally by looking over the diff.
-The command also updates the translation files for each language: `locale/*/gitlab.po`
-These changes can be discarded, the language files will be updated by Crowdin
-automatically.
-
-Discard all of them at once like this:
-
-```sh
-git checkout locale/*/gitlab.po
-```
-
### Validating PO files
To make sure we keep our translation files up to date, there's a linter that is
diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md
index 9d0d7348df9..ca8ebcdc984 100644
--- a/doc/development/i18n/proofreader.md
+++ b/doc/development/i18n/proofreader.md
@@ -11,6 +11,7 @@ are very appreciative of the work done by translators and proofreaders!
- Chinese Traditional
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
- Weizhe Ding - [GitLab](https://gitlab.com/d.weizhe), [Crowdin](https://crowdin.com/profile/d.weizhe)
+ - Yi-Jyun Pan - [GitLab](https://gitlab.com/pan93412), [Crowdin](https://crowdin.com/profile/pan93412)
- Chinese Traditional, Hong Kong
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
- Dutch
diff --git a/doc/development/licensing.md b/doc/development/licensing.md
index c06bc0d4731..ddaf636a742 100644
--- a/doc/development/licensing.md
+++ b/doc/development/licensing.md
@@ -60,7 +60,7 @@ Libraries with the following licenses are acceptable for use:
## Unacceptable Licenses
-Libraries with the following licenses are unacceptable for use:
+Libraries with the following licenses require legal approval for use:
- [GNU GPL][GPL] (version 1, [version 2][GPLv2], [version 3][GPLv3], or any future versions): GPL-licensed libraries cannot be linked to from non-GPL projects.
- [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects.
@@ -68,6 +68,26 @@ Libraries with the following licenses are unacceptable for use:
- [Facebook BSD + PATENTS][Facebook]: is a 3-clause BSD license with a patent grant that has been deemed [Category X][x-list] by the Apache foundation.
- [WTFPL][WTFPL]: is a public domain dedication [rejected by the OSI (3.2)][WTFPL-OSI]. Also has a strong language which is not in accordance with our diversity policy.
+## GPL Cooperation Commitment
+
+Before filing or continuing to prosecute any legal proceeding or claim (other than a Defensive Action) arising from termination of a Covered License, GitLab commits to extend to the person or entity (“you”) accused of violating the Covered License the following provisions regarding cure and reinstatement, taken from GPL version 3. As used here, the term ‘this License’ refers to the specific Covered License being enforced.
+
+However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation.
+
+Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice.
+
+GitLab intends this Commitment to be irrevocable, and binding and enforceable against GitLab and assignees of or successors to GitLab’s copyrights.
+
+GitLab may modify this Commitment by publishing a new edition on this page or a successor location.
+
+Definitions
+
+‘Covered License’ means the GNU General Public License, version 2 (GPLv2), the GNU Lesser General Public License, version 2.1 (LGPLv2.1), or the GNU Library General Public License, version 2 (LGPLv2), all as published by the Free Software Foundation.
+
+‘Defensive Action’ means a legal proceeding or claim that GitLab brings against you in response to a prior proceeding or claim initiated by you or your affiliate.
+
+GitLab means GitLab Inc. and its affiliates and subsidiaries.
+
## Requesting Approval for Licenses
Libraries that are not listed in the [Acceptable Licenses][Acceptable-Licenses] or [Unacceptable Licenses][Unacceptable-Licenses] list can be submitted to the legal team for review. Please email `legal@gitlab.com` with the details. After a decision has been made, the original requestor is responsible for updating this document.
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index 3b2b9c8c947..f8993653aec 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -172,6 +172,10 @@ object which can be treated like any other jasmine spy object.
Further documentation on the babel rewire pluign API can be found on
[its repository Readme doc](https://github.com/speedskater/babel-plugin-rewire#babel-plugin-rewire).
+#### Waiting in tests
+
+If you cannot avoid using [`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout) in tests, please use the [Jasmine mock clock](https://jasmine.github.io/api/2.9/Clock.html).
+
### Vue.js unit tests
See this [section][vue-test].
diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md
index 10e8059756d..dd8d95a3bca 100644
--- a/doc/gitlab-basics/create-project.md
+++ b/doc/gitlab-basics/create-project.md
@@ -30,6 +30,9 @@
idea to fill this in.
- Changing the **Visibility Level** modifies the project's
[viewing and access rights](../public_access/public_access.md) for users.
+ - Selecting the **Initialize repository with a README** option creates a
+ README so that the Git repository is initialized, has a default branch and
+ can be cloned.
1. Click **Create project**.
diff --git a/doc/gitlab-basics/img/create_new_project_info.png b/doc/gitlab-basics/img/create_new_project_info.png
index ce4f7d1204b..b4119dc046a 100644
--- a/doc/gitlab-basics/img/create_new_project_info.png
+++ b/doc/gitlab-basics/img/create_new_project_info.png
Binary files differ
diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md
index 2a14c0397ca..9094d1f2419 100644
--- a/doc/integration/bitbucket.md
+++ b/doc/integration/bitbucket.md
@@ -1,5 +1,8 @@
# Integrate your GitLab server with Bitbucket
+NOTE: **Note:**
+You need to [enable OmniAuth](omniauth.md) in order to use this.
+
Import projects from Bitbucket.org and login to your GitLab instance with your
Bitbucket.org account.
@@ -76,13 +79,13 @@ you to use.
sudo -u git -H editor /home/git/gitlab/config/gitlab.yml
```
-1. Follow the [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration)
- for initial settings.
1. Add the Bitbucket provider configuration:
For Omnibus packages:
```ruby
+ gitlab_rails['omniauth_enabled'] = true
+
gitlab_rails['omniauth_providers'] = [
{
"name" => "bitbucket",
@@ -96,10 +99,13 @@ you to use.
For installations from source:
```yaml
- - { name: 'bitbucket',
- app_id: 'BITBUCKET_APP_KEY',
- app_secret: 'BITBUCKET_APP_SECRET',
- url: 'https://bitbucket.org/' }
+ omniauth:
+ enabled: true
+ providers:
+ - { name: 'bitbucket',
+ app_id: 'BITBUCKET_APP_KEY',
+ app_secret: 'BITBUCKET_APP_SECRET',
+ url: 'https://bitbucket.org/' }
```
---
@@ -121,6 +127,9 @@ well, the user will be returned to GitLab and will be signed in.
Once the above configuration is set up, you can use Bitbucket to sign into
GitLab and [start importing your projects][bb-import].
+If you don't want to enable signing in with Bitbucket but just want to import
+projects from Bitbucket, you could [disable it in the admin panel](omniauth.md#enable-or-disable-sign-in-with-an-omniauth-provider-without-disabling-import-sources).
+
[init-oauth]: omniauth.md#initial-omniauth-configuration
[bb-import]: ../workflow/importing/import_projects_from_bitbucket.md
[bb-old]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-14-stable/doc/integration/bitbucket.md
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index db06efdae53..25f396bc9c4 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -1,5 +1,8 @@
# SAML OmniAuth Provider
+NOTE: **Note:**
+You need to [enable OmniAuth](omniauth.md) in order to use this.
+
GitLab can be configured to act as a SAML 2.0 Service Provider (SP). This allows
GitLab to consume assertions from a SAML 2.0 Identity Provider (IdP) such as
Microsoft ADFS to authenticate users.
@@ -15,33 +18,33 @@ in your SAML IdP:
For omnibus package:
```sh
- sudo editor /etc/gitlab/gitlab.rb
+ sudo editor /etc/gitlab/gitlab.rb
```
For installations from source:
```sh
- cd /home/git/gitlab
+ cd /home/git/gitlab
- sudo -u git -H editor config/gitlab.yml
+ sudo -u git -H editor config/gitlab.yml
```
-1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration)
- for initial settings.
-
1. To allow your users to use SAML to sign up without having to manually create
an account first, don't forget to add the following values to your configuration:
For omnibus package:
```ruby
- gitlab_rails['omniauth_allow_single_sign_on'] = ['saml']
- gitlab_rails['omniauth_block_auto_created_users'] = false
+ gitlab_rails['omniauth_enabled'] = true
+ gitlab_rails['omniauth_allow_single_sign_on'] = ['saml']
+ gitlab_rails['omniauth_block_auto_created_users'] = false
```
For installations from source:
```yaml
+ omniauth:
+ enabled: true
allow_single_sign_on: ["saml"]
block_auto_created_users: false
```
@@ -52,13 +55,13 @@ in your SAML IdP:
For omnibus package:
```ruby
- gitlab_rails['omniauth_auto_link_saml_user'] = true
+ gitlab_rails['omniauth_auto_link_saml_user'] = true
```
For installations from source:
```yaml
- auto_link_saml_user: true
+ auto_link_saml_user: true
```
1. Add the provider configuration:
@@ -66,35 +69,37 @@ in your SAML IdP:
For omnibus package:
```ruby
- gitlab_rails['omniauth_providers'] = [
- {
- name: 'saml',
- args: {
- assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
- idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
- idp_sso_target_url: 'https://login.example.com/idp',
- issuer: 'https://gitlab.example.com',
- name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
- },
- label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
- }
- ]
- ```
-
- For installations from source:
-
- ```yaml
- - {
- name: 'saml',
- args: {
+ gitlab_rails['omniauth_providers'] = [
+ {
+ name: 'saml',
+ args: {
assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
},
- label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
- }
+ label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
+ }
+ ]
+ ```
+
+ For installations from source:
+
+ ```yaml
+ omniauth:
+ providers:
+ - {
+ name: 'saml',
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
+ },
+ label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
+ }
```
1. Change the value for `assertion_consumer_service_url` to match the HTTPS endpoint
@@ -140,8 +145,8 @@ This setting is only available on GitLab 8.7 and above.
SAML login includes support for automatically identifying whether a user should
be considered an [external](../user/permissions.md) user based on the user's group
membership in the SAML identity provider. This feature **does not** allow you to
-automatically add users to GitLab [Groups](../user/group/index.md), it simply
-allows you to mark users as External if they are members of certain groups in the
+automatically add users to GitLab [Groups](../user/group/index.md), it simply
+allows you to mark users as External if they are members of certain groups in the
Identity Provider.
### Requirements
@@ -189,28 +194,28 @@ If you want some SAML authentication methods to count as 2FA on a per session ba
1. Edit `/etc/gitlab/gitlab.rb`:
```ruby
- gitlab_rails['omniauth_providers'] = [
- {
- name: 'saml',
- args: {
- assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
- idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
- idp_sso_target_url: 'https://login.example.com/idp',
- issuer: 'https://gitlab.example.com',
- name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
- upstream_two_factor_authn_contexts:
- %w(
- urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport
- urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS
- urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN
- )
-
- },
- label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
- }
- ]
+ gitlab_rails['omniauth_providers'] = [
+ {
+ name: 'saml',
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
+ upstream_two_factor_authn_contexts:
+ %w(
+ urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport
+ urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS
+ urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN
+ )
+
+ },
+ label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
+ }
+ ]
```
-
+
1. Save the file and [reconfigure][] GitLab for the changes to take effect.
---
@@ -218,40 +223,41 @@ If you want some SAML authentication methods to count as 2FA on a per session ba
**For installations from source:**
1. Edit `config/gitlab.yml`:
-
- ```yaml
- - {
- name: 'saml',
- args: {
- assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
- idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
- idp_sso_target_url: 'https://login.example.com/idp',
- issuer: 'https://gitlab.example.com',
- name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
- upstream_two_factor_authn_contexts:
- [
- 'urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport',
- 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS',
- 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN'
- ]
-
- },
- label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
- }
+
+ ```yaml
+ omniauth:
+ providers:
+ - {
+ name: 'saml',
+ args: {
+ assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+ idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+ idp_sso_target_url: 'https://login.example.com/idp',
+ issuer: 'https://gitlab.example.com',
+ name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent',
+ upstream_two_factor_authn_contexts:
+ [
+ 'urn:oasis:names:tc:SAML:2.0:ac:classes:CertificateProtectedTransport',
+ 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorOTPSMS',
+ 'urn:oasis:names:tc:SAML:2.0:ac:classes:SecondFactorIGTOKEN'
+ ]
+ },
+ label: 'Company Login' # optional label for SAML login button, defaults to "Saml"
+ }
```
-
+
1. Save the file and [restart GitLab][] for the changes ot take effect
-
+
In addition to the changes in GitLab, make sure that your Idp is returning the
`AuthnContext`. For example:
```xml
- <saml:AuthnStatement>
- <saml:AuthnContext>
- <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:MediumStrongCertificateProtectedTransport</saml:AuthnContextClassRef>
- </saml:AuthnContext>
- </saml:AuthnStatement>
+<saml:AuthnStatement>
+ <saml:AuthnContext>
+ <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:MediumStrongCertificateProtectedTransport</saml:AuthnContextClassRef>
+ </saml:AuthnContext>
+</saml:AuthnStatement>
```
## Customization
diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md
index e8f4c73120c..81ee7338e4e 100644
--- a/doc/public_access/public_access.md
+++ b/doc/public_access/public_access.md
@@ -30,6 +30,12 @@ in users.
Any logged in user will have [Guest](../user/permissions.md) permissions
on the repository.
+### Private projects
+
+Private projects can only be cloned and viewed by project members, and
+they will only appear to project members on the public access directory
+(`https://gitlab.example.com/public`).
+
### How to change project visibility
1. Go to your project's **Settings**
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index bab196e7609..63f0a654fcf 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -77,6 +77,8 @@ Note that Public SSH key may also be named as follows:
If you want to change the password of your SSH key pair, you can use
`ssh-keygen -p <keyname>`.
+## Adding a SSH key to your GitLab account
+
1. The next step is to copy the public SSH key as we will need it afterwards.
To copy your public SSH key to the clipboard, use the appropriate code below:
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 8e87c896a72..bd199b55a61 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -9,7 +9,7 @@
> For the best result, we encourage you to check this document out as rendered
by GitLab: [markdown.md]
-_GitLab uses (as of 11.1) the [CommonMark Ruby Library][commonmarker] for Markdown processing of all new issues, merge requests, comments, and other Markdown content in the GitLab system. Previous content and Markdown files `.md` in the repositories are still processed using the [Redcarpet Ruby library][redcarpet]._
+_GitLab uses (as of 11.1) the [CommonMark Ruby Library][commonmarker] for Markdown processing of all new issues, merge requests, comments, and other Markdown content in the GitLab system. Previous content, wiki pages and Markdown files (`.md`) in the repositories are still processed using the [Redcarpet Ruby library][redcarpet]._
_Where there are significant differences, we will try to call them out in this document._
@@ -22,7 +22,7 @@ You can use GFM in the following areas:
- merge requests
- milestones
- snippets (the snippet must be named with a `.md` extension)
-- wiki pages
+- wiki pages (currently only rendered by Redcarpet)
- markdown documents inside the repository (currently only rendered by Redcarpet)
You can also use other rich text files in GitLab. You might have to install a
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index b25b09f7b1f..baa6efe50d5 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -153,6 +153,13 @@ GitLab provides a one-click install for various applications which will be
added directly to your configured cluster. Those applications are needed for
[Review Apps](../../../ci/review_apps/index.md) and [deployments](../../../ci/environments.md).
+NOTE: **Note:**
+The applications will be installed in a dedicated namespace called
+`gitlab-managed-apps`. In case you have added an existing Kubernetes cluster
+with Tiller already installed, you should be careful as GitLab cannot
+detect it. By installing it via the applications will result into having it
+twice, which can lead to confusion during deployments.
+
| Application | GitLab version | Description |
| ----------- | :------------: | ----------- |
| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. |
@@ -206,7 +213,7 @@ kubectl get svc --all-namespaces -o jsonpath='{range.items[?(@.status.loadBalanc
> **Note**: Some Kubernetes clusters return a hostname instead, like [Amazon EKS](https://aws.amazon.com/eks/). For these platforms, run:
> ```bash
-> kubectl get service ingress-nginx-ingress-controller -n gitlab-managed-apps -o jsonpath="{.status.loadBalancer.ingress[0].hostname}"`.
+> kubectl get service ingress-nginx-ingress-controller -n gitlab-managed-apps -o jsonpath="{.status.loadBalancer.ingress[0].hostname}".
> ```
The output is the external IP address of your cluster. This information can then
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index b7aadc27e71..6769855b899 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -112,9 +112,9 @@ module API
can_push = params[:can_push].nil? ? deploy_keys_project.can_push : params[:can_push]
title = params[:title] || deploy_keys_project.deploy_key.title
- result = deploy_keys_project.update_attributes(can_push: can_push,
- deploy_key_attributes: { id: params[:key_id],
- title: title })
+ result = deploy_keys_project.update(can_push: can_push,
+ deploy_key_attributes: { id: params[:key_id],
+ title: title })
if result
present deploy_keys_project, with: Entities::DeployKeysProject
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 3a6e707fd5b..40df1e79bc7 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -532,6 +532,12 @@ module API
end
class MergeRequestBasic < ProjectEntity
+ expose :title_html, if: -> (_, options) { options[:render_html] } do |entity|
+ MarkupHelper.markdown_field(entity, :title)
+ end
+ expose :description_html, if: -> (_, options) { options[:render_html] } do |entity|
+ MarkupHelper.markdown_field(entity, :description)
+ end
expose :target_branch, :source_branch
expose :upvotes do |merge_request, options|
if options[:issuable_metadata]
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 0f46bc4c98e..2621c9f8fc2 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -232,6 +232,7 @@ module API
params do
requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request'
+ optional :render_html, type: Boolean, desc: 'Returns the description and title rendered HTML'
end
desc 'Get a single merge request' do
success Entities::MergeRequest
@@ -239,7 +240,7 @@ module API
get ':id/merge_requests/:merge_request_iid' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
- present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
+ present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project, render_html: params[:render_html]
end
desc 'Get the participants of a merge request' do
diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb
index 5ef4e9d530c..15c57a2fc02 100644
--- a/lib/api/project_export.rb
+++ b/lib/api/project_export.rb
@@ -23,9 +23,13 @@ module API
get ':id/export/download' do
path = user_project.export_project_path
- render_api_error!('404 Not found or has expired', 404) unless path
-
- present_disk_file!(path, File.basename(path), 'application/gzip')
+ if path
+ present_disk_file!(path, File.basename(path), 'application/gzip')
+ elsif user_project.export_project_object_exists?
+ present_carrierwave_file!(user_project.import_export_upload.export_file)
+ else
+ render_api_error!('404 Not found or has expired', 404)
+ end
end
desc 'Start export' do
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index 68921ae439b..4760a1c08d7 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -80,7 +80,7 @@ module API
update_params = declared_params(include_missing: false)
- if hook.update_attributes(update_params)
+ if hook.update(update_params)
present hook, with: Entities::ProjectHook
else
error!("Invalid url given", 422) if hook.errors[:url].present?
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 794fdab8f2b..553e8dff4b9 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -787,7 +787,7 @@ module API
service = user_project.find_or_initialize_service(service_slug.underscore)
service_params = declared_params(include_missing: false).merge(active: true)
- if service.update_attributes(service_params)
+ if service.update(service_params)
present service, with: Entities::ProjectService
else
render_api_error!('400 Bad Request', 400)
@@ -807,7 +807,7 @@ module API
hash.merge!(key => nil)
end
- unless service.update_attributes(attrs.merge(active: false))
+ unless service.update(attrs.merge(active: false))
render_api_error!('400 Bad Request', 400)
end
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index e8df2c5a74a..5aaaf104dff 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -186,7 +186,7 @@ module API
identity = user.identities.find_by(provider: identity_attrs[:provider])
if identity
- identity.update_attributes(identity_attrs)
+ identity.update(identity_attrs)
else
identity = user.identities.build(identity_attrs)
identity.save
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index 60a12dca9d3..b39b11009b3 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -100,6 +100,11 @@ module Banzai
ref_pattern = object_class.reference_pattern
link_pattern = object_class.link_reference_pattern
+ # Compile often used regexps only once outside of the loop
+ ref_pattern_anchor = /\A#{ref_pattern}\z/
+ link_pattern_start = /\A#{link_pattern}/
+ link_pattern_anchor = /\A#{link_pattern}\z/
+
nodes.each do |node|
if text_node?(node) && ref_pattern
replace_text_when_pattern_matches(node, ref_pattern) do |content|
@@ -108,7 +113,7 @@ module Banzai
elsif element_node?(node)
yield_valid_link(node) do |link, inner_html|
- if ref_pattern && link =~ /\A#{ref_pattern}\z/
+ if ref_pattern && link =~ ref_pattern_anchor
replace_link_node_with_href(node, link) do
object_link_filter(link, ref_pattern, link_content: inner_html)
end
@@ -118,7 +123,7 @@ module Banzai
next unless link_pattern
- if link == inner_html && inner_html =~ /\A#{link_pattern}/
+ if link == inner_html && inner_html =~ link_pattern_start
replace_link_node_with_text(node, link) do
object_link_filter(inner_html, link_pattern, link_reference: true)
end
@@ -126,7 +131,7 @@ module Banzai
next
end
- if link =~ /\A#{link_pattern}\z/
+ if link =~ link_pattern_anchor
replace_link_node_with_href(node, link) do
object_link_filter(link, link_pattern, link_content: inner_html, link_reference: true)
end
diff --git a/lib/banzai/filter/markdown_engines/common_mark.rb b/lib/banzai/filter/markdown_engines/common_mark.rb
index bc9597df894..dbb25280849 100644
--- a/lib/banzai/filter/markdown_engines/common_mark.rb
+++ b/lib/banzai/filter/markdown_engines/common_mark.rb
@@ -18,7 +18,7 @@ module Banzai
PARSE_OPTIONS = [
:FOOTNOTES, # parse footnotes.
:STRIKETHROUGH_DOUBLE_TILDE, # parse strikethroughs by double tildes (as redcarpet does).
- :VALIDATE_UTF8 # replace illegal sequences with the replacement character U+FFFD.
+ :VALIDATE_UTF8 # replace illegal sequences with the replacement character U+FFFD.
].freeze
# The `:GITHUB_PRE_LANG` option is not used intentionally because
diff --git a/lib/declarative_policy/base.rb b/lib/declarative_policy/base.rb
index 47542194497..da3fabba39b 100644
--- a/lib/declarative_policy/base.rb
+++ b/lib/declarative_policy/base.rb
@@ -119,8 +119,8 @@ module DeclarativePolicy
# a PolicyDsl which is used for registering the rule with
# this class. PolicyDsl will call back into Base.enable_when,
# Base.prevent_when, and Base.prevent_all_when.
- def rule(&b)
- rule = RuleDsl.new(self).instance_eval(&b)
+ def rule(&block)
+ rule = RuleDsl.new(self).instance_eval(&block)
PolicyDsl.new(self, rule)
end
@@ -222,8 +222,8 @@ module DeclarativePolicy
# computes the given ability and prints a helpful debugging output
# showing which
- def debug(ability, *a)
- runner(ability).debug(*a)
+ def debug(ability, *args)
+ runner(ability).debug(*args)
end
desc "Unknown user"
@@ -274,7 +274,7 @@ module DeclarativePolicy
#
# NOTE we can't use ||= here because the value might be the
# boolean `false`
- def cache(key, &b)
+ def cache(key)
return @cache[key] if cached?(key)
@cache[key] = yield
diff --git a/lib/declarative_policy/delegate_dsl.rb b/lib/declarative_policy/delegate_dsl.rb
index f544dffe888..ca2eb98e3e8 100644
--- a/lib/declarative_policy/delegate_dsl.rb
+++ b/lib/declarative_policy/delegate_dsl.rb
@@ -7,10 +7,10 @@ module DeclarativePolicy
@delegate_name = delegate_name
end
- def method_missing(m, *a, &b)
- return super unless a.empty? && !block_given?
+ def method_missing(msg, *args)
+ return super unless args.empty? && !block_given?
- @rule_dsl.delegate(@delegate_name, m)
+ @rule_dsl.delegate(@delegate_name, msg)
end
end
end
diff --git a/lib/declarative_policy/policy_dsl.rb b/lib/declarative_policy/policy_dsl.rb
index f11b6e9f730..c96049768a1 100644
--- a/lib/declarative_policy/policy_dsl.rb
+++ b/lib/declarative_policy/policy_dsl.rb
@@ -15,8 +15,8 @@ module DeclarativePolicy
@rule = rule
end
- def policy(&b)
- instance_eval(&b)
+ def policy(&block)
+ instance_eval(&block)
end
def enable(*abilities)
@@ -31,14 +31,14 @@ module DeclarativePolicy
@context_class.prevent_all_when(@rule)
end
- def method_missing(m, *a, &b)
- return super unless @context_class.respond_to?(m)
+ def method_missing(msg, *args, &block)
+ return super unless @context_class.respond_to?(msg)
- @context_class.__send__(m, *a, &b) # rubocop:disable GitlabSecurity/PublicSend
+ @context_class.__send__(msg, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
- def respond_to_missing?(m)
- @context_class.respond_to?(m) || super
+ def respond_to_missing?(msg)
+ @context_class.respond_to?(msg) || super
end
end
end
diff --git a/lib/declarative_policy/preferred_scope.rb b/lib/declarative_policy/preferred_scope.rb
index 5c214408dd0..c77784cb49d 100644
--- a/lib/declarative_policy/preferred_scope.rb
+++ b/lib/declarative_policy/preferred_scope.rb
@@ -2,7 +2,7 @@ module DeclarativePolicy # rubocop:disable Naming/FileName
PREFERRED_SCOPE_KEY = :"DeclarativePolicy.preferred_scope"
class << self
- def with_preferred_scope(scope, &b)
+ def with_preferred_scope(scope)
Thread.current[PREFERRED_SCOPE_KEY], old_scope = scope, Thread.current[PREFERRED_SCOPE_KEY]
yield
ensure
@@ -13,12 +13,12 @@ module DeclarativePolicy # rubocop:disable Naming/FileName
Thread.current[PREFERRED_SCOPE_KEY]
end
- def user_scope(&b)
- with_preferred_scope(:user, &b)
+ def user_scope(&block)
+ with_preferred_scope(:user, &block)
end
- def subject_scope(&b)
- with_preferred_scope(:subject, &b)
+ def subject_scope(&block)
+ with_preferred_scope(:subject, &block)
end
def preferred_scope=(scope)
diff --git a/lib/declarative_policy/rule.rb b/lib/declarative_policy/rule.rb
index e309244a3b3..407398cc770 100644
--- a/lib/declarative_policy/rule.rb
+++ b/lib/declarative_policy/rule.rb
@@ -8,8 +8,8 @@ module DeclarativePolicy
# how that affects the actual ability decision - for that, a
# `Step` is used.
class Base
- def self.make(*a)
- new(*a).simplify
+ def self.make(*args)
+ new(*args).simplify
end
# true or false whether this rule passes.
diff --git a/lib/declarative_policy/rule_dsl.rb b/lib/declarative_policy/rule_dsl.rb
index e948b7f2de1..7254b08eda5 100644
--- a/lib/declarative_policy/rule_dsl.rb
+++ b/lib/declarative_policy/rule_dsl.rb
@@ -32,13 +32,13 @@ module DeclarativePolicy
Rule::DelegatedCondition.new(delegate_name, condition)
end
- def method_missing(m, *a, &b)
- return super unless a.empty? && !block_given?
+ def method_missing(msg, *args)
+ return super unless args.empty? && !block_given?
- if @context_class.delegations.key?(m)
- DelegateDsl.new(self, m)
+ if @context_class.delegations.key?(msg)
+ DelegateDsl.new(self, msg)
else
- cond(m.to_sym)
+ cond(msg.to_sym)
end
end
end
diff --git a/lib/declarative_policy/runner.rb b/lib/declarative_policy/runner.rb
index 87f14b3b0d2..fec672f4b8c 100644
--- a/lib/declarative_policy/runner.rb
+++ b/lib/declarative_policy/runner.rb
@@ -127,7 +127,7 @@ module DeclarativePolicy
#
# For each step, we yield the step object along with the computed score
# for debugging purposes.
- def steps_by_score(&b)
+ def steps_by_score
flatten_steps!
if @steps.size > 50
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index a9b04c183ad..e8dbde176ef 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -139,6 +139,11 @@ module ExtractsPath
def lfs_blob_ids
blob_ids = tree.blobs.map(&:id)
+
+ # When current endpoint is a Blob then `tree.blobs` will be empty, it means we need to analyze
+ # the current Blob in order to determine if it's a LFS object
+ blob_ids = Array.wrap(@repo.blob_at(@commit.id, @path)&.id) if blob_ids.empty? # rubocop:disable Gitlab/ModuleWithInstanceVariables
+
@lfs_blob_ids = Gitlab::Git::Blob.batch_lfs_pointers(@project.repository, blob_ids).map(&:id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
index b9a148f35bf..ab6b609d099 100644
--- a/lib/gitlab.rb
+++ b/lib/gitlab.rb
@@ -9,10 +9,6 @@ module Gitlab
Settings
end
- def self.migrations_hash
- @_migrations_hash ||= Digest::MD5.hexdigest(ActiveRecord::Migrator.get_all_versions.to_s)
- end
-
def self.revision
@_revision ||= begin
if File.exist?(root.join("REVISION"))
diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb
index e7283b2f9e8..589e8062226 100644
--- a/lib/gitlab/auth/o_auth/user.rb
+++ b/lib/gitlab/auth/o_auth/user.rb
@@ -48,7 +48,7 @@ module Gitlab
gl_user
rescue ActiveRecord::RecordInvalid => e
log.info "(#{provider}) Error saving user #{auth_hash.uid} (#{auth_hash.email}): #{gl_user.errors.full_messages}"
- return self, e.record.errors
+ [self, e.record.errors]
end
def gl_user
diff --git a/lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb b/lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb
index d5cf9e0d53a..cb2bdea755c 100644
--- a/lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb
+++ b/lib/gitlab/background_migration/add_merge_request_diff_commits_count.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true
# rubocop:disable Style/Documentation
-# rubocop:disable Metrics/LineLength
module Gitlab
module BackgroundMigration
diff --git a/lib/gitlab/background_migration/archive_legacy_traces.rb b/lib/gitlab/background_migration/archive_legacy_traces.rb
index 5a4e5b2c471..92096e29ef1 100644
--- a/lib/gitlab/background_migration/archive_legacy_traces.rb
+++ b/lib/gitlab/background_migration/archive_legacy_traces.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/AbcSize
# rubocop:disable Style/Documentation
module Gitlab
diff --git a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
index 1b4a9e8a194..ccd1f9b4dba 100644
--- a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
+++ b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/LineLength
# rubocop:disable Style/Documentation
module Gitlab
diff --git a/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb b/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb
index c2bf42f846d..da8265a3a5f 100644
--- a/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb
+++ b/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/LineLength
# rubocop:disable Style/Documentation
class Gitlab::BackgroundMigration::CreateGpgKeySubkeysFromGpgKeys
diff --git a/lib/gitlab/background_migration/delete_diff_files.rb b/lib/gitlab/background_migration/delete_diff_files.rb
index 0b785e1b056..8fb2c334048 100644
--- a/lib/gitlab/background_migration/delete_diff_files.rb
+++ b/lib/gitlab/background_migration/delete_diff_files.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/AbcSize
# rubocop:disable Style/Documentation
module Gitlab
diff --git a/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb b/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb
index a357538a885..58df74cfa9b 100644
--- a/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb
+++ b/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true
# rubocop:disable Metrics/MethodLength
-# rubocop:disable Metrics/LineLength
# rubocop:disable Metrics/AbcSize
# rubocop:disable Style/Documentation
diff --git a/lib/gitlab/background_migration/fill_file_store_job_artifact.rb b/lib/gitlab/background_migration/fill_file_store_job_artifact.rb
index 22b0ac71920..103bd98af14 100644
--- a/lib/gitlab/background_migration/fill_file_store_job_artifact.rb
+++ b/lib/gitlab/background_migration/fill_file_store_job_artifact.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/AbcSize
# rubocop:disable Style/Documentation
module Gitlab
diff --git a/lib/gitlab/background_migration/fill_file_store_lfs_object.rb b/lib/gitlab/background_migration/fill_file_store_lfs_object.rb
index d0816ae3ed5..77c1f1ffaf0 100644
--- a/lib/gitlab/background_migration/fill_file_store_lfs_object.rb
+++ b/lib/gitlab/background_migration/fill_file_store_lfs_object.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/AbcSize
# rubocop:disable Style/Documentation
module Gitlab
diff --git a/lib/gitlab/background_migration/fill_store_upload.rb b/lib/gitlab/background_migration/fill_store_upload.rb
index 94c65459a67..cba3e21cea6 100644
--- a/lib/gitlab/background_migration/fill_store_upload.rb
+++ b/lib/gitlab/background_migration/fill_store_upload.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/AbcSize
# rubocop:disable Style/Documentation
module Gitlab
diff --git a/lib/gitlab/background_migration/fix_cross_project_label_links.rb b/lib/gitlab/background_migration/fix_cross_project_label_links.rb
new file mode 100644
index 00000000000..0a12401c35f
--- /dev/null
+++ b/lib/gitlab/background_migration/fix_cross_project_label_links.rb
@@ -0,0 +1,140 @@
+# frozen_string_literal: true
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class FixCrossProjectLabelLinks
+ GROUP_NESTED_LEVEL = 10.freeze
+
+ class Project < ActiveRecord::Base
+ self.table_name = 'projects'
+ end
+
+ class Label < ActiveRecord::Base
+ self.inheritance_column = :_type_disabled
+ self.table_name = 'labels'
+ end
+
+ class LabelLink < ActiveRecord::Base
+ self.table_name = 'label_links'
+ end
+
+ class Issue < ActiveRecord::Base
+ self.table_name = 'issues'
+ end
+
+ class MergeRequest < ActiveRecord::Base
+ self.table_name = 'merge_requests'
+ end
+
+ class Namespace < ActiveRecord::Base
+ self.inheritance_column = :_type_disabled
+ self.table_name = 'namespaces'
+
+ def self.groups_with_descendants_ids(start_id, stop_id)
+ # To isolate migration code, we avoid usage of
+ # Gitlab::GroupHierarchy#base_and_descendants which already
+ # does this job better
+ ids = Namespace.where(type: 'Group', id: Label.where(type: 'GroupLabel').select('distinct group_id')).where(id: start_id..stop_id).pluck(:id)
+ group_ids = ids
+
+ GROUP_NESTED_LEVEL.times do
+ ids = Namespace.where(type: 'Group', parent_id: ids).pluck(:id)
+ break if ids.empty?
+
+ group_ids += ids
+ end
+
+ group_ids.uniq
+ end
+ end
+
+ def perform(start_id, stop_id)
+ group_ids = Namespace.groups_with_descendants_ids(start_id, stop_id)
+ project_ids = Project.where(namespace_id: group_ids).select(:id)
+
+ fix_issues(project_ids)
+ fix_merge_requests(project_ids)
+ end
+
+ private
+
+ # select IDs of issues which reference a label which is:
+ # a) a project label of a different project, or
+ # b) a group label of a different group than issue's project group
+ def fix_issues(project_ids)
+ issue_ids = Label
+ .joins('INNER JOIN label_links ON label_links.label_id = labels.id AND label_links.target_type = \'Issue\'
+ INNER JOIN issues ON issues.id = label_links.target_id
+ INNER JOIN projects ON projects.id = issues.project_id')
+ .where('issues.project_id in (?)', project_ids)
+ .where('(labels.project_id is not null and labels.project_id != issues.project_id) '\
+ 'or (labels.group_id is not null and labels.group_id != projects.namespace_id)')
+ .select('distinct issues.id')
+
+ Issue.where(id: issue_ids).find_each { |issue| check_resource_labels(issue, issue.project_id) }
+ end
+
+ # select IDs of MRs which reference a label which is:
+ # a) a project label of a different project, or
+ # b) a group label of a different group than MR's project group
+ def fix_merge_requests(project_ids)
+ mr_ids = Label
+ .joins('INNER JOIN label_links ON label_links.label_id = labels.id AND label_links.target_type = \'MergeRequest\'
+ INNER JOIN merge_requests ON merge_requests.id = label_links.target_id
+ INNER JOIN projects ON projects.id = merge_requests.target_project_id')
+ .where('merge_requests.target_project_id in (?)', project_ids)
+ .where('(labels.project_id is not null and labels.project_id != merge_requests.target_project_id) '\
+ 'or (labels.group_id is not null and labels.group_id != projects.namespace_id)')
+ .select('distinct merge_requests.id')
+
+ MergeRequest.where(id: mr_ids).find_each { |merge_request| check_resource_labels(merge_request, merge_request.target_project_id) }
+ end
+
+ def check_resource_labels(resource, project_id)
+ local_labels = available_labels(project_id)
+
+ # get all label links for the given resource (issue/MR)
+ # which reference a label not included in avaiable_labels
+ # (other than its project labels and labels of ancestor groups)
+ cross_labels = LabelLink
+ .select('label_id, labels.title as title, labels.color as color, label_links.id as label_link_id')
+ .joins('INNER JOIN labels ON labels.id = label_links.label_id')
+ .where(target_type: resource.class.name.demodulize, target_id: resource.id)
+ .where('labels.id not in (?)', local_labels.select(:id))
+
+ cross_labels.each do |label|
+ matching_label = local_labels.find {|l| l.title == label.title && l.color == label.color}
+
+ next unless matching_label
+
+ Rails.logger.info "#{resource.class.name.demodulize} #{resource.id}: replacing #{label.label_id} with #{matching_label.id}"
+ LabelLink.update(label.label_link_id, label_id: matching_label.id)
+ end
+ end
+
+ # get all labels available for the project (including
+ # group labels of ancestor groups)
+ def available_labels(project_id)
+ @labels ||= {}
+ @labels[project_id] ||= Label
+ .where("(type = 'GroupLabel' and group_id in (?)) or (type = 'ProjectLabel' and id = ?)",
+ project_group_ids(project_id),
+ project_id)
+ end
+
+ def project_group_ids(project_id)
+ ids = [Project.find(project_id).namespace_id]
+
+ GROUP_NESTED_LEVEL.times do
+ group = Namespace.find(ids.last)
+ break unless group.parent_id
+
+ ids << group.parent_id
+ end
+
+ ids
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/migrate_build_stage.rb b/lib/gitlab/background_migration/migrate_build_stage.rb
index 242e3143e71..268c6083d3c 100644
--- a/lib/gitlab/background_migration/migrate_build_stage.rb
+++ b/lib/gitlab/background_migration/migrate_build_stage.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/AbcSize
# rubocop:disable Style/Documentation
module Gitlab
diff --git a/lib/gitlab/background_migration/migrate_events_to_push_event_payloads.rb b/lib/gitlab/background_migration/migrate_events_to_push_event_payloads.rb
index 7088aa0860a..38fecac1bfe 100644
--- a/lib/gitlab/background_migration/migrate_events_to_push_event_payloads.rb
+++ b/lib/gitlab/background_migration/migrate_events_to_push_event_payloads.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/LineLength
# rubocop:disable Style/Documentation
module Gitlab
diff --git a/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder.rb b/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder.rb
index 7f243073fd0..ef50fe4adb1 100644
--- a/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder.rb
+++ b/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/LineLength
# rubocop:disable Style/Documentation
module Gitlab
diff --git a/lib/gitlab/background_migration/move_personal_snippet_files.rb b/lib/gitlab/background_migration/move_personal_snippet_files.rb
index a4ef51fd0e8..5b2b2af718a 100644
--- a/lib/gitlab/background_migration/move_personal_snippet_files.rb
+++ b/lib/gitlab/background_migration/move_personal_snippet_files.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/LineLength
# rubocop:disable Style/Documentation
module Gitlab
diff --git a/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
index d9d3d2e667b..698f5e46c0c 100644
--- a/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
+++ b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true
# rubocop:disable Metrics/MethodLength
-# rubocop:disable Metrics/LineLength
# rubocop:disable Metrics/ClassLength
# rubocop:disable Metrics/BlockLength
# rubocop:disable Style/Documentation
diff --git a/lib/gitlab/background_migration/populate_fork_networks_range.rb b/lib/gitlab/background_migration/populate_fork_networks_range.rb
index a976cb4c243..aa4f130538c 100644
--- a/lib/gitlab/background_migration/populate_fork_networks_range.rb
+++ b/lib/gitlab/background_migration/populate_fork_networks_range.rb
@@ -19,7 +19,7 @@ module Gitlab
create_fork_networks_for_missing_projects(start_id, end_id)
create_fork_networks_memberships_for_root_projects(start_id, end_id)
- delay = BackgroundMigration::CreateForkNetworkMembershipsRange::RESCHEDULE_DELAY # rubocop:disable Metrics/LineLength
+ delay = BackgroundMigration::CreateForkNetworkMembershipsRange::RESCHEDULE_DELAY
BackgroundMigrationWorker.perform_in(
delay, "CreateForkNetworkMembershipsRange", [start_id, end_id]
)
diff --git a/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data.rb b/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data.rb
index 8a901a9bf39..d89ce358bb9 100644
--- a/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data.rb
+++ b/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data.rb
@@ -1,7 +1,4 @@
# frozen_string_literal: true
-# rubocop:disable Metrics/LineLength
-# rubocop:disable Metrics/MethodLength
-# rubocop:disable Metrics/ClassLength
# rubocop:disable Style/Documentation
module Gitlab
diff --git a/lib/gitlab/background_migration/populate_untracked_uploads.rb b/lib/gitlab/background_migration/populate_untracked_uploads.rb
index 9232f20a063..a19dc9747fb 100644
--- a/lib/gitlab/background_migration/populate_untracked_uploads.rb
+++ b/lib/gitlab/background_migration/populate_untracked_uploads.rb
@@ -4,7 +4,7 @@ module Gitlab
module BackgroundMigration
# This class processes a batch of rows in `untracked_files_for_uploads` by
# adding each file to the `uploads` table if it does not exist.
- class PopulateUntrackedUploads # rubocop:disable Metrics/ClassLength
+ class PopulateUntrackedUploads
def perform(start_id, end_id)
return unless migrate?
diff --git a/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb b/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb
index a2c5acbde71..4a9a62aaeb5 100644
--- a/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb
+++ b/lib/gitlab/background_migration/populate_untracked_uploads_dependencies.rb
@@ -4,7 +4,7 @@ module Gitlab
module PopulateUntrackedUploadsDependencies
# This class is responsible for producing the attributes necessary to
# track an uploaded file in the `uploads` table.
- class UntrackedFile < ActiveRecord::Base # rubocop:disable Metrics/ClassLength, Metrics/LineLength
+ class UntrackedFile < ActiveRecord::Base # rubocop:disable Metrics/ClassLength
self.table_name = 'untracked_files_for_uploads'
# Ends with /:random_hex/:filename
@@ -134,7 +134,7 @@ module Gitlab
# Not including a leading slash
def path_relative_to_upload_dir
- upload_dir = Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR # rubocop:disable Metrics/LineLength
+ upload_dir = Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR
base = %r{\A#{Regexp.escape(upload_dir)}/}
@path_relative_to_upload_dir ||= path.sub(base, '')
end
diff --git a/lib/gitlab/background_migration/prepare_untracked_uploads.rb b/lib/gitlab/background_migration/prepare_untracked_uploads.rb
index 522c69a0bb1..81ca2b0a9b7 100644
--- a/lib/gitlab/background_migration/prepare_untracked_uploads.rb
+++ b/lib/gitlab/background_migration/prepare_untracked_uploads.rb
@@ -144,7 +144,7 @@ module Gitlab
def table_columns_and_values_for_insert(file_paths)
values = file_paths.map do |file_path|
- ActiveRecord::Base.send(:sanitize_sql_array, ['(?)', file_path]) # rubocop:disable GitlabSecurity/PublicSend, Metrics/LineLength
+ ActiveRecord::Base.send(:sanitize_sql_array, ['(?)', file_path]) # rubocop:disable GitlabSecurity/PublicSend
end.join(', ')
"#{UntrackedFile.table_name} (path) VALUES #{values}"
diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb
index 35eadf6fa93..e780f8c646b 100644
--- a/lib/gitlab/ci/ansi2html.rb
+++ b/lib/gitlab/ci/ansi2html.rb
@@ -29,105 +29,105 @@ module Gitlab
end
class Converter
- def on_0(s) reset() end
+ def on_0(_) reset() end
- def on_1(s) enable(STYLE_SWITCHES[:bold]) end
+ def on_1(_) enable(STYLE_SWITCHES[:bold]) end
- def on_3(s) enable(STYLE_SWITCHES[:italic]) end
+ def on_3(_) enable(STYLE_SWITCHES[:italic]) end
- def on_4(s) enable(STYLE_SWITCHES[:underline]) end
+ def on_4(_) enable(STYLE_SWITCHES[:underline]) end
- def on_8(s) enable(STYLE_SWITCHES[:conceal]) end
+ def on_8(_) enable(STYLE_SWITCHES[:conceal]) end
- def on_9(s) enable(STYLE_SWITCHES[:cross]) end
+ def on_9(_) enable(STYLE_SWITCHES[:cross]) end
- def on_21(s) disable(STYLE_SWITCHES[:bold]) end
+ def on_21(_) disable(STYLE_SWITCHES[:bold]) end
- def on_22(s) disable(STYLE_SWITCHES[:bold]) end
+ def on_22(_) disable(STYLE_SWITCHES[:bold]) end
- def on_23(s) disable(STYLE_SWITCHES[:italic]) end
+ def on_23(_) disable(STYLE_SWITCHES[:italic]) end
- def on_24(s) disable(STYLE_SWITCHES[:underline]) end
+ def on_24(_) disable(STYLE_SWITCHES[:underline]) end
- def on_28(s) disable(STYLE_SWITCHES[:conceal]) end
+ def on_28(_) disable(STYLE_SWITCHES[:conceal]) end
- def on_29(s) disable(STYLE_SWITCHES[:cross]) end
+ def on_29(_) disable(STYLE_SWITCHES[:cross]) end
- def on_30(s) set_fg_color(0) end
+ def on_30(_) set_fg_color(0) end
- def on_31(s) set_fg_color(1) end
+ def on_31(_) set_fg_color(1) end
- def on_32(s) set_fg_color(2) end
+ def on_32(_) set_fg_color(2) end
- def on_33(s) set_fg_color(3) end
+ def on_33(_) set_fg_color(3) end
- def on_34(s) set_fg_color(4) end
+ def on_34(_) set_fg_color(4) end
- def on_35(s) set_fg_color(5) end
+ def on_35(_) set_fg_color(5) end
- def on_36(s) set_fg_color(6) end
+ def on_36(_) set_fg_color(6) end
- def on_37(s) set_fg_color(7) end
+ def on_37(_) set_fg_color(7) end
- def on_38(s) set_fg_color_256(s) end
+ def on_38(stack) set_fg_color_256(stack) end
- def on_39(s) set_fg_color(9) end
+ def on_39(_) set_fg_color(9) end
- def on_40(s) set_bg_color(0) end
+ def on_40(_) set_bg_color(0) end
- def on_41(s) set_bg_color(1) end
+ def on_41(_) set_bg_color(1) end
- def on_42(s) set_bg_color(2) end
+ def on_42(_) set_bg_color(2) end
- def on_43(s) set_bg_color(3) end
+ def on_43(_) set_bg_color(3) end
- def on_44(s) set_bg_color(4) end
+ def on_44(_) set_bg_color(4) end
- def on_45(s) set_bg_color(5) end
+ def on_45(_) set_bg_color(5) end
- def on_46(s) set_bg_color(6) end
+ def on_46(_) set_bg_color(6) end
- def on_47(s) set_bg_color(7) end
+ def on_47(_) set_bg_color(7) end
- def on_48(s) set_bg_color_256(s) end
+ def on_48(stack) set_bg_color_256(stack) end
- def on_49(s) set_bg_color(9) end
+ def on_49(_) set_bg_color(9) end
- def on_90(s) set_fg_color(0, 'l') end
+ def on_90(_) set_fg_color(0, 'l') end
- def on_91(s) set_fg_color(1, 'l') end
+ def on_91(_) set_fg_color(1, 'l') end
- def on_92(s) set_fg_color(2, 'l') end
+ def on_92(_) set_fg_color(2, 'l') end
- def on_93(s) set_fg_color(3, 'l') end
+ def on_93(_) set_fg_color(3, 'l') end
- def on_94(s) set_fg_color(4, 'l') end
+ def on_94(_) set_fg_color(4, 'l') end
- def on_95(s) set_fg_color(5, 'l') end
+ def on_95(_) set_fg_color(5, 'l') end
- def on_96(s) set_fg_color(6, 'l') end
+ def on_96(_) set_fg_color(6, 'l') end
- def on_97(s) set_fg_color(7, 'l') end
+ def on_97(_) set_fg_color(7, 'l') end
- def on_99(s) set_fg_color(9, 'l') end
+ def on_99(_) set_fg_color(9, 'l') end
- def on_100(s) set_bg_color(0, 'l') end
+ def on_100(_) set_bg_color(0, 'l') end
- def on_101(s) set_bg_color(1, 'l') end
+ def on_101(_) set_bg_color(1, 'l') end
- def on_102(s) set_bg_color(2, 'l') end
+ def on_102(_) set_bg_color(2, 'l') end
- def on_103(s) set_bg_color(3, 'l') end
+ def on_103(_) set_bg_color(3, 'l') end
- def on_104(s) set_bg_color(4, 'l') end
+ def on_104(_) set_bg_color(4, 'l') end
- def on_105(s) set_bg_color(5, 'l') end
+ def on_105(_) set_bg_color(5, 'l') end
- def on_106(s) set_bg_color(6, 'l') end
+ def on_106(_) set_bg_color(6, 'l') end
- def on_107(s) set_bg_color(7, 'l') end
+ def on_107(_) set_bg_color(7, 'l') end
- def on_109(s) set_bg_color(9, 'l') end
+ def on_109(_) set_bg_color(9, 'l') end
attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask
@@ -188,19 +188,19 @@ module Gitlab
)
end
- def handle_section(s)
- action = s[1]
- timestamp = s[2]
- section = s[3]
- line = s.matched()[0...-5] # strips \r\033[0K
+ def handle_section(scanner)
+ action = scanner[1]
+ timestamp = scanner[2]
+ section = scanner[3]
+ line = scanner.matched()[0...-5] # strips \r\033[0K
@out << %{<div class="hidden" data-action="#{action}" data-timestamp="#{timestamp}" data-section="#{section}">#{line}</div>}
end
- def handle_sequence(s)
- indicator = s[1]
- commands = s[2].split ';'
- terminator = s[3]
+ def handle_sequence(scanner)
+ indicator = scanner[1]
+ commands = scanner[2].split ';'
+ terminator = scanner[3]
# We are only interested in color and text style changes - triggered by
# sequences starting with '\e[' and ending with 'm'. Any other control
diff --git a/lib/gitlab/ci/config/entry/configurable.rb b/lib/gitlab/ci/config/entry/configurable.rb
index db47c2f6185..7cddd2c7b7e 100644
--- a/lib/gitlab/ci/config/entry/configurable.rb
+++ b/lib/gitlab/ci/config/entry/configurable.rb
@@ -47,7 +47,7 @@ module Gitlab
Hash[(@nodes || {}).map { |key, factory| [key, factory.dup] }]
end
- private # rubocop:disable Lint/UselessAccessModifier
+ private
def entry(key, entry, metadata)
factory = Entry::Factory.new(entry)
diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb
index a52d71225bb..ee54b893598 100644
--- a/lib/gitlab/ci/trace.rb
+++ b/lib/gitlab/ci/trace.rb
@@ -6,6 +6,7 @@ module Gitlab
LEASE_TIMEOUT = 1.hour
ArchiveError = Class.new(StandardError)
+ AlreadyArchivedError = Class.new(StandardError)
attr_reader :job
@@ -81,7 +82,9 @@ module Gitlab
def write(mode)
stream = Gitlab::Ci::Trace::Stream.new do
- if current_path
+ if trace_artifact
+ raise AlreadyArchivedError, 'Could not write to the archived trace'
+ elsif current_path
File.open(current_path, mode)
elsif Feature.enabled?('ci_enable_live_trace')
Gitlab::Ci::Trace::ChunkedIO.new(job)
@@ -98,14 +101,17 @@ module Gitlab
end
def erase!
- trace_artifact&.destroy
-
- paths.each do |trace_path|
- FileUtils.rm(trace_path, force: true)
- end
-
- job.trace_chunks.fast_destroy_all
- job.erase_old_trace!
+ ##
+ # Erase the archived trace
+ trace_artifact&.destroy!
+
+ ##
+ # Erase the live trace
+ job.trace_chunks.fast_destroy_all # Destroy chunks of a live trace
+ FileUtils.rm_f(current_path) if current_path # Remove a trace file of a live trace
+ job.erase_old_trace! if job.has_old_trace? # Remove a trace in database of a live trace
+ ensure
+ @current_path = nil
end
def archive!
@@ -117,7 +123,7 @@ module Gitlab
private
def unsafe_archive!
- raise ArchiveError, 'Already archived' if trace_artifact
+ raise AlreadyArchivedError, 'Could not archive again' if trace_artifact
raise ArchiveError, 'Job is not finished yet' unless job.complete?
if job.trace_chunks.any?
diff --git a/lib/gitlab/ci/trace/section_parser.rb b/lib/gitlab/ci/trace/section_parser.rb
index 9bb0166c9e3..c09089d6475 100644
--- a/lib/gitlab/ci/trace/section_parser.rb
+++ b/lib/gitlab/ci/trace/section_parser.rb
@@ -75,19 +75,19 @@ module Gitlab
@beginning_of_section_regex ||= /section_/.freeze
end
- def find_next_marker(s)
+ def find_next_marker(scanner)
beginning_of_section_len = 8
- maybe_marker = s.exist?(beginning_of_section_regex)
+ maybe_marker = scanner.exist?(beginning_of_section_regex)
if maybe_marker.nil?
- s.terminate
+ scanner.terminate
else
# repositioning at the beginning of the match
- s.pos += maybe_marker - beginning_of_section_len
+ scanner.pos += maybe_marker - beginning_of_section_len
if block_given?
- good_marker = yield(s)
+ good_marker = yield(scanner)
# if not a good marker: Consuming the matched beginning_of_section_regex
- s.pos += beginning_of_section_len unless good_marker
+ scanner.pos += beginning_of_section_len unless good_marker
end
end
end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 3cf35f499cd..9147ef401da 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -60,7 +60,7 @@ module Gitlab
def in_memory_application_settings
with_fallback_to_fake_application_settings do
- @in_memory_application_settings ||= ::ApplicationSetting.build_from_defaults # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ @in_memory_application_settings ||= ::ApplicationSetting.build_from_defaults
end
end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 4ad106e7b0a..872e70f9a5d 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -42,6 +42,21 @@ module Gitlab
!self.read_only?
end
+ # check whether the underlying database is in read-only mode
+ def self.db_read_only?
+ if postgresql?
+ ActiveRecord::Base.connection.execute('SELECT pg_is_in_recovery()')
+ .first
+ .fetch('pg_is_in_recovery') == 't'
+ else
+ false
+ end
+ end
+
+ def self.db_read_write?
+ !self.db_read_only?
+ end
+
def self.version
@version ||= database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
end
diff --git a/lib/gitlab/diff/image_point.rb b/lib/gitlab/diff/image_point.rb
index 65332dfd239..1f157354ea4 100644
--- a/lib/gitlab/diff/image_point.rb
+++ b/lib/gitlab/diff/image_point.rb
@@ -3,11 +3,11 @@ module Gitlab
class ImagePoint
attr_reader :width, :height, :x, :y
- def initialize(width, height, x, y)
+ def initialize(width, height, new_x, new_y)
@width = width
@height = height
- @x = x
- @y = y
+ @x = new_x
+ @y = new_y
end
def to_h
diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb
index 54783a07919..99970779c67 100644
--- a/lib/gitlab/diff/inline_diff.rb
+++ b/lib/gitlab/diff/inline_diff.rb
@@ -93,7 +93,7 @@ module Gitlab
private
- def longest_common_prefix(a, b)
+ def longest_common_prefix(a, b) # rubocop:disable Naming/UncommunicativeMethodParamName
max_length = [a.length, b.length].max
length = 0
@@ -109,7 +109,7 @@ module Gitlab
length
end
- def longest_common_suffix(a, b)
+ def longest_common_suffix(a, b) # rubocop:disable Naming/UncommunicativeMethodParamName
longest_common_prefix(a.reverse, b.reverse)
end
end
diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb
index 0b8f6cfe3cb..d1fd5dfe0cb 100644
--- a/lib/gitlab/encoding_helper.rb
+++ b/lib/gitlab/encoding_helper.rb
@@ -65,17 +65,17 @@ module Gitlab
clean(message)
end
rescue ArgumentError
- return nil
+ nil
end
- def encode_binary(s)
- return "" if s.nil?
+ def encode_binary(str)
+ return "" if str.nil?
- s.dup.force_encoding(Encoding::ASCII_8BIT)
+ str.dup.force_encoding(Encoding::ASCII_8BIT)
end
- def binary_stringio(s)
- StringIO.new(s || '').tap { |io| io.set_encoding(Encoding::ASCII_8BIT) }
+ def binary_stringio(str)
+ StringIO.new(str || '').tap { |io| io.set_encoding(Encoding::ASCII_8BIT) }
end
private
diff --git a/lib/gitlab/exclusive_lease_helpers.rb b/lib/gitlab/exclusive_lease_helpers.rb
new file mode 100644
index 00000000000..e998548cff9
--- /dev/null
+++ b/lib/gitlab/exclusive_lease_helpers.rb
@@ -0,0 +1,29 @@
+module Gitlab
+ # This module provides helper methods which are intregrated with GitLab::ExclusiveLease
+ module ExclusiveLeaseHelpers
+ FailedToObtainLockError = Class.new(StandardError)
+
+ ##
+ # This helper method blocks a process/thread until the other process cancel the obrainted lease key.
+ #
+ # Note: It's basically discouraged to use this method in the unicorn's thread,
+ # because it holds the connection until all `retries` is consumed.
+ # This could potentially eat up all connection pools.
+ def in_lock(key, ttl: 1.minute, retries: 10, sleep_sec: 0.01.seconds)
+ lease = Gitlab::ExclusiveLease.new(key, timeout: ttl)
+
+ until uuid = lease.try_obtain
+ # Keep trying until we obtain the lease. To prevent hammering Redis too
+ # much we'll wait for a bit.
+ sleep(sleep_sec)
+ break if (retries -= 1) < 0
+ end
+
+ raise FailedToObtainLockError, 'Failed to obtain a lock' unless uuid
+
+ yield
+ ensure
+ Gitlab::ExclusiveLease.cancel(key, uuid)
+ end
+ end
+end
diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb
index 8953bc8c148..a91de278cf3 100644
--- a/lib/gitlab/fogbugz_import/importer.rb
+++ b/lib/gitlab/fogbugz_import/importer.rb
@@ -191,19 +191,19 @@ module Gitlab
end
end
- def linkify_issues(s)
- s = s.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2')
- s = s.gsub(/([Cc]ase) ([0-9]+)/, '\1 #\2')
- s
+ def linkify_issues(str)
+ str = str.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2')
+ str = str.gsub(/([Cc]ase) ([0-9]+)/, '\1 #\2')
+ str
end
- def escape_for_markdown(s)
- s = s.gsub(/^#/, "\\#")
- s = s.gsub(/^-/, "\\-")
- s = s.gsub("`", "\\~")
- s = s.delete("\r")
- s = s.gsub("\n", " \n")
- s
+ def escape_for_markdown(str)
+ str = str.gsub(/^#/, "\\#")
+ str = str.gsub(/^-/, "\\-")
+ str = str.gsub("`", "\\~")
+ str = str.delete("\r")
+ str = str.gsub("\n", " \n")
+ str
end
def format_content(raw_content)
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 604bb11e712..96fa94d5790 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -50,13 +50,7 @@ module Gitlab
end
def raw(repository, sha)
- Gitlab::GitalyClient.migrate(:git_blob_raw) do |is_enabled|
- if is_enabled
- repository.gitaly_blob_client.get_blob(oid: sha, limit: MAX_DATA_DISPLAY_SIZE)
- else
- rugged_raw(repository, sha, limit: MAX_DATA_DISPLAY_SIZE)
- end
- end
+ repository.gitaly_blob_client.get_blob(oid: sha, limit: MAX_DATA_DISPLAY_SIZE)
end
# Returns an array of Blob instances, specified in blob_references as
@@ -207,16 +201,7 @@ module Gitlab
return if @loaded_all_data
- @data = Gitlab::GitalyClient.migrate(:git_blob_load_all_data) do |is_enabled|
- begin
- if is_enabled
- repository.gitaly_blob_client.get_blob(oid: id, limit: -1).data
- else
- repository.lookup(id).content
- end
- end
- end
-
+ @data = repository.gitaly_blob_client.get_blob(oid: id, limit: -1).data
@loaded_all_data = true
@loaded_size = @data.bytesize
end
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index c67826da1d2..4e2d817d12c 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -63,12 +63,8 @@ module Gitlab
# This saves us an RPC round trip.
return nil if commit_id.include?(':')
- commit = repo.gitaly_migrate(:find_commit) do |is_enabled|
- if is_enabled
- repo.gitaly_commit_client.find_commit(commit_id)
- else
- rugged_find(repo, commit_id)
- end
+ commit = repo.wrapped_gitaly_errors do
+ repo.gitaly_commit_client.find_commit(commit_id)
end
decorate(repo, commit) if commit
@@ -78,12 +74,6 @@ module Gitlab
nil
end
- def rugged_find(repo, commit_id)
- obj = repo.rev_parse_target(commit_id)
-
- obj.is_a?(Rugged::Commit) ? obj : nil
- end
-
# Get last commit for HEAD
#
# Ex.
@@ -174,13 +164,8 @@ module Gitlab
# relation to each other. The last 10 commits for a branch for example,
# should go through .where
def batch_by_oid(repo, oids)
- repo.gitaly_migrate(:list_commits_by_oid,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- repo.gitaly_commit_client.list_commits_by_oid(oids)
- else
- oids.map { |oid| find(repo, oid) }.compact
- end
+ repo.wrapped_gitaly_errors do
+ repo.gitaly_commit_client.list_commits_by_oid(oids)
end
end
@@ -299,14 +284,7 @@ module Gitlab
def deltas
@deltas ||= begin
- deltas = Gitlab::GitalyClient.migrate(:commit_deltas) do |is_enabled|
- if is_enabled
- @repository.gitaly_commit_client.commit_deltas(self)
- else
- rugged_diff_from_parent.each_delta
- end
- end
-
+ deltas = @repository.gitaly_commit_client.commit_deltas(self)
deltas.map { |delta| Gitlab::Git::Diff.new(delta) }
end
end
diff --git a/lib/gitlab/git/conflict/resolver.rb b/lib/gitlab/git/conflict/resolver.rb
index c3cb0264112..0e4a973301f 100644
--- a/lib/gitlab/git/conflict/resolver.rb
+++ b/lib/gitlab/git/conflict/resolver.rb
@@ -12,14 +12,8 @@ module Gitlab
end
def conflicts
- @conflicts ||= begin
- @target_repository.gitaly_migrate(:conflicts_list_conflict_files) do |is_enabled|
- if is_enabled
- gitaly_conflicts_client(@target_repository).list_conflict_files.to_a
- else
- rugged_list_conflict_files
- end
- end
+ @conflicts ||= @target_repository.wrapped_gitaly_errors do
+ gitaly_conflicts_client(@target_repository).list_conflict_files.to_a
end
rescue GRPC::FailedPrecondition => e
raise Gitlab::Git::Conflict::Resolver::ConflictSideMissing.new(e.message)
@@ -28,12 +22,8 @@ module Gitlab
end
def resolve_conflicts(source_repository, resolution, source_branch:, target_branch:)
- source_repository.gitaly_migrate(:conflicts_resolve_conflicts) do |is_enabled|
- if is_enabled
- gitaly_conflicts_client(source_repository).resolve_conflicts(@target_repository, resolution, source_branch, target_branch)
- else
- rugged_resolve_conflicts(source_repository, resolution, source_branch, target_branch)
- end
+ source_repository.wrapped_gitaly_errors do
+ gitaly_conflicts_client(source_repository).resolve_conflicts(@target_repository, resolution, source_branch, target_branch)
end
end
@@ -61,57 +51,6 @@ module Gitlab
def gitaly_conflicts_client(repository)
repository.gitaly_conflicts_client(@our_commit_oid, @their_commit_oid)
end
-
- def write_resolved_file_to_index(repository, index, file, params)
- if params[:sections]
- resolved_lines = file.resolve_lines(params[:sections])
- new_file = resolved_lines.map { |line| line[:full_line] }.join("\n")
-
- new_file << "\n" if file.our_blob.data.end_with?("\n")
- elsif params[:content]
- new_file = file.resolve_content(params[:content])
- end
-
- our_path = file.our_path
-
- oid = repository.rugged.write(new_file, :blob)
- index.add(path: our_path, oid: oid, mode: file.our_mode)
- index.conflict_remove(our_path)
- end
-
- def rugged_list_conflict_files
- target_index = @target_repository.rugged.merge_commits(@our_commit_oid, @their_commit_oid)
-
- # We don't need to do `with_repo_branch_commit` here, because the target
- # project always fetches source refs when creating merge request diffs.
- conflict_files(@target_repository, target_index)
- end
-
- def rugged_resolve_conflicts(source_repository, resolution, source_branch, target_branch)
- source_repository.with_repo_branch_commit(@target_repository, target_branch) do
- index = source_repository.rugged.merge_commits(@our_commit_oid, @their_commit_oid)
- conflicts = conflict_files(source_repository, index)
-
- resolution.files.each do |file_params|
- conflict_file = conflict_for_path(conflicts, file_params[:old_path], file_params[:new_path])
-
- write_resolved_file_to_index(source_repository, index, conflict_file, file_params)
- end
-
- unless index.conflicts.empty?
- missing_files = index.conflicts.map { |file| file[:ours][:path] }
-
- raise ResolutionError, "Missing resolutions for the following files: #{missing_files.join(', ')}"
- end
-
- commit_params = {
- message: resolution.commit_message,
- parents: [@our_commit_oid, @their_commit_oid]
- }
-
- source_repository.commit_index(resolution.user, source_branch, index, commit_params)
- end
- end
end
end
end
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index 6a601561c2a..219c69893ad 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -42,12 +42,10 @@ module Gitlab
return if @overflow
return if @iterator.nil?
- Gitlab::GitalyClient.migrate(:commit_raw_diffs) do |is_enabled|
- if is_enabled && @iterator.is_a?(Gitlab::GitalyClient::DiffStitcher)
- each_gitaly_patch(&block)
- else
- each_rugged_patch(&block)
- end
+ if @iterator.is_a?(Gitlab::GitalyClient::DiffStitcher)
+ each_gitaly_patch(&block)
+ else
+ each_serialized_patch(&block)
end
@populated = true
@@ -118,7 +116,7 @@ module Gitlab
end
end
- def each_rugged_patch
+ def each_serialized_patch
i = @array.length
@iterator.each do |raw|
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index bbfe6ab1d95..76404366e8e 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -439,31 +439,11 @@ module Gitlab
raise ArgumentError.new("invalid Repository#log limit: #{limit.inspect}")
end
- gitaly_migrate(:find_commits) do |is_enabled|
- if is_enabled
- gitaly_commit_client.find_commits(options)
- else
- raw_log(options).map { |c| Commit.decorate(self, c) }
- end
+ wrapped_gitaly_errors do
+ gitaly_commit_client.find_commits(options)
end
end
- # Used in gitaly-ruby
- def raw_log(options)
- sha =
- unless options[:all]
- actual_ref = options[:ref] || root_ref
- begin
- sha_from_ref(actual_ref)
- rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
- # Return an empty array if the ref wasn't found
- return []
- end
- end
-
- log_by_shell(sha, options)
- end
-
def count_commits(options)
options = process_count_commits_options(options.dup)
@@ -555,13 +535,7 @@ module Gitlab
# diff options. The +options+ hash can also include :break_rewrites to
# split larger rewrites into delete/add pairs.
def diff(from, to, options = {}, *paths)
- iterator = gitaly_migrate(:diff_between) do |is_enabled|
- if is_enabled
- gitaly_commit_client.diff(from, to, options.merge(paths: paths))
- else
- diff_patches(from, to, options, *paths)
- end
- end
+ iterator = gitaly_commit_client.diff(from, to, options.merge(paths: paths))
Gitlab::Git::DiffCollection.new(iterator, options)
end
@@ -651,7 +625,13 @@ module Gitlab
end
def update_branch(branch_name, user:, newrev:, oldrev:)
- OperationService.new(user, self).update_branch(branch_name, newrev, oldrev)
+ gitaly_migrate(:operation_user_update_branch) do |is_enabled|
+ if is_enabled
+ gitaly_operations_client.user_update_branch(branch_name, user, newrev, oldrev)
+ else
+ OperationService.new(user, self).update_branch(branch_name, newrev, oldrev)
+ end
+ end
end
def rm_branch(branch_name, user:)
@@ -1095,7 +1075,6 @@ module Gitlab
true
end
- # rubocop:disable Metrics/ParameterLists
def multi_action(
user, branch_name:, message:, actions:,
author_email: nil, author_name: nil,
@@ -1107,7 +1086,6 @@ module Gitlab
start_branch_name, start_repository)
end
end
- # rubocop:enable Metrics/ParameterLists
def write_config(full_path:)
return unless full_path.present?
@@ -1115,8 +1093,18 @@ module Gitlab
# This guard avoids Gitaly log/error spam
raise NoRepository, 'repository does not exist' unless exists?
+ set_config('gitlab.fullpath' => full_path)
+ end
+
+ def set_config(entries)
wrapped_gitaly_errors do
- gitaly_repository_client.write_config(full_path: full_path)
+ gitaly_repository_client.set_config(entries)
+ end
+ end
+
+ def delete_config(*keys)
+ wrapped_gitaly_errors do
+ gitaly_repository_client.delete_config(keys)
end
end
@@ -1219,12 +1207,10 @@ module Gitlab
end
def find_commits_by_message(query, ref, path, limit, offset)
- gitaly_migrate(:commits_by_message) do |is_enabled|
- if is_enabled
- find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
- else
- find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
- end
+ wrapped_gitaly_errors do
+ gitaly_commit_client
+ .commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset)
+ .map { |c| commit(c) }
end
end
@@ -1234,12 +1220,8 @@ module Gitlab
end
def last_commit_for_path(sha, path)
- gitaly_migrate(:last_commit_for_path) do |is_enabled|
- if is_enabled
- last_commit_for_path_by_gitaly(sha, path)
- else
- last_commit_for_path_by_rugged(sha, path)
- end
+ wrapped_gitaly_errors do
+ gitaly_commit_client.last_commit_for_path(sha, path)
end
end
@@ -1450,46 +1432,6 @@ module Gitlab
end
end
- # Gitaly note: JV: although #log_by_shell shells out to Git I think the
- # complexity is such that we should migrate it as Ruby before trying to
- # do it in Go.
- def log_by_shell(sha, options)
- limit = options[:limit].to_i
- offset = options[:offset].to_i
- use_follow_flag = options[:follow] && options[:path].present?
-
- # We will perform the offset in Ruby because --follow doesn't play well with --skip.
- # See: https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
- offset_in_ruby = use_follow_flag && options[:offset].present?
- limit += offset if offset_in_ruby
-
- cmd = %w[log]
- cmd << "--max-count=#{limit}"
- cmd << '--format=%H'
- cmd << "--skip=#{offset}" unless offset_in_ruby
- cmd << '--follow' if use_follow_flag
- cmd << '--no-merges' if options[:skip_merges]
- cmd << "--after=#{options[:after].iso8601}" if options[:after]
- cmd << "--before=#{options[:before].iso8601}" if options[:before]
-
- if options[:all]
- cmd += %w[--all --reverse]
- else
- cmd << sha
- end
-
- # :path can be a string or an array of strings
- if options[:path].present?
- cmd << '--'
- cmd += Array(options[:path])
- end
-
- raw_output, _status = run_git(cmd)
- lines = offset_in_ruby ? raw_output.lines.drop(offset) : raw_output.lines
-
- lines.map! { |c| Rugged::Commit.new(rugged, c.strip) }
- end
-
# We are trying to deprecate this method because it does a lot of work
# but it seems to be used only to look up submodule URL's.
# https://gitlab.com/gitlab-org/gitaly/issues/329
@@ -1619,11 +1561,6 @@ module Gitlab
end
end
- def last_commit_for_path_by_rugged(sha, path)
- sha = last_commit_id_for_path_by_shelling_out(sha, path)
- commit(sha)
- end
-
# Returns true if the given ref name exists
#
# Ref names must start with `refs/`.
@@ -1816,35 +1753,6 @@ module Gitlab
raise CommandError, @gitlab_projects.output
end
- def find_commits_by_message_by_shelling_out(query, ref, path, limit, offset)
- ref ||= root_ref
-
- args = %W(
- log #{ref} --pretty=%H --skip #{offset}
- --max-count #{limit} --grep=#{query} --regexp-ignore-case
- )
- args = args.concat(%W(-- #{path})) if path.present?
-
- git_log_results = run_git(args).first.lines
-
- git_log_results.map { |c| commit(c.chomp) }.compact
- end
-
- def find_commits_by_message_by_gitaly(query, ref, path, limit, offset)
- gitaly_commit_client
- .commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset)
- .map { |c| commit(c) }
- end
-
- def last_commit_for_path_by_gitaly(sha, path)
- gitaly_commit_client.last_commit_for_path(sha, path)
- end
-
- def last_commit_id_for_path_by_shelling_out(sha, path)
- args = %W(rev-list --max-count=1 #{sha} -- #{path})
- run_git_with_timeout(args, Gitlab::Git::Popen::FAST_GIT_PROCESS_TIMEOUT).first.strip
- end
-
def rugged_merge_base(from, to)
rugged.merge_base(from, to)
rescue Rugged::ReferenceError
@@ -1858,35 +1766,6 @@ module Gitlab
def sha_from_ref(ref)
rev_parse_target(ref).oid
end
-
- def build_git_cmd(*args)
- object_directories = alternate_object_directories.join(File::PATH_SEPARATOR)
-
- env = { 'PWD' => self.path }
- env['GIT_ALTERNATE_OBJECT_DIRECTORIES'] = object_directories if object_directories.present?
-
- [
- env,
- ::Gitlab.config.git.bin_path,
- *args,
- { chdir: self.path }
- ]
- end
-
- def git_diff_cmd(old_rev, new_rev)
- old_rev = old_rev == ::Gitlab::Git::BLANK_SHA ? ::Gitlab::Git::EMPTY_TREE_ID : old_rev
-
- build_git_cmd('diff', old_rev, new_rev, '--raw')
- end
-
- def git_cat_file_cmd
- format = '%(objectname) %(objectsize) %(rest)'
- build_git_cmd('cat-file', "--batch-check=#{format}")
- end
-
- def format_git_cat_file_script
- File.expand_path('../support/format-git-cat-file-input', __FILE__)
- end
end
end
end
diff --git a/lib/gitlab/git/support/format-git-cat-file-input b/lib/gitlab/git/support/format-git-cat-file-input
deleted file mode 100755
index 2e93c646d0f..00000000000
--- a/lib/gitlab/git/support/format-git-cat-file-input
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/env ruby
-
-# This script formats the output of the `git diff <old_rev> <new_rev> --raw`
-# command so it can be processed by `git cat-file`
-
-# We need to convert this:
-# ":100644 100644 5f53439... 85bc2f9... R060\tfiles/js/commit.js.coffee\tfiles/js/commit.coffee"
-# To:
-# "85bc2f9 R\tfiles/js/commit.js.coffee\tfiles/js/commit.coffee"
-
-ARGF.each do |line|
- _, _, old_blob_id, new_blob_id, rest = line.split(/\s/, 5)
-
- old_blob_id.gsub!(/[^\h]/, '')
- new_blob_id.gsub!(/[^\h]/, '')
-
- # We can't pass '0000000...' to `git cat-file` given it will not return info about the deleted file
- blob_id = new_blob_id =~ /\A0+\z/ ? old_blob_id : new_blob_id
-
- $stdout.puts "#{blob_id} #{rest}"
-end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 66e781a8e5b..58a4060cc96 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -401,8 +401,8 @@ module Gitlab
path.read.chomp
end
- def self.timestamp(t)
- Google::Protobuf::Timestamp.new(seconds: t.to_i)
+ def self.timestamp(time)
+ Google::Protobuf::Timestamp.new(seconds: time.to_i)
end
# The default timeout on all Gitaly calls
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 72e1e59d8df..6a97cd8ed17 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -399,8 +399,8 @@ module Gitlab
end
end
- def encode_repeated(a)
- Google::Protobuf::RepeatedField.new(:bytes, a.map { |s| encode_binary(s) } )
+ def encode_repeated(array)
+ Google::Protobuf::RepeatedField.new(:bytes, array.map { |s| encode_binary(s) } )
end
def call_find_commit(revision)
diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb
index ab2c61f6782..555733d1834 100644
--- a/lib/gitlab/gitaly_client/operation_service.rb
+++ b/lib/gitlab/gitaly_client/operation_service.rb
@@ -68,6 +68,22 @@ module Gitlab
raise Gitlab::Git::Repository::InvalidRef, ex
end
+ def user_update_branch(branch_name, user, newrev, oldrev)
+ request = Gitaly::UserUpdateBranchRequest.new(
+ repository: @gitaly_repo,
+ branch_name: encode_binary(branch_name),
+ user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
+ newrev: encode_binary(newrev),
+ oldrev: encode_binary(oldrev)
+ )
+
+ response = GitalyClient.call(@repository.storage, :operation_service, :user_update_branch, request)
+
+ if pre_receive_error = response.pre_receive_error.presence
+ raise Gitlab::Git::PreReceiveError, pre_receive_error
+ end
+ end
+
def user_delete_branch(branch_name, user)
request = Gitaly::UserDeleteBranchRequest.new(
repository: @gitaly_repo,
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index 982f8d0963b..64b9af4d70c 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -265,17 +265,39 @@ module Gitlab
true
end
- def write_config(full_path:)
- request = Gitaly::WriteConfigRequest.new(repository: @gitaly_repo, full_path: full_path)
- response = GitalyClient.call(
+ def set_config(entries)
+ return if entries.empty?
+
+ request = Gitaly::SetConfigRequest.new(repository: @gitaly_repo)
+ entries.each do |key, value|
+ request.entries << build_set_config_entry(key, value)
+ end
+
+ GitalyClient.call(
+ @storage,
+ :repository_service,
+ :set_config,
+ request,
+ timeout: GitalyClient.fast_timeout
+ )
+
+ nil
+ end
+
+ def delete_config(keys)
+ return if keys.empty?
+
+ request = Gitaly::DeleteConfigRequest.new(repository: @gitaly_repo, keys: keys)
+
+ GitalyClient.call(
@storage,
:repository_service,
- :write_config,
+ :delete_config,
request,
timeout: GitalyClient.fast_timeout
)
- raise Gitlab::Git::OSError.new(response.error) unless response.error.empty?
+ nil
end
def license_short_name
@@ -352,6 +374,23 @@ module Gitlab
timeout: timeout
)
end
+
+ def build_set_config_entry(key, value)
+ entry = Gitaly::SetConfigRequest::Entry.new(key: key)
+
+ case value
+ when String
+ entry.value_str = value
+ when Integer
+ entry.value_int32 = value
+ when TrueClass, FalseClass
+ entry.value_bool = value
+ else
+ raise InvalidArgument, "invalid git config value: #{value.inspect}"
+ end
+
+ entry
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/storage_settings.rb b/lib/gitlab/gitaly_client/storage_settings.rb
index 02fcb413abd..8e530de174d 100644
--- a/lib/gitlab/gitaly_client/storage_settings.rb
+++ b/lib/gitlab/gitaly_client/storage_settings.rb
@@ -60,8 +60,8 @@ module Gitlab
private
- def method_missing(m, *args, &block)
- @hash.public_send(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(msg, *args, &block)
+ @hash.public_send(msg, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
end
end
diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb
index 46b49128140..5070f4e3cfe 100644
--- a/lib/gitlab/google_code_import/importer.rb
+++ b/lib/gitlab/google_code_import/importer.rb
@@ -200,27 +200,27 @@ module Gitlab
"Status: #{name}"
end
- def linkify_issues(s)
- s = s.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2')
- s = s.gsub(/([Cc]omment) #([0-9]+)/, '\1 \2')
- s
+ def linkify_issues(str)
+ str = str.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2')
+ str = str.gsub(/([Cc]omment) #([0-9]+)/, '\1 \2')
+ str
end
- def escape_for_markdown(s)
+ def escape_for_markdown(str)
# No headings and lists
- s = s.gsub(/^#/, "\\#")
- s = s.gsub(/^-/, "\\-")
+ str = str.gsub(/^#/, "\\#")
+ str = str.gsub(/^-/, "\\-")
# No inline code
- s = s.gsub("`", "\\`")
+ str = str.gsub("`", "\\`")
# Carriage returns make me sad
- s = s.delete("\r")
+ str = str.delete("\r")
# Markdown ignores single newlines, but we need them as <br />.
- s = s.gsub("\n", " \n")
+ str = str.gsub("\n", " \n")
- s
+ str
end
def create_label(name)
diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb
index 6d2278d0876..2716834f566 100644
--- a/lib/gitlab/gpg/commit.rb
+++ b/lib/gitlab/gpg/commit.rb
@@ -39,7 +39,7 @@ module Gitlab
def update_signature!(cached_signature)
using_keychain do |gpg_key|
- cached_signature.update_attributes!(attributes(gpg_key))
+ cached_signature.update!(attributes(gpg_key))
end
@signature = cached_signature
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index 53fe2f8e436..be3710c5b7f 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -40,6 +40,10 @@ module Gitlab
"#{basename[0..FILENAME_LIMIT]}_export.tar.gz"
end
+ def object_storage?
+ Feature.enabled?(:import_export_object_storage)
+ end
+
def version
VERSION
end
diff --git a/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb b/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
index aef371d81eb..83134bb0769 100644
--- a/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
+++ b/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
@@ -2,6 +2,7 @@ module Gitlab
module ImportExport
module AfterExportStrategies
class BaseAfterExportStrategy
+ extend Gitlab::ImportExport::CommandLineUtil
include ActiveModel::Validations
extend Forwardable
@@ -24,9 +25,10 @@ module Gitlab
end
def execute(current_user, project)
- return unless project&.export_project_path
-
@project = project
+
+ return unless @project.export_status == :finished
+
@current_user = current_user
if invalid?
@@ -51,9 +53,12 @@ module Gitlab
end
def self.lock_file_path(project)
- return unless project&.export_path
+ return unless project.export_path || object_storage?
- File.join(project.export_path, AFTER_EXPORT_LOCK_FILE_NAME)
+ lock_path = project.import_export_shared.archive_path
+
+ mkdir_p(lock_path)
+ File.join(lock_path, AFTER_EXPORT_LOCK_FILE_NAME)
end
protected
@@ -77,6 +82,10 @@ module Gitlab
def log_validation_errors
errors.full_messages.each { |msg| project.import_export_shared.add_error_message(msg) }
end
+
+ def object_storage?
+ project.export_project_object_exists?
+ end
end
end
end
diff --git a/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
index 938664a95a1..dce8f89c0ab 100644
--- a/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
+++ b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
@@ -38,14 +38,20 @@ module Gitlab
private
def send_file
- export_file = File.open(project.export_project_path)
-
- Gitlab::HTTP.public_send(http_method.downcase, url, send_file_options(export_file)) # rubocop:disable GitlabSecurity/PublicSend
+ Gitlab::HTTP.public_send(http_method.downcase, url, send_file_options) # rubocop:disable GitlabSecurity/PublicSend
ensure
- export_file.close if export_file
+ export_file.close if export_file && !object_storage?
+ end
+
+ def export_file
+ if object_storage?
+ project.import_export_upload.export_file.file.open
+ else
+ File.open(project.export_project_path)
+ end
end
- def send_file_options(export_file)
+ def send_file_options
{
body_stream: export_file,
headers: headers
@@ -53,7 +59,15 @@ module Gitlab
end
def headers
- { 'Content-Length' => File.size(project.export_project_path).to_s }
+ { 'Content-Length' => export_size.to_s }
+ end
+
+ def export_size
+ if object_storage?
+ project.import_export_upload.export_file.file.size
+ else
+ File.size(project.export_project_path)
+ end
end
end
end
diff --git a/lib/gitlab/import_export/saver.rb b/lib/gitlab/import_export/saver.rb
index 2daeba90a51..3cd153a4fd2 100644
--- a/lib/gitlab/import_export/saver.rb
+++ b/lib/gitlab/import_export/saver.rb
@@ -15,15 +15,22 @@ module Gitlab
def save
if compress_and_save
remove_export_path
+
Rails.logger.info("Saved project export #{archive_file}")
- archive_file
+
+ save_on_object_storage if use_object_storage?
else
- @shared.error(Gitlab::ImportExport::Error.new("Unable to save #{archive_file} into #{@shared.export_path}"))
+ @shared.error(Gitlab::ImportExport::Error.new(error_message))
false
end
rescue => e
@shared.error(e)
false
+ ensure
+ if use_object_storage?
+ remove_archive
+ remove_export_path
+ end
end
private
@@ -36,9 +43,29 @@ module Gitlab
FileUtils.rm_rf(@shared.export_path)
end
+ def remove_archive
+ FileUtils.rm_rf(@shared.archive_path)
+ end
+
def archive_file
@archive_file ||= File.join(@shared.archive_path, Gitlab::ImportExport.export_filename(project: @project))
end
+
+ def save_on_object_storage
+ upload = ImportExportUpload.find_or_initialize_by(project: @project)
+
+ File.open(archive_file) { |file| upload.export_file = file }
+
+ upload.save!
+ end
+
+ def use_object_storage?
+ Gitlab::ImportExport.object_storage?
+ end
+
+ def error_message
+ "Unable to save #{archive_file} into #{@shared.export_path}. Object storage enabled: #{use_object_storage?}"
+ end
end
end
end
diff --git a/lib/gitlab/kubernetes/helm/base_command.rb b/lib/gitlab/kubernetes/helm/base_command.rb
index 3d778da90c7..f9ebe53d6af 100644
--- a/lib/gitlab/kubernetes/helm/base_command.rb
+++ b/lib/gitlab/kubernetes/helm/base_command.rb
@@ -18,7 +18,7 @@ module Gitlab
ALPINE_VERSION=$(cat /etc/alpine-release | cut -d '.' -f 1,2)
echo http://mirror.clarkson.edu/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
echo http://mirror1.hs-esslingen.de/pub/Mirrors/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
- apk add -U ca-certificates openssl >/dev/null
+ apk add -U wget ca-certificates openssl >/dev/null
wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{Gitlab::Kubernetes::Helm::HELM_VERSION}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
mv /tmp/linux-amd64/helm /usr/bin/
HEREDOC
diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb
index 344784c866f..db04356a5e9 100644
--- a/lib/gitlab/mail_room.rb
+++ b/lib/gitlab/mail_room.rb
@@ -53,7 +53,7 @@ module Gitlab
end
def config_file
- ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || File.expand_path('../../../config/gitlab.yml', __FILE__)
+ ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || File.expand_path('../../config/gitlab.yml', __dir__)
end
end
end
diff --git a/lib/gitlab/metrics/influx_db.rb b/lib/gitlab/metrics/influx_db.rb
index 66f30e3b397..04135dac4ff 100644
--- a/lib/gitlab/metrics/influx_db.rb
+++ b/lib/gitlab/metrics/influx_db.rb
@@ -162,7 +162,6 @@ module Gitlab
# When enabled this should be set before being used as the usual pattern
# "@foo ||= bar" is _not_ thread-safe.
- # rubocop:disable Gitlab/ModuleWithInstanceVariables
def pool
if influx_metrics_enabled?
if @pool.nil?
@@ -180,7 +179,6 @@ module Gitlab
@pool
end
end
- # rubocop:enable Gitlab/ModuleWithInstanceVariables
end
end
end
diff --git a/lib/gitlab/metrics/method_call.rb b/lib/gitlab/metrics/method_call.rb
index b11520a79bb..f3290e3149c 100644
--- a/lib/gitlab/metrics/method_call.rb
+++ b/lib/gitlab/metrics/method_call.rb
@@ -1,5 +1,3 @@
-# rubocop:disable Style/ClassVars
-
module Gitlab
module Metrics
# Class for tracking timing information about method calls
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
index 9753be6d5c3..18f91db98fc 100644
--- a/lib/gitlab/middleware/multipart.rb
+++ b/lib/gitlab/middleware/multipart.rb
@@ -84,7 +84,7 @@ module Gitlab
def open_file(params, key)
::UploadedFile.from_params(
params, key,
- Gitlab.config.uploads.storage_path)
+ [FileUploader.root, Gitlab.config.uploads.storage_path])
end
end
diff --git a/lib/gitlab/middleware/read_only/controller.rb b/lib/gitlab/middleware/read_only/controller.rb
index 4a99b7cca5c..8dca431c005 100644
--- a/lib/gitlab/middleware/read_only/controller.rb
+++ b/lib/gitlab/middleware/read_only/controller.rb
@@ -69,6 +69,7 @@ module Gitlab
@route_hash ||= Rails.application.routes.recognize_path(request.url, { method: request.request_method }) rescue {}
end
+ # Overridden in EE module
def whitelisted_routes
grack_route || ReadOnly.internal_routes.any? { |path| request.path.include?(path) } || lfs_route || sidekiq_route
end
diff --git a/lib/gitlab/omniauth_initializer.rb b/lib/gitlab/omniauth_initializer.rb
index 35ed3a5ac05..a71acda8701 100644
--- a/lib/gitlab/omniauth_initializer.rb
+++ b/lib/gitlab/omniauth_initializer.rb
@@ -1,5 +1,10 @@
module Gitlab
class OmniauthInitializer
+ def self.enabled?
+ Gitlab.config.omniauth.enabled ||
+ Gitlab.config.omniauth.auto_sign_in_with_provider.present?
+ end
+
def initialize(devise_config)
@devise_config = devise_config
end
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 5cedd9e84c2..e222541992a 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -74,17 +74,10 @@ module Gitlab
relative_path = name.dup
relative_path << '.git' unless relative_path.end_with?('.git')
- gitaly_migrate(:create_repository,
- status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
- if is_enabled
- repository = Gitlab::Git::Repository.new(storage, relative_path, '')
- repository.gitaly_repository_client.create_repository
- true
- else
- repo_path = File.join(Gitlab.config.repositories.storages[storage].legacy_disk_path, relative_path)
- Gitlab::Git::Repository.create(repo_path, bare: true, symlink_hooks_to: gitlab_shell_hooks_path)
- end
- end
+ repository = Gitlab::Git::Repository.new(storage, relative_path, '')
+ wrapped_gitaly_errors { repository.gitaly_repository_client.create_repository }
+
+ true
rescue => err # Once the Rugged codes gets removes this can be improved
Rails.logger.error("Failed to add repository #{storage}/#{name}: #{err}")
false
@@ -448,7 +441,11 @@ module Gitlab
end
def gitaly_migrate(method, status: Gitlab::GitalyClient::MigrationStatus::OPT_IN, &block)
- Gitlab::GitalyClient.migrate(method, status: status, &block)
+ wrapped_gitaly_errors { Gitlab::GitalyClient.migrate(method, status: status, &block) }
+ end
+
+ def wrapped_gitaly_errors
+ yield
rescue GRPC::NotFound, GRPC::BadStatus => e
# Old Popen code returns [Error, output] to the caller, so we
# need to do the same here...
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index e893e46ee86..55c899912f9 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -37,21 +37,14 @@ module Gitlab
end
def send_git_blob(repository, blob)
- params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_raw_show, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
- {
- 'GitalyServer' => gitaly_server_hash(repository),
- 'GetBlobRequest' => {
- repository: repository.gitaly_repository.to_h,
- oid: blob.id,
- limit: -1
- }
- }
- else
- {
- 'RepoPath' => repository.path_to_repo,
- 'BlobId' => blob.id
- }
- end
+ params = {
+ 'GitalyServer' => gitaly_server_hash(repository),
+ 'GetBlobRequest' => {
+ repository: repository.gitaly_repository.to_h,
+ oid: blob.id,
+ limit: -1
+ }
+ }
[
SEND_DATA_HEADER,
@@ -91,16 +84,12 @@ module Gitlab
end
def send_git_diff(repository, diff_refs)
- params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
- {
- 'GitalyServer' => gitaly_server_hash(repository),
- 'RawDiffRequest' => Gitaly::RawDiffRequest.new(
- gitaly_diff_or_patch_hash(repository, diff_refs)
- ).to_json
- }
- else
- workhorse_diff_or_patch_hash(repository, diff_refs)
- end
+ params = {
+ 'GitalyServer' => gitaly_server_hash(repository),
+ 'RawDiffRequest' => Gitaly::RawDiffRequest.new(
+ gitaly_diff_or_patch_hash(repository, diff_refs)
+ ).to_json
+ }
[
SEND_DATA_HEADER,
diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb
index be0d97370d0..e877ab10248 100644
--- a/lib/rouge/formatters/html_gitlab.rb
+++ b/lib/rouge/formatters/html_gitlab.rb
@@ -11,7 +11,7 @@ module Rouge
@tag = tag
end
- def stream(tokens, &b)
+ def stream(tokens)
is_first = true
token_lines(tokens) do |line|
yield "\n" unless is_first
diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake
index 21998dd2f5b..6df7fe81437 100644
--- a/lib/tasks/gettext.rake
+++ b/lib/tasks/gettext.rake
@@ -19,6 +19,23 @@ namespace :gettext do
Rake::Task['gettext:po_to_json'].invoke
end
+ task :regenerate do
+ # Remove all translated files, this speeds up finding
+ FileUtils.rm Dir['locale/**/gitlab.*']
+ # remove the `pot` file to ensure it's completely regenerated
+ FileUtils.rm_f 'locale/gitlab.pot'
+
+ Rake::Task['gettext:find'].invoke
+
+ # leave only the required changes.
+ `git checkout -- locale/*/gitlab.po`
+
+ puts <<~MSG
+ All done. Please commit the changes to `locale/gitlab.pot`.
+
+ MSG
+ end
+
desc 'Lint all po files in `locale/'
task lint: :environment do
require 'simple_po_parser'
@@ -50,13 +67,12 @@ namespace :gettext do
end
task :updated_check do
+ pot_file = 'locale/gitlab.pot'
# Removing all pre-translated files speeds up `gettext:find` as the
# files don't need to be merged.
# Having `LC_MESSAGES/gitlab.mo files present also confuses the output.
FileUtils.rm Dir['locale/**/gitlab.*']
-
- # Make sure we start out with a clean pot.file
- `git checkout -- locale/gitlab.pot`
+ FileUtils.rm_f pot_file
# `gettext:find` writes touches to temp files to `stderr` which would cause
# `static-analysis` to report failures. We can ignore these.
@@ -64,18 +80,18 @@ namespace :gettext do
Rake::Task['gettext:find'].invoke
end
- pot_diff = `git diff -- locale/gitlab.pot`.strip
+ pot_diff = `git diff -- #{pot_file} | grep -E '^(\\+|-)msgid'`.strip
# reset the locale folder for potential next tasks
`git checkout -- locale`
if pot_diff.present?
raise <<~MSG
- Newly translated strings found, please add them to `gitlab.pot` by running:
+ Newly translated strings found, please add them to `#{pot_file}` by running:
- rm locale/**/gitlab.*; bin/rake gettext:find; git checkout -- locale/*/gitlab.po
+ bin/rake gettext:regenerate
- Then commit and push the resulting changes to `locale/gitlab.pot`.
+ Then commit and push the resulting changes to `#{pot_file}`.
The diff was:
diff --git a/lib/tasks/gitlab/uploads/migrate.rake b/lib/tasks/gitlab/uploads/migrate.rake
index 78e18992a8e..f548a266b99 100644
--- a/lib/tasks/gitlab/uploads/migrate.rake
+++ b/lib/tasks/gitlab/uploads/migrate.rake
@@ -8,7 +8,7 @@ namespace :gitlab do
@uploader_class = args.uploader_class.constantize
@model_class = args.model_class.constantize
- uploads.each_batch(of: batch_size, &method(:enqueue_batch)) # rubocop: disable Cop/InBatches
+ uploads.each_batch(of: batch_size, &method(:enqueue_batch))
end
def enqueue_batch(batch, index)
diff --git a/lib/uploaded_file.rb b/lib/uploaded_file.rb
index 5dc85b2baea..4b9cb59eab5 100644
--- a/lib/uploaded_file.rb
+++ b/lib/uploaded_file.rb
@@ -28,7 +28,7 @@ class UploadedFile
@tempfile = File.new(path, 'rb')
end
- def self.from_params(params, field, upload_path)
+ def self.from_params(params, field, upload_paths)
unless params["#{field}.path"]
raise InvalidPathError, "file is invalid" if params["#{field}.remote_id"]
@@ -37,7 +37,8 @@ class UploadedFile
file_path = File.realpath(params["#{field}.path"])
- unless self.allowed_path?(file_path, [upload_path, Dir.tmpdir].compact)
+ paths = Array(upload_paths) << Dir.tmpdir
+ unless self.allowed_path?(file_path, paths.compact)
raise InvalidPathError, "insecure path used '#{file_path}'"
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 0385761de45..ca488053a1f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-07-01 21:24+1000\n"
-"PO-Revision-Date: 2018-07-01 21:24+1000\n"
+"POT-Creation-Date: 2018-07-09 08:28+0200\n"
+"PO-Revision-Date: 2018-07-09 08:28+0200\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -369,9 +369,18 @@ msgstr ""
msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import."
msgstr ""
+msgid "An error accured whilst committing your changes."
+msgstr ""
+
msgid "An error occured creating the new branch."
msgstr ""
+msgid "An error occured whilst fetching the job trace."
+msgstr ""
+
+msgid "An error occured whilst fetching the latest pipline."
+msgstr ""
+
msgid "An error occured whilst loading all the files."
msgstr ""
@@ -390,6 +399,9 @@ msgstr ""
msgid "An error occured whilst loading the merge request."
msgstr ""
+msgid "An error occured whilst loading the pipelines jobs."
+msgstr ""
+
msgid "An error occurred previewing the blob"
msgstr ""
@@ -1085,9 +1097,6 @@ msgstr ""
msgid "ClusterIntegration|Add Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|Add an existing Kubernetes cluster"
-msgstr ""
-
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
@@ -1109,9 +1118,6 @@ msgstr ""
msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
msgstr ""
-msgid "ClusterIntegration|Choose how to set up Kubernetes cluster integration"
-msgstr ""
-
msgid "ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster."
msgstr ""
@@ -1139,18 +1145,6 @@ msgstr ""
msgid "ClusterIntegration|Create Kubernetes cluster"
msgstr ""
-msgid "ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine"
-msgstr ""
-
-msgid "ClusterIntegration|Create a new Kubernetes cluster on Google Kubernetes Engine right from GitLab"
-msgstr ""
-
-msgid "ClusterIntegration|Create on Google Kubernetes Engine"
-msgstr ""
-
-msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
-msgstr ""
-
msgid "ClusterIntegration|Enter the details for your Kubernetes cluster"
msgstr ""
@@ -2150,15 +2144,9 @@ msgstr ""
msgid "Error Reporting and Logging"
msgstr ""
-msgid "Error committing changes. Please try again."
-msgstr ""
-
msgid "Error fetching contributors data."
msgstr ""
-msgid "Error fetching job trace"
-msgstr ""
-
msgid "Error fetching labels."
msgstr ""
@@ -2177,6 +2165,9 @@ msgstr ""
msgid "Error loading last commit."
msgstr ""
+msgid "Error loading markdown preview"
+msgstr ""
+
msgid "Error loading merge requests."
msgstr ""
@@ -2234,6 +2225,9 @@ msgstr ""
msgid "Expand sidebar"
msgstr ""
+msgid "Explore groups"
+msgstr ""
+
msgid "Explore projects"
msgstr ""
@@ -2449,6 +2443,27 @@ msgstr ""
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr ""
+msgid "GroupsDropdown|Frequently visited"
+msgstr ""
+
+msgid "GroupsDropdown|Groups you visit often will appear here"
+msgstr ""
+
+msgid "GroupsDropdown|Loading groups"
+msgstr ""
+
+msgid "GroupsDropdown|Search your groups"
+msgstr ""
+
+msgid "GroupsDropdown|Something went wrong on our end."
+msgstr ""
+
+msgid "GroupsDropdown|Sorry, no groups matched your search"
+msgstr ""
+
+msgid "GroupsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "GroupsEmptyState|A group is a collection of several projects."
msgstr ""
@@ -2631,7 +2646,7 @@ msgstr ""
msgid "Introducing Cycle Analytics"
msgstr ""
-msgid "Issue Board"
+msgid "Issue Boards"
msgstr ""
msgid "Issue events"
@@ -2825,9 +2840,6 @@ msgstr ""
msgid "Locked to current projects"
msgstr ""
-msgid "Login"
-msgstr ""
-
msgid "Manage all notifications"
msgstr ""
@@ -2864,6 +2876,9 @@ msgstr ""
msgid "Members"
msgstr ""
+msgid "Merge Request"
+msgstr ""
+
msgid "Merge Request:"
msgstr ""
@@ -2906,12 +2921,42 @@ msgstr ""
msgid "Messages"
msgstr ""
+msgid "Metrics"
+msgstr ""
+
msgid "Metrics - Influx"
msgstr ""
msgid "Metrics - Prometheus"
msgstr ""
+msgid "Metrics|Check out the CI/CD documentation on deploying to an environment"
+msgstr ""
+
+msgid "Metrics|Environment"
+msgstr ""
+
+msgid "Metrics|Learn about environments"
+msgstr ""
+
+msgid "Metrics|No deployed environments"
+msgstr ""
+
+msgid "Metrics|There was an error fetching the environments data, please try again"
+msgstr ""
+
+msgid "Metrics|There was an error getting deployment information."
+msgstr ""
+
+msgid "Metrics|There was an error getting environments information."
+msgstr ""
+
+msgid "Metrics|Unexpected deployment data response from prometheus endpoint"
+msgstr ""
+
+msgid "Metrics|Unexpected metrics data response from prometheus endpoint"
+msgstr ""
+
msgid "Milestone"
msgstr ""
@@ -2995,12 +3040,6 @@ msgid_plural "New Issues"
msgstr[0] ""
msgstr[1] ""
-msgid "New Kubernetes Cluster"
-msgstr ""
-
-msgid "New Kubernetes cluster"
-msgstr ""
-
msgid "New Label"
msgstr ""
@@ -3217,6 +3256,9 @@ msgstr ""
msgid "Only project members can comment."
msgstr ""
+msgid "Oops, are you sure?"
+msgstr ""
+
msgid "Open in Xcode"
msgstr ""
@@ -3505,6 +3547,9 @@ msgstr ""
msgid "Profiles|Account scheduled for removal."
msgstr ""
+msgid "Profiles|Add key"
+msgstr ""
+
msgid "Profiles|Change username"
msgstr ""
@@ -3532,9 +3577,15 @@ msgstr ""
msgid "Profiles|Path"
msgstr ""
+msgid "Profiles|This doesn't look like a public SSH key, are you sure you want to add it?"
+msgstr ""
+
msgid "Profiles|Type your %{confirmationValue} to confirm:"
msgstr ""
+msgid "Profiles|Typically starts with \"ssh-rsa …\""
+msgstr ""
+
msgid "Profiles|Update username"
msgstr ""
@@ -3553,6 +3604,9 @@ msgstr ""
msgid "Profiles|Your account is currently an owner in these groups:"
msgstr ""
+msgid "Profiles|e.g. My MacBook key"
+msgstr ""
+
msgid "Profiles|your account"
msgstr ""
@@ -3640,9 +3694,6 @@ msgstr ""
msgid "ProjectsDropdown|Sorry, no projects matched your search"
msgstr ""
-msgid "ProjectsDropdown|This feature requires browser localStorage support"
-msgstr ""
-
msgid "PrometheusDashboard|Time"
msgstr ""
@@ -4528,12 +4579,6 @@ msgstr ""
msgid "There are problems accessing Git storage: "
msgstr ""
-msgid "There was an error loading jobs"
-msgstr ""
-
-msgid "There was an error loading latest pipeline"
-msgstr ""
-
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -4814,6 +4859,9 @@ msgstr ""
msgid "Tip:"
msgstr ""
+msgid "Title"
+msgstr ""
+
msgid "To GitLab"
msgstr ""
@@ -5182,6 +5230,9 @@ msgstr ""
msgid "Yes"
msgstr ""
+msgid "Yes, add it"
+msgstr ""
+
msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr ""
@@ -5441,6 +5492,9 @@ msgstr ""
msgid "mrWidget|Merged by"
msgstr ""
+msgid "mrWidget|Open in Web IDE"
+msgstr ""
+
msgid "mrWidget|Plain diff"
msgstr ""
@@ -5510,9 +5564,6 @@ msgstr ""
msgid "mrWidget|This project is archived, write access has been disabled"
msgstr ""
-msgid "mrWidget|Web IDE"
-msgstr ""
-
msgid "mrWidget|You can merge this merge request manually using the"
msgstr ""
diff --git a/package.json b/package.json
index 6980416503e..26b87c70e98 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
"webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
},
"dependencies": {
- "@gitlab-org/gitlab-svgs": "^1.24.0",
+ "@gitlab-org/gitlab-svgs": "^1.25.0",
"autosize": "^4.0.0",
"axios": "^0.17.1",
"babel-core": "^6.26.3",
@@ -66,7 +66,7 @@
"katex": "^0.8.3",
"marked": "^0.3.12",
"monaco-editor": "0.13.1",
- "monaco-editor-webpack-plugin": "^1.2.1",
+ "monaco-editor-webpack-plugin": "^1.4.0",
"mousetrap": "^1.4.6",
"pikaday": "^1.6.1",
"popper.js": "^1.14.3",
diff --git a/qa/qa.rb b/qa/qa.rb
index 5013024e60f..6d0521f8ed0 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -40,6 +40,7 @@ module QA
autoload :Issue, 'qa/factory/resource/issue'
autoload :Project, 'qa/factory/resource/project'
autoload :MergeRequest, 'qa/factory/resource/merge_request'
+ autoload :ProjectImportedFromGithub, 'qa/factory/resource/project_imported_from_github'
autoload :DeployKey, 'qa/factory/resource/deploy_key'
autoload :Branch, 'qa/factory/resource/branch'
autoload :SecretVariable, 'qa/factory/resource/secret_variable'
@@ -79,6 +80,7 @@ module QA
autoload :Instance, 'qa/scenario/test/instance'
module Integration
+ autoload :Github, 'qa/scenario/test/integration/github'
autoload :LDAP, 'qa/scenario/test/integration/ldap'
autoload :Kubernetes, 'qa/scenario/test/integration/kubernetes'
autoload :Mattermost, 'qa/scenario/test/integration/mattermost'
@@ -132,6 +134,10 @@ module QA
autoload :Show, 'qa/page/project/show'
autoload :Activity, 'qa/page/project/activity'
+ module Import
+ autoload :Github, 'qa/page/project/import/github'
+ end
+
module Pipeline
autoload :Index, 'qa/page/project/pipeline/index'
autoload :Show, 'qa/page/project/pipeline/show'
@@ -184,6 +190,10 @@ module QA
autoload :PersonalAccessTokens, 'qa/page/profile/personal_access_tokens'
end
+ module Issuable
+ autoload :Sidebar, 'qa/page/issuable/sidebar'
+ end
+
module MergeRequest
autoload :New, 'qa/page/merge_request/new'
autoload :Show, 'qa/page/merge_request/show'
@@ -206,6 +216,7 @@ module QA
#
module Component
autoload :Dropzone, 'qa/page/component/dropzone'
+ autoload :Select2, 'qa/page/component/select2'
end
end
diff --git a/qa/qa/factory/resource/group.rb b/qa/qa/factory/resource/group.rb
index 9f13e26f35c..531fccd2ad8 100644
--- a/qa/qa/factory/resource/group.rb
+++ b/qa/qa/factory/resource/group.rb
@@ -23,7 +23,7 @@ module QA
Page::Group::New.perform do |group|
group.set_path(@path)
group.set_description(@description)
- group.set_visibility('Private')
+ group.set_visibility('Public')
group.create
end
end
diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb
index cda1b35ba6a..7bc64c6ae5d 100644
--- a/qa/qa/factory/resource/project.rb
+++ b/qa/qa/factory/resource/project.rb
@@ -5,16 +5,12 @@ module QA
module Resource
class Project < Factory::Base
attr_writer :description
+ attr_reader :name
dependency Factory::Resource::Group, as: :group
- def name=(name)
- @name = "#{name}-#{SecureRandom.hex(8)}"
- @description = 'My awesome project'
- end
-
- product :name do
- Page::Project::Show.act { project_name }
+ product :name do |factory|
+ factory.name
end
product :repository_ssh_location do
@@ -24,6 +20,14 @@ module QA
end
end
+ def initialize
+ @description = 'My awesome project'
+ end
+
+ def name=(raw_name)
+ @name = "#{raw_name}-#{SecureRandom.hex(8)}"
+ end
+
def fabricate!
group.visit!
diff --git a/qa/qa/factory/resource/project_imported_from_github.rb b/qa/qa/factory/resource/project_imported_from_github.rb
new file mode 100644
index 00000000000..df2a3340d60
--- /dev/null
+++ b/qa/qa/factory/resource/project_imported_from_github.rb
@@ -0,0 +1,37 @@
+require 'securerandom'
+
+module QA
+ module Factory
+ module Resource
+ class ProjectImportedFromGithub < Resource::Project
+ attr_writer :personal_access_token, :github_repository_path
+
+ dependency Factory::Resource::Group, as: :group
+
+ product :name do |factory|
+ factory.name
+ end
+
+ def fabricate!
+ group.visit!
+
+ Page::Group::Show.act { go_to_new_project }
+
+ Page::Project::New.perform do |page|
+ page.go_to_import_project
+ end
+
+ Page::Project::New.perform do |page|
+ page.go_to_github_import
+ end
+
+ Page::Project::Import::Github.perform do |page|
+ page.add_personal_access_token(@personal_access_token)
+ page.list_repos
+ page.import!(@github_repository_path, @name)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb
index ad376988e82..4f6039f300f 100644
--- a/qa/qa/factory/resource/sandbox.rb
+++ b/qa/qa/factory/resource/sandbox.rb
@@ -21,8 +21,8 @@ module QA
Page::Group::New.perform do |group|
group.set_path(@name)
- group.set_description('GitLab QA Sandbox')
- group.set_visibility('Private')
+ group.set_description('GitLab QA Sandbox Group')
+ group.set_visibility('Public')
group.create
end
end
diff --git a/qa/qa/page/component/select2.rb b/qa/qa/page/component/select2.rb
new file mode 100644
index 00000000000..30829eb0221
--- /dev/null
+++ b/qa/qa/page/component/select2.rb
@@ -0,0 +1,11 @@
+module QA
+ module Page
+ module Component
+ module Select2
+ def select_item(item_text)
+ find('ul.select2-result-sub > li', text: item_text).click
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/issuable/sidebar.rb b/qa/qa/page/issuable/sidebar.rb
new file mode 100644
index 00000000000..dec2ce1eab3
--- /dev/null
+++ b/qa/qa/page/issuable/sidebar.rb
@@ -0,0 +1,17 @@
+module QA
+ module Page
+ module Issuable
+ class Sidebar < Page::Base
+ view 'app/views/shared/issuable/_sidebar.html.haml' do
+ element :labels_block, ".issuable-show-labels"
+ end
+
+ def has_label?(label)
+ page.within('.issuable-show-labels') do
+ !!find('span', text: label)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/menu/main.rb b/qa/qa/page/menu/main.rb
index fda9c45c091..aef5c9f9c82 100644
--- a/qa/qa/page/menu/main.rb
+++ b/qa/qa/page/menu/main.rb
@@ -16,7 +16,7 @@ module QA
view 'app/views/layouts/nav/_dashboard.html.haml' do
element :admin_area_link
element :projects_dropdown
- element :groups_link
+ element :groups_dropdown
end
view 'app/views/layouts/nav/projects_dropdown/_show.html.haml' do
@@ -25,7 +25,13 @@ module QA
end
def go_to_groups
- within_top_menu { click_element :groups_link }
+ within_top_menu do
+ click_element :groups_dropdown
+ end
+
+ page.within('.qa-groups-dropdown-sidebar') do
+ click_element :your_groups_link
+ end
end
def go_to_projects
diff --git a/qa/qa/page/menu/side.rb b/qa/qa/page/menu/side.rb
index 6bf4825cf00..333d871c51a 100644
--- a/qa/qa/page/menu/side.rb
+++ b/qa/qa/page/menu/side.rb
@@ -10,6 +10,8 @@ module QA
element :operations_kubernetes_link, "title: _('Kubernetes')"
element :issues_link, /link_to.*shortcuts-issues/
element :issues_link_text, "Issues"
+ element :merge_requests_link, /link_to.*shortcuts-merge_requests/
+ element :merge_requests_link_text, "Merge Requests"
element :top_level_items, '.sidebar-top-level-items'
element :operations_section, "class: 'shortcuts-operations'"
element :activity_link, "title: 'Activity'"
@@ -62,6 +64,12 @@ module QA
end
end
+ def click_merge_requests
+ within_sidebar do
+ click_link('Merge Requests')
+ end
+ end
+
def click_wiki
within_sidebar do
click_link('Wiki')
diff --git a/qa/qa/page/project/import/github.rb b/qa/qa/page/project/import/github.rb
new file mode 100644
index 00000000000..36567927194
--- /dev/null
+++ b/qa/qa/page/project/import/github.rb
@@ -0,0 +1,66 @@
+module QA
+ module Page
+ module Project
+ module Import
+ class Github < Page::Base
+ include Page::Component::Select2
+
+ view 'app/views/import/github/new.html.haml' do
+ element :personal_access_token_field, 'text_field_tag :personal_access_token'
+ element :list_repos_button, "submit_tag _('List your GitHub repositories')"
+ end
+
+ view 'app/views/import/_githubish_status.html.haml' do
+ element :project_import_row, 'data: { qa: { repo_path: repo.full_name } }'
+ element :project_namespace_select
+ element :project_namespace_field, 'select_tag :namespace_id'
+ element :project_path_field, 'text_field_tag :path, repo.name'
+ element :import_button, "_('Import')"
+ end
+
+ def add_personal_access_token(personal_access_token)
+ fill_in 'personal_access_token', with: personal_access_token
+ end
+
+ def list_repos
+ click_button 'List your GitHub repositories'
+ end
+
+ def import!(full_path, name)
+ choose_test_namespace(full_path)
+ set_path(full_path, name)
+ import_project(full_path)
+ end
+
+ private
+
+ def within_repo_path(full_path)
+ page.within(%Q(tr[data-qa-repo-path="#{full_path}"])) do
+ yield
+ end
+ end
+
+ def choose_test_namespace(full_path)
+ within_repo_path(full_path) do
+ click_element :project_namespace_select
+ end
+
+ select_item(Runtime::Namespace.path)
+ end
+
+ def set_path(full_path, name)
+ within_repo_path(full_path) do
+ fill_in 'path', with: name
+ end
+ end
+
+ def import_project(full_path)
+ within_repo_path(full_path) do
+ click_button 'Import'
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb
index 186a4724326..7976e96d43b 100644
--- a/qa/qa/page/project/new.rb
+++ b/qa/qa/page/project/new.rb
@@ -2,6 +2,12 @@ module QA
module Page
module Project
class New < Page::Base
+ include Page::Component::Select2
+
+ view 'app/views/projects/new.html.haml' do
+ element :import_project_tab, "Import project"
+ end
+
view 'app/views/projects/_new_project_fields.html.haml' do
element :project_namespace_select
element :project_namespace_field, /select :namespace_id.*class: 'select2/
@@ -10,10 +16,18 @@ module QA
element :project_create_button, "submit 'Create project'"
end
+ view 'app/views/projects/_import_project_pane.html.haml' do
+ element :import_github, "icon('github', text: 'GitHub')"
+ end
+
def choose_test_namespace
click_element :project_namespace_select
- find('ul.select2-result-sub > li', text: Runtime::Namespace.path).click
+ select_item(Runtime::Namespace.path)
+ end
+
+ def go_to_import_project
+ click_on 'Import project'
end
def choose_name(name)
@@ -27,6 +41,10 @@ module QA
def create_new_project
click_on 'Create project'
end
+
+ def go_to_github_import
+ click_link 'GitHub'
+ end
end
end
end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index 1406edece17..1dcdb59490a 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -22,6 +22,10 @@ module QA
element :branches_dropdown
end
+ view 'app/views/projects/_files.html.haml' do
+ element :tree_holder, '.tree-holder'
+ end
+
def project_name
find('.qa-project-name').text
end
@@ -46,6 +50,12 @@ module QA
click_element :create_merge_request
end
+ def wait_for_import
+ wait(reload: true) do
+ has_css?('.tree-holder')
+ end
+ end
+
def go_to_new_issue
click_element :new_menu_toggle
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index 7610c7f3f43..5dc194e0aef 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -66,6 +66,17 @@ module QA
def has_gcloud_credentials?
%w[GCLOUD_ACCOUNT_KEY GCLOUD_ACCOUNT_EMAIL].none? { |var| ENV[var].to_s.empty? }
end
+
+ # Specifies the token that can be used for the GitHub API
+ def github_access_token
+ ENV['GITHUB_ACCESS_TOKEN'].to_s.strip
+ end
+
+ def require_github_access_token!
+ return unless github_access_token.empty?
+
+ raise ArgumentError, "Please provide GITHUB_ACCESS_TOKEN"
+ end
end
end
end
diff --git a/qa/qa/runtime/namespace.rb b/qa/qa/runtime/namespace.rb
index 8d05b387416..ccfa8b44db3 100644
--- a/qa/qa/runtime/namespace.rb
+++ b/qa/qa/runtime/namespace.rb
@@ -16,7 +16,7 @@ module QA
end
def sandbox_name
- Runtime::Env.sandbox_name || 'gitlab-qa-sandbox'
+ Runtime::Env.sandbox_name || 'gitlab-qa-sandbox-group'
end
end
end
diff --git a/qa/qa/runtime/release.rb b/qa/qa/runtime/release.rb
index 12e56404cf6..4f83a773645 100644
--- a/qa/qa/runtime/release.rb
+++ b/qa/qa/runtime/release.rb
@@ -21,7 +21,7 @@ module QA
end
def self.method_missing(name, *args)
- self.new.strategy.public_send(name, *args) # rubocop:disable GitlabSecurity/PublicSend
+ self.new.strategy.public_send(name, *args)
end
end
end
diff --git a/qa/qa/scenario/test/integration/github.rb b/qa/qa/scenario/test/integration/github.rb
new file mode 100644
index 00000000000..1d22b532aa5
--- /dev/null
+++ b/qa/qa/scenario/test/integration/github.rb
@@ -0,0 +1,18 @@
+module QA
+ module Scenario
+ module Test
+ module Integration
+ class Github < Test::Instance
+ tags :github
+
+ def perform(address, *rspec_options)
+ # This test suite requires a GitHub personal access token
+ Runtime::Env.require_github_access_token!
+
+ super
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/project/import_from_github_spec.rb b/qa/qa/specs/features/project/import_from_github_spec.rb
new file mode 100644
index 00000000000..221b5c27fba
--- /dev/null
+++ b/qa/qa/specs/features/project/import_from_github_spec.rb
@@ -0,0 +1,106 @@
+module QA
+ describe 'user imports a GitHub repo', :core, :github do
+ let(:imported_project) do
+ Factory::Resource::ProjectImportedFromGithub.fabricate! do |project|
+ project.name = 'imported-project'
+ project.personal_access_token = Runtime::Env.github_access_token
+ project.github_repository_path = 'gitlab-qa/test-project'
+ end
+ end
+
+ after do
+ # We need to delete the imported project because it's impossible to import
+ # the same GitHub project twice for a given user.
+ api_client = Runtime::API::Client.new(:gitlab)
+ delete_project_request = Runtime::API::Request.new(api_client, "/projects/#{CGI.escape("#{Runtime::Namespace.path}/#{imported_project.name}")}")
+ delete delete_project_request.url
+
+ expect_status(202)
+ end
+
+ it 'user imports a GitHub repo' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ imported_project # import the project
+
+ Page::Menu::Main.act { go_to_projects }
+ Page::Dashboard::Projects.perform do |dashboard|
+ dashboard.go_to_project(imported_project.name)
+ end
+
+ Page::Project::Show.act { wait_for_import }
+
+ verify_repository_import
+ verify_issues_import
+ verify_merge_requests_import
+ verify_labels_import
+ verify_milestones_import
+ verify_wiki_import
+ end
+
+ def verify_repository_import
+ expect(page).to have_content('This test project is used for automated GitHub import by GitLab QA.')
+ expect(page).to have_content(imported_project.name)
+ end
+
+ def verify_issues_import
+ Page::Menu::Side.act { click_issues }
+ expect(page).to have_content('This is a sample issue')
+
+ click_link 'This is a sample issue'
+
+ expect(page).to have_content('We should populate this project with issues, pull requests and wiki pages.')
+
+ # Comments
+ expect(page).to have_content('This is a comment from @rymai.')
+
+ Page::Issuable::Sidebar.perform do |issuable|
+ expect(issuable).to have_label('enhancement')
+ expect(issuable).to have_label('help wanted')
+ expect(issuable).to have_label('good first issue')
+ end
+ end
+
+ def verify_merge_requests_import
+ Page::Menu::Side.act { click_merge_requests }
+ expect(page).to have_content('Improve README.md')
+
+ click_link 'Improve README.md'
+
+ expect(page).to have_content('This improves the README file a bit.')
+
+ # Review comment are not supported yet
+ expect(page).not_to have_content('Really nice change.')
+
+ # Comments
+ expect(page).to have_content('Nice work! This is a comment from @rymai.')
+
+ # Diff comments
+ expect(page).to have_content('[Review comment] I like that!')
+ expect(page).to have_content('[Review comment] Nice blank line.')
+ expect(page).to have_content('[Single diff comment] Much better without this line!')
+
+ Page::Issuable::Sidebar.perform do |issuable|
+ expect(issuable).to have_label('bug')
+ expect(issuable).to have_label('enhancement')
+ end
+ end
+
+ def verify_labels_import
+ # TODO: Waiting on https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19228
+ # to build upon it.
+ end
+
+ def verify_milestones_import
+ # TODO: Waiting on https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18727
+ # to build upon it.
+ end
+
+ def verify_wiki_import
+ Page::Menu::Side.act { click_wiki }
+
+ expect(page).to have_content('Welcome to the test-project wiki!')
+ end
+ end
+end
diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb
index 2b6365dbc41..851026c71f0 100644
--- a/qa/spec/runtime/env_spec.rb
+++ b/qa/spec/runtime/env_spec.rb
@@ -76,4 +76,27 @@ describe QA::Runtime::Env do
expect { described_class.user_type }.to raise_error(ArgumentError)
end
end
+
+ describe '.github_access_token' do
+ it 'returns "" if GITHUB_ACCESS_TOKEN is not defined' do
+ expect(described_class.github_access_token).to eq('')
+ end
+
+ it 'returns stripped string if GITHUB_ACCESS_TOKEN is defined' do
+ stub_env('GITHUB_ACCESS_TOKEN', ' abc123 ')
+ expect(described_class.github_access_token).to eq('abc123')
+ end
+ end
+
+ describe '.require_github_access_token!' do
+ it 'raises ArgumentError if GITHUB_ACCESS_TOKEN is not defined' do
+ expect { described_class.require_github_access_token! }.to raise_error(ArgumentError)
+ end
+
+ it 'does not raise if GITHUB_ACCESS_TOKEN is defined' do
+ stub_env('GITHUB_ACCESS_TOKEN', ' abc123 ')
+
+ expect { described_class.require_github_access_token! }.not_to raise_error
+ end
+ end
end
diff --git a/rubocop/cop/line_break_around_conditional_block.rb b/rubocop/cop/line_break_around_conditional_block.rb
index 8b6052fee1b..011f2bcf8bf 100644
--- a/rubocop/cop/line_break_around_conditional_block.rb
+++ b/rubocop/cop/line_break_around_conditional_block.rb
@@ -43,6 +43,8 @@ module RuboCop
# end
# end
class LineBreakAroundConditionalBlock < RuboCop::Cop::Cop
+ include RangeHelp
+
MSG = 'Add a line break around conditional blocks'
def on_if(node)
diff --git a/scripts/frontend/prettier.js b/scripts/frontend/prettier.js
index 6e4e36b9b2d..b66ba885701 100644
--- a/scripts/frontend/prettier.js
+++ b/scripts/frontend/prettier.js
@@ -49,7 +49,7 @@ const stagedFiles =
if (stagedFiles) {
if (!stagedFiles.length || (stagedFiles.length === 1 && !stagedFiles[0])) {
console.log('No matching staged files.');
- return;
+ process.exit(1);
}
console.log(`Matching staged Files : ${stagedFiles.length}`);
}
@@ -78,7 +78,7 @@ files = prettierIgnore.filter(files);
if (!files.length) {
console.log('No Files found to process with Prettier');
- return;
+ process.exit(1);
}
console.log(`${shouldSave ? 'Updating' : 'Checking'} ${files.length} file(s)`);
diff --git a/spec/controllers/concerns/group_tree_spec.rb b/spec/controllers/concerns/group_tree_spec.rb
index ba84fbf8564..503eb416962 100644
--- a/spec/controllers/concerns/group_tree_spec.rb
+++ b/spec/controllers/concerns/group_tree_spec.rb
@@ -63,6 +63,17 @@ describe GroupTree do
expect(assigns(:groups)).to contain_exactly(parent, subgroup)
end
+
+ it 'preloads parents regardless of pagination' do
+ allow(Kaminari.config).to receive(:default_per_page).and_return(1)
+ group = create(:group, :public)
+ subgroup = create(:group, :public, parent: group)
+ search_result = create(:group, :public, name: 'result', parent: subgroup)
+
+ get :index, filter: 'resu', format: :json
+
+ expect(assigns(:groups)).to contain_exactly(group, subgroup, search_result)
+ end
end
context 'json content' do
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index e20623c0ac1..945b6142abf 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -31,7 +31,7 @@ describe Projects::ForksController do
context 'when fork is private' do
before do
- forked_project.update_attributes(visibility_level: Project::PRIVATE, group: group)
+ forked_project.update(visibility_level: Project::PRIVATE, group: group)
end
it 'is not be visible for non logged in users' do
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
index 72f6af112b3..78c6f7839b4 100644
--- a/spec/controllers/projects/group_links_controller_spec.rb
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -23,7 +23,7 @@ describe Projects::GroupLinksController do
context 'when project is not allowed to be shared with a group' do
before do
- group.update_attributes(share_with_group_lock: false)
+ group.update(share_with_group_lock: false)
end
include_context 'link project to group'
diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb
index 812833cc86b..6f06210f3de 100644
--- a/spec/controllers/projects/imports_controller_spec.rb
+++ b/spec/controllers/projects/imports_controller_spec.rb
@@ -29,7 +29,7 @@ describe Projects::ImportsController do
context 'when import is in progress' do
before do
- project.update_attributes(import_status: :started)
+ project.update(import_status: :started)
end
it 'renders template' do
@@ -47,7 +47,7 @@ describe Projects::ImportsController do
context 'when import failed' do
before do
- project.update_attributes(import_status: :failed)
+ project.update(import_status: :failed)
end
it 'redirects to new_namespace_project_import_path' do
@@ -59,7 +59,7 @@ describe Projects::ImportsController do
context 'when import finished' do
before do
- project.update_attributes(import_status: :finished)
+ project.update(import_status: :finished)
end
context 'when project is a fork' do
@@ -108,7 +108,7 @@ describe Projects::ImportsController do
context 'when import never happened' do
before do
- project.update_attributes(import_status: :none)
+ project.update(import_status: :none)
end
it 'redirects to namespace_project_path' do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 7f5f0b76c51..444415011a9 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -315,7 +315,7 @@ describe Projects::MergeRequestsController do
context 'when the merge request is not mergeable' do
before do
- merge_request.update_attributes(title: "WIP: #{merge_request.title}")
+ merge_request.update(title: "WIP: #{merge_request.title}")
post :merge, base_params
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 27f04be3fdf..fd7d867f9e5 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -166,7 +166,7 @@ describe ProjectsController do
User.project_views.keys.each do |project_view|
context "with #{project_view} view set" do
before do
- user.update_attributes(project_view: project_view)
+ user.update(project_view: project_view)
get :show, namespace_id: empty_project.namespace, id: empty_project
end
@@ -188,7 +188,7 @@ describe ProjectsController do
User.project_views.keys.each do |project_view|
context "with #{project_view} view set" do
before do
- user.update_attributes(project_view: project_view)
+ user.update(project_view: project_view)
get :show, namespace_id: empty_project.namespace, id: empty_project
end
@@ -616,13 +616,40 @@ describe ProjectsController do
end
describe 'POST #preview_markdown' do
- it 'renders json in a correct format' do
+ before do
sign_in(user)
+ end
+ it 'renders json in a correct format' do
post :preview_markdown, namespace_id: public_project.namespace, id: public_project, text: '*Markdown* text'
expect(JSON.parse(response.body).keys).to match_array(%w(body references))
end
+
+ context 'state filter on references' do
+ let(:issue) { create(:issue, :closed, project: public_project) }
+ let(:merge_request) { create(:merge_request, :closed, target_project: public_project) }
+
+ it 'renders JSON body with state filter for issues' do
+ post :preview_markdown, namespace_id: public_project.namespace,
+ id: public_project,
+ text: issue.to_reference
+
+ json_response = JSON.parse(response.body)
+
+ expect(json_response['body']).to match(/\##{issue.iid} \(closed\)/)
+ end
+
+ it 'renders JSON body with state filter for MRs' do
+ post :preview_markdown, namespace_id: public_project.namespace,
+ id: public_project,
+ text: merge_request.to_reference
+
+ json_response = JSON.parse(response.body)
+
+ expect(json_response['body']).to match(/\!#{merge_request.iid} \(closed\)/)
+ end
+ end
end
describe '#ensure_canonical_path' do
@@ -763,23 +790,55 @@ describe ProjectsController do
project.add_master(user)
end
- context 'when project export is enabled' do
- it 'returns 302' do
- get :download_export, namespace_id: project.namespace, id: project
+ context 'object storage disabled' do
+ before do
+ stub_feature_flags(import_export_object_storage: false)
+ end
- expect(response).to have_gitlab_http_status(302)
+ context 'when project export is enabled' do
+ it 'returns 302' do
+ get :download_export, namespace_id: project.namespace, id: project
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ context 'when project export is disabled' do
+ before do
+ stub_application_setting(project_export_enabled?: false)
+ end
+
+ it 'returns 404' do
+ get :download_export, namespace_id: project.namespace, id: project
+
+ expect(response).to have_gitlab_http_status(404)
+ end
end
end
- context 'when project export is disabled' do
+ context 'object storage enabled' do
before do
- stub_application_setting(project_export_enabled?: false)
+ stub_feature_flags(import_export_object_storage: true)
end
- it 'returns 404' do
- get :download_export, namespace_id: project.namespace, id: project
+ context 'when project export is enabled' do
+ it 'returns 302' do
+ get :download_export, namespace_id: project.namespace, id: project
- expect(response).to have_gitlab_http_status(404)
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ context 'when project export is disabled' do
+ before do
+ stub_application_setting(project_export_enabled?: false)
+ end
+
+ it 'returns 404' do
+ get :download_export, namespace_id: project.namespace, id: project
+
+ expect(response).to have_gitlab_http_status(404)
+ end
end
end
end
diff --git a/spec/factories/ci/build_trace_chunks.rb b/spec/factories/ci/build_trace_chunks.rb
index c0b9a25bfe8..3e8e2736423 100644
--- a/spec/factories/ci/build_trace_chunks.rb
+++ b/spec/factories/ci/build_trace_chunks.rb
@@ -3,5 +3,53 @@ FactoryBot.define do
build factory: :ci_build
chunk_index 0
data_store :redis
+
+ trait :redis_with_data do
+ data_store :redis
+
+ transient do
+ initial_data 'test data'
+ end
+
+ after(:create) do |build_trace_chunk, evaluator|
+ Ci::BuildTraceChunks::Redis.new.set_data(build_trace_chunk, evaluator.initial_data)
+ end
+ end
+
+ trait :redis_without_data do
+ data_store :redis
+ end
+
+ trait :database_with_data do
+ data_store :database
+
+ transient do
+ initial_data 'test data'
+ end
+
+ after(:build) do |build_trace_chunk, evaluator|
+ Ci::BuildTraceChunks::Database.new.set_data(build_trace_chunk, evaluator.initial_data)
+ end
+ end
+
+ trait :database_without_data do
+ data_store :database
+ end
+
+ trait :fog_with_data do
+ data_store :fog
+
+ transient do
+ initial_data 'test data'
+ end
+
+ after(:create) do |build_trace_chunk, evaluator|
+ Ci::BuildTraceChunks::Fog.new.set_data(build_trace_chunk, evaluator.initial_data)
+ end
+ end
+
+ trait :fog_without_data do
+ data_store :fog
+ end
end
end
diff --git a/spec/factories/import_export_uploads.rb b/spec/factories/import_export_uploads.rb
new file mode 100644
index 00000000000..7750d49b1d0
--- /dev/null
+++ b/spec/factories/import_export_uploads.rb
@@ -0,0 +1,5 @@
+FactoryBot.define do
+ factory :import_export_upload do
+ project { create(:project) }
+ end
+end
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 3a35bdd25de..4d4f74e101e 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -27,7 +27,7 @@ FactoryBot.define do
end
after(:create) do |issue, evaluator|
- issue.update_attributes(labels: evaluator.labels)
+ issue.update(labels: evaluator.labels)
end
end
end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 3441ce1b8cb..f722bb9cb0d 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -117,7 +117,7 @@ FactoryBot.define do
end
after(:create) do |merge_request, evaluator|
- merge_request.update_attributes(labels: evaluator.labels)
+ merge_request.update(labels: evaluator.labels)
end
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index f6b05bac0e8..f77ded23b18 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -103,6 +103,22 @@ FactoryBot.define do
end
trait :with_export do
+ before(:create) do |_project, _evaluator|
+ allow(Feature).to receive(:enabled?).with(:import_export_object_storage) { false }
+ allow(Feature).to receive(:enabled?).with('import_export_object_storage') { false }
+ end
+
+ after(:create) do |project, _evaluator|
+ ProjectExportWorker.new.perform(project.creator.id, project.id)
+ end
+ end
+
+ trait :with_object_export do
+ before(:create) do |_project, _evaluator|
+ allow(Feature).to receive(:enabled?).with(:import_export_object_storage) { true }
+ allow(Feature).to receive(:enabled?).with('import_export_object_storage') { true }
+ end
+
after(:create) do |project, evaluator|
ProjectExportWorker.new.perform(project.creator.id, project.id)
end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 87fa3f60826..29fcf89fa7a 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -89,7 +89,7 @@ describe 'Commits' do
context 'Download artifacts' do
before do
- build.update_attributes(legacy_artifacts_file: artifacts_file)
+ build.update(legacy_artifacts_file: artifacts_file)
end
it do
@@ -146,7 +146,7 @@ describe 'Commits' do
context "when logged as reporter" do
before do
project.add_reporter(user)
- build.update_attributes(legacy_artifacts_file: artifacts_file)
+ build.update(legacy_artifacts_file: artifacts_file)
visit pipeline_path(pipeline)
end
@@ -168,7 +168,7 @@ describe 'Commits' do
project.update(
visibility_level: Gitlab::VisibilityLevel::INTERNAL,
public_builds: false)
- build.update_attributes(legacy_artifacts_file: artifacts_file)
+ build.update(legacy_artifacts_file: artifacts_file)
visit pipeline_path(pipeline)
end
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 46935662288..4daacc61d85 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -29,9 +29,37 @@ describe 'Dashboard Projects' do
end
end
+ context 'when user has access to the project' do
+ it 'shows role badge' do
+ visit dashboard_projects_path
+
+ page.within '.user-access-role' do
+ expect(page).to have_content('Developer')
+ end
+ end
+
+ context 'when role changes', :use_clean_rails_memory_store_fragment_caching do
+ it 'displays the right role' do
+ visit dashboard_projects_path
+
+ page.within '.user-access-role' do
+ expect(page).to have_content('Developer')
+ end
+
+ project.members.last.update(access_level: 40)
+
+ visit dashboard_projects_path
+
+ page.within '.user-access-role' do
+ expect(page).to have_content('Maintainer')
+ end
+ end
+ end
+ end
+
context 'when last_repository_updated_at, last_activity_at and update_at are present' do
it 'shows the last_repository_updated_at attribute as the update date' do
- project.update_attributes!(last_repository_updated_at: Time.now, last_activity_at: 1.hour.ago)
+ project.update!(last_repository_updated_at: Time.now, last_activity_at: 1.hour.ago)
visit dashboard_projects_path
@@ -39,7 +67,7 @@ describe 'Dashboard Projects' do
end
it 'shows the last_activity_at attribute as the update date' do
- project.update_attributes!(last_repository_updated_at: 1.hour.ago, last_activity_at: Time.now)
+ project.update!(last_repository_updated_at: 1.hour.ago, last_activity_at: Time.now)
visit dashboard_projects_path
@@ -49,7 +77,7 @@ describe 'Dashboard Projects' do
context 'when last_repository_updated_at and last_activity_at are missing' do
it 'shows the updated_at attribute as the update date' do
- project.update_attributes!(last_repository_updated_at: nil, last_activity_at: nil)
+ project.update!(last_repository_updated_at: nil, last_activity_at: nil)
project.touch
visit dashboard_projects_path
diff --git a/spec/features/groups/members/request_access_spec.rb b/spec/features/groups/members/request_access_spec.rb
index 1c833c36a61..94510f917a3 100644
--- a/spec/features/groups/members/request_access_spec.rb
+++ b/spec/features/groups/members/request_access_spec.rb
@@ -13,7 +13,7 @@ describe 'Groups > Members > Request access' do
end
it 'request access feature is disabled' do
- group.update_attributes(request_access_enabled: false)
+ group.update(request_access_enabled: false)
visit group_path(group)
expect(page).not_to have_content 'Request Access'
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index b4cb3835a13..3050f23c130 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -112,7 +112,7 @@ describe 'Issue Sidebar' do
context 'editing issue labels', :js do
before do
- issue.update_attributes(labels: [label])
+ issue.update(labels: [label])
page.within('.block.labels') do
find('.edit-link').click
end
diff --git a/spec/features/merge_request/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb
index 13cc5f256eb..02d19db3828 100644
--- a/spec/features/merge_request/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb
@@ -198,7 +198,7 @@ describe 'Merge request > User posts diff notes', :js do
context 'when the MR only supports legacy diff notes' do
before do
- merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
+ merge_request.merge_request_diff.update(start_commit_sha: nil)
visit diffs_project_merge_request_path(project, merge_request, view: 'inline')
end
diff --git a/spec/features/milestones/user_edits_milestone_spec.rb b/spec/features/milestones/user_edits_milestone_spec.rb
new file mode 100644
index 00000000000..077295f1cc0
--- /dev/null
+++ b/spec/features/milestones/user_edits_milestone_spec.rb
@@ -0,0 +1,22 @@
+require "rails_helper"
+
+describe "User edits milestone", :js do
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+ set(:milestone) { create(:milestone, project: project, start_date: Date.today, due_date: 5.days.from_now) }
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ visit(edit_project_milestone_path(project, milestone))
+ end
+
+ it "shows the right start date and due date" do
+ start_date = milestone.start_date.strftime("%F")
+ due_date = milestone.due_date.strftime("%F")
+
+ expect(page).to have_field(with: start_date)
+ expect(page).to have_field(with: due_date)
+ end
+end
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
index bfb17a56613..e6586fc8a0a 100644
--- a/spec/features/profiles/keys_spec.rb
+++ b/spec/features/profiles/keys_spec.rb
@@ -30,6 +30,20 @@ describe 'Profile > SSH Keys' do
expect(find('.breadcrumbs-sub-title')).to have_link(attrs[:title])
end
+ it 'shows a confirmable warning if the key does not start with ssh-' do
+ attrs = attributes_for(:key)
+
+ fill_in('Key', with: 'invalid-key')
+ fill_in('Title', with: attrs[:title])
+ click_button('Add key')
+
+ expect(page).to have_selector('.js-add-ssh-key-validation-warning')
+
+ find('.js-add-ssh-key-validation-confirm-submit').click
+
+ expect(page).to have_content('Key is invalid')
+ end
+
context 'when only DSA and ECDSA keys are allowed' do
before do
forbidden = ApplicationSetting::FORBIDDEN_KEY_VALUE
diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb
index f9c6ff90ca1..5e3569e4beb 100644
--- a/spec/features/profiles/password_spec.rb
+++ b/spec/features/profiles/password_spec.rb
@@ -117,7 +117,7 @@ describe 'Profile > Password' do
before do
sign_in(user)
- user.update_attributes(password_expires_at: 1.hour.ago)
+ user.update(password_expires_at: 1.hour.ago)
user.identities.delete
expect(user.ldap_user?).to eq false
end
diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb
index 41f6c52fb8a..f56174fc85c 100644
--- a/spec/features/projects/files/user_browses_files_spec.rb
+++ b/spec/features/projects/files/user_browses_files_spec.rb
@@ -147,11 +147,8 @@ describe "User browses files" do
page.within(".tree-table") do
click_link("README.md")
end
-
- # rubocop:disable Lint/Void
# Test the full URLs of links instead of relative paths by `have_link(text: "...", href: "...")`.
find("a", text: /^empty$/)["href"] == project_blob_url(project, "markdown/d/README.md")
- # rubocop:enable Lint/Void
end
it "shows correct content of directory" do
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 8a418356541..eb281cd2122 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -25,6 +25,7 @@ describe 'Import/Export - project export integration test', :js do
before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ stub_feature_flags(import_export_object_storage: false)
end
after do
diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb
index 7d056b0c140..9bb8a2063b5 100644
--- a/spec/features/projects/import_export/namespace_export_file_spec.rb
+++ b/spec/features/projects/import_export/namespace_export_file_spec.rb
@@ -5,6 +5,7 @@ describe 'Import/Export - Namespace export file cleanup', :js do
before do
allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ stub_feature_flags(import_export_object_storage: false)
end
after do
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index ceba4dfec57..3b5df47e0b6 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/jobs/permissions_spec.rb b/spec/features/projects/jobs/permissions_spec.rb
index e9588daf37d..e639f0cf82e 100644
--- a/spec/features/projects/jobs/permissions_spec.rb
+++ b/spec/features/projects/jobs/permissions_spec.rb
@@ -90,7 +90,7 @@ describe 'Project Jobs Permissions' do
before do
archive = fixture_file_upload('spec/fixtures/ci_build_artifacts.zip')
- job.update_attributes(legacy_artifacts_file: archive)
+ job.update(legacy_artifacts_file: archive)
end
context 'when public access for jobs is disabled' do
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index c742eb79392..35b1c46ecf6 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -187,7 +187,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
context "Download artifacts" do
before do
- job.update_attributes(legacy_artifacts_file: artifacts_file)
+ job.update(legacy_artifacts_file: artifacts_file)
visit project_job_path(project, job)
end
@@ -198,8 +198,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
context 'Artifacts expire date' do
before do
- job.update_attributes(legacy_artifacts_file: artifacts_file,
- artifacts_expire_at: expire_at)
+ job.update(legacy_artifacts_file: artifacts_file,
+ artifacts_expire_at: expire_at)
visit project_job_path(project, job)
end
@@ -530,14 +530,14 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
describe "GET /:project/jobs/:id/download" do
before do
- job.update_attributes(legacy_artifacts_file: artifacts_file)
+ job.update(legacy_artifacts_file: artifacts_file)
visit project_job_path(project, job)
click_link 'Download'
end
context "Build from other project" do
before do
- job2.update_attributes(legacy_artifacts_file: artifacts_file)
+ job2.update(legacy_artifacts_file: artifacts_file)
visit download_project_job_artifacts_path(project, job2)
end
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 35d6ac1c650..5599cc9bf1b 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -11,7 +11,7 @@ describe 'Projects > Members > User requests access', :js do
end
it 'request access feature is disabled' do
- project.update_attributes(request_access_enabled: false)
+ project.update(request_access_enabled: false)
visit project_path(project)
expect(page).not_to have_content 'Request Access'
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 9c165b17704..7d47e342e92 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -606,7 +606,7 @@ describe 'Pipelines', :js do
describe 'user clicks the button' do
context 'when project already has jobs_cache_index' do
before do
- project.update_attributes(jobs_cache_index: 1)
+ project.update(jobs_cache_index: 1)
end
it 'increments jobs_cache_index' do
diff --git a/spec/features/projects/remote_mirror_spec.rb b/spec/features/projects/remote_mirror_spec.rb
index 53a3f6f474b..97db4a2b8f2 100644
--- a/spec/features/projects/remote_mirror_spec.rb
+++ b/spec/features/projects/remote_mirror_spec.rb
@@ -13,7 +13,7 @@ describe 'Project remote mirror', :feature do
context 'when last_error is present but last_update_at is not' do
it 'renders error message without timstamp' do
- remote_mirror.update_attributes(last_error: 'Some new error', last_update_at: nil)
+ remote_mirror.update(last_error: 'Some new error', last_update_at: nil)
visit project_mirror_path(project)
@@ -23,7 +23,7 @@ describe 'Project remote mirror', :feature do
context 'when last_error and last_update_at are present' do
it 'renders error message with timestamp' do
- remote_mirror.update_attributes(last_error: 'Some new error', last_update_at: Time.now - 5.minutes)
+ remote_mirror.update(last_error: 'Some new error', last_update_at: Time.now - 5.minutes)
visit project_mirror_path(project)
diff --git a/spec/features/projects/services/user_activates_slack_notifications_spec.rb b/spec/features/projects/services/user_activates_slack_notifications_spec.rb
index fae9ebd1bd6..727b1fe2d11 100644
--- a/spec/features/projects/services/user_activates_slack_notifications_spec.rb
+++ b/spec/features/projects/services/user_activates_slack_notifications_spec.rb
@@ -29,7 +29,7 @@ describe 'User activates Slack notifications' do
context 'when service is already configured' do
before do
service.fields
- service.update_attributes(
+ service.update(
push_channel: 1,
issue_channel: 2,
merge_request_channel: 3,
diff --git a/spec/features/projects/show/user_sees_deletion_failure_message_spec.rb b/spec/features/projects/show/user_sees_deletion_failure_message_spec.rb
index aa23bef6fd8..d9d57298929 100644
--- a/spec/features/projects/show/user_sees_deletion_failure_message_spec.rb
+++ b/spec/features/projects/show/user_sees_deletion_failure_message_spec.rb
@@ -8,7 +8,7 @@ describe 'Projects > Show > User sees a deletion failure message' do
end
it 'shows error message if deletion for project fails' do
- project.update_attributes(delete_error: "Something went wrong", pending_delete: false)
+ project.update(delete_error: "Something went wrong", pending_delete: false)
visit project_path(project)
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
index ef15e7e1ff9..0bec5f185d6 100644
--- a/spec/features/projects/wiki/markdown_preview_spec.rb
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -9,6 +9,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
[relative link 1](../relative)
[relative link 2](./relative)
[relative link 3](./e/f/relative)
+[spaced link](title with spaces)
HEREDOC
end
@@ -42,6 +43,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/relative\">relative link 1</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/relative\">relative link 2</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/e/f/relative\">relative link 3</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
end
end
@@ -64,6 +66,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
end
end
@@ -86,6 +89,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
end
end
end
@@ -119,6 +123,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/relative\">relative link 1</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/relative\">relative link 2</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a/b/c/e/f/relative\">relative link 3</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
end
end
@@ -136,6 +141,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
end
end
@@ -153,6 +159,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/relative\">relative link 1</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>")
expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>")
+ expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>")
end
end
end
diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb
index db141ef7096..bc9443c6093 100644
--- a/spec/features/signed_commits_spec.rb
+++ b/spec/features/signed_commits_spec.rb
@@ -23,7 +23,7 @@ describe 'GPG signed commits', :js do
# user changes his email which makes the gpg key verified
Sidekiq::Testing.inline! do
user.skip_reconfirmation!
- user.update_attributes!(email: GpgHelpers::User1.emails.first)
+ user.update!(email: GpgHelpers::User1.emails.first)
end
visit project_commits_path(project, :'signed-commits')
diff --git a/spec/features/snippets/show_spec.rb b/spec/features/snippets/show_spec.rb
index 74b693f3eff..f31457db92f 100644
--- a/spec/features/snippets/show_spec.rb
+++ b/spec/features/snippets/show_spec.rb
@@ -68,6 +68,26 @@ describe 'Snippet', :js do
end
end
+ context 'with cached Redcarpet html' do
+ let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content, cached_markdown_version: CacheMarkdownField::CACHE_REDCARPET_VERSION) }
+ let(:file_name) { 'test.md' }
+ let(:content) { "1. one\n - sublist\n" }
+
+ it 'renders correctly' do
+ expect(page).to have_xpath("//ol//li//ul")
+ end
+ end
+
+ context 'with cached CommonMark html' do
+ let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content, cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
+ let(:file_name) { 'test.md' }
+ let(:content) { "1. one\n - sublist\n" }
+
+ it 'renders correctly' do
+ expect(page).not_to have_xpath("//ol//li//ul")
+ end
+ end
+
context 'switching to the simple viewer' do
before do
find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb
index b5bbb2c0ea5..3e2fb704bc6 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -14,4 +14,28 @@ describe 'User page' do
expect(page).to have_link('Snippets')
end
end
+
+ context 'signup disabled' do
+ it 'shows the sign in link' do
+ stub_application_setting(signup_enabled: false)
+
+ visit(user_path(user))
+
+ page.within '.navbar-nav' do
+ expect(page).to have_link('Sign in')
+ end
+ end
+ end
+
+ context 'signup enabled' do
+ it 'shows the sign in and register link' do
+ stub_application_setting(signup_enabled: true)
+
+ visit(user_path(user))
+
+ page.within '.navbar-nav' do
+ expect(page).to have_link('Sign in / Register')
+ end
+ end
+ end
end
diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb
index be80ee7d767..0a69c03e491 100644
--- a/spec/finders/group_projects_finder_spec.rb
+++ b/spec/finders/group_projects_finder_spec.rb
@@ -81,7 +81,7 @@ describe GroupProjectsFinder do
context "with external user" do
before do
- current_user.update_attributes(external: true)
+ current_user.update(external: true)
end
it { is_expected.to match_array([shared_project_2, shared_project_1]) }
@@ -112,7 +112,7 @@ describe GroupProjectsFinder do
context "with external user" do
before do
- current_user.update_attributes(external: true)
+ current_user.update(external: true)
end
context 'with subgroups projects', :nested_groups do
diff --git a/spec/finders/joined_groups_finder_spec.rb b/spec/finders/joined_groups_finder_spec.rb
index 29a47e005a6..7f77a713b12 100644
--- a/spec/finders/joined_groups_finder_spec.rb
+++ b/spec/finders/joined_groups_finder_spec.rb
@@ -53,7 +53,7 @@ describe JoinedGroupsFinder do
context 'external users' do
before do
- profile_visitor.update_attributes(external: true)
+ profile_visitor.update(external: true)
end
context 'if not a member' do
diff --git a/spec/finders/move_to_project_finder_spec.rb b/spec/finders/move_to_project_finder_spec.rb
index 74639d4147f..e1faf3d569c 100644
--- a/spec/finders/move_to_project_finder_spec.rb
+++ b/spec/finders/move_to_project_finder_spec.rb
@@ -45,7 +45,7 @@ describe MoveToProjectFinder do
it 'does not return projects for which issues are disabled' do
reporter_project.add_reporter(user)
- reporter_project.update_attributes(issues_enabled: false)
+ reporter_project.update(issues_enabled: false)
other_reporter_project = create(:project)
other_reporter_project.add_reporter(user)
diff --git a/spec/finders/personal_projects_finder_spec.rb b/spec/finders/personal_projects_finder_spec.rb
index 00c551a1f65..ef7dd0cd4a8 100644
--- a/spec/finders/personal_projects_finder_spec.rb
+++ b/spec/finders/personal_projects_finder_spec.rb
@@ -35,7 +35,7 @@ describe PersonalProjectsFinder do
context 'external' do
before do
- current_user.update_attributes(external: true)
+ current_user.update(external: true)
end
it { is_expected.to eq([public_project, private_project]) }
diff --git a/spec/fixtures/project_export.tar.gz b/spec/fixtures/project_export.tar.gz
new file mode 100644
index 00000000000..72ab2d71f35
--- /dev/null
+++ b/spec/fixtures/project_export.tar.gz
Binary files differ
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index a3e010c3206..1c216b3fe97 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -65,9 +65,9 @@ describe BlobHelper do
describe "#sanitize_svg_data" do
let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') }
- let(:data) { open(input_svg_path).read }
+ let(:data) { File.read(input_svg_path) }
let(:expected_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'sanitized.svg') }
- let(:expected) { open(expected_svg_path).read }
+ let(:expected) { File.read(expected_svg_path) }
it 'retains essential elements' do
expect(sanitize_svg_data(data)).to eq(expected)
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 6c94bd4e504..115807f954b 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -206,8 +206,9 @@ describe GroupsHelper do
let(:group) { create(:group, :public) }
let(:user) { create(:user) }
before do
+ group.add_owner(user)
allow(helper).to receive(:current_user) { user }
- allow(helper).to receive(:can?) { true }
+ allow(helper).to receive(:can?) { |*args| Ability.allowed?(*args) }
helper.instance_variable_set(:@group, group)
end
@@ -231,7 +232,10 @@ describe GroupsHelper do
cross_project_features = [:activity, :issues, :labels, :milestones,
:merge_requests]
- expect(helper).to receive(:can?).with(user, :read_cross_project) { false }
+ allow(Ability).to receive(:allowed?).and_call_original
+ cross_project_features.each do |feature|
+ expect(Ability).to receive(:allowed?).with(user, "read_group_#{feature}".to_sym, group) { false }
+ end
expect(helper.group_sidebar_links).not_to include(*cross_project_features)
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index d246beb9888..f76ed4bfda4 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -184,6 +184,7 @@ describe IssuablesHelper do
issuableRef: "##{issue.iid}",
markdownPreviewPath: "/#{@project.full_path}/preview_markdown",
markdownDocsPath: '/help/user/markdown',
+ markdownVersion: 11,
issuableTemplates: [],
projectPath: @project.path,
projectNamespace: @project.namespace.path,
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index 1a720aae55c..d5ed5c59c61 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -205,7 +205,9 @@ describe MarkupHelper do
it "uses Wiki pipeline for markdown files" do
allow(@wiki).to receive(:format).and_return(:markdown)
- expect(helper).to receive(:markdown_unsafe).with('wiki content', pipeline: :wiki, project: project, project_wiki: @wiki, page_slug: "nested/page", issuable_state_filter_enabled: true)
+ expect(helper).to receive(:markdown_unsafe).with('wiki content',
+ pipeline: :wiki, project: project, project_wiki: @wiki, page_slug: "nested/page",
+ issuable_state_filter_enabled: true, markdown_engine: :redcarpet)
helper.render_wiki_content(@wiki)
end
@@ -236,19 +238,32 @@ describe MarkupHelper do
expect(helper.markup('foo.rst', content).encoding.name).to eq('UTF-8')
end
- it "delegates to #markdown_unsafe when file name corresponds to Markdown" do
+ it 'delegates to #markdown_unsafe when file name corresponds to Markdown' do
expect(helper).to receive(:gitlab_markdown?).with('foo.md').and_return(true)
expect(helper).to receive(:markdown_unsafe).and_return('NOEL')
expect(helper.markup('foo.md', content)).to eq('NOEL')
end
- it "delegates to #asciidoc_unsafe when file name corresponds to AsciiDoc" do
+ it 'delegates to #asciidoc_unsafe when file name corresponds to AsciiDoc' do
expect(helper).to receive(:asciidoc?).with('foo.adoc').and_return(true)
expect(helper).to receive(:asciidoc_unsafe).and_return('NOEL')
expect(helper.markup('foo.adoc', content)).to eq('NOEL')
end
+
+ it 'uses passed in rendered content' do
+ expect(helper).not_to receive(:gitlab_markdown?)
+ expect(helper).not_to receive(:markdown_unsafe)
+
+ expect(helper.markup('foo.md', content, rendered: '<p>NOEL</p>')).to eq('<p>NOEL</p>')
+ end
+
+ it 'defaults to Redcarpet' do
+ expect(helper).to receive(:markdown_unsafe).with(content, hash_including(markdown_engine: :redcarpet)).and_return('NOEL')
+
+ expect(helper.markup('foo.md', content)).to eq('NOEL')
+ end
end
describe '#first_line_in_markdown' do
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 80147b13739..beb6e8ea273 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -80,6 +80,7 @@ describe ProjectsHelper do
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?).with(user, :read_cross_project) { true }
+ allow(user).to receive(:max_member_access_for_project).and_return(40)
end
it "includes the route" do
@@ -125,6 +126,10 @@ describe ProjectsHelper do
expect(helper.project_list_cache_key(project)).to include("pipeline-status/#{project.commit.sha}-success")
end
+
+ it "includes the user max member access" do
+ expect(helper.project_list_cache_key(project)).to include('access:40')
+ end
end
describe '#load_pipeline_status' do
diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js
index 1c1edfac68c..9b994543e19 100644
--- a/spec/javascripts/diffs/components/diff_file_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_spec.js
@@ -31,12 +31,12 @@ describe('DiffFile', () => {
describe('collapsed', () => {
it('should not have file content', done => {
- expect(vm.$el.querySelectorAll('.diff-content.hidden').length).toEqual(0);
+ expect(vm.$el.querySelectorAll('.diff-content').length).toEqual(1);
expect(vm.file.collapsed).toEqual(false);
vm.file.collapsed = true;
vm.$nextTick(() => {
- expect(vm.$el.querySelectorAll('.diff-content.hidden').length).toEqual(1);
+ expect(vm.$el.querySelectorAll('.diff-content').length).toEqual(0);
done();
});
diff --git a/spec/javascripts/diffs/components/inline_diff_view_spec.js b/spec/javascripts/diffs/components/inline_diff_view_spec.js
index e1adf60962e..b02328dd359 100644
--- a/spec/javascripts/diffs/components/inline_diff_view_spec.js
+++ b/spec/javascripts/diffs/components/inline_diff_view_spec.js
@@ -13,7 +13,7 @@ describe('InlineDiffView', () => {
beforeEach(() => {
const diffFile = getDiffFileMock();
- store.dispatch('setInlineDiffViewType');
+ store.dispatch('diffs/setInlineDiffViewType');
component = createComponentWithStore(Vue.extend(InlineDiffView), store, {
diffFile,
diffLines: diffFile.highlightedDiffLines,
diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js
index 7945ddea911..7a94f18778b 100644
--- a/spec/javascripts/diffs/store/getters_spec.js
+++ b/spec/javascripts/diffs/store/getters_spec.js
@@ -1,24 +1,66 @@
-import getters from '~/diffs/store/getters';
+import * as getters from '~/diffs/store/getters';
+import state from '~/diffs/store/modules/diff_state';
import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants';
describe('DiffsStoreGetters', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ });
+
describe('isParallelView', () => {
it('should return true if view set to parallel view', () => {
- expect(getters.isParallelView({ diffViewType: PARALLEL_DIFF_VIEW_TYPE })).toBeTruthy();
+ localState.diffViewType = PARALLEL_DIFF_VIEW_TYPE;
+
+ expect(getters.isParallelView(localState)).toEqual(true);
});
it('should return false if view not to parallel view', () => {
- expect(getters.isParallelView({ diffViewType: 'foo' })).toBeFalsy();
+ localState.diffViewType = INLINE_DIFF_VIEW_TYPE;
+
+ expect(getters.isParallelView(localState)).toEqual(false);
});
});
describe('isInlineView', () => {
it('should return true if view set to inline view', () => {
- expect(getters.isInlineView({ diffViewType: INLINE_DIFF_VIEW_TYPE })).toBeTruthy();
+ localState.diffViewType = INLINE_DIFF_VIEW_TYPE;
+
+ expect(getters.isInlineView(localState)).toEqual(true);
});
it('should return false if view not to inline view', () => {
- expect(getters.isInlineView({ diffViewType: PARALLEL_DIFF_VIEW_TYPE })).toBeFalsy();
+ localState.diffViewType = PARALLEL_DIFF_VIEW_TYPE;
+
+ expect(getters.isInlineView(localState)).toEqual(false);
+ });
+ });
+
+ describe('areAllFilesCollapsed', () => {
+ it('returns true when all files are collapsed', () => {
+ localState.diffFiles = [{ collapsed: true }, { collapsed: true }];
+ expect(getters.areAllFilesCollapsed(localState)).toEqual(true);
+ });
+
+ it('returns false when at least one file is not collapsed', () => {
+ localState.diffFiles = [{ collapsed: false }, { collapsed: true }];
+ expect(getters.areAllFilesCollapsed(localState)).toEqual(false);
+ });
+ });
+
+ describe('commitId', () => {
+ it('returns commit id when is set', () => {
+ const commitID = '800f7a91';
+ localState.commit = {
+ id: commitID,
+ };
+
+ expect(getters.commitId(localState)).toEqual(commitID);
+ });
+
+ it('returns null when no commit is set', () => {
+ expect(getters.commitId(localState)).toEqual(null);
});
});
});
diff --git a/spec/javascripts/frequent_items/components/app_spec.js b/spec/javascripts/frequent_items/components/app_spec.js
new file mode 100644
index 00000000000..834f919524d
--- /dev/null
+++ b/spec/javascripts/frequent_items/components/app_spec.js
@@ -0,0 +1,251 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import Vue from 'vue';
+import appComponent from '~/frequent_items/components/app.vue';
+import eventHub from '~/frequent_items/event_hub';
+import store from '~/frequent_items/store';
+import { FREQUENT_ITEMS, HOUR_IN_MS } from '~/frequent_items/constants';
+import { getTopFrequentItems } from '~/frequent_items/utils';
+import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { currentSession, mockFrequentProjects, mockSearchedProjects } from '../mock_data';
+
+let session;
+const createComponentWithStore = (namespace = 'projects') => {
+ session = currentSession[namespace];
+ gon.api_version = session.apiVersion;
+ const Component = Vue.extend(appComponent);
+
+ return mountComponentWithStore(Component, {
+ store,
+ props: {
+ namespace,
+ currentUserName: session.username,
+ currentItem: session.project || session.group,
+ },
+ });
+};
+
+describe('Frequent Items App Component', () => {
+ let vm;
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ vm = createComponentWithStore();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ vm.$destroy();
+ });
+
+ describe('methods', () => {
+ describe('dropdownOpenHandler', () => {
+ it('should fetch frequent items when no search has been previously made on desktop', () => {
+ spyOn(vm, 'fetchFrequentItems');
+
+ vm.dropdownOpenHandler();
+
+ expect(vm.fetchFrequentItems).toHaveBeenCalledWith();
+ });
+ });
+
+ describe('logItemAccess', () => {
+ let storage;
+
+ beforeEach(() => {
+ storage = {};
+
+ spyOn(window.localStorage, 'setItem').and.callFake((storageKey, value) => {
+ storage[storageKey] = value;
+ });
+
+ spyOn(window.localStorage, 'getItem').and.callFake(storageKey => {
+ if (storage[storageKey]) {
+ return storage[storageKey];
+ }
+
+ return null;
+ });
+ });
+
+ it('should create a project store if it does not exist and adds a project', () => {
+ vm.logItemAccess(session.storageKey, session.project);
+
+ const projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects.length).toBe(1);
+ expect(projects[0].frequency).toBe(1);
+ expect(projects[0].lastAccessedOn).toBeDefined();
+ });
+
+ it('should prevent inserting same report multiple times into store', () => {
+ vm.logItemAccess(session.storageKey, session.project);
+ vm.logItemAccess(session.storageKey, session.project);
+
+ const projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects.length).toBe(1);
+ });
+
+ it('should increase frequency of report if it was logged multiple times over the course of an hour', () => {
+ let projects;
+ const newTimestamp = Date.now() + HOUR_IN_MS + 1;
+
+ vm.logItemAccess(session.storageKey, session.project);
+ projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects[0].frequency).toBe(1);
+
+ vm.logItemAccess(session.storageKey, {
+ ...session.project,
+ lastAccessedOn: newTimestamp,
+ });
+ projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects[0].frequency).toBe(2);
+ expect(projects[0].lastAccessedOn).not.toBe(session.project.lastAccessedOn);
+ });
+
+ it('should always update project metadata', () => {
+ let projects;
+ const oldProject = {
+ ...session.project,
+ };
+
+ const newProject = {
+ ...session.project,
+ name: 'New Name',
+ avatarUrl: 'new/avatar.png',
+ namespace: 'New / Namespace',
+ webUrl: 'http://localhost/new/web/url',
+ };
+
+ vm.logItemAccess(session.storageKey, oldProject);
+ projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects[0].name).toBe(oldProject.name);
+ expect(projects[0].avatarUrl).toBe(oldProject.avatarUrl);
+ expect(projects[0].namespace).toBe(oldProject.namespace);
+ expect(projects[0].webUrl).toBe(oldProject.webUrl);
+
+ vm.logItemAccess(session.storageKey, newProject);
+ projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects[0].name).toBe(newProject.name);
+ expect(projects[0].avatarUrl).toBe(newProject.avatarUrl);
+ expect(projects[0].namespace).toBe(newProject.namespace);
+ expect(projects[0].webUrl).toBe(newProject.webUrl);
+ });
+
+ it('should not add more than 20 projects in store', () => {
+ for (let id = 0; id < FREQUENT_ITEMS.MAX_COUNT; id += 1) {
+ const project = {
+ ...session.project,
+ id,
+ };
+ vm.logItemAccess(session.storageKey, project);
+ }
+
+ const projects = JSON.parse(storage[session.storageKey]);
+
+ expect(projects.length).toBe(FREQUENT_ITEMS.MAX_COUNT);
+ });
+ });
+ });
+
+ describe('created', () => {
+ it('should bind event listeners on eventHub', done => {
+ spyOn(eventHub, '$on');
+
+ createComponentWithStore().$mount();
+
+ Vue.nextTick(() => {
+ expect(eventHub.$on).toHaveBeenCalledWith('projects-dropdownOpen', jasmine.any(Function));
+ done();
+ });
+ });
+ });
+
+ describe('beforeDestroy', () => {
+ it('should unbind event listeners on eventHub', done => {
+ spyOn(eventHub, '$off');
+
+ vm.$mount();
+ vm.$destroy();
+
+ Vue.nextTick(() => {
+ expect(eventHub.$off).toHaveBeenCalledWith('projects-dropdownOpen', jasmine.any(Function));
+ done();
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render search input', () => {
+ expect(vm.$el.querySelector('.search-input-container')).toBeDefined();
+ });
+
+ it('should render loading animation', done => {
+ vm.$store.dispatch('fetchSearchedItems');
+
+ Vue.nextTick(() => {
+ const loadingEl = vm.$el.querySelector('.loading-animation');
+
+ expect(loadingEl).toBeDefined();
+ expect(loadingEl.classList.contains('prepend-top-20')).toBe(true);
+ expect(loadingEl.querySelector('i').getAttribute('aria-label')).toBe('Loading projects');
+ done();
+ });
+ });
+
+ it('should render frequent projects list header', done => {
+ Vue.nextTick(() => {
+ const sectionHeaderEl = vm.$el.querySelector('.section-header');
+
+ expect(sectionHeaderEl).toBeDefined();
+ expect(sectionHeaderEl.innerText.trim()).toBe('Frequently visited');
+ done();
+ });
+ });
+
+ it('should render frequent projects list', done => {
+ const expectedResult = getTopFrequentItems(mockFrequentProjects);
+ spyOn(window.localStorage, 'getItem').and.callFake(() =>
+ JSON.stringify(mockFrequentProjects),
+ );
+
+ expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(1);
+
+ vm.fetchFrequentItems();
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(
+ expectedResult.length,
+ );
+ done();
+ });
+ });
+
+ it('should render searched projects list', done => {
+ mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects);
+
+ expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(1);
+
+ vm.$store.dispatch('setSearchQuery', 'gitlab');
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.loading-animation')).toBeDefined();
+ })
+ .then(vm.$nextTick)
+ .then(vm.$nextTick)
+ .then(() => {
+ expect(vm.$el.querySelectorAll('.frequent-items-list-container li').length).toBe(
+ mockSearchedProjects.length,
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js b/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js
index c193258474e..201aca77b10 100644
--- a/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js
+++ b/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js
@@ -1,23 +1,21 @@
import Vue from 'vue';
-
-import projectsListItemComponent from '~/projects_dropdown/components/projects_list_item.vue';
-
+import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockProject } from '../mock_data';
+import { mockProject } from '../mock_data'; // can also use 'mockGroup', but not useful to test here
const createComponent = () => {
- const Component = Vue.extend(projectsListItemComponent);
+ const Component = Vue.extend(frequentItemsListItemComponent);
return mountComponent(Component, {
- projectId: mockProject.id,
- projectName: mockProject.name,
+ itemId: mockProject.id,
+ itemName: mockProject.name,
namespace: mockProject.namespace,
webUrl: mockProject.webUrl,
avatarUrl: mockProject.avatarUrl,
});
};
-describe('ProjectsListItemComponent', () => {
+describe('FrequentItemsListItemComponent', () => {
let vm;
beforeEach(() => {
@@ -32,22 +30,22 @@ describe('ProjectsListItemComponent', () => {
describe('hasAvatar', () => {
it('should return `true` or `false` if whether avatar is present or not', () => {
vm.avatarUrl = 'path/to/avatar.png';
- expect(vm.hasAvatar).toBeTruthy();
+ expect(vm.hasAvatar).toBe(true);
vm.avatarUrl = null;
- expect(vm.hasAvatar).toBeFalsy();
+ expect(vm.hasAvatar).toBe(false);
});
});
- describe('highlightedProjectName', () => {
+ describe('highlightedItemName', () => {
it('should enclose part of project name in <b> & </b> which matches with `matcher` prop', () => {
vm.matcher = 'lab';
- expect(vm.highlightedProjectName).toContain('<b>Lab</b>');
+ expect(vm.highlightedItemName).toContain('<b>Lab</b>');
});
it('should return project name as it is if `matcher` is not available', () => {
vm.matcher = null;
- expect(vm.highlightedProjectName).toBe(mockProject.name);
+ expect(vm.highlightedItemName).toBe(mockProject.name);
});
});
@@ -66,12 +64,12 @@ describe('ProjectsListItemComponent', () => {
describe('template', () => {
it('should render component element', () => {
- expect(vm.$el.classList.contains('projects-list-item-container')).toBeTruthy();
+ expect(vm.$el.classList.contains('frequent-items-list-item-container')).toBeTruthy();
expect(vm.$el.querySelectorAll('a').length).toBe(1);
- expect(vm.$el.querySelectorAll('.project-item-avatar-container').length).toBe(1);
- expect(vm.$el.querySelectorAll('.project-item-metadata-container').length).toBe(1);
- expect(vm.$el.querySelectorAll('.project-title').length).toBe(1);
- expect(vm.$el.querySelectorAll('.project-namespace').length).toBe(1);
+ expect(vm.$el.querySelectorAll('.frequent-items-item-avatar-container').length).toBe(1);
+ expect(vm.$el.querySelectorAll('.frequent-items-item-metadata-container').length).toBe(1);
+ expect(vm.$el.querySelectorAll('.frequent-items-item-title').length).toBe(1);
+ expect(vm.$el.querySelectorAll('.frequent-items-item-namespace').length).toBe(1);
});
});
});
diff --git a/spec/javascripts/frequent_items/components/frequent_items_list_spec.js b/spec/javascripts/frequent_items/components/frequent_items_list_spec.js
new file mode 100644
index 00000000000..3003b7ee000
--- /dev/null
+++ b/spec/javascripts/frequent_items/components/frequent_items_list_spec.js
@@ -0,0 +1,84 @@
+import Vue from 'vue';
+import frequentItemsListComponent from '~/frequent_items/components/frequent_items_list.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { mockFrequentProjects } from '../mock_data';
+
+const createComponent = (namespace = 'projects') => {
+ const Component = Vue.extend(frequentItemsListComponent);
+
+ return mountComponent(Component, {
+ namespace,
+ items: mockFrequentProjects,
+ isFetchFailed: false,
+ hasSearchQuery: false,
+ matcher: 'lab',
+ });
+};
+
+describe('FrequentItemsListComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('isListEmpty', () => {
+ it('should return `true` or `false` representing whether if `items` is empty or not with projects', () => {
+ vm.items = [];
+ expect(vm.isListEmpty).toBe(true);
+
+ vm.items = mockFrequentProjects;
+ expect(vm.isListEmpty).toBe(false);
+ });
+ });
+
+ describe('fetched item messages', () => {
+ it('should return appropriate empty list message based on value of `localStorageFailed` prop with projects', () => {
+ vm.isFetchFailed = true;
+ expect(vm.listEmptyMessage).toBe('This feature requires browser localStorage support');
+
+ vm.isFetchFailed = false;
+ expect(vm.listEmptyMessage).toBe('Projects you visit often will appear here');
+ });
+ });
+
+ describe('searched item messages', () => {
+ it('should return appropriate empty list message based on value of `searchFailed` prop with projects', () => {
+ vm.hasSearchQuery = true;
+ vm.isFetchFailed = true;
+ expect(vm.listEmptyMessage).toBe('Something went wrong on our end.');
+
+ vm.isFetchFailed = false;
+ expect(vm.listEmptyMessage).toBe('Sorry, no projects matched your search');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render component element with list of projects', done => {
+ vm.items = mockFrequentProjects;
+
+ Vue.nextTick(() => {
+ expect(vm.$el.classList.contains('frequent-items-list-container')).toBe(true);
+ expect(vm.$el.querySelectorAll('ul.list-unstyled').length).toBe(1);
+ expect(vm.$el.querySelectorAll('li.frequent-items-list-item-container').length).toBe(5);
+ done();
+ });
+ });
+
+ it('should render component element with empty message', done => {
+ vm.items = [];
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('li.section-empty').length).toBe(1);
+ expect(vm.$el.querySelectorAll('li.frequent-items-list-item-container').length).toBe(0);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js b/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js
new file mode 100644
index 00000000000..6a11038e70a
--- /dev/null
+++ b/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js
@@ -0,0 +1,77 @@
+import Vue from 'vue';
+import searchComponent from '~/frequent_items/components/frequent_items_search_input.vue';
+import eventHub from '~/frequent_items/event_hub';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+const createComponent = (namespace = 'projects') => {
+ const Component = Vue.extend(searchComponent);
+
+ return mountComponent(Component, { namespace });
+};
+
+describe('FrequentItemsSearchInputComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('methods', () => {
+ describe('setFocus', () => {
+ it('should set focus to search input', () => {
+ spyOn(vm.$refs.search, 'focus');
+
+ vm.setFocus();
+ expect(vm.$refs.search.focus).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('mounted', () => {
+ it('should listen `dropdownOpen` event', done => {
+ spyOn(eventHub, '$on');
+ const vmX = createComponent();
+
+ Vue.nextTick(() => {
+ expect(eventHub.$on).toHaveBeenCalledWith(
+ `${vmX.namespace}-dropdownOpen`,
+ jasmine.any(Function),
+ );
+ done();
+ });
+ });
+ });
+
+ describe('beforeDestroy', () => {
+ it('should unbind event listeners on eventHub', done => {
+ const vmX = createComponent();
+ spyOn(eventHub, '$off');
+
+ vmX.$mount();
+ vmX.$destroy();
+
+ Vue.nextTick(() => {
+ expect(eventHub.$off).toHaveBeenCalledWith(
+ `${vmX.namespace}-dropdownOpen`,
+ jasmine.any(Function),
+ );
+ done();
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('should render component element', () => {
+ const inputEl = vm.$el.querySelector('input.form-control');
+
+ expect(vm.$el.classList.contains('search-input-container')).toBeTruthy();
+ expect(inputEl).not.toBe(null);
+ expect(inputEl.getAttribute('placeholder')).toBe('Search your projects');
+ expect(vm.$el.querySelector('.search-icon')).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/frequent_items/mock_data.js b/spec/javascripts/frequent_items/mock_data.js
new file mode 100644
index 00000000000..cf3602f42d6
--- /dev/null
+++ b/spec/javascripts/frequent_items/mock_data.js
@@ -0,0 +1,168 @@
+export const currentSession = {
+ groups: {
+ username: 'root',
+ storageKey: 'root/frequent-groups',
+ apiVersion: 'v4',
+ group: {
+ id: 1,
+ name: 'dummy-group',
+ full_name: 'dummy-parent-group',
+ webUrl: `${gl.TEST_HOST}/dummy-group`,
+ avatarUrl: null,
+ lastAccessedOn: Date.now(),
+ },
+ },
+ projects: {
+ username: 'root',
+ storageKey: 'root/frequent-projects',
+ apiVersion: 'v4',
+ project: {
+ id: 1,
+ name: 'dummy-project',
+ namespace: 'SampleGroup / Dummy-Project',
+ webUrl: `${gl.TEST_HOST}/samplegroup/dummy-project`,
+ avatarUrl: null,
+ lastAccessedOn: Date.now(),
+ },
+ },
+};
+
+export const mockNamespace = 'projects';
+
+export const mockStorageKey = 'test-user/frequent-projects';
+
+export const mockGroup = {
+ id: 1,
+ name: 'Sub451',
+ namespace: 'Commit451 / Sub451',
+ webUrl: `${gl.TEST_HOST}/Commit451/Sub451`,
+ avatarUrl: null,
+};
+
+export const mockRawGroup = {
+ id: 1,
+ name: 'Sub451',
+ full_name: 'Commit451 / Sub451',
+ web_url: `${gl.TEST_HOST}/Commit451/Sub451`,
+ avatar_url: null,
+};
+
+export const mockFrequentGroups = [
+ {
+ id: 3,
+ name: 'Subgroup451',
+ full_name: 'Commit451 / Subgroup451',
+ webUrl: '/Commit451/Subgroup451',
+ avatarUrl: null,
+ frequency: 7,
+ lastAccessedOn: 1497979281815,
+ },
+ {
+ id: 1,
+ name: 'Commit451',
+ full_name: 'Commit451',
+ webUrl: '/Commit451',
+ avatarUrl: null,
+ frequency: 3,
+ lastAccessedOn: 1497979281815,
+ },
+];
+
+export const mockSearchedGroups = [mockRawGroup];
+export const mockProcessedSearchedGroups = [mockGroup];
+
+export const mockProject = {
+ id: 1,
+ name: 'GitLab Community Edition',
+ namespace: 'gitlab-org / gitlab-ce',
+ webUrl: `${gl.TEST_HOST}/gitlab-org/gitlab-ce`,
+ avatarUrl: null,
+};
+
+export const mockRawProject = {
+ id: 1,
+ name: 'GitLab Community Edition',
+ name_with_namespace: 'gitlab-org / gitlab-ce',
+ web_url: `${gl.TEST_HOST}/gitlab-org/gitlab-ce`,
+ avatar_url: null,
+};
+
+export const mockFrequentProjects = [
+ {
+ id: 1,
+ name: 'GitLab Community Edition',
+ namespace: 'gitlab-org / gitlab-ce',
+ webUrl: `${gl.TEST_HOST}/gitlab-org/gitlab-ce`,
+ avatarUrl: null,
+ frequency: 1,
+ lastAccessedOn: Date.now(),
+ },
+ {
+ id: 2,
+ name: 'GitLab CI',
+ namespace: 'gitlab-org / gitlab-ci',
+ webUrl: `${gl.TEST_HOST}/gitlab-org/gitlab-ci`,
+ avatarUrl: null,
+ frequency: 9,
+ lastAccessedOn: Date.now(),
+ },
+ {
+ id: 3,
+ name: 'Typeahead.Js',
+ namespace: 'twitter / typeahead-js',
+ webUrl: `${gl.TEST_HOST}/twitter/typeahead-js`,
+ avatarUrl: '/uploads/-/system/project/avatar/7/TWBS.png',
+ frequency: 2,
+ lastAccessedOn: Date.now(),
+ },
+ {
+ id: 4,
+ name: 'Intel',
+ namespace: 'platform / hardware / bsp / intel',
+ webUrl: `${gl.TEST_HOST}/platform/hardware/bsp/intel`,
+ avatarUrl: null,
+ frequency: 3,
+ lastAccessedOn: Date.now(),
+ },
+ {
+ id: 5,
+ name: 'v4.4',
+ namespace: 'platform / hardware / bsp / kernel / common / v4.4',
+ webUrl: `${gl.TEST_HOST}/platform/hardware/bsp/kernel/common/v4.4`,
+ avatarUrl: null,
+ frequency: 8,
+ lastAccessedOn: Date.now(),
+ },
+];
+
+export const mockSearchedProjects = [mockRawProject];
+export const mockProcessedSearchedProjects = [mockProject];
+
+export const unsortedFrequentItems = [
+ { id: 1, frequency: 12, lastAccessedOn: 1491400843391 },
+ { id: 2, frequency: 14, lastAccessedOn: 1488240890738 },
+ { id: 3, frequency: 44, lastAccessedOn: 1497675908472 },
+ { id: 4, frequency: 8, lastAccessedOn: 1497979281815 },
+ { id: 5, frequency: 34, lastAccessedOn: 1488089211943 },
+ { id: 6, frequency: 14, lastAccessedOn: 1493517292488 },
+ { id: 7, frequency: 42, lastAccessedOn: 1486815299875 },
+ { id: 8, frequency: 33, lastAccessedOn: 1500762279114 },
+ { id: 10, frequency: 46, lastAccessedOn: 1483251641543 },
+];
+
+/**
+ * This const has a specific order which tests authenticity
+ * of `getTopFrequentItems` method so
+ * DO NOT change order of items in this const.
+ */
+export const sortedFrequentItems = [
+ { id: 10, frequency: 46, lastAccessedOn: 1483251641543 },
+ { id: 3, frequency: 44, lastAccessedOn: 1497675908472 },
+ { id: 7, frequency: 42, lastAccessedOn: 1486815299875 },
+ { id: 5, frequency: 34, lastAccessedOn: 1488089211943 },
+ { id: 8, frequency: 33, lastAccessedOn: 1500762279114 },
+ { id: 6, frequency: 14, lastAccessedOn: 1493517292488 },
+ { id: 2, frequency: 14, lastAccessedOn: 1488240890738 },
+ { id: 1, frequency: 12, lastAccessedOn: 1491400843391 },
+ { id: 4, frequency: 8, lastAccessedOn: 1497979281815 },
+];
diff --git a/spec/javascripts/frequent_items/store/actions_spec.js b/spec/javascripts/frequent_items/store/actions_spec.js
new file mode 100644
index 00000000000..0cdd033d38f
--- /dev/null
+++ b/spec/javascripts/frequent_items/store/actions_spec.js
@@ -0,0 +1,225 @@
+import testAction from 'spec/helpers/vuex_action_helper';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import AccessorUtilities from '~/lib/utils/accessor';
+import * as actions from '~/frequent_items/store/actions';
+import * as types from '~/frequent_items/store/mutation_types';
+import state from '~/frequent_items/store/state';
+import {
+ mockNamespace,
+ mockStorageKey,
+ mockFrequentProjects,
+ mockSearchedProjects,
+} from '../mock_data';
+
+describe('Frequent Items Dropdown Store Actions', () => {
+ let mockedState;
+ let mock;
+
+ beforeEach(() => {
+ mockedState = state();
+ mock = new MockAdapter(axios);
+
+ mockedState.namespace = mockNamespace;
+ mockedState.storageKey = mockStorageKey;
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('setNamespace', () => {
+ it('should set namespace', done => {
+ testAction(
+ actions.setNamespace,
+ mockNamespace,
+ mockedState,
+ [{ type: types.SET_NAMESPACE, payload: mockNamespace }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setStorageKey', () => {
+ it('should set storage key', done => {
+ testAction(
+ actions.setStorageKey,
+ mockStorageKey,
+ mockedState,
+ [{ type: types.SET_STORAGE_KEY, payload: mockStorageKey }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('requestFrequentItems', () => {
+ it('should request frequent items', done => {
+ testAction(
+ actions.requestFrequentItems,
+ null,
+ mockedState,
+ [{ type: types.REQUEST_FREQUENT_ITEMS }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveFrequentItemsSuccess', () => {
+ it('should set frequent items', done => {
+ testAction(
+ actions.receiveFrequentItemsSuccess,
+ mockFrequentProjects,
+ mockedState,
+ [{ type: types.RECEIVE_FREQUENT_ITEMS_SUCCESS, payload: mockFrequentProjects }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveFrequentItemsError', () => {
+ it('should set frequent items error state', done => {
+ testAction(
+ actions.receiveFrequentItemsError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_FREQUENT_ITEMS_ERROR }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchFrequentItems', () => {
+ it('should dispatch `receiveFrequentItemsSuccess`', done => {
+ mockedState.namespace = mockNamespace;
+ mockedState.storageKey = mockStorageKey;
+
+ testAction(
+ actions.fetchFrequentItems,
+ null,
+ mockedState,
+ [],
+ [{ type: 'requestFrequentItems' }, { type: 'receiveFrequentItemsSuccess', payload: [] }],
+ done,
+ );
+ });
+
+ it('should dispatch `receiveFrequentItemsError`', done => {
+ spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').and.returnValue(false);
+ mockedState.namespace = mockNamespace;
+ mockedState.storageKey = mockStorageKey;
+
+ testAction(
+ actions.fetchFrequentItems,
+ null,
+ mockedState,
+ [],
+ [{ type: 'requestFrequentItems' }, { type: 'receiveFrequentItemsError' }],
+ done,
+ );
+ });
+ });
+
+ describe('requestSearchedItems', () => {
+ it('should request searched items', done => {
+ testAction(
+ actions.requestSearchedItems,
+ null,
+ mockedState,
+ [{ type: types.REQUEST_SEARCHED_ITEMS }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveSearchedItemsSuccess', () => {
+ it('should set searched items', done => {
+ testAction(
+ actions.receiveSearchedItemsSuccess,
+ mockSearchedProjects,
+ mockedState,
+ [{ type: types.RECEIVE_SEARCHED_ITEMS_SUCCESS, payload: mockSearchedProjects }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveSearchedItemsError', () => {
+ it('should set searched items error state', done => {
+ testAction(
+ actions.receiveSearchedItemsError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_SEARCHED_ITEMS_ERROR }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchSearchedItems', () => {
+ beforeEach(() => {
+ gon.api_version = 'v4';
+ });
+
+ it('should dispatch `receiveSearchedItemsSuccess`', done => {
+ mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects);
+
+ testAction(
+ actions.fetchSearchedItems,
+ null,
+ mockedState,
+ [],
+ [
+ { type: 'requestSearchedItems' },
+ { type: 'receiveSearchedItemsSuccess', payload: mockSearchedProjects },
+ ],
+ done,
+ );
+ });
+
+ it('should dispatch `receiveSearchedItemsError`', done => {
+ gon.api_version = 'v4';
+ mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(500);
+
+ testAction(
+ actions.fetchSearchedItems,
+ null,
+ mockedState,
+ [],
+ [{ type: 'requestSearchedItems' }, { type: 'receiveSearchedItemsError' }],
+ done,
+ );
+ });
+ });
+
+ describe('setSearchQuery', () => {
+ it('should commit query and dispatch `fetchSearchedItems` when query is present', done => {
+ testAction(
+ actions.setSearchQuery,
+ { query: 'test' },
+ mockedState,
+ [{ type: types.SET_SEARCH_QUERY }],
+ [{ type: 'fetchSearchedItems', payload: { query: 'test' } }],
+ done,
+ );
+ });
+
+ it('should commit query and dispatch `fetchFrequentItems` when query is empty', done => {
+ testAction(
+ actions.setSearchQuery,
+ null,
+ mockedState,
+ [{ type: types.SET_SEARCH_QUERY }],
+ [{ type: 'fetchFrequentItems' }],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/frequent_items/store/getters_spec.js b/spec/javascripts/frequent_items/store/getters_spec.js
new file mode 100644
index 00000000000..1cd12eb6832
--- /dev/null
+++ b/spec/javascripts/frequent_items/store/getters_spec.js
@@ -0,0 +1,24 @@
+import state from '~/frequent_items/store/state';
+import * as getters from '~/frequent_items/store/getters';
+
+describe('Frequent Items Dropdown Store Getters', () => {
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = state();
+ });
+
+ describe('hasSearchQuery', () => {
+ it('should return `true` when search query is present', () => {
+ mockedState.searchQuery = 'test';
+
+ expect(getters.hasSearchQuery(mockedState)).toBe(true);
+ });
+
+ it('should return `false` when search query is empty', () => {
+ mockedState.searchQuery = '';
+
+ expect(getters.hasSearchQuery(mockedState)).toBe(false);
+ });
+ });
+});
diff --git a/spec/javascripts/frequent_items/store/mutations_spec.js b/spec/javascripts/frequent_items/store/mutations_spec.js
new file mode 100644
index 00000000000..d36964b2600
--- /dev/null
+++ b/spec/javascripts/frequent_items/store/mutations_spec.js
@@ -0,0 +1,117 @@
+import state from '~/frequent_items/store/state';
+import mutations from '~/frequent_items/store/mutations';
+import * as types from '~/frequent_items/store/mutation_types';
+import {
+ mockNamespace,
+ mockStorageKey,
+ mockFrequentProjects,
+ mockSearchedProjects,
+ mockProcessedSearchedProjects,
+ mockSearchedGroups,
+ mockProcessedSearchedGroups,
+} from '../mock_data';
+
+describe('Frequent Items dropdown mutations', () => {
+ let stateCopy;
+
+ beforeEach(() => {
+ stateCopy = state();
+ });
+
+ describe('SET_NAMESPACE', () => {
+ it('should set namespace', () => {
+ mutations[types.SET_NAMESPACE](stateCopy, mockNamespace);
+
+ expect(stateCopy.namespace).toEqual(mockNamespace);
+ });
+ });
+
+ describe('SET_STORAGE_KEY', () => {
+ it('should set storage key', () => {
+ mutations[types.SET_STORAGE_KEY](stateCopy, mockStorageKey);
+
+ expect(stateCopy.storageKey).toEqual(mockStorageKey);
+ });
+ });
+
+ describe('SET_SEARCH_QUERY', () => {
+ it('should set search query', () => {
+ const searchQuery = 'gitlab-ce';
+
+ mutations[types.SET_SEARCH_QUERY](stateCopy, searchQuery);
+
+ expect(stateCopy.searchQuery).toEqual(searchQuery);
+ });
+ });
+
+ describe('REQUEST_FREQUENT_ITEMS', () => {
+ it('should set view states when requesting frequent items', () => {
+ mutations[types.REQUEST_FREQUENT_ITEMS](stateCopy);
+
+ expect(stateCopy.isLoadingItems).toEqual(true);
+ expect(stateCopy.hasSearchQuery).toEqual(false);
+ });
+ });
+
+ describe('RECEIVE_FREQUENT_ITEMS_SUCCESS', () => {
+ it('should set view states when receiving frequent items', () => {
+ mutations[types.RECEIVE_FREQUENT_ITEMS_SUCCESS](stateCopy, mockFrequentProjects);
+
+ expect(stateCopy.items).toEqual(mockFrequentProjects);
+ expect(stateCopy.isLoadingItems).toEqual(false);
+ expect(stateCopy.hasSearchQuery).toEqual(false);
+ expect(stateCopy.isFetchFailed).toEqual(false);
+ });
+ });
+
+ describe('RECEIVE_FREQUENT_ITEMS_ERROR', () => {
+ it('should set items and view states when error occurs retrieving frequent items', () => {
+ mutations[types.RECEIVE_FREQUENT_ITEMS_ERROR](stateCopy);
+
+ expect(stateCopy.items).toEqual([]);
+ expect(stateCopy.isLoadingItems).toEqual(false);
+ expect(stateCopy.hasSearchQuery).toEqual(false);
+ expect(stateCopy.isFetchFailed).toEqual(true);
+ });
+ });
+
+ describe('REQUEST_SEARCHED_ITEMS', () => {
+ it('should set view states when requesting searched items', () => {
+ mutations[types.REQUEST_SEARCHED_ITEMS](stateCopy);
+
+ expect(stateCopy.isLoadingItems).toEqual(true);
+ expect(stateCopy.hasSearchQuery).toEqual(true);
+ });
+ });
+
+ describe('RECEIVE_SEARCHED_ITEMS_SUCCESS', () => {
+ it('should set items and view states when receiving searched items', () => {
+ mutations[types.RECEIVE_SEARCHED_ITEMS_SUCCESS](stateCopy, mockSearchedProjects);
+
+ expect(stateCopy.items).toEqual(mockProcessedSearchedProjects);
+ expect(stateCopy.isLoadingItems).toEqual(false);
+ expect(stateCopy.hasSearchQuery).toEqual(true);
+ expect(stateCopy.isFetchFailed).toEqual(false);
+ });
+
+ it('should also handle the different `full_name` key for namespace in groups payload', () => {
+ mutations[types.RECEIVE_SEARCHED_ITEMS_SUCCESS](stateCopy, mockSearchedGroups);
+
+ expect(stateCopy.items).toEqual(mockProcessedSearchedGroups);
+ expect(stateCopy.isLoadingItems).toEqual(false);
+ expect(stateCopy.hasSearchQuery).toEqual(true);
+ expect(stateCopy.isFetchFailed).toEqual(false);
+ });
+ });
+
+ describe('RECEIVE_SEARCHED_ITEMS_ERROR', () => {
+ it('should set view states when error occurs retrieving searched items', () => {
+ mutations[types.RECEIVE_SEARCHED_ITEMS_ERROR](stateCopy);
+
+ expect(stateCopy.items).toEqual([]);
+ expect(stateCopy.isLoadingItems).toEqual(false);
+ expect(stateCopy.hasSearchQuery).toEqual(true);
+ expect(stateCopy.isFetchFailed).toEqual(true);
+ });
+ });
+});
diff --git a/spec/javascripts/frequent_items/utils_spec.js b/spec/javascripts/frequent_items/utils_spec.js
new file mode 100644
index 00000000000..cd27d79b29a
--- /dev/null
+++ b/spec/javascripts/frequent_items/utils_spec.js
@@ -0,0 +1,89 @@
+import bp from '~/breakpoints';
+import { isMobile, getTopFrequentItems, updateExistingFrequentItem } from '~/frequent_items/utils';
+import { HOUR_IN_MS, FREQUENT_ITEMS } from '~/frequent_items/constants';
+import { mockProject, unsortedFrequentItems, sortedFrequentItems } from './mock_data';
+
+describe('Frequent Items utils spec', () => {
+ describe('isMobile', () => {
+ it('returns true when the screen is small ', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('sm');
+
+ expect(isMobile()).toBe(true);
+ });
+
+ it('returns true when the screen is extra-small ', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('xs');
+
+ expect(isMobile()).toBe(true);
+ });
+
+ it('returns false when the screen is larger than small ', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('md');
+
+ expect(isMobile()).toBe(false);
+ });
+ });
+
+ describe('getTopFrequentItems', () => {
+ it('returns empty array if no items provided', () => {
+ const result = getTopFrequentItems();
+
+ expect(result.length).toBe(0);
+ });
+
+ it('returns correct amount of items for mobile', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('sm');
+ const result = getTopFrequentItems(unsortedFrequentItems);
+
+ expect(result.length).toBe(FREQUENT_ITEMS.LIST_COUNT_MOBILE);
+ });
+
+ it('returns correct amount of items for desktop', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('lg');
+ const result = getTopFrequentItems(unsortedFrequentItems);
+
+ expect(result.length).toBe(FREQUENT_ITEMS.LIST_COUNT_DESKTOP);
+ });
+
+ it('sorts frequent items in order of frequency and lastAccessedOn', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('lg');
+ const result = getTopFrequentItems(unsortedFrequentItems);
+ const expectedResult = sortedFrequentItems.slice(0, FREQUENT_ITEMS.LIST_COUNT_DESKTOP);
+
+ expect(result).toEqual(expectedResult);
+ });
+ });
+
+ describe('updateExistingFrequentItem', () => {
+ let mockedProject;
+
+ beforeEach(() => {
+ mockedProject = {
+ ...mockProject,
+ frequency: 1,
+ lastAccessedOn: 1497979281815,
+ };
+ });
+
+ it('updates item if accessed over an hour ago', () => {
+ const newTimestamp = Date.now() + HOUR_IN_MS + 1;
+ const newItem = {
+ ...mockedProject,
+ lastAccessedOn: newTimestamp,
+ };
+ const result = updateExistingFrequentItem(mockedProject, newItem);
+
+ expect(result.frequency).toBe(mockedProject.frequency + 1);
+ });
+
+ it('does not update item if accessed within the hour', () => {
+ const newItem = {
+ ...mockedProject,
+ lastAccessedOn: mockedProject.lastAccessedOn + HOUR_IN_MS,
+ };
+ const result = updateExistingFrequentItem(mockedProject, newItem);
+
+ expect(result.frequency).toBe(mockedProject.frequency);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/components/merge_requests/info_spec.js b/spec/javascripts/ide/components/merge_requests/info_spec.js
new file mode 100644
index 00000000000..98a29e5128b
--- /dev/null
+++ b/spec/javascripts/ide/components/merge_requests/info_spec.js
@@ -0,0 +1,51 @@
+import Vue from 'vue';
+import '~/behaviors/markdown/render_gfm';
+import { createStore } from '~/ide/stores';
+import Info from '~/ide/components/merge_requests/info.vue';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+
+describe('IDE merge request details', () => {
+ let Component;
+ let vm;
+
+ beforeAll(() => {
+ Component = Vue.extend(Info);
+ });
+
+ beforeEach(() => {
+ const store = createStore();
+ store.state.currentProjectId = 'gitlab-ce';
+ store.state.currentMergeRequestId = 1;
+ store.state.projects['gitlab-ce'] = {
+ mergeRequests: {
+ 1: {
+ iid: 1,
+ title: 'Testing',
+ title_html: '<span class="title-html">Testing</span>',
+ description: 'Description',
+ description_html: '<p class="description-html">Description HTML</p>',
+ },
+ },
+ };
+
+ vm = createComponentWithStore(Component, store).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders merge request IID', () => {
+ expect(vm.$el.querySelector('.detail-page-header').textContent).toContain('!1');
+ });
+
+ it('renders title as HTML', () => {
+ expect(vm.$el.querySelector('.title-html')).not.toBe(null);
+ expect(vm.$el.querySelector('.title').textContent).toContain('Testing');
+ });
+
+ it('renders description as HTML', () => {
+ expect(vm.$el.querySelector('.description-html')).not.toBe(null);
+ expect(vm.$el.querySelector('.description').textContent).toContain('Description HTML');
+ });
+});
diff --git a/spec/javascripts/ide/components/panes/right_spec.js b/spec/javascripts/ide/components/panes/right_spec.js
new file mode 100644
index 00000000000..99879fb0930
--- /dev/null
+++ b/spec/javascripts/ide/components/panes/right_spec.js
@@ -0,0 +1,72 @@
+import Vue from 'vue';
+import '~/behaviors/markdown/render_gfm';
+import { createStore } from '~/ide/stores';
+import RightPane from '~/ide/components/panes/right.vue';
+import { rightSidebarViews } from '~/ide/constants';
+import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
+
+describe('IDE right pane', () => {
+ let Component;
+ let vm;
+
+ beforeAll(() => {
+ Component = Vue.extend(RightPane);
+ });
+
+ beforeEach(() => {
+ const store = createStore();
+
+ vm = createComponentWithStore(Component, store).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('active', () => {
+ it('renders merge request button as active', done => {
+ vm.$store.state.rightPane = rightSidebarViews.mergeRequestInfo;
+ vm.$store.state.currentMergeRequestId = '123';
+ vm.$store.state.currentProjectId = 'gitlab-ce';
+ vm.$store.state.currentMergeRequestId = 1;
+ vm.$store.state.projects['gitlab-ce'] = {
+ mergeRequests: {
+ 1: {
+ iid: 1,
+ title: 'Testing',
+ title_html: '<span class="title-html">Testing</span>',
+ description: 'Description',
+ description_html: '<p class="description-html">Description HTML</p>',
+ },
+ },
+ };
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.ide-sidebar-link.active')).not.toBe(null);
+ expect(
+ vm.$el.querySelector('.ide-sidebar-link.active').getAttribute('data-original-title'),
+ ).toBe('Merge Request');
+
+ done();
+ });
+ });
+ });
+
+ describe('click', () => {
+ beforeEach(() => {
+ spyOn(vm, 'setRightPane');
+ });
+
+ it('sets view to merge request', done => {
+ vm.$store.state.currentMergeRequestId = '123';
+
+ vm.$nextTick(() => {
+ vm.$el.querySelector('.ide-sidebar-link').click();
+
+ expect(vm.setRightPane).toHaveBeenCalledWith(rightSidebarViews.mergeRequestInfo);
+
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js
index c99ccc70c6a..90c28c769f7 100644
--- a/spec/javascripts/ide/stores/actions/merge_request_spec.js
+++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js
@@ -39,7 +39,9 @@ describe('IDE store merge request actions', () => {
store
.dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
.then(() => {
- expect(service.getProjectMergeRequestData).toHaveBeenCalledWith('abcproject', 1);
+ expect(service.getProjectMergeRequestData).toHaveBeenCalledWith('abcproject', 1, {
+ render_html: true,
+ });
done();
})
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index 1b040c924aa..be2a8ba67fe 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -165,6 +165,7 @@ export const note = {
report_abuse_path:
'/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1',
path: '/gitlab-org/gitlab-ce/notes/546',
+ cached_markdown_version: 11,
};
export const discussionMock = {
diff --git a/spec/javascripts/profile/add_ssh_key_validation_spec.js b/spec/javascripts/profile/add_ssh_key_validation_spec.js
new file mode 100644
index 00000000000..c71a2885acc
--- /dev/null
+++ b/spec/javascripts/profile/add_ssh_key_validation_spec.js
@@ -0,0 +1,69 @@
+import AddSshKeyValidation from '../../../app/assets/javascripts/profile/add_ssh_key_validation';
+
+describe('AddSshKeyValidation', () => {
+ describe('submit', () => {
+ it('returns true if isValid is true', () => {
+ const addSshKeyValidation = new AddSshKeyValidation({});
+ spyOn(AddSshKeyValidation, 'isPublicKey').and.returnValue(true);
+
+ expect(addSshKeyValidation.submit()).toBeTruthy();
+ });
+
+ it('calls preventDefault and toggleWarning if isValid is false', () => {
+ const addSshKeyValidation = new AddSshKeyValidation({});
+ const event = jasmine.createSpyObj('event', ['preventDefault']);
+ spyOn(AddSshKeyValidation, 'isPublicKey').and.returnValue(false);
+ spyOn(addSshKeyValidation, 'toggleWarning');
+
+ addSshKeyValidation.submit(event);
+
+ expect(event.preventDefault).toHaveBeenCalled();
+ expect(addSshKeyValidation.toggleWarning).toHaveBeenCalledWith(true);
+ });
+ });
+
+ describe('toggleWarning', () => {
+ it('shows warningElement and hides originalSubmitElement if isVisible is true', () => {
+ const warningElement = document.createElement('div');
+ const originalSubmitElement = document.createElement('div');
+ warningElement.classList.add('hide');
+
+ const addSshKeyValidation = new AddSshKeyValidation(
+ {},
+ warningElement,
+ originalSubmitElement,
+ );
+ addSshKeyValidation.toggleWarning(true);
+
+ expect(warningElement.classList.contains('hide')).toBeFalsy();
+ expect(originalSubmitElement.classList.contains('hide')).toBeTruthy();
+ });
+
+ it('hides warningElement and shows originalSubmitElement if isVisible is false', () => {
+ const warningElement = document.createElement('div');
+ const originalSubmitElement = document.createElement('div');
+ originalSubmitElement.classList.add('hide');
+
+ const addSshKeyValidation = new AddSshKeyValidation(
+ {},
+ warningElement,
+ originalSubmitElement,
+ );
+ addSshKeyValidation.toggleWarning(false);
+
+ expect(warningElement.classList.contains('hide')).toBeTruthy();
+ expect(originalSubmitElement.classList.contains('hide')).toBeFalsy();
+ });
+ });
+
+ describe('isPublicKey', () => {
+ it('returns false if probably invalid public ssh key', () => {
+ expect(AddSshKeyValidation.isPublicKey('nope')).toBeFalsy();
+ });
+
+ it('returns true if probably valid public ssh key', () => {
+ expect(AddSshKeyValidation.isPublicKey('ssh-')).toBeTruthy();
+ expect(AddSshKeyValidation.isPublicKey('ecdsa-sha2-')).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/javascripts/projects_dropdown/components/app_spec.js b/spec/javascripts/projects_dropdown/components/app_spec.js
deleted file mode 100644
index 38b31c3d727..00000000000
--- a/spec/javascripts/projects_dropdown/components/app_spec.js
+++ /dev/null
@@ -1,349 +0,0 @@
-import Vue from 'vue';
-
-import bp from '~/breakpoints';
-import appComponent from '~/projects_dropdown/components/app.vue';
-import eventHub from '~/projects_dropdown/event_hub';
-import ProjectsStore from '~/projects_dropdown/store/projects_store';
-import ProjectsService from '~/projects_dropdown/service/projects_service';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { currentSession, mockProject, mockRawProject } from '../mock_data';
-
-const createComponent = () => {
- gon.api_version = currentSession.apiVersion;
- const Component = Vue.extend(appComponent);
- const store = new ProjectsStore();
- const service = new ProjectsService(currentSession.username);
-
- return mountComponent(Component, {
- store,
- service,
- currentUserName: currentSession.username,
- currentProject: currentSession.project,
- });
-};
-
-const returnServicePromise = (data, failed) =>
- new Promise((resolve, reject) => {
- if (failed) {
- reject(data);
- } else {
- resolve({
- json() {
- return data;
- },
- });
- }
- });
-
-describe('AppComponent', () => {
- describe('computed', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('frequentProjects', () => {
- it('should return list of frequently accessed projects from store', () => {
- expect(vm.frequentProjects).toBeDefined();
- expect(vm.frequentProjects.length).toBe(0);
-
- vm.store.setFrequentProjects([mockProject]);
- expect(vm.frequentProjects).toBeDefined();
- expect(vm.frequentProjects.length).toBe(1);
- });
- });
-
- describe('searchProjects', () => {
- it('should return list of frequently accessed projects from store', () => {
- expect(vm.searchProjects).toBeDefined();
- expect(vm.searchProjects.length).toBe(0);
-
- vm.store.setSearchedProjects([mockRawProject]);
- expect(vm.searchProjects).toBeDefined();
- expect(vm.searchProjects.length).toBe(1);
- });
- });
- });
-
- describe('methods', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('toggleFrequentProjectsList', () => {
- it('should toggle props which control visibility of Frequent Projects list from state passed', () => {
- vm.toggleFrequentProjectsList(true);
- expect(vm.isLoadingProjects).toBeFalsy();
- expect(vm.isSearchListVisible).toBeFalsy();
- expect(vm.isFrequentsListVisible).toBeTruthy();
-
- vm.toggleFrequentProjectsList(false);
- expect(vm.isLoadingProjects).toBeTruthy();
- expect(vm.isSearchListVisible).toBeTruthy();
- expect(vm.isFrequentsListVisible).toBeFalsy();
- });
- });
-
- describe('toggleSearchProjectsList', () => {
- it('should toggle props which control visibility of Searched Projects list from state passed', () => {
- vm.toggleSearchProjectsList(true);
- expect(vm.isLoadingProjects).toBeFalsy();
- expect(vm.isFrequentsListVisible).toBeFalsy();
- expect(vm.isSearchListVisible).toBeTruthy();
-
- vm.toggleSearchProjectsList(false);
- expect(vm.isLoadingProjects).toBeTruthy();
- expect(vm.isFrequentsListVisible).toBeTruthy();
- expect(vm.isSearchListVisible).toBeFalsy();
- });
- });
-
- describe('toggleLoader', () => {
- it('should toggle props which control visibility of list loading animation from state passed', () => {
- vm.toggleLoader(true);
- expect(vm.isFrequentsListVisible).toBeFalsy();
- expect(vm.isSearchListVisible).toBeFalsy();
- expect(vm.isLoadingProjects).toBeTruthy();
-
- vm.toggleLoader(false);
- expect(vm.isFrequentsListVisible).toBeTruthy();
- expect(vm.isSearchListVisible).toBeTruthy();
- expect(vm.isLoadingProjects).toBeFalsy();
- });
- });
-
- describe('fetchFrequentProjects', () => {
- it('should set props for loading animation to `true` while frequent projects list is being loaded', () => {
- spyOn(vm, 'toggleLoader');
-
- vm.fetchFrequentProjects();
- expect(vm.isLocalStorageFailed).toBeFalsy();
- expect(vm.toggleLoader).toHaveBeenCalledWith(true);
- });
-
- it('should set props for loading animation to `false` and props for frequent projects list to `true` once data is loaded', () => {
- const mockData = [mockProject];
-
- spyOn(vm.service, 'getFrequentProjects').and.returnValue(mockData);
- spyOn(vm.store, 'setFrequentProjects');
- spyOn(vm, 'toggleFrequentProjectsList');
-
- vm.fetchFrequentProjects();
- expect(vm.service.getFrequentProjects).toHaveBeenCalled();
- expect(vm.store.setFrequentProjects).toHaveBeenCalledWith(mockData);
- expect(vm.toggleFrequentProjectsList).toHaveBeenCalledWith(true);
- });
-
- it('should set props for failure message to `true` when method fails to fetch frequent projects list', () => {
- spyOn(vm.service, 'getFrequentProjects').and.returnValue(null);
- spyOn(vm.store, 'setFrequentProjects');
- spyOn(vm, 'toggleFrequentProjectsList');
-
- expect(vm.isLocalStorageFailed).toBeFalsy();
-
- vm.fetchFrequentProjects();
- expect(vm.service.getFrequentProjects).toHaveBeenCalled();
- expect(vm.store.setFrequentProjects).toHaveBeenCalledWith([]);
- expect(vm.toggleFrequentProjectsList).toHaveBeenCalledWith(true);
- expect(vm.isLocalStorageFailed).toBeTruthy();
- });
-
- it('should set props for search results list to `true` if search query was already made previously', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('md');
- spyOn(vm.service, 'getFrequentProjects');
- spyOn(vm, 'toggleSearchProjectsList');
-
- vm.searchQuery = 'test';
- vm.fetchFrequentProjects();
- expect(vm.service.getFrequentProjects).not.toHaveBeenCalled();
- expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true);
- });
-
- it('should set props for frequent projects list to `true` if search query was already made but screen size is less than 768px', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('sm');
- spyOn(vm, 'toggleSearchProjectsList');
- spyOn(vm.service, 'getFrequentProjects');
-
- vm.searchQuery = 'test';
- vm.fetchFrequentProjects();
- expect(vm.service.getFrequentProjects).toHaveBeenCalled();
- expect(vm.toggleSearchProjectsList).not.toHaveBeenCalled();
- });
- });
-
- describe('fetchSearchedProjects', () => {
- const searchQuery = 'test';
-
- it('should perform search with provided search query', done => {
- const mockData = [mockRawProject];
- spyOn(vm, 'toggleLoader');
- spyOn(vm, 'toggleSearchProjectsList');
- spyOn(vm.service, 'getSearchedProjects').and.returnValue(returnServicePromise(mockData));
- spyOn(vm.store, 'setSearchedProjects');
-
- vm.fetchSearchedProjects(searchQuery);
- setTimeout(() => {
- expect(vm.searchQuery).toBe(searchQuery);
- expect(vm.toggleLoader).toHaveBeenCalledWith(true);
- expect(vm.service.getSearchedProjects).toHaveBeenCalledWith(searchQuery);
- expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true);
- expect(vm.store.setSearchedProjects).toHaveBeenCalledWith(mockData);
- done();
- }, 0);
- });
-
- it('should update props for showing search failure', done => {
- spyOn(vm, 'toggleSearchProjectsList');
- spyOn(vm.service, 'getSearchedProjects').and.returnValue(returnServicePromise({}, true));
-
- vm.fetchSearchedProjects(searchQuery);
- setTimeout(() => {
- expect(vm.searchQuery).toBe(searchQuery);
- expect(vm.service.getSearchedProjects).toHaveBeenCalledWith(searchQuery);
- expect(vm.isSearchFailed).toBeTruthy();
- expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true);
- done();
- }, 0);
- });
- });
-
- describe('logCurrentProjectAccess', () => {
- it('should log current project access via service', done => {
- spyOn(vm.service, 'logProjectAccess');
-
- vm.currentProject = mockProject;
- vm.logCurrentProjectAccess();
-
- setTimeout(() => {
- expect(vm.service.logProjectAccess).toHaveBeenCalledWith(mockProject);
- done();
- }, 1);
- });
- });
-
- describe('handleSearchClear', () => {
- it('should show frequent projects list when search input is cleared', () => {
- spyOn(vm.store, 'clearSearchedProjects');
- spyOn(vm, 'toggleFrequentProjectsList');
-
- vm.handleSearchClear();
-
- expect(vm.toggleFrequentProjectsList).toHaveBeenCalledWith(true);
- expect(vm.store.clearSearchedProjects).toHaveBeenCalled();
- expect(vm.searchQuery).toBe('');
- });
- });
-
- describe('handleSearchFailure', () => {
- it('should show failure message within dropdown', () => {
- spyOn(vm, 'toggleSearchProjectsList');
-
- vm.handleSearchFailure();
- expect(vm.toggleSearchProjectsList).toHaveBeenCalledWith(true);
- expect(vm.isSearchFailed).toBeTruthy();
- });
- });
- });
-
- describe('created', () => {
- it('should bind event listeners on eventHub', done => {
- spyOn(eventHub, '$on');
-
- createComponent().$mount();
-
- Vue.nextTick(() => {
- expect(eventHub.$on).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function));
- expect(eventHub.$on).toHaveBeenCalledWith('searchProjects', jasmine.any(Function));
- expect(eventHub.$on).toHaveBeenCalledWith('searchCleared', jasmine.any(Function));
- expect(eventHub.$on).toHaveBeenCalledWith('searchFailed', jasmine.any(Function));
- done();
- });
- });
- });
-
- describe('beforeDestroy', () => {
- it('should unbind event listeners on eventHub', done => {
- const vm = createComponent();
- spyOn(eventHub, '$off');
-
- vm.$mount();
- vm.$destroy();
-
- Vue.nextTick(() => {
- expect(eventHub.$off).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function));
- expect(eventHub.$off).toHaveBeenCalledWith('searchProjects', jasmine.any(Function));
- expect(eventHub.$off).toHaveBeenCalledWith('searchCleared', jasmine.any(Function));
- expect(eventHub.$off).toHaveBeenCalledWith('searchFailed', jasmine.any(Function));
- done();
- });
- });
- });
-
- describe('template', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('should render search input', () => {
- expect(vm.$el.querySelector('.search-input-container')).toBeDefined();
- });
-
- it('should render loading animation', done => {
- vm.toggleLoader(true);
- Vue.nextTick(() => {
- const loadingEl = vm.$el.querySelector('.loading-animation');
-
- expect(loadingEl).toBeDefined();
- expect(loadingEl.classList.contains('prepend-top-20')).toBeTruthy();
- expect(loadingEl.querySelector('i').getAttribute('aria-label')).toBe('Loading projects');
- done();
- });
- });
-
- it('should render frequent projects list header', done => {
- vm.toggleFrequentProjectsList(true);
- Vue.nextTick(() => {
- const sectionHeaderEl = vm.$el.querySelector('.section-header');
-
- expect(sectionHeaderEl).toBeDefined();
- expect(sectionHeaderEl.innerText.trim()).toBe('Frequently visited');
- done();
- });
- });
-
- it('should render frequent projects list', done => {
- vm.toggleFrequentProjectsList(true);
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.projects-list-frequent-container')).toBeDefined();
- done();
- });
- });
-
- it('should render searched projects list', done => {
- vm.toggleSearchProjectsList(true);
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.section-header')).toBe(null);
- expect(vm.$el.querySelector('.projects-list-search-container')).toBeDefined();
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js
deleted file mode 100644
index 2bafb4e81ca..00000000000
--- a/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import Vue from 'vue';
-
-import projectsListFrequentComponent from '~/projects_dropdown/components/projects_list_frequent.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockFrequents } from '../mock_data';
-
-const createComponent = () => {
- const Component = Vue.extend(projectsListFrequentComponent);
-
- return mountComponent(Component, {
- projects: mockFrequents,
- localStorageFailed: false,
- });
-};
-
-describe('ProjectsListFrequentComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('isListEmpty', () => {
- it('should return `true` or `false` representing whether if `projects` is empty of not', () => {
- vm.projects = [];
- expect(vm.isListEmpty).toBeTruthy();
-
- vm.projects = mockFrequents;
- expect(vm.isListEmpty).toBeFalsy();
- });
- });
-
- describe('listEmptyMessage', () => {
- it('should return appropriate empty list message based on value of `localStorageFailed` prop', () => {
- vm.localStorageFailed = true;
- expect(vm.listEmptyMessage).toBe('This feature requires browser localStorage support');
-
- vm.localStorageFailed = false;
- expect(vm.listEmptyMessage).toBe('Projects you visit often will appear here');
- });
- });
- });
-
- describe('template', () => {
- it('should render component element with list of projects', (done) => {
- vm.projects = mockFrequents;
-
- Vue.nextTick(() => {
- expect(vm.$el.classList.contains('projects-list-frequent-container')).toBeTruthy();
- expect(vm.$el.querySelectorAll('ul.list-unstyled').length).toBe(1);
- expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(5);
- done();
- });
- });
-
- it('should render component element with empty message', (done) => {
- vm.projects = [];
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('li.section-empty').length).toBe(1);
- expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(0);
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js
deleted file mode 100644
index c4b86d77034..00000000000
--- a/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import Vue from 'vue';
-
-import projectsListSearchComponent from '~/projects_dropdown/components/projects_list_search.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockProject } from '../mock_data';
-
-const createComponent = () => {
- const Component = Vue.extend(projectsListSearchComponent);
-
- return mountComponent(Component, {
- projects: [mockProject],
- matcher: 'lab',
- searchFailed: false,
- });
-};
-
-describe('ProjectsListSearchComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('isListEmpty', () => {
- it('should return `true` or `false` representing whether if `projects` is empty of not', () => {
- vm.projects = [];
- expect(vm.isListEmpty).toBeTruthy();
-
- vm.projects = [mockProject];
- expect(vm.isListEmpty).toBeFalsy();
- });
- });
-
- describe('listEmptyMessage', () => {
- it('should return appropriate empty list message based on value of `searchFailed` prop', () => {
- vm.searchFailed = true;
- expect(vm.listEmptyMessage).toBe('Something went wrong on our end.');
-
- vm.searchFailed = false;
- expect(vm.listEmptyMessage).toBe('Sorry, no projects matched your search');
- });
- });
- });
-
- describe('template', () => {
- it('should render component element with list of projects', (done) => {
- vm.projects = [mockProject];
-
- Vue.nextTick(() => {
- expect(vm.$el.classList.contains('projects-list-search-container')).toBeTruthy();
- expect(vm.$el.querySelectorAll('ul.list-unstyled').length).toBe(1);
- expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(1);
- done();
- });
- });
-
- it('should render component element with empty message', (done) => {
- vm.projects = [];
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('li.section-empty').length).toBe(1);
- expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(0);
- done();
- });
- });
-
- it('should render component element with failure message', (done) => {
- vm.searchFailed = true;
- vm.projects = [];
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('li.section-empty.section-failure').length).toBe(1);
- expect(vm.$el.querySelectorAll('li.projects-list-item-container').length).toBe(0);
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/projects_dropdown/components/search_spec.js b/spec/javascripts/projects_dropdown/components/search_spec.js
deleted file mode 100644
index 427f5024e3a..00000000000
--- a/spec/javascripts/projects_dropdown/components/search_spec.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import Vue from 'vue';
-
-import searchComponent from '~/projects_dropdown/components/search.vue';
-import eventHub from '~/projects_dropdown/event_hub';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-const createComponent = () => {
- const Component = Vue.extend(searchComponent);
-
- return mountComponent(Component);
-};
-
-describe('SearchComponent', () => {
- describe('methods', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('setFocus', () => {
- it('should set focus to search input', () => {
- spyOn(vm.$refs.search, 'focus');
-
- vm.setFocus();
- expect(vm.$refs.search.focus).toHaveBeenCalled();
- });
- });
-
- describe('emitSearchEvents', () => {
- it('should emit `searchProjects` event via eventHub when `searchQuery` present', () => {
- const searchQuery = 'test';
- spyOn(eventHub, '$emit');
- vm.searchQuery = searchQuery;
- vm.emitSearchEvents();
- expect(eventHub.$emit).toHaveBeenCalledWith('searchProjects', searchQuery);
- });
-
- it('should emit `searchCleared` event via eventHub when `searchQuery` is cleared', () => {
- spyOn(eventHub, '$emit');
- vm.searchQuery = '';
- vm.emitSearchEvents();
- expect(eventHub.$emit).toHaveBeenCalledWith('searchCleared');
- });
- });
- });
-
- describe('mounted', () => {
- it('should listen `dropdownOpen` event', (done) => {
- spyOn(eventHub, '$on');
- createComponent();
-
- Vue.nextTick(() => {
- expect(eventHub.$on).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function));
- done();
- });
- });
- });
-
- describe('beforeDestroy', () => {
- it('should unbind event listeners on eventHub', (done) => {
- const vm = createComponent();
- spyOn(eventHub, '$off');
-
- vm.$mount();
- vm.$destroy();
-
- Vue.nextTick(() => {
- expect(eventHub.$off).toHaveBeenCalledWith('dropdownOpen', jasmine.any(Function));
- done();
- });
- });
- });
-
- describe('template', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('should render component element', () => {
- const inputEl = vm.$el.querySelector('input.form-control');
-
- expect(vm.$el.classList.contains('search-input-container')).toBeTruthy();
- expect(inputEl).not.toBe(null);
- expect(inputEl.getAttribute('placeholder')).toBe('Search your projects');
- expect(vm.$el.querySelector('.search-icon')).toBeDefined();
- });
- });
-});
diff --git a/spec/javascripts/projects_dropdown/mock_data.js b/spec/javascripts/projects_dropdown/mock_data.js
deleted file mode 100644
index d6a79fb8ac1..00000000000
--- a/spec/javascripts/projects_dropdown/mock_data.js
+++ /dev/null
@@ -1,96 +0,0 @@
-export const currentSession = {
- username: 'root',
- storageKey: 'root/frequent-projects',
- apiVersion: 'v4',
- project: {
- id: 1,
- name: 'dummy-project',
- namespace: 'SamepleGroup / Dummy-Project',
- webUrl: 'http://127.0.0.1/samplegroup/dummy-project',
- avatarUrl: null,
- lastAccessedOn: Date.now(),
- },
-};
-
-export const mockProject = {
- id: 1,
- name: 'GitLab Community Edition',
- namespace: 'gitlab-org / gitlab-ce',
- webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-ce',
- avatarUrl: null,
-};
-
-export const mockRawProject = {
- id: 1,
- name: 'GitLab Community Edition',
- name_with_namespace: 'gitlab-org / gitlab-ce',
- web_url: 'http://127.0.0.1:3000/gitlab-org/gitlab-ce',
- avatar_url: null,
-};
-
-export const mockFrequents = [
- {
- id: 1,
- name: 'GitLab Community Edition',
- namespace: 'gitlab-org / gitlab-ce',
- webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-ce',
- avatarUrl: null,
- },
- {
- id: 2,
- name: 'GitLab CI',
- namespace: 'gitlab-org / gitlab-ci',
- webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-ci',
- avatarUrl: null,
- },
- {
- id: 3,
- name: 'Typeahead.Js',
- namespace: 'twitter / typeahead-js',
- webUrl: 'http://127.0.0.1:3000/twitter/typeahead-js',
- avatarUrl: '/uploads/-/system/project/avatar/7/TWBS.png',
- },
- {
- id: 4,
- name: 'Intel',
- namespace: 'platform / hardware / bsp / intel',
- webUrl: 'http://127.0.0.1:3000/platform/hardware/bsp/intel',
- avatarUrl: null,
- },
- {
- id: 5,
- name: 'v4.4',
- namespace: 'platform / hardware / bsp / kernel / common / v4.4',
- webUrl: 'http://localhost:3000/platform/hardware/bsp/kernel/common/v4.4',
- avatarUrl: null,
- },
-];
-
-export const unsortedFrequents = [
- { id: 1, frequency: 12, lastAccessedOn: 1491400843391 },
- { id: 2, frequency: 14, lastAccessedOn: 1488240890738 },
- { id: 3, frequency: 44, lastAccessedOn: 1497675908472 },
- { id: 4, frequency: 8, lastAccessedOn: 1497979281815 },
- { id: 5, frequency: 34, lastAccessedOn: 1488089211943 },
- { id: 6, frequency: 14, lastAccessedOn: 1493517292488 },
- { id: 7, frequency: 42, lastAccessedOn: 1486815299875 },
- { id: 8, frequency: 33, lastAccessedOn: 1500762279114 },
- { id: 10, frequency: 46, lastAccessedOn: 1483251641543 },
-];
-
-/**
- * This const has a specific order which tests authenticity
- * of `ProjectsService.getTopFrequentProjects` method so
- * DO NOT change order of items in this const.
- */
-export const sortedFrequents = [
- { id: 10, frequency: 46, lastAccessedOn: 1483251641543 },
- { id: 3, frequency: 44, lastAccessedOn: 1497675908472 },
- { id: 7, frequency: 42, lastAccessedOn: 1486815299875 },
- { id: 5, frequency: 34, lastAccessedOn: 1488089211943 },
- { id: 8, frequency: 33, lastAccessedOn: 1500762279114 },
- { id: 6, frequency: 14, lastAccessedOn: 1493517292488 },
- { id: 2, frequency: 14, lastAccessedOn: 1488240890738 },
- { id: 1, frequency: 12, lastAccessedOn: 1491400843391 },
- { id: 4, frequency: 8, lastAccessedOn: 1497979281815 },
-];
diff --git a/spec/javascripts/projects_dropdown/service/projects_service_spec.js b/spec/javascripts/projects_dropdown/service/projects_service_spec.js
deleted file mode 100644
index cfd1bb7d24f..00000000000
--- a/spec/javascripts/projects_dropdown/service/projects_service_spec.js
+++ /dev/null
@@ -1,179 +0,0 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-
-import bp from '~/breakpoints';
-import ProjectsService from '~/projects_dropdown/service/projects_service';
-import { FREQUENT_PROJECTS } from '~/projects_dropdown/constants';
-import { currentSession, unsortedFrequents, sortedFrequents } from '../mock_data';
-
-Vue.use(VueResource);
-
-FREQUENT_PROJECTS.MAX_COUNT = 3;
-
-describe('ProjectsService', () => {
- let service;
-
- beforeEach(() => {
- gon.api_version = currentSession.apiVersion;
- gon.current_user_id = 1;
- service = new ProjectsService(currentSession.username);
- });
-
- describe('contructor', () => {
- it('should initialize default properties of class', () => {
- expect(service.isLocalStorageAvailable).toBeTruthy();
- expect(service.currentUserName).toBe(currentSession.username);
- expect(service.storageKey).toBe(currentSession.storageKey);
- expect(service.projectsPath).toBeDefined();
- });
- });
-
- describe('getSearchedProjects', () => {
- it('should return promise from VueResource HTTP GET', () => {
- spyOn(service.projectsPath, 'get').and.stub();
-
- const searchQuery = 'lab';
- const queryParams = {
- simple: true,
- per_page: 20,
- membership: true,
- order_by: 'last_activity_at',
- search: searchQuery,
- };
-
- service.getSearchedProjects(searchQuery);
- expect(service.projectsPath.get).toHaveBeenCalledWith(queryParams);
- });
- });
-
- describe('logProjectAccess', () => {
- let storage;
-
- beforeEach(() => {
- storage = {};
-
- spyOn(window.localStorage, 'setItem').and.callFake((storageKey, value) => {
- storage[storageKey] = value;
- });
-
- spyOn(window.localStorage, 'getItem').and.callFake((storageKey) => {
- if (storage[storageKey]) {
- return storage[storageKey];
- }
-
- return null;
- });
- });
-
- it('should create a project store if it does not exist and adds a project', () => {
- service.logProjectAccess(currentSession.project);
-
- const projects = JSON.parse(storage[currentSession.storageKey]);
- expect(projects.length).toBe(1);
- expect(projects[0].frequency).toBe(1);
- expect(projects[0].lastAccessedOn).toBeDefined();
- });
-
- it('should prevent inserting same report multiple times into store', () => {
- service.logProjectAccess(currentSession.project);
- service.logProjectAccess(currentSession.project);
-
- const projects = JSON.parse(storage[currentSession.storageKey]);
- expect(projects.length).toBe(1);
- });
-
- it('should increase frequency of report if it was logged multiple times over the course of an hour', () => {
- let projects;
- spyOn(Math, 'abs').and.returnValue(3600001); // this will lead to `diff` > 1;
- service.logProjectAccess(currentSession.project);
-
- projects = JSON.parse(storage[currentSession.storageKey]);
- expect(projects[0].frequency).toBe(1);
-
- service.logProjectAccess(currentSession.project);
- projects = JSON.parse(storage[currentSession.storageKey]);
- expect(projects[0].frequency).toBe(2);
- expect(projects[0].lastAccessedOn).not.toBe(currentSession.project.lastAccessedOn);
- });
-
- it('should always update project metadata', () => {
- let projects;
- const oldProject = {
- ...currentSession.project,
- };
-
- const newProject = {
- ...currentSession.project,
- name: 'New Name',
- avatarUrl: 'new/avatar.png',
- namespace: 'New / Namespace',
- webUrl: 'http://localhost/new/web/url',
- };
-
- service.logProjectAccess(oldProject);
- projects = JSON.parse(storage[currentSession.storageKey]);
- expect(projects[0].name).toBe(oldProject.name);
- expect(projects[0].avatarUrl).toBe(oldProject.avatarUrl);
- expect(projects[0].namespace).toBe(oldProject.namespace);
- expect(projects[0].webUrl).toBe(oldProject.webUrl);
-
- service.logProjectAccess(newProject);
- projects = JSON.parse(storage[currentSession.storageKey]);
- expect(projects[0].name).toBe(newProject.name);
- expect(projects[0].avatarUrl).toBe(newProject.avatarUrl);
- expect(projects[0].namespace).toBe(newProject.namespace);
- expect(projects[0].webUrl).toBe(newProject.webUrl);
- });
-
- it('should not add more than 20 projects in store', () => {
- for (let i = 1; i <= 5; i += 1) {
- const project = Object.assign(currentSession.project, { id: i });
- service.logProjectAccess(project);
- }
-
- const projects = JSON.parse(storage[currentSession.storageKey]);
- expect(projects.length).toBe(3);
- });
- });
-
- describe('getTopFrequentProjects', () => {
- let storage = {};
-
- beforeEach(() => {
- storage[currentSession.storageKey] = JSON.stringify(unsortedFrequents);
-
- spyOn(window.localStorage, 'getItem').and.callFake((storageKey) => {
- if (storage[storageKey]) {
- return storage[storageKey];
- }
-
- return null;
- });
- });
-
- it('should return top 5 frequently accessed projects for desktop screens', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('md');
- const frequentProjects = service.getTopFrequentProjects();
-
- expect(frequentProjects.length).toBe(5);
- frequentProjects.forEach((project, index) => {
- expect(project.id).toBe(sortedFrequents[index].id);
- });
- });
-
- it('should return top 3 frequently accessed projects for mobile screens', () => {
- spyOn(bp, 'getBreakpointSize').and.returnValue('sm');
- const frequentProjects = service.getTopFrequentProjects();
-
- expect(frequentProjects.length).toBe(3);
- frequentProjects.forEach((project, index) => {
- expect(project.id).toBe(sortedFrequents[index].id);
- });
- });
-
- it('should return empty array if there are no projects available in store', () => {
- storage = {};
- expect(service.getTopFrequentProjects().length).toBe(0);
- });
- });
-});
diff --git a/spec/javascripts/projects_dropdown/store/projects_store_spec.js b/spec/javascripts/projects_dropdown/store/projects_store_spec.js
deleted file mode 100644
index e57399d37cd..00000000000
--- a/spec/javascripts/projects_dropdown/store/projects_store_spec.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import ProjectsStore from '~/projects_dropdown/store/projects_store';
-import { mockProject, mockRawProject } from '../mock_data';
-
-describe('ProjectsStore', () => {
- let store;
-
- beforeEach(() => {
- store = new ProjectsStore();
- });
-
- describe('setFrequentProjects', () => {
- it('should set frequent projects list to state', () => {
- store.setFrequentProjects([mockProject]);
-
- expect(store.getFrequentProjects().length).toBe(1);
- expect(store.getFrequentProjects()[0].id).toBe(mockProject.id);
- });
- });
-
- describe('setSearchedProjects', () => {
- it('should set searched projects list to state', () => {
- store.setSearchedProjects([mockRawProject]);
-
- const processedProjects = store.getSearchedProjects();
- expect(processedProjects.length).toBe(1);
- expect(processedProjects[0].id).toBe(mockRawProject.id);
- expect(processedProjects[0].namespace).toBe(mockRawProject.name_with_namespace);
- expect(processedProjects[0].webUrl).toBe(mockRawProject.web_url);
- expect(processedProjects[0].avatarUrl).toBe(mockRawProject.avatar_url);
- });
- });
-
- describe('clearSearchedProjects', () => {
- it('should clear searched projects list from state', () => {
- store.setSearchedProjects([mockRawProject]);
- expect(store.getSearchedProjects().length).toBe(1);
- store.clearSearchedProjects();
- expect(store.getSearchedProjects().length).toBe(0);
- });
- });
-});
diff --git a/spec/javascripts/vue_mr_widget/components/deployment_spec.js b/spec/javascripts/vue_mr_widget/components/deployment_spec.js
index c82ba61a5b1..50c2b0e2bd0 100644
--- a/spec/javascripts/vue_mr_widget/components/deployment_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/deployment_spec.js
@@ -153,7 +153,7 @@ describe('Deployment component', () => {
it('renders external URL', () => {
expect(el.querySelector('.js-deploy-url').getAttribute('href')).toEqual(deploymentMockData.external_url);
- expect(el.querySelector('.js-deploy-url').innerText).toContain(deploymentMockData.external_url_formatted);
+ expect(el.querySelector('.js-deploy-url').innerText).toContain('View app');
});
it('renders stop button', () => {
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
index 3d36e46d863..61b7bd2c226 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
@@ -145,7 +145,7 @@ describe('MRWidgetHeader', () => {
it('renders web ide button', () => {
const button = vm.$el.querySelector('.js-web-ide');
- expect(button.textContent.trim()).toEqual('Web IDE');
+ expect(button.textContent.trim()).toEqual('Open in Web IDE');
expect(button.getAttribute('href')).toEqual('/-/ide/projectabc');
});
@@ -154,7 +154,7 @@ describe('MRWidgetHeader', () => {
const button = vm.$el.querySelector('.js-web-ide');
- expect(button.textContent.trim()).toEqual('Web IDE');
+ expect(button.textContent.trim()).toEqual('Open in Web IDE');
expect(button.getAttribute('href')).toEqual('/-/ide/projectabc');
});
@@ -253,8 +253,8 @@ describe('MRWidgetHeader', () => {
});
it('renders diverged commits info', () => {
- expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual(
- '(12 commits behind)',
+ expect(vm.$el.querySelector('.diverged-commits-count').textContent).toMatch(
+ /(mr-widget-refactor[\s\S]+?is 12 commits behind[\s\S]+?master)/,
);
});
});
diff --git a/spec/lib/banzai/filter/markdown_filter_spec.rb b/spec/lib/banzai/filter/markdown_filter_spec.rb
index ab14d77d552..a515d07b072 100644
--- a/spec/lib/banzai/filter/markdown_filter_spec.rb
+++ b/spec/lib/banzai/filter/markdown_filter_spec.rb
@@ -3,17 +3,61 @@ require 'spec_helper'
describe Banzai::Filter::MarkdownFilter do
include FilterSpecHelper
- context 'code block' do
- it 'adds language to lang attribute when specified' do
- result = filter("```html\nsome code\n```")
+ describe 'markdown engine from context' do
+ it 'defaults to CommonMark' do
+ expect_any_instance_of(Banzai::Filter::MarkdownEngines::CommonMark).to receive(:render).and_return('test')
- expect(result).to start_with("<pre><code lang=\"html\">")
+ filter('test')
end
- it 'does not add language to lang attribute when not specified' do
- result = filter("```\nsome code\n```")
+ it 'uses Redcarpet' do
+ expect_any_instance_of(Banzai::Filter::MarkdownEngines::Redcarpet).to receive(:render).and_return('test')
- expect(result).to start_with("<pre><code>")
+ filter('test', { markdown_engine: :redcarpet })
+ end
+
+ it 'uses CommonMark' do
+ expect_any_instance_of(Banzai::Filter::MarkdownEngines::CommonMark).to receive(:render).and_return('test')
+
+ filter('test', { markdown_engine: :common_mark })
+ end
+ end
+
+ describe 'code block' do
+ context 'using CommonMark' do
+ before do
+ stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark)
+ end
+
+ it 'adds language to lang attribute when specified' do
+ result = filter("```html\nsome code\n```")
+
+ expect(result).to start_with("<pre><code lang=\"html\">")
+ end
+
+ it 'does not add language to lang attribute when not specified' do
+ result = filter("```\nsome code\n```")
+
+ expect(result).to start_with("<pre><code>")
+ end
+ end
+
+ context 'using Redcarpet' do
+ before do
+ stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :redcarpet)
+ end
+
+ it 'adds language to lang attribute when specified' do
+ result = filter("```html\nsome code\n```")
+
+ expect(result).to start_with("\n<pre><code lang=\"html\">")
+ end
+
+ it 'does not add language to lang attribute when not specified' do
+ result = filter("```\nsome code\n```")
+
+ expect(result).to start_with("\n<pre><code>")
+ end
end
end
end
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index e13406d1972..8947e2ac4fb 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -203,4 +203,30 @@ describe ExtractsPath do
expect(extract_ref_without_atom('foo.atom')).to eq(nil)
end
end
+
+ describe '#lfs_blob_ids' do
+ shared_examples '#lfs_blob_ids' do
+ let(:tag) { @project.repository.add_tag(@project.owner, 'my-annotated-tag', 'master', 'test tag') }
+ let(:ref) { tag.target }
+ let(:params) { { ref: ref, path: 'README.md' } }
+
+ before do
+ @project = create(:project, :repository)
+ end
+
+ it 'handles annotated tags' do
+ assign_ref_vars
+
+ expect(lfs_blob_ids).to eq([])
+ end
+ end
+
+ context 'when gitaly is enabled' do
+ it_behaves_like '#lfs_blob_ids'
+ end
+
+ context 'when gitaly is disabled', :skip_gitaly_mock do
+ it_behaves_like '#lfs_blob_ids'
+ end
+ end
end
diff --git a/spec/lib/gitlab/background_migration/fix_cross_project_label_links_spec.rb b/spec/lib/gitlab/background_migration/fix_cross_project_label_links_spec.rb
new file mode 100644
index 00000000000..20af63bc6c8
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/fix_cross_project_label_links_spec.rb
@@ -0,0 +1,109 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::FixCrossProjectLabelLinks, :migration, schema: 20180702120647 do
+ let(:namespaces_table) { table(:namespaces) }
+ let(:projects_table) { table(:projects) }
+ let(:issues_table) { table(:issues) }
+ let(:merge_requests_table) { table(:merge_requests) }
+ let(:labels_table) { table(:labels) }
+ let(:label_links_table) { table(:label_links) }
+
+ let!(:group1) { namespaces_table.create(id: 10, type: 'Group', name: 'group1', path: 'group1') }
+ let!(:group2) { namespaces_table.create(id: 20, type: 'Group', name: 'group2', path: 'group2') }
+
+ let!(:project1) { projects_table.create(id: 1, name: 'project1', path: 'group1/project1', namespace_id: 10) }
+ let!(:project2) { projects_table.create(id: 3, name: 'project2', path: 'group1/project2', namespace_id: 20) }
+
+ let!(:label1) { labels_table.create(id: 1, title: 'bug', color: 'red', group_id: 10, type: 'GroupLabel') }
+ let!(:label2) { labels_table.create(id: 2, title: 'bug', color: 'red', group_id: 20, type: 'GroupLabel') }
+
+ def create_merge_request(id, project_id)
+ merge_requests_table.create(id: id,
+ target_project_id: project_id,
+ target_branch: 'master',
+ source_project_id: project_id,
+ source_branch: 'mr name',
+ title: "mr name#{id}")
+ end
+
+ def create_issue(id, project_id)
+ issues_table.create(id: id, title: "issue#{id}", project_id: project_id)
+ end
+
+ def create_resource(target_type, id, project_id)
+ target_type == 'Issue' ? create_issue(id, project_id) : create_merge_request(id, project_id)
+ end
+
+ shared_examples_for 'resource with cross-project labels' do
+ it 'updates only cross-project label links which exist in the local project or group' do
+ create_resource(target_type, 1, 1)
+ create_resource(target_type, 2, 3)
+ labels_table.create(id: 3, title: 'bug', color: 'red', project_id: 3, type: 'ProjectLabel')
+ link = label_links_table.create(label_id: 2, target_type: target_type, target_id: 1)
+ link2 = label_links_table.create(label_id: 3, target_type: target_type, target_id: 2)
+
+ subject.perform(1, 100)
+
+ expect(link.reload.label_id).to eq(1)
+ expect(link2.reload.label_id).to eq(3)
+ end
+
+ it 'ignores cross-project label links if label color is different' do
+ labels_table.create(id: 3, title: 'bug', color: 'green', group_id: 20, type: 'GroupLabel')
+ create_resource(target_type, 1, 1)
+ link = label_links_table.create(label_id: 3, target_type: target_type, target_id: 1)
+
+ subject.perform(1, 100)
+
+ expect(link.reload.label_id).to eq(3)
+ end
+
+ it 'ignores cross-project label links if label name is different' do
+ labels_table.create(id: 3, title: 'bug1', color: 'red', group_id: 20, type: 'GroupLabel')
+ create_resource(target_type, 1, 1)
+ link = label_links_table.create(label_id: 3, target_type: target_type, target_id: 1)
+
+ subject.perform(1, 100)
+
+ expect(link.reload.label_id).to eq(3)
+ end
+
+ context 'with nested group' do
+ before do
+ namespaces_table.create(id: 11, type: 'Group', name: 'subgroup1', path: 'group1/subgroup1', parent_id: 10)
+ projects_table.create(id: 2, name: 'subproject1', path: 'group1/subgroup1/subproject1', namespace_id: 11)
+ create_resource(target_type, 1, 2)
+ end
+
+ it 'ignores label links referencing ancestor group labels', :nested_groups do
+ labels_table.create(id: 4, title: 'bug', color: 'red', project_id: 2, type: 'ProjectLabel')
+ label_links_table.create(label_id: 4, target_type: target_type, target_id: 1)
+ link = label_links_table.create(label_id: 1, target_type: target_type, target_id: 1)
+
+ subject.perform(1, 100)
+
+ expect(link.reload.label_id).to eq(1)
+ end
+
+ it 'checks also issues and MRs in subgroups', :nested_groups do
+ link = label_links_table.create(label_id: 2, target_type: target_type, target_id: 1)
+
+ subject.perform(1, 100)
+
+ expect(link.reload.label_id).to eq(1)
+ end
+ end
+ end
+
+ context 'resource is Issue' do
+ it_behaves_like 'resource with cross-project labels' do
+ let(:target_type) { 'Issue' }
+ end
+ end
+
+ context 'resource is Merge Request' do
+ it_behaves_like 'resource with cross-project labels' do
+ let(:target_type) { 'MergeRequest' }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb b/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb
index ee60e498b59..2e77e80ee46 100644
--- a/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb
+++ b/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb
@@ -7,7 +7,7 @@ describe Gitlab::BackgroundMigration::MovePersonalSnippetFiles do
let(:snippet) do
snippet = create(:personal_snippet)
create_upload_for_snippet(snippet)
- snippet.update_attributes!(description: markdown_linking_file(snippet))
+ snippet.update!(description: markdown_linking_file(snippet))
snippet
end
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
index b411aaa19da..0a8c77b0ad9 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
@@ -281,7 +281,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :
it "doesn't break when the namespace was renamed" do
subject.rename_namespace(namespace)
- namespace.update_attributes!(path: 'renamed-afterwards')
+ namespace.update!(path: 'renamed-afterwards')
expect { subject.revert_renames }.not_to raise_error
end
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
index b4896d69077..d4d7a83921c 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
@@ -169,7 +169,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :de
it "doesn't break when the project was renamed" do
subject.rename_project(project)
- project.update_attributes!(path: 'renamed-afterwards')
+ project.update!(path: 'renamed-afterwards')
expect { subject.revert_renames }.not_to raise_error
end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 8bb246aa4bd..782e4e45a91 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -357,6 +357,35 @@ describe Gitlab::Database do
end
end
+ describe '.db_read_only?' do
+ context 'when using PostgreSQL' do
+ before do
+ allow(ActiveRecord::Base.connection).to receive(:execute).and_call_original
+ expect(described_class).to receive(:postgresql?).and_return(true)
+ end
+
+ it 'detects a read only database' do
+ allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => "t" }])
+
+ expect(described_class.db_read_only?).to be_truthy
+ end
+
+ it 'detects a read write database' do
+ allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => "f" }])
+
+ expect(described_class.db_read_only?).to be_falsey
+ end
+ end
+
+ context 'when using MySQL' do
+ before do
+ expect(described_class).to receive(:postgresql?).and_return(false)
+ end
+
+ it { expect(described_class.db_read_only?).to be_falsey }
+ end
+ end
+
describe '#sanitize_timestamp' do
let(:max_timestamp) { Time.at((1 << 31) - 1) }
diff --git a/spec/lib/gitlab/exclusive_lease_helpers_spec.rb b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb
new file mode 100644
index 00000000000..2e3656b52fb
--- /dev/null
+++ b/spec/lib/gitlab/exclusive_lease_helpers_spec.rb
@@ -0,0 +1,76 @@
+require 'spec_helper'
+
+describe Gitlab::ExclusiveLeaseHelpers, :clean_gitlab_redis_shared_state do
+ include ::ExclusiveLeaseHelpers
+
+ let(:class_instance) { (Class.new { include ::Gitlab::ExclusiveLeaseHelpers }).new }
+ let(:unique_key) { SecureRandom.hex(10) }
+
+ describe '#in_lock' do
+ subject { class_instance.in_lock(unique_key, **options) { } }
+
+ let(:options) { {} }
+
+ context 'when the lease is not obtained yet' do
+ before do
+ stub_exclusive_lease(unique_key, 'uuid')
+ end
+
+ it 'calls the given block' do
+ expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_control.once
+ end
+
+ it 'calls the given block continuously' do
+ expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_control.once
+ expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_control.once
+ expect { |b| class_instance.in_lock(unique_key, &b) }.to yield_control.once
+ end
+
+ it 'cancels the exclusive lease after the block' do
+ expect_to_cancel_exclusive_lease(unique_key, 'uuid')
+
+ subject
+ end
+ end
+
+ context 'when the lease is obtained already' do
+ let!(:lease) { stub_exclusive_lease_taken(unique_key) }
+
+ it 'retries to obtain a lease and raises an error' do
+ expect(lease).to receive(:try_obtain).exactly(11).times
+
+ expect { subject }.to raise_error('Failed to obtain a lock')
+ end
+
+ context 'when ttl is specified' do
+ let(:options) { { ttl: 10.minutes } }
+
+ it 'receives the specified argument' do
+ expect(Gitlab::ExclusiveLease).to receive(:new).with(unique_key, { timeout: 10.minutes } )
+
+ expect { subject }.to raise_error('Failed to obtain a lock')
+ end
+ end
+
+ context 'when retry count is specified' do
+ let(:options) { { retries: 3 } }
+
+ it 'retries for the specified times' do
+ expect(lease).to receive(:try_obtain).exactly(4).times
+
+ expect { subject }.to raise_error('Failed to obtain a lock')
+ end
+ end
+
+ context 'when sleep second is specified' do
+ let(:options) { { retries: 0, sleep_sec: 0.05.seconds } }
+
+ it 'receives the specified argument' do
+ expect(class_instance).to receive(:sleep).with(0.05.seconds).once
+
+ expect { subject }.to raise_error('Failed to obtain a lock')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index b6061df349d..6900c4189b7 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -532,8 +532,8 @@ describe Gitlab::Git::Blob, seed_helper: true do
subject { blob.load_all_data!(repository) }
it 'loads missing data' do
- expect(Gitlab::GitalyClient).to receive(:migrate)
- .with(:git_blob_load_all_data).and_return(full_data)
+ expect(repository.gitaly_blob_client).to receive(:get_blob)
+ .and_return(double(:response, data: full_data))
subject
@@ -544,8 +544,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
let(:blob) { Gitlab::Git::Blob.new(name: 'test', size: 4, data: full_data) }
it "doesn't perform any loading" do
- expect(Gitlab::GitalyClient).not_to receive(:migrate)
- .with(:git_blob_load_all_data)
+ expect(repository.gitaly_blob_client).not_to receive(:get_blob)
subject
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 5dd7af3a552..e6268a05d44 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -1856,6 +1856,54 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ describe '#set_config' do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
+ let(:rugged) { repository_rugged }
+ let(:entries) do
+ {
+ 'test.foo1' => 'bla bla',
+ 'test.foo2' => 1234,
+ 'test.foo3' => true
+ }
+ end
+
+ it 'can set config settings' do
+ expect(repository.set_config(entries)).to be_nil
+
+ expect(rugged.config['test.foo1']).to eq('bla bla')
+ expect(rugged.config['test.foo2']).to eq('1234')
+ expect(rugged.config['test.foo3']).to eq('true')
+ end
+
+ after do
+ entries.keys.each { |k| rugged.config.delete(k) }
+ end
+ end
+
+ describe '#delete_config' do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
+ let(:rugged) { repository_rugged }
+ let(:entries) do
+ {
+ 'test.foo1' => 'bla bla',
+ 'test.foo2' => 1234,
+ 'test.foo3' => true
+ }
+ end
+
+ it 'can delete config settings' do
+ entries.each do |key, value|
+ rugged.config[key] = value
+ end
+
+ expect(repository.delete_config(*%w[does.not.exist test.foo1 test.foo2])).to be_nil
+
+ config_keys = rugged.config.each_key.to_a
+ expect(config_keys).not_to include('test.foo1')
+ expect(config_keys).not_to include('test.foo2')
+ end
+ end
+
describe '#merge' do
let(:repository) do
Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index 9709f1f5646..031d1e87dc1 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -53,6 +53,47 @@ describe Gitlab::GitalyClient::OperationService do
end
end
+ describe '#user_update_branch' do
+ let(:branch_name) { 'my-branch' }
+ let(:newrev) { '01e' }
+ let(:oldrev) { '01d' }
+ let(:request) do
+ Gitaly::UserUpdateBranchRequest.new(
+ repository: repository.gitaly_repository,
+ branch_name: branch_name,
+ newrev: newrev,
+ oldrev: oldrev,
+ user: gitaly_user
+ )
+ end
+ let(:response) { Gitaly::UserUpdateBranchResponse.new }
+
+ subject { client.user_update_branch(branch_name, user, newrev, oldrev) }
+
+ it 'sends a user_update_branch message' do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_update_branch).with(request, kind_of(Hash))
+ .and_return(response)
+
+ subject
+ end
+
+ context "when pre_receive_error is present" do
+ let(:response) do
+ Gitaly::UserUpdateBranchResponse.new(pre_receive_error: "something failed")
+ end
+
+ it "throws a PreReceive exception" do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_update_branch).with(request, kind_of(Hash))
+ .and_return(response)
+
+ expect { subject }.to raise_error(
+ Gitlab::Git::PreReceiveError, "something failed")
+ end
+ end
+ end
+
describe '#user_delete_branch' do
let(:branch_name) { 'my-branch' }
let(:request) do
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
index 51fad6c6838..b2e544e6fed 100644
--- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
@@ -27,9 +27,9 @@ describe Gitlab::GithubImport::Importer::PullRequestsImporter do
milestone: double(:milestone, number: 4),
user: double(:user, id: 4, login: 'alice'),
assignee: double(:user, id: 4, login: 'alice'),
- created_at: Time.zone.now,
- updated_at: Time.zone.now,
- merged_at: Time.zone.now
+ created_at: 1.second.ago,
+ updated_at: 1.second.ago,
+ merged_at: 1.second.ago
)
end
diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
index 6fbffc38444..1a2c6ef25c4 100644
--- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
+++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
@@ -141,7 +141,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
expect(invalid_gpg_signature.reload.verification_status).to eq 'unverified_key'
# InvalidGpgSignatureUpdater is called by the after_update hook
- user.update_attributes!(email: GpgHelpers::User1.emails.first)
+ user.update!(email: GpgHelpers::User1.emails.first)
expect(invalid_gpg_signature.reload).to have_attributes(
project: project,
@@ -166,7 +166,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
)
# InvalidGpgSignatureUpdater is called by the after_update hook
- user.update_attributes!(email: 'still.unrelated@example.com')
+ user.update!(email: 'still.unrelated@example.com')
expect(invalid_gpg_signature.reload).to have_attributes(
project: project,
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_object_storage_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_object_storage_spec.rb
new file mode 100644
index 00000000000..5059d68e54b
--- /dev/null
+++ b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_object_storage_spec.rb
@@ -0,0 +1,105 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy do
+ let!(:service) { described_class.new }
+ let!(:project) { create(:project, :with_object_export) }
+ let(:shared) { project.import_export_shared }
+ let!(:user) { create(:user) }
+
+ describe '#execute' do
+ before do
+ allow(service).to receive(:strategy_execute)
+ stub_feature_flags(import_export_object_storage: true)
+ end
+
+ it 'returns if project exported file is not found' do
+ allow(project).to receive(:export_project_object_exists?).and_return(false)
+
+ expect(service).not_to receive(:strategy_execute)
+
+ service.execute(user, project)
+ end
+
+ it 'creates a lock file in the export dir' do
+ allow(service).to receive(:delete_after_export_lock)
+
+ service.execute(user, project)
+
+ expect(lock_path_exist?).to be_truthy
+ end
+
+ context 'when the method succeeds' do
+ it 'removes the lock file' do
+ service.execute(user, project)
+
+ expect(lock_path_exist?).to be_falsey
+ end
+ end
+
+ context 'when the method fails' do
+ before do
+ allow(service).to receive(:strategy_execute).and_call_original
+ end
+
+ context 'when validation fails' do
+ before do
+ allow(service).to receive(:invalid?).and_return(true)
+ end
+
+ it 'does not create the lock file' do
+ expect(service).not_to receive(:create_or_update_after_export_lock)
+
+ service.execute(user, project)
+ end
+
+ it 'does not execute main logic' do
+ expect(service).not_to receive(:strategy_execute)
+
+ service.execute(user, project)
+ end
+
+ it 'logs validation errors in shared context' do
+ expect(service).to receive(:log_validation_errors)
+
+ service.execute(user, project)
+ end
+ end
+
+ context 'when an exception is raised' do
+ it 'removes the lock' do
+ expect { service.execute(user, project) }.to raise_error(NotImplementedError)
+
+ expect(lock_path_exist?).to be_falsey
+ end
+ end
+ end
+ end
+
+ describe '#log_validation_errors' do
+ it 'add the message to the shared context' do
+ errors = %w(test_message test_message2)
+
+ allow(service).to receive(:invalid?).and_return(true)
+ allow(service.errors).to receive(:full_messages).and_return(errors)
+
+ expect(shared).to receive(:add_error_message).twice.and_call_original
+
+ service.execute(user, project)
+
+ expect(shared.errors).to eq errors
+ end
+ end
+
+ describe '#to_json' do
+ it 'adds the current strategy class to the serialized attributes' do
+ params = { param1: 1 }
+ result = params.merge(klass: described_class.to_s).to_json
+
+ expect(described_class.new(params).to_json).to eq result
+ end
+ end
+
+ def lock_path_exist?
+ File.exist?(described_class.lock_file_path(project))
+ end
+end
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
index ed54d87de4a..566b7f46c87 100644
--- a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
+++ b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
@@ -9,6 +9,7 @@ describe Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy do
describe '#execute' do
before do
allow(service).to receive(:strategy_execute)
+ stub_feature_flags(import_export_object_storage: false)
end
it 'returns if project exported file is not found' do
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
index 5fe57d9987b..7f2e0a4ee2c 100644
--- a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
+++ b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
@@ -24,13 +24,34 @@ describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do
end
describe '#execute' do
- it 'removes the exported project file after the upload' do
- allow(strategy).to receive(:send_file)
- allow(strategy).to receive(:handle_response_error)
+ context 'without object storage' do
+ before do
+ stub_feature_flags(import_export_object_storage: false)
+ end
+
+ it 'removes the exported project file after the upload' do
+ allow(strategy).to receive(:send_file)
+ allow(strategy).to receive(:handle_response_error)
+
+ expect(project).to receive(:remove_exported_project_file)
+
+ strategy.execute(user, project)
+ end
+ end
+
+ context 'with object storage' do
+ before do
+ stub_feature_flags(import_export_object_storage: true)
+ end
- expect(project).to receive(:remove_exported_project_file)
+ it 'removes the exported project file after the upload' do
+ allow(strategy).to receive(:send_file)
+ allow(strategy).to receive(:handle_response_error)
- strategy.execute(user, project)
+ expect(project).to receive(:remove_exported_project_file)
+
+ strategy.execute(user, project)
+ end
end
end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 2ea66479c1b..084ce3066d6 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -293,6 +293,7 @@ project:
- deploy_tokens
- settings
- ci_cd_settings
+- import_export_upload
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/import_export/saver_spec.rb b/spec/lib/gitlab/import_export/saver_spec.rb
new file mode 100644
index 00000000000..02f1a4b81aa
--- /dev/null
+++ b/spec/lib/gitlab/import_export/saver_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+require 'fileutils'
+
+describe Gitlab::ImportExport::Saver do
+ let!(:project) { create(:project, :public, name: 'project') }
+ let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
+ let(:shared) { project.import_export_shared }
+ subject { described_class.new(project: project, shared: shared) }
+
+ before do
+ allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+
+ FileUtils.mkdir_p(shared.export_path)
+ FileUtils.touch("#{shared.export_path}/tmp.bundle")
+ end
+
+ after do
+ FileUtils.rm_rf(export_path)
+ end
+
+ context 'local archive' do
+ it 'saves the repo to disk' do
+ stub_feature_flags(import_export_object_storage: false)
+
+ subject.save
+
+ expect(shared.errors).to be_empty
+ expect(Dir.empty?(shared.archive_path)).to be false
+ end
+ end
+
+ context 'object storage' do
+ it 'saves the repo using object storage' do
+ stub_feature_flags(import_export_object_storage: true)
+ stub_uploads_object_storage(ImportExportUploader)
+
+ subject.save
+
+ expect(ImportExportUpload.find_by(project: project).export_file.url)
+ .to match(%r[\/uploads\/-\/system\/import_export_upload\/export_file.*])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb
index b4837a1689a..f788f8ee276 100644
--- a/spec/lib/gitlab/middleware/multipart_spec.rb
+++ b/spec/lib/gitlab/middleware/multipart_spec.rb
@@ -75,6 +75,33 @@ describe Gitlab::Middleware::Multipart do
it_behaves_like 'multipart upload files'
end
+ it 'allows symlinks for uploads dir' do
+ Tempfile.open('two-levels') do |tempfile|
+ symlinked_dir = '/some/dir/uploads'
+ symlinked_path = File.join(symlinked_dir, File.basename(tempfile.path))
+ env = post_env({ 'file' => symlinked_path }, { 'file.name' => original_filename, 'file.path' => symlinked_path }, Gitlab::Workhorse.secret, 'gitlab-workhorse')
+
+ allow(FileUploader).to receive(:root).and_return(symlinked_dir)
+ allow(UploadedFile).to receive(:allowed_paths).and_return([symlinked_dir, Gitlab.config.uploads.storage_path])
+ allow(File).to receive(:realpath).and_call_original
+ allow(File).to receive(:realpath).with(symlinked_dir).and_return(Dir.tmpdir)
+ allow(File).to receive(:realpath).with(symlinked_path).and_return(tempfile.path)
+ allow(File).to receive(:exist?).and_call_original
+ allow(File).to receive(:exist?).with(symlinked_dir).and_return(true)
+
+ # override Dir.tmpdir because this dir is in the list of allowed paths
+ # and it would match FileUploader.root path (which in this test is linked
+ # to /tmp too)
+ allow(Dir).to receive(:tmpdir).and_return(File.join(Dir.tmpdir, 'tmpsubdir'))
+
+ expect(app).to receive(:call) do |env|
+ expect(Rack::Request.new(env).params['file']).to be_a(::UploadedFile)
+ end
+
+ middleware.call(env)
+ end
+ end
+
def post_env(rewritten_fields, params, secret, issuer)
token = JWT.encode({ 'iss' => issuer, 'rewritten_fields' => rewritten_fields }, secret, 'HS256')
Rack::MockRequest.env_for(
diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb
index 5c398bc2063..8fbeaa065fa 100644
--- a/spec/lib/gitlab/middleware/read_only_spec.rb
+++ b/spec/lib/gitlab/middleware/read_only_spec.rb
@@ -4,28 +4,6 @@ describe Gitlab::Middleware::ReadOnly do
include Rack::Test::Methods
using RSpec::Parameterized::TableSyntax
- RSpec::Matchers.define :be_a_redirect do
- match do |response|
- response.status == 301
- end
- end
-
- RSpec::Matchers.define :disallow_request do
- match do |middleware|
- alert = middleware.env['rack.session'].to_hash
- .dig('flash', 'flashes', 'alert')
-
- alert&.include?('You cannot perform write operations')
- end
- end
-
- RSpec::Matchers.define :disallow_request_in_json do
- match do |response|
- json_response = JSON.parse(response.body)
- response.body.include?('You cannot perform write operations') && json_response.key?('message')
- end
- end
-
let(:rack_stack) do
rack = Rack::Builder.new do
use ActionDispatch::Session::CacheStore
@@ -66,38 +44,38 @@ describe Gitlab::Middleware::ReadOnly do
it 'expects PATCH requests to be disallowed' do
response = request.patch('/test_request')
- expect(response).to be_a_redirect
+ expect(response).to be_redirect
expect(subject).to disallow_request
end
it 'expects PUT requests to be disallowed' do
response = request.put('/test_request')
- expect(response).to be_a_redirect
+ expect(response).to be_redirect
expect(subject).to disallow_request
end
it 'expects POST requests to be disallowed' do
response = request.post('/test_request')
- expect(response).to be_a_redirect
+ expect(response).to be_redirect
expect(subject).to disallow_request
end
it 'expects a internal POST request to be allowed after a disallowed request' do
response = request.post('/test_request')
- expect(response).to be_a_redirect
+ expect(response).to be_redirect
response = request.post("/api/#{API::API.version}/internal")
- expect(response).not_to be_a_redirect
+ expect(response).not_to be_redirect
end
it 'expects DELETE requests to be disallowed' do
response = request.delete('/test_request')
- expect(response).to be_a_redirect
+ expect(response).to be_redirect
expect(subject).to disallow_request
end
@@ -105,7 +83,7 @@ describe Gitlab::Middleware::ReadOnly do
expect(Rails.application.routes).to receive(:recognize_path).and_call_original
response = request.post('/root/gitlab-ce/new/master/app/info/lfs/objects/batch')
- expect(response).to be_a_redirect
+ expect(response).to be_redirect
expect(subject).to disallow_request
end
@@ -120,19 +98,19 @@ describe Gitlab::Middleware::ReadOnly do
expect(Rails.application.routes).not_to receive(:recognize_path)
response = request.post("/api/#{API::API.version}/internal")
- expect(response).not_to be_a_redirect
+ expect(response).not_to be_redirect
expect(subject).not_to disallow_request
end
it 'expects requests to sidekiq admin to be allowed' do
response = request.post('/admin/sidekiq')
- expect(response).not_to be_a_redirect
+ expect(response).not_to be_redirect
expect(subject).not_to disallow_request
response = request.get('/admin/sidekiq')
- expect(response).not_to be_a_redirect
+ expect(response).not_to be_redirect
expect(subject).not_to disallow_request
end
@@ -150,7 +128,7 @@ describe Gitlab::Middleware::ReadOnly do
expect(Rails.application.routes).to receive(:recognize_path).and_call_original
response = request.post(path)
- expect(response).not_to be_a_redirect
+ expect(response).not_to be_redirect
expect(subject).not_to disallow_request
end
end
diff --git a/spec/lib/gitlab/sanitizers/svg_spec.rb b/spec/lib/gitlab/sanitizers/svg_spec.rb
index 030c2063ab2..df46a874528 100644
--- a/spec/lib/gitlab/sanitizers/svg_spec.rb
+++ b/spec/lib/gitlab/sanitizers/svg_spec.rb
@@ -7,9 +7,9 @@ describe Gitlab::Sanitizers::SVG do
describe '.clean' do
let(:input_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'unsanitized.svg') }
- let(:data) { open(input_svg_path).read }
+ let(:data) { File.read(input_svg_path) }
let(:sanitized_svg_path) { File.join(Rails.root, 'spec', 'fixtures', 'sanitized.svg') }
- let(:sanitized) { open(sanitized_svg_path).read }
+ let(:sanitized) { File.read(sanitized_svg_path) }
it 'delegates sanitization to scrubber' do
expect_any_instance_of(Gitlab::Sanitizers::SVG::Scrubber).to receive(:scrub).at_least(:once)
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 660671cefaf..7802ff1f5f6 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -143,34 +143,22 @@ describe Gitlab::Workhorse do
let(:diff_refs) { double(base_sha: "base", head_sha: "head") }
subject { described_class.send_git_diff(repository, diff_refs) }
- context 'when Gitaly workhorse_send_git_diff feature is enabled' do
- it 'sets the header correctly' do
- key, command, params = decode_workhorse_header(subject)
-
- expect(key).to eq("Gitlab-Workhorse-Send-Data")
- expect(command).to eq("git-diff")
- expect(params).to eq({
- 'GitalyServer' => {
- address: Gitlab::GitalyClient.address(project.repository_storage),
- token: Gitlab::GitalyClient.token(project.repository_storage)
- },
- 'RawDiffRequest' => Gitaly::RawDiffRequest.new(
- repository: repository.gitaly_repository,
- left_commit_id: 'base',
- right_commit_id: 'head'
- ).to_json
- }.deep_stringify_keys)
- end
- end
-
- context 'when Gitaly workhorse_send_git_diff feature is disabled', :disable_gitaly do
- it 'sets the header correctly' do
- key, command, params = decode_workhorse_header(subject)
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
- expect(key).to eq("Gitlab-Workhorse-Send-Data")
- expect(command).to eq("git-diff")
- expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head")
- end
+ expect(key).to eq("Gitlab-Workhorse-Send-Data")
+ expect(command).to eq("git-diff")
+ expect(params).to eq({
+ 'GitalyServer' => {
+ address: Gitlab::GitalyClient.address(project.repository_storage),
+ token: Gitlab::GitalyClient.token(project.repository_storage)
+ },
+ 'RawDiffRequest' => Gitaly::RawDiffRequest.new(
+ repository: repository.gitaly_repository,
+ left_commit_id: 'base',
+ right_commit_id: 'head'
+ ).to_json
+ }.deep_stringify_keys)
end
end
@@ -189,7 +177,7 @@ describe Gitlab::Workhorse do
end
it 'accepts a trailing newline' do
- open(described_class.secret_path, 'a') { |f| f.write "\n" }
+ File.open(described_class.secret_path, 'a') { |f| f.write "\n" }
expect(subject.length).to eq(32)
end
@@ -425,34 +413,22 @@ describe Gitlab::Workhorse do
subject { described_class.send_git_blob(repository, blob) }
- context 'when Gitaly workhorse_raw_show feature is enabled' do
- it 'sets the header correctly' do
- key, command, params = decode_workhorse_header(subject)
-
- expect(key).to eq('Gitlab-Workhorse-Send-Data')
- expect(command).to eq('git-blob')
- expect(params).to eq({
- 'GitalyServer' => {
- address: Gitlab::GitalyClient.address(project.repository_storage),
- token: Gitlab::GitalyClient.token(project.repository_storage)
- },
- 'GetBlobRequest' => {
- repository: repository.gitaly_repository.to_h,
- oid: blob.id,
- limit: -1
- }
- }.deep_stringify_keys)
- end
- end
-
- context 'when Gitaly workhorse_raw_show feature is disabled', :disable_gitaly do
- it 'sets the header correctly' do
- key, command, params = decode_workhorse_header(subject)
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
- expect(key).to eq('Gitlab-Workhorse-Send-Data')
- expect(command).to eq('git-blob')
- expect(params).to eq('RepoPath' => repository.path_to_repo, 'BlobId' => blob.id)
- end
+ expect(key).to eq('Gitlab-Workhorse-Send-Data')
+ expect(command).to eq('git-blob')
+ expect(params).to eq({
+ 'GitalyServer' => {
+ address: Gitlab::GitalyClient.address(project.repository_storage),
+ token: Gitlab::GitalyClient.token(project.repository_storage)
+ },
+ 'GetBlobRequest' => {
+ repository: repository.gitaly_repository.to_h,
+ oid: blob.id,
+ limit: -1
+ }
+ }.deep_stringify_keys)
end
end
diff --git a/spec/migrations/issues_moved_to_id_foreign_key_spec.rb b/spec/migrations/issues_moved_to_id_foreign_key_spec.rb
index dd2b08099f2..495e86ee888 100644
--- a/spec/migrations/issues_moved_to_id_foreign_key_spec.rb
+++ b/spec/migrations/issues_moved_to_id_foreign_key_spec.rb
@@ -14,7 +14,7 @@ describe IssuesMovedToIdForeignKey, :migration, schema: 20171114150259 do
it 'removes the orphaned moved_to_id' do
subject.down
- issue_third.update_attributes(moved_to_id: 100000)
+ issue_third.update(moved_to_id: 100000)
subject.up
diff --git a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
index df009cec25c..ba4c66057d4 100644
--- a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
+++ b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb
@@ -12,7 +12,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do
class KubernetesService < ActiveRecord::Base
self.table_name = 'services'
- serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
+ serialize :properties, JSON
default_value_for :active, true
default_value_for :type, 'KubernetesService'
@@ -175,7 +175,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do
end
end
- def tr(s)
- s.delete("'")
+ def tr(str)
+ str.delete("'")
end
end
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb
index 7e75d5a5411..6dba132184c 100644
--- a/spec/models/ci/build_metadata_spec.rb
+++ b/spec/models/ci/build_metadata_spec.rb
@@ -30,7 +30,7 @@ describe Ci::BuildMetadata do
context 'when runner is assigned to the job' do
before do
- build.update_attributes(runner: runner)
+ build.update(runner: runner)
end
context 'when runner timeout is lower than project timeout' do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 0da1234ee3b..234d2d8aa3a 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -186,18 +186,18 @@ describe Ci::Build do
let(:runner) { create(:ci_runner, :project, projects: [build.project]) }
before do
- runner.update_attributes(contacted_at: 1.second.ago)
+ runner.update(contacted_at: 1.second.ago)
end
it { is_expected.to be_truthy }
it 'that is inactive' do
- runner.update_attributes(active: false)
+ runner.update(active: false)
is_expected.to be_falsey
end
it 'that is not online' do
- runner.update_attributes(contacted_at: nil)
+ runner.update(contacted_at: nil)
is_expected.to be_falsey
end
@@ -261,7 +261,7 @@ describe Ci::Build do
context 'artifacts metadata does not exist' do
before do
- build.update_attributes(legacy_artifacts_metadata: nil)
+ build.update(legacy_artifacts_metadata: nil)
end
it { is_expected.to be_falsy }
@@ -514,6 +514,22 @@ describe Ci::Build do
end
end
+ describe '#has_old_trace?' do
+ subject { build.has_old_trace? }
+
+ context 'when old trace exists' do
+ before do
+ build.update_column(:trace, 'old trace')
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when old trace does not exist' do
+ it { is_expected.to be_falsy }
+ end
+ end
+
describe '#trace=' do
it "expect to fail trace=" do
expect { build.trace = "new" }.to raise_error(NotImplementedError)
@@ -533,16 +549,32 @@ describe Ci::Build do
end
describe '#erase_old_trace!' do
- subject { build.send(:read_attribute, :trace) }
+ subject { build.erase_old_trace! }
- before do
- build.send(:write_attribute, :trace, 'old trace')
+ context 'when old trace exists' do
+ before do
+ build.update_column(:trace, 'old trace')
+ end
+
+ it "erases old trace" do
+ subject
+
+ expect(build.old_trace).to be_nil
+ end
+
+ it "executes UPDATE query" do
+ recorded = ActiveRecord::QueryRecorder.new { subject }
+
+ expect(recorded.log.select { |l| l.match?(/UPDATE.*ci_builds/) }.count).to eq(1)
+ end
end
- it "expect to receive data from database" do
- build.erase_old_trace!
+ context 'when old trace does not exist' do
+ it 'does not execute UPDATE query' do
+ recorded = ActiveRecord::QueryRecorder.new { subject }
- is_expected.to be_nil
+ expect(recorded.log.select { |l| l.match?(/UPDATE.*ci_builds/) }.count).to eq(0)
+ end
end
end
@@ -1503,7 +1535,7 @@ describe Ci::Build do
expect(ProjectStatistics)
.not_to receive(:increment_statistic)
- build.project.update_attributes(pending_delete: true)
+ build.project.update(pending_delete: true)
build.project.destroy!
end
end
@@ -1630,7 +1662,7 @@ describe Ci::Build do
end
before do
- build.update_attributes(user: user)
+ build.update(user: user)
end
it { user_variables.each { |v| is_expected.to include(v) } }
@@ -1708,7 +1740,7 @@ describe Ci::Build do
context 'when build started manually' do
before do
- build.update_attributes(when: :manual)
+ build.update(when: :manual)
end
let(:manual_variable) do
@@ -1724,7 +1756,7 @@ describe Ci::Build do
end
before do
- build.update_attributes(tag: true)
+ build.update(tag: true)
end
it { is_expected.to include(tag_variable) }
diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb
index 464897de306..774a638b430 100644
--- a/spec/models/ci/build_trace_chunk_spec.rb
+++ b/spec/models/ci/build_trace_chunk_spec.rb
@@ -14,6 +14,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
before do
stub_feature_flags(ci_enable_live_trace: true)
+ stub_artifacts_object_storage
end
context 'FastDestroyAll' do
@@ -37,6 +38,22 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
end
+ describe '.all_stores' do
+ subject { described_class.all_stores }
+
+ it 'returns a correctly ordered array' do
+ is_expected.to eq(%w[redis database fog])
+ end
+
+ it 'returns redis store as the the lowest precedence' do
+ expect(subject.first).to eq('redis')
+ end
+
+ it 'returns fog store as the the highest precedence' do
+ expect(subject.last).to eq('fog')
+ end
+ end
+
describe '#data' do
subject { build_trace_chunk.data }
@@ -44,181 +61,269 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data_store) { :redis }
before do
- build_trace_chunk.send(:redis_set_data, 'Sample data in redis')
+ build_trace_chunk.send(:unsafe_set_data!, 'Sample data in redis')
end
it { is_expected.to eq('Sample data in redis') }
end
context 'when data_store is database' do
- let(:data_store) { :db }
- let(:raw_data) { 'Sample data in db' }
+ let(:data_store) { :database }
+ let(:raw_data) { 'Sample data in database' }
- it { is_expected.to eq('Sample data in db') }
+ it { is_expected.to eq('Sample data in database') }
end
- end
-
- describe '#set_data' do
- subject { build_trace_chunk.send(:set_data, value) }
- let(:value) { 'Sample data' }
+ context 'when data_store is fog' do
+ let(:data_store) { :fog }
- context 'when value bytesize is bigger than CHUNK_SIZE' do
- let(:value) { 'a' * (described_class::CHUNK_SIZE + 1) }
+ before do
+ build_trace_chunk.send(:unsafe_set_data!, 'Sample data in fog')
+ end
- it { expect { subject }.to raise_error('too much data') }
+ it { is_expected.to eq('Sample data in fog') }
end
+ end
- context 'when data_store is redis' do
- let(:data_store) { :redis }
+ describe '#append' do
+ subject { build_trace_chunk.append(new_data, offset) }
- it do
- expect(build_trace_chunk.send(:redis_data)).to be_nil
+ let(:new_data) { 'Sample new data' }
+ let(:offset) { 0 }
+ let(:merged_data) { data + new_data.to_s }
- subject
+ shared_examples_for 'Appending correctly' do
+ context 'when offset is negative' do
+ let(:offset) { -1 }
+
+ it { expect { subject }.to raise_error('Offset is out of range') }
+ end
- expect(build_trace_chunk.send(:redis_data)).to eq(value)
+ context 'when offset is bigger than data size' do
+ let(:offset) { data.bytesize + 1 }
+
+ it { expect { subject }.to raise_error('Offset is out of range') }
end
- context 'when fullfilled chunk size' do
- let(:value) { 'a' * described_class::CHUNK_SIZE }
+ context 'when new data overflows chunk size' do
+ let(:new_data) { 'a' * (described_class::CHUNK_SIZE + 1) }
- it 'schedules stashing data' do
- expect(Ci::BuildTraceChunkFlushWorker).to receive(:perform_async).once
+ it { expect { subject }.to raise_error('Chunk size overflow') }
+ end
+
+ context 'when offset is EOF' do
+ let(:offset) { data.bytesize }
+ it 'appends' do
subject
+
+ expect(build_trace_chunk.data).to eq(merged_data)
end
- end
- end
- context 'when data_store is database' do
- let(:data_store) { :db }
+ context 'when the other process is appending' do
+ let(:lease_key) { "trace_write:#{build_trace_chunk.build.id}:chunks:#{build_trace_chunk.chunk_index}" }
- it 'sets data' do
- expect(build_trace_chunk.raw_data).to be_nil
+ before do
+ stub_exclusive_lease_taken(lease_key)
+ end
- subject
+ it 'raise an error' do
+ expect { subject }.to raise_error('Failed to obtain a lock')
+ end
+ end
- expect(build_trace_chunk.raw_data).to eq(value)
- expect(build_trace_chunk.persisted?).to be_truthy
- end
+ context 'when new_data is nil' do
+ let(:new_data) { nil }
- context 'when raw_data is not changed' do
- it 'does not execute UPDATE' do
- expect(build_trace_chunk.raw_data).to be_nil
- build_trace_chunk.save!
+ it 'raises an error' do
+ expect { subject }.to raise_error('New data is missing')
+ end
+ end
- # First set
- expect(ActiveRecord::QueryRecorder.new { subject }.count).to be > 0
- expect(build_trace_chunk.raw_data).to eq(value)
- expect(build_trace_chunk.persisted?).to be_truthy
+ context 'when new_data is empty' do
+ let(:new_data) { '' }
- # Second set
- build_trace_chunk.reload
- expect(ActiveRecord::QueryRecorder.new { subject }.count).to be(0)
+ it 'does not append' do
+ subject
+
+ expect(build_trace_chunk.data).to eq(data)
+ end
+
+ it 'does not execute UPDATE' do
+ ActiveRecord::QueryRecorder.new { subject }.log.map do |query|
+ expect(query).not_to include('UPDATE')
+ end
+ end
end
end
- context 'when fullfilled chunk size' do
- it 'does not schedule stashing data' do
- expect(Ci::BuildTraceChunkFlushWorker).not_to receive(:perform_async)
+ context 'when offset is middle of datasize' do
+ let(:offset) { data.bytesize / 2 }
+ it 'appends' do
subject
+
+ expect(build_trace_chunk.data).to eq(data.byteslice(0, offset) + new_data)
end
end
end
- end
- describe '#truncate' do
- subject { build_trace_chunk.truncate(offset) }
+ shared_examples_for 'Scheduling sidekiq worker to flush data to persist store' do
+ context 'when new data fullfilled chunk size' do
+ let(:new_data) { 'a' * described_class::CHUNK_SIZE }
- shared_examples_for 'truncates' do
- context 'when offset is negative' do
- let(:offset) { -1 }
+ it 'schedules trace chunk flush worker' do
+ expect(Ci::BuildTraceChunkFlushWorker).to receive(:perform_async).once
- it { expect { subject }.to raise_error('Offset is out of range') }
- end
+ subject
+ end
- context 'when offset is bigger than data size' do
- let(:offset) { data.bytesize + 1 }
+ it 'migrates data to object storage' do
+ Sidekiq::Testing.inline! do
+ subject
- it { expect { subject }.to raise_error('Offset is out of range') }
+ build_trace_chunk.reload
+ expect(build_trace_chunk.fog?).to be_truthy
+ expect(build_trace_chunk.data).to eq(new_data)
+ end
+ end
end
+ end
- context 'when offset is 10' do
- let(:offset) { 10 }
+ shared_examples_for 'Scheduling no sidekiq worker' do
+ context 'when new data fullfilled chunk size' do
+ let(:new_data) { 'a' * described_class::CHUNK_SIZE }
+
+ it 'does not schedule trace chunk flush worker' do
+ expect(Ci::BuildTraceChunkFlushWorker).not_to receive(:perform_async)
- it 'truncates' do
subject
+ end
- expect(build_trace_chunk.data).to eq(data.byteslice(0, offset))
+ it 'does not migrate data to object storage' do
+ Sidekiq::Testing.inline! do
+ data_store = build_trace_chunk.data_store
+
+ subject
+
+ build_trace_chunk.reload
+ expect(build_trace_chunk.data_store).to eq(data_store)
+ end
end
end
end
context 'when data_store is redis' do
let(:data_store) { :redis }
- let(:data) { 'Sample data in redis' }
- before do
- build_trace_chunk.send(:redis_set_data, data)
+ context 'when there are no data' do
+ let(:data) { '' }
+
+ it 'has no data' do
+ expect(build_trace_chunk.data).to be_empty
+ end
+
+ it_behaves_like 'Appending correctly'
+ it_behaves_like 'Scheduling sidekiq worker to flush data to persist store'
end
- it_behaves_like 'truncates'
- end
+ context 'when there are some data' do
+ let(:data) { 'Sample data in redis' }
- context 'when data_store is database' do
- let(:data_store) { :db }
- let(:raw_data) { 'Sample data in db' }
- let(:data) { raw_data }
+ before do
+ build_trace_chunk.send(:unsafe_set_data!, data)
+ end
- it_behaves_like 'truncates'
+ it 'has data' do
+ expect(build_trace_chunk.data).to eq(data)
+ end
+
+ it_behaves_like 'Appending correctly'
+ it_behaves_like 'Scheduling sidekiq worker to flush data to persist store'
+ end
end
- end
- describe '#append' do
- subject { build_trace_chunk.append(new_data, offset) }
+ context 'when data_store is database' do
+ let(:data_store) { :database }
- let(:new_data) { 'Sample new data' }
- let(:offset) { 0 }
- let(:total_data) { data + new_data }
+ context 'when there are no data' do
+ let(:data) { '' }
- shared_examples_for 'appends' do
- context 'when offset is negative' do
- let(:offset) { -1 }
+ it 'has no data' do
+ expect(build_trace_chunk.data).to be_empty
+ end
- it { expect { subject }.to raise_error('Offset is out of range') }
+ it_behaves_like 'Appending correctly'
+ it_behaves_like 'Scheduling no sidekiq worker'
end
- context 'when offset is bigger than data size' do
- let(:offset) { data.bytesize + 1 }
+ context 'when there are some data' do
+ let(:raw_data) { 'Sample data in database' }
+ let(:data) { raw_data }
- it { expect { subject }.to raise_error('Offset is out of range') }
+ it 'has data' do
+ expect(build_trace_chunk.data).to eq(data)
+ end
+
+ it_behaves_like 'Appending correctly'
+ it_behaves_like 'Scheduling no sidekiq worker'
end
+ end
- context 'when offset is bigger than data size' do
- let(:new_data) { 'a' * (described_class::CHUNK_SIZE + 1) }
+ context 'when data_store is fog' do
+ let(:data_store) { :fog }
- it { expect { subject }.to raise_error('Chunk size overflow') }
+ context 'when there are no data' do
+ let(:data) { '' }
+
+ it 'has no data' do
+ expect(build_trace_chunk.data).to be_empty
+ end
+
+ it_behaves_like 'Appending correctly'
+ it_behaves_like 'Scheduling no sidekiq worker'
end
- context 'when offset is EOF' do
- let(:offset) { data.bytesize }
+ context 'when there are some data' do
+ let(:data) { 'Sample data in fog' }
- it 'appends' do
- subject
+ before do
+ build_trace_chunk.send(:unsafe_set_data!, data)
+ end
- expect(build_trace_chunk.data).to eq(total_data)
+ it 'has data' do
+ expect(build_trace_chunk.data).to eq(data)
end
+
+ it_behaves_like 'Appending correctly'
+ it_behaves_like 'Scheduling no sidekiq worker'
+ end
+ end
+ end
+
+ describe '#truncate' do
+ subject { build_trace_chunk.truncate(offset) }
+
+ shared_examples_for 'truncates' do
+ context 'when offset is negative' do
+ let(:offset) { -1 }
+
+ it { expect { subject }.to raise_error('Offset is out of range') }
+ end
+
+ context 'when offset is bigger than data size' do
+ let(:offset) { data.bytesize + 1 }
+
+ it { expect { subject }.to raise_error('Offset is out of range') }
end
context 'when offset is 10' do
let(:offset) { 10 }
- it 'appends' do
+ it 'truncates' do
subject
- expect(build_trace_chunk.data).to eq(data.byteslice(0, offset) + new_data)
+ expect(build_trace_chunk.data).to eq(data.byteslice(0, offset))
end
end
end
@@ -228,18 +333,29 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data) { 'Sample data in redis' }
before do
- build_trace_chunk.send(:redis_set_data, data)
+ build_trace_chunk.send(:unsafe_set_data!, data)
end
- it_behaves_like 'appends'
+ it_behaves_like 'truncates'
end
context 'when data_store is database' do
- let(:data_store) { :db }
- let(:raw_data) { 'Sample data in db' }
+ let(:data_store) { :database }
+ let(:raw_data) { 'Sample data in database' }
let(:data) { raw_data }
- it_behaves_like 'appends'
+ it_behaves_like 'truncates'
+ end
+
+ context 'when data_store is fog' do
+ let(:data_store) { :fog }
+ let(:data) { 'Sample data in fog' }
+
+ before do
+ build_trace_chunk.send(:unsafe_set_data!, data)
+ end
+
+ it_behaves_like 'truncates'
end
end
@@ -253,7 +369,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data) { 'Sample data in redis' }
before do
- build_trace_chunk.send(:redis_set_data, data)
+ build_trace_chunk.send(:unsafe_set_data!, data)
end
it { is_expected.to eq(data.bytesize) }
@@ -265,10 +381,10 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
context 'when data_store is database' do
- let(:data_store) { :db }
+ let(:data_store) { :database }
context 'when data exists' do
- let(:raw_data) { 'Sample data in db' }
+ let(:raw_data) { 'Sample data in database' }
let(:data) { raw_data }
it { is_expected.to eq(data.bytesize) }
@@ -278,10 +394,43 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
it { is_expected.to eq(0) }
end
end
+
+ context 'when data_store is fog' do
+ let(:data_store) { :fog }
+
+ context 'when data exists' do
+ let(:data) { 'Sample data in fog' }
+ let(:key) { "tmp/builds/#{build.id}/chunks/#{chunk_index}.log" }
+
+ before do
+ build_trace_chunk.send(:unsafe_set_data!, data)
+ end
+
+ it { is_expected.to eq(data.bytesize) }
+ end
+
+ context 'when data does not exist' do
+ it { is_expected.to eq(0) }
+ end
+ end
end
- describe '#use_database!' do
- subject { build_trace_chunk.use_database! }
+ describe '#persist_data!' do
+ subject { build_trace_chunk.persist_data! }
+
+ shared_examples_for 'Atomic operation' do
+ context 'when the other process is persisting' do
+ let(:lease_key) { "trace_write:#{build_trace_chunk.build.id}:chunks:#{build_trace_chunk.chunk_index}" }
+
+ before do
+ stub_exclusive_lease_taken(lease_key)
+ end
+
+ it 'raise an error' do
+ expect { subject }.to raise_error('Failed to obtain a lock')
+ end
+ end
+ end
context 'when data_store is redis' do
let(:data_store) { :redis }
@@ -290,46 +439,93 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data) { 'Sample data in redis' }
before do
- build_trace_chunk.send(:redis_set_data, data)
+ build_trace_chunk.send(:unsafe_set_data!, data)
end
- it 'stashes the data' do
- expect(build_trace_chunk.data_store).to eq('redis')
- expect(build_trace_chunk.send(:redis_data)).to eq(data)
- expect(build_trace_chunk.raw_data).to be_nil
+ it 'persists the data' do
+ expect(build_trace_chunk.redis?).to be_truthy
+ expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to eq(data)
+ expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
+ expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound)
subject
- expect(build_trace_chunk.data_store).to eq('db')
- expect(build_trace_chunk.send(:redis_data)).to be_nil
- expect(build_trace_chunk.raw_data).to eq(data)
+ expect(build_trace_chunk.fog?).to be_truthy
+ expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
end
+
+ it_behaves_like 'Atomic operation'
end
context 'when data does not exist' do
- it 'does not call UPDATE' do
- expect(ActiveRecord::QueryRecorder.new { subject }.count).to eq(0)
+ it 'does not persist' do
+ expect { subject }.to raise_error('Can not persist empty data')
end
end
end
context 'when data_store is database' do
- let(:data_store) { :db }
+ let(:data_store) { :database }
- it 'does not call UPDATE' do
- expect(ActiveRecord::QueryRecorder.new { subject }.count).to eq(0)
+ context 'when data exists' do
+ let(:data) { 'Sample data in database' }
+
+ before do
+ build_trace_chunk.send(:unsafe_set_data!, data)
+ end
+
+ it 'persists the data' do
+ expect(build_trace_chunk.database?).to be_truthy
+ expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to eq(data)
+ expect { Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk) }.to raise_error(Excon::Error::NotFound)
+
+ subject
+
+ expect(build_trace_chunk.fog?).to be_truthy
+ expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
+ end
+
+ it_behaves_like 'Atomic operation'
end
- end
- end
- describe 'ExclusiveLock' do
- before do
- stub_exclusive_lease_taken
- stub_const('Ci::BuildTraceChunk::WRITE_LOCK_RETRY', 1)
+ context 'when data does not exist' do
+ it 'does not persist' do
+ expect { subject }.to raise_error('Can not persist empty data')
+ end
+ end
end
- it 'raise an error' do
- expect { build_trace_chunk.append('ABC', 0) }.to raise_error('Failed to obtain write lock')
+ context 'when data_store is fog' do
+ let(:data_store) { :fog }
+
+ context 'when data exists' do
+ let(:data) { 'Sample data in fog' }
+
+ before do
+ build_trace_chunk.send(:unsafe_set_data!, data)
+ end
+
+ it 'does not change data store' do
+ expect(build_trace_chunk.fog?).to be_truthy
+ expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
+
+ subject
+
+ expect(build_trace_chunk.fog?).to be_truthy
+ expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
+ expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
+ end
+
+ it_behaves_like 'Atomic operation'
+ end
end
end
diff --git a/spec/models/ci/build_trace_chunks/database_spec.rb b/spec/models/ci/build_trace_chunks/database_spec.rb
new file mode 100644
index 00000000000..d8fc9d57e95
--- /dev/null
+++ b/spec/models/ci/build_trace_chunks/database_spec.rb
@@ -0,0 +1,105 @@
+require 'spec_helper'
+
+describe Ci::BuildTraceChunks::Database do
+ let(:data_store) { described_class.new }
+
+ describe '#available?' do
+ subject { data_store.available? }
+
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#data' do
+ subject { data_store.data(model) }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :database_with_data, initial_data: 'sample data in database') }
+
+ it 'returns the data' do
+ is_expected.to eq('sample data in database')
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :database_without_data) }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#set_data' do
+ subject { data_store.set_data(model, data) }
+
+ let(:data) { 'abc123' }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :database_with_data, initial_data: 'sample data in database') }
+
+ it 'overwrites data' do
+ expect(data_store.data(model)).to eq('sample data in database')
+
+ subject
+
+ expect(data_store.data(model)).to eq('abc123')
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :database_without_data) }
+
+ it 'sets new data' do
+ expect(data_store.data(model)).to be_nil
+
+ subject
+
+ expect(data_store.data(model)).to eq('abc123')
+ end
+ end
+ end
+
+ describe '#delete_data' do
+ subject { data_store.delete_data(model) }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :database_with_data, initial_data: 'sample data in database') }
+
+ it 'deletes data' do
+ expect(data_store.data(model)).to eq('sample data in database')
+
+ subject
+
+ expect(data_store.data(model)).to be_nil
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :database_without_data) }
+
+ it 'does nothing' do
+ expect(data_store.data(model)).to be_nil
+
+ subject
+
+ expect(data_store.data(model)).to be_nil
+ end
+ end
+ end
+
+ describe '#keys' do
+ subject { data_store.keys(relation) }
+
+ let(:build) { create(:ci_build) }
+ let(:relation) { build.trace_chunks }
+
+ before do
+ create(:ci_build_trace_chunk, :database_with_data, chunk_index: 0, build: build)
+ create(:ci_build_trace_chunk, :database_with_data, chunk_index: 1, build: build)
+ end
+
+ it 'returns empty array' do
+ is_expected.to eq([])
+ end
+ end
+end
diff --git a/spec/models/ci/build_trace_chunks/fog_spec.rb b/spec/models/ci/build_trace_chunks/fog_spec.rb
new file mode 100644
index 00000000000..8f49190af13
--- /dev/null
+++ b/spec/models/ci/build_trace_chunks/fog_spec.rb
@@ -0,0 +1,146 @@
+require 'spec_helper'
+
+describe Ci::BuildTraceChunks::Fog do
+ let(:data_store) { described_class.new }
+
+ before do
+ stub_artifacts_object_storage
+ end
+
+ describe '#available?' do
+ subject { data_store.available? }
+
+ context 'when object storage is enabled' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when object storage is disabled' do
+ before do
+ stub_artifacts_object_storage(enabled: false)
+ end
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '#data' do
+ subject { data_store.data(model) }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :fog_with_data, initial_data: 'sample data in fog') }
+
+ it 'returns the data' do
+ is_expected.to eq('sample data in fog')
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :fog_without_data) }
+
+ it 'returns nil' do
+ expect { data_store.data(model) }.to raise_error(Excon::Error::NotFound)
+ end
+ end
+ end
+
+ describe '#set_data' do
+ subject { data_store.set_data(model, data) }
+
+ let(:data) { 'abc123' }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :fog_with_data, initial_data: 'sample data in fog') }
+
+ it 'overwrites data' do
+ expect(data_store.data(model)).to eq('sample data in fog')
+
+ subject
+
+ expect(data_store.data(model)).to eq('abc123')
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :fog_without_data) }
+
+ it 'sets new data' do
+ expect { data_store.data(model) }.to raise_error(Excon::Error::NotFound)
+
+ subject
+
+ expect(data_store.data(model)).to eq('abc123')
+ end
+ end
+ end
+
+ describe '#delete_data' do
+ subject { data_store.delete_data(model) }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :fog_with_data, initial_data: 'sample data in fog') }
+
+ it 'deletes data' do
+ expect(data_store.data(model)).to eq('sample data in fog')
+
+ subject
+
+ expect { data_store.data(model) }.to raise_error(Excon::Error::NotFound)
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :fog_without_data) }
+
+ it 'does nothing' do
+ expect { data_store.data(model) }.to raise_error(Excon::Error::NotFound)
+
+ subject
+
+ expect { data_store.data(model) }.to raise_error(Excon::Error::NotFound)
+ end
+ end
+ end
+
+ describe '#keys' do
+ subject { data_store.keys(relation) }
+
+ let(:build) { create(:ci_build) }
+ let(:relation) { build.trace_chunks }
+
+ before do
+ create(:ci_build_trace_chunk, :fog_with_data, chunk_index: 0, build: build)
+ create(:ci_build_trace_chunk, :fog_with_data, chunk_index: 1, build: build)
+ end
+
+ it 'returns keys' do
+ is_expected.to eq([[build.id, 0], [build.id, 1]])
+ end
+ end
+
+ describe '#delete_keys' do
+ subject { data_store.delete_keys(keys) }
+
+ let(:build) { create(:ci_build) }
+ let(:relation) { build.trace_chunks }
+ let(:keys) { data_store.keys(relation) }
+
+ before do
+ create(:ci_build_trace_chunk, :fog_with_data, chunk_index: 0, build: build)
+ create(:ci_build_trace_chunk, :fog_with_data, chunk_index: 1, build: build)
+ end
+
+ it 'deletes multiple data' do
+ ::Fog::Storage.new(JobArtifactUploader.object_store_credentials).tap do |connection|
+ expect(connection.get_object('artifacts', "tmp/builds/#{build.id}/chunks/0.log")[:body]).to be_present
+ expect(connection.get_object('artifacts', "tmp/builds/#{build.id}/chunks/1.log")[:body]).to be_present
+ end
+
+ subject
+
+ ::Fog::Storage.new(JobArtifactUploader.object_store_credentials).tap do |connection|
+ expect { connection.get_object('artifacts', "tmp/builds/#{build.id}/chunks/0.log")[:body] }.to raise_error(Excon::Error::NotFound)
+ expect { connection.get_object('artifacts', "tmp/builds/#{build.id}/chunks/1.log")[:body] }.to raise_error(Excon::Error::NotFound)
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/build_trace_chunks/redis_spec.rb b/spec/models/ci/build_trace_chunks/redis_spec.rb
new file mode 100644
index 00000000000..9da1e6a95ee
--- /dev/null
+++ b/spec/models/ci/build_trace_chunks/redis_spec.rb
@@ -0,0 +1,132 @@
+require 'spec_helper'
+
+describe Ci::BuildTraceChunks::Redis, :clean_gitlab_redis_shared_state do
+ let(:data_store) { described_class.new }
+
+ describe '#available?' do
+ subject { data_store.available? }
+
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#data' do
+ subject { data_store.data(model) }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'sample data in redis') }
+
+ it 'returns the data' do
+ is_expected.to eq('sample data in redis')
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :redis_without_data) }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#set_data' do
+ subject { data_store.set_data(model, data) }
+
+ let(:data) { 'abc123' }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'sample data in redis') }
+
+ it 'overwrites data' do
+ expect(data_store.data(model)).to eq('sample data in redis')
+
+ subject
+
+ expect(data_store.data(model)).to eq('abc123')
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :redis_without_data) }
+
+ it 'sets new data' do
+ expect(data_store.data(model)).to be_nil
+
+ subject
+
+ expect(data_store.data(model)).to eq('abc123')
+ end
+ end
+ end
+
+ describe '#delete_data' do
+ subject { data_store.delete_data(model) }
+
+ context 'when data exists' do
+ let(:model) { create(:ci_build_trace_chunk, :redis_with_data, initial_data: 'sample data in redis') }
+
+ it 'deletes data' do
+ expect(data_store.data(model)).to eq('sample data in redis')
+
+ subject
+
+ expect(data_store.data(model)).to be_nil
+ end
+ end
+
+ context 'when data does not exist' do
+ let(:model) { create(:ci_build_trace_chunk, :redis_without_data) }
+
+ it 'does nothing' do
+ expect(data_store.data(model)).to be_nil
+
+ subject
+
+ expect(data_store.data(model)).to be_nil
+ end
+ end
+ end
+
+ describe '#keys' do
+ subject { data_store.keys(relation) }
+
+ let(:build) { create(:ci_build) }
+ let(:relation) { build.trace_chunks }
+
+ before do
+ create(:ci_build_trace_chunk, :redis_with_data, chunk_index: 0, build: build)
+ create(:ci_build_trace_chunk, :redis_with_data, chunk_index: 1, build: build)
+ end
+
+ it 'returns keys' do
+ is_expected.to eq([[build.id, 0], [build.id, 1]])
+ end
+ end
+
+ describe '#delete_keys' do
+ subject { data_store.delete_keys(keys) }
+
+ let(:build) { create(:ci_build) }
+ let(:relation) { build.trace_chunks }
+ let(:keys) { data_store.keys(relation) }
+
+ before do
+ create(:ci_build_trace_chunk, :redis_with_data, chunk_index: 0, build: build)
+ create(:ci_build_trace_chunk, :redis_with_data, chunk_index: 1, build: build)
+ end
+
+ it 'deletes multiple data' do
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.exists("gitlab:ci:trace:#{build.id}:chunks:0")).to be_truthy
+ expect(redis.exists("gitlab:ci:trace:#{build.id}:chunks:1")).to be_truthy
+ end
+
+ subject
+
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis.exists("gitlab:ci:trace:#{build.id}:chunks:0")).to be_falsy
+ expect(redis.exists("gitlab:ci:trace:#{build.id}:chunks:1")).to be_falsy
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index efddd4e7662..0fd7612c011 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -25,7 +25,7 @@ describe Ci::JobArtifact do
end
it 'does not schedule the migration' do
- expect(ObjectStorageUploadWorker).not_to receive(:perform_async)
+ expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
subject
end
@@ -163,7 +163,7 @@ describe Ci::JobArtifact do
expect(ProjectStatistics)
.not_to receive(:increment_statistic)
- project.update_attributes(pending_delete: true)
+ project.update(pending_delete: true)
project.destroy!
end
end
diff --git a/spec/models/concerns/batch_destroy_dependent_associations_spec.rb b/spec/models/concerns/batch_destroy_dependent_associations_spec.rb
index c16b245bea8..e5392fe0462 100644
--- a/spec/models/concerns/batch_destroy_dependent_associations_spec.rb
+++ b/spec/models/concerns/batch_destroy_dependent_associations_spec.rb
@@ -4,8 +4,8 @@ describe BatchDestroyDependentAssociations do
class TestProject < ActiveRecord::Base
self.table_name = 'projects'
- has_many :builds, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
+ has_many :builds, dependent: :destroy
+ has_many :notification_settings, as: :source, dependent: :delete_all
has_many :pages_domains
has_many :todos
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 2d75422ee68..da26d802688 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -370,4 +370,20 @@ describe CacheMarkdownField do
end
end
end
+
+ describe CacheMarkdownField::MarkdownEngine do
+ subject { lambda { |version| CacheMarkdownField::MarkdownEngine.from_version(version) } }
+
+ it 'returns :common_mark as a default' do
+ expect(subject.call(nil)).to eq :common_mark
+ end
+
+ it 'returns :common_mark' do
+ expect(subject.call(CacheMarkdownField::CACHE_COMMONMARK_VERSION)).to eq :common_mark
+ end
+
+ it 'returns :redcarpet' do
+ expect(subject.call(CacheMarkdownField::CACHE_REDCARPET_VERSION)).to eq :redcarpet
+ end
+ end
end
diff --git a/spec/models/concerns/cacheable_attributes_spec.rb b/spec/models/concerns/cacheable_attributes_spec.rb
index c6331c5ec15..f8c2e29fadd 100644
--- a/spec/models/concerns/cacheable_attributes_spec.rb
+++ b/spec/models/concerns/cacheable_attributes_spec.rb
@@ -52,7 +52,7 @@ describe CacheableAttributes do
describe '.cache_key' do
it 'excludes cache attributes' do
- expect(minimal_test_class.cache_key).to eq("TestClass:#{Gitlab::VERSION}:#{Gitlab.migrations_hash}:#{Rails.version}")
+ expect(minimal_test_class.cache_key).to eq("TestClass:#{Gitlab::VERSION}:#{Rails.version}")
end
end
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index c73ea6aa94c..a9b237fa9ea 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -136,7 +136,7 @@ describe Issue, "Mentionable" do
expect(SystemNoteService).not_to receive(:cross_reference)
- issue.update_attributes(description: 'New description')
+ issue.update(description: 'New description')
issue.create_new_cross_references!
end
@@ -145,7 +145,7 @@ describe Issue, "Mentionable" do
expect(SystemNoteService).to receive(:cross_reference).with(issues[1], any_args)
- issue.update_attributes(description: issues[1].to_reference)
+ issue.update(description: issues[1].to_reference)
issue.create_new_cross_references!
end
@@ -155,7 +155,7 @@ describe Issue, "Mentionable" do
expect(SystemNoteService).to receive(:cross_reference).with(issues[1], any_args)
- note.update_attributes(note: issues[1].to_reference)
+ note.update(note: issues[1].to_reference)
note.create_new_cross_references!
end
end
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index 8cb50d7465c..ed3e28fbeca 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -29,7 +29,7 @@ describe Group, 'Routable' do
end
it 'updates route record on path change' do
- group.update_attributes(path: 'wow', name: 'much')
+ group.update(path: 'wow', name: 'much')
expect(group.route.path).to eq('wow')
expect(group.route.name).to eq('much')
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 25d6597084c..4bded9efe91 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -562,7 +562,7 @@ describe Environment do
it "is not regenerated if name changes" do
original_slug = environment.slug
- environment.update_attributes!(name: environment.name.reverse)
+ environment.update!(name: environment.name.reverse)
expect(environment.slug).to eq(original_slug)
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 9fe1186a8c9..aeec485358e 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -617,7 +617,7 @@ describe Group do
expect(group).to receive(:system_hook_service).and_return(system_hook_service)
expect(system_hook_service).to receive(:execute_hooks_for).with(group, :rename)
- group.update_attributes!(path: new_path)
+ group.update!(path: new_path)
end
end
@@ -625,7 +625,7 @@ describe Group do
it 'does not trigger system hook' do
expect(group).not_to receive(:system_hook_service)
- group.update_attributes!(name: 'new name')
+ group.update!(name: 'new name')
end
end
end
diff --git a/spec/models/import_export_upload_spec.rb b/spec/models/import_export_upload_spec.rb
new file mode 100644
index 00000000000..58af84b8a08
--- /dev/null
+++ b/spec/models/import_export_upload_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe ImportExportUpload do
+ subject { described_class.new(project: create(:project)) }
+
+ shared_examples 'stores the Import/Export file' do |method|
+ it 'stores the import file' do
+ subject.public_send("#{method}=", fixture_file_upload('spec/fixtures/project_export.tar.gz'))
+
+ subject.save!
+
+ url = "/uploads/-/system/import_export_upload/#{method}/#{subject.id}/project_export.tar.gz"
+
+ expect(subject.public_send(method).url).to eq(url)
+ end
+ end
+
+ context 'import' do
+ it_behaves_like 'stores the Import/Export file', :import_file
+ end
+
+ context 'export' do
+ it_behaves_like 'stores the Import/Export file', :export_file
+ end
+end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 8c6b411ec9a..c7eead2bdaa 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1891,7 +1891,7 @@ describe MergeRequest do
end
it 'returns false if the merge request is merged' do
- merge_request.update_attributes(state: 'merged')
+ merge_request.update(state: 'merged')
expect(merge_request.reload.reopenable?).to be_falsey
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 70f1a1c8b38..c1b385aaf76 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -200,7 +200,7 @@ describe Namespace do
end
it "moves dir if path changed" do
- namespace.update_attributes(path: namespace.full_path + '_new')
+ namespace.update(path: namespace.full_path + '_new')
expect(gitlab_shell.exists?(project.repository_storage, "#{namespace.path}/#{project.path}.git")).to be_truthy
end
@@ -279,7 +279,7 @@ describe Namespace do
it "repository directory remains unchanged if path changed" do
before_disk_path = project.disk_path
- namespace.update_attributes(path: namespace.full_path + '_new')
+ namespace.update(path: namespace.full_path + '_new')
expect(before_disk_path).to eq(project.disk_path)
expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_truthy
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index 12681a147b4..d7c5f26ab67 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -58,7 +58,7 @@ RSpec.describe NotificationSetting do
1.upto(4) do |i|
setting = create(:notification_setting, user: user)
- setting.project.update_attributes(pending_delete: true) if i.even?
+ setting.project.update(pending_delete: true) if i.even?
end
end
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index 63c6fbda3f2..cd7f77024da 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -77,7 +77,7 @@ describe ProjectFeature do
context 'repository related features' do
before do
- project.project_feature.update_attributes(
+ project.project_feature.update(
merge_requests_access_level: ProjectFeature::DISABLED,
builds_access_level: ProjectFeature::DISABLED,
repository_access_level: ProjectFeature::PRIVATE
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index c3aa6cd6fed..bbf37ca59b9 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -567,15 +567,15 @@ describe Project do
end
it 'returns the most recent timestamp' do
- project.update_attributes(updated_at: nil,
- last_activity_at: timestamp,
- last_repository_updated_at: timestamp - 1.hour)
+ project.update(updated_at: nil,
+ last_activity_at: timestamp,
+ last_repository_updated_at: timestamp - 1.hour)
expect(project.last_activity_date).to be_like_time(timestamp)
- project.update_attributes(updated_at: timestamp,
- last_activity_at: timestamp - 1.hour,
- last_repository_updated_at: nil)
+ project.update(updated_at: timestamp,
+ last_activity_at: timestamp - 1.hour,
+ last_repository_updated_at: nil)
expect(project.last_activity_date).to be_like_time(timestamp)
end
@@ -1768,7 +1768,7 @@ describe Project do
it 'resets project import_error' do
error_message = 'Some error'
mirror = create(:project_empty_repo, :import_started)
- mirror.import_state.update_attributes(last_error: error_message)
+ mirror.import_state.update(last_error: error_message)
expect { mirror.import_finish }.to change { mirror.import_error }.from(error_message).to(nil)
end
@@ -1929,7 +1929,7 @@ describe Project do
end
it 'returns false when remote mirror is disabled' do
- project.remote_mirrors.first.update_attributes(enabled: false)
+ project.remote_mirrors.first.update(enabled: false)
is_expected.to be_falsy
end
@@ -1959,7 +1959,7 @@ describe Project do
end
it 'does not sync disabled remote mirrors' do
- project.remote_mirrors.first.update_attributes(enabled: false)
+ project.remote_mirrors.first.update(enabled: false)
expect_any_instance_of(RemoteMirror).not_to receive(:sync)
@@ -2782,6 +2782,10 @@ describe Project do
let(:legacy_project) { create(:project, :legacy_storage, :with_export) }
let(:project) { create(:project, :with_export) }
+ before do
+ stub_feature_flags(import_export_object_storage: false)
+ end
+
it 'removes the exports directory for the project' do
expect(File.exist?(project.export_path)).to be_truthy
@@ -2830,12 +2834,14 @@ describe Project do
let(:project) { create(:project, :with_export) }
it 'removes the exported project file' do
+ stub_feature_flags(import_export_object_storage: false)
+
exported_file = project.export_project_path
expect(File.exist?(exported_file)).to be_truthy
- allow(FileUtils).to receive(:rm_f).and_call_original
- expect(FileUtils).to receive(:rm_f).with(exported_file).and_call_original
+ allow(FileUtils).to receive(:rm_rf).and_call_original
+ expect(FileUtils).to receive(:rm_rf).with(exported_file).and_call_original
project.remove_exported_project_file
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
index 3597b080021..c2ef0435c8e 100644
--- a/spec/models/remote_mirror_spec.rb
+++ b/spec/models/remote_mirror_spec.rb
@@ -85,7 +85,7 @@ describe RemoteMirror do
expect(RepositoryRemoveRemoteWorker).to receive(:perform_async).with(mirror.project.id, mirror.remote_name).and_call_original
- mirror.update_attributes(url: 'http://test.com')
+ mirror.update(url: 'http://test.com')
end
end
end
@@ -167,7 +167,7 @@ describe RemoteMirror do
context 'with remote mirroring disabled' do
it 'returns nil' do
- remote_mirror.update_attributes(enabled: false)
+ remote_mirror.update(enabled: false)
expect(remote_mirror.sync).to be_nil
end
@@ -229,7 +229,7 @@ describe RemoteMirror do
end
before do
- remote_mirror.update_attributes(last_update_started_at: Time.now)
+ remote_mirror.update(last_update_started_at: Time.now)
end
context 'when remote mirror does not have status failed' do
@@ -244,7 +244,7 @@ describe RemoteMirror do
context 'when remote mirror has status failed' do
it 'returns false when last update started after the timestamp' do
- remote_mirror.update_attributes(update_status: 'failed')
+ remote_mirror.update(update_status: 'failed')
expect(remote_mirror.updated_since?(timestamp)).to be false
end
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index 01238a89a81..48799781b87 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -29,12 +29,12 @@ describe Route do
context 'after update' do
it 'calls #create_redirect_for_old_path' do
expect(route).to receive(:create_redirect_for_old_path)
- route.update_attributes(path: 'foo')
+ route.update(path: 'foo')
end
it 'calls #delete_conflicting_redirects' do
expect(route).to receive(:delete_conflicting_redirects)
- route.update_attributes(path: 'foo')
+ route.update(path: 'foo')
end
end
@@ -70,7 +70,7 @@ describe Route do
context 'path update' do
context 'when route name is set' do
before do
- route.update_attributes(path: 'bar')
+ route.update(path: 'bar')
end
it 'updates children routes with new path' do
@@ -89,7 +89,7 @@ describe Route do
end
it "does not fail" do
- expect(route.update_attributes(path: 'bar')).to be_truthy
+ expect(route.update(path: 'bar')).to be_truthy
end
end
@@ -100,7 +100,7 @@ describe Route do
let!(:conflicting_redirect3) { route.create_redirect('gitlab-org') }
it 'deletes the conflicting redirects' do
- route.update_attributes(path: 'bar')
+ route.update(path: 'bar')
expect(RedirectRoute.exists?(path: 'bar/test')).to be_falsey
expect(RedirectRoute.exists?(path: 'bar/test/foo')).to be_falsey
@@ -111,7 +111,7 @@ describe Route do
context 'name update' do
it 'updates children routes with new path' do
- route.update_attributes(name: 'bar')
+ route.update(name: 'bar')
expect(described_class.exists?(name: 'bar')).to be_truthy
expect(described_class.exists?(name: 'bar / test')).to be_truthy
@@ -123,7 +123,7 @@ describe Route do
# Note: using `update_columns` to skip all validation and callbacks
route.update_columns(name: nil)
- expect { route.update_attributes(name: 'bar') }
+ expect { route.update(name: 'bar') }
.to change { route.name }.from(nil).to('bar')
end
end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index a849af062c5..029ad7f3e9f 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -280,7 +280,7 @@ describe Service do
service.save!
expect do
- service.update_attributes(active: false)
+ service.update(active: false)
end.to change { service.project.has_external_issue_tracker }.from(true).to(false)
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 097144d04ce..6d7b733dd4f 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -383,7 +383,7 @@ describe User do
let(:secondary) { create(:email, :confirmed, email: 'secondary@example.com', user: user) }
it 'allows a verfied secondary email to be used as the primary without needing reconfirmation' do
- user.update_attributes!(email: secondary.email)
+ user.update!(email: secondary.email)
user.reload
expect(user.email).to eq secondary.email
expect(user.unconfirmed_email).to eq nil
@@ -405,11 +405,11 @@ describe User do
it 'gets called when email updated' do
expect(@user).to receive(:update_emails_with_primary_email)
- @user.update_attributes!(email: 'new_primary@example.com')
+ @user.update!(email: 'new_primary@example.com')
end
it 'adds old primary to secondary emails when secondary is a new email ' do
- @user.update_attributes!(email: 'new_primary@example.com')
+ @user.update!(email: 'new_primary@example.com')
@user.reload
expect(@user.emails.count).to eq 2
@@ -417,7 +417,7 @@ describe User do
end
it 'adds old primary to secondary emails if secondary is becoming a primary' do
- @user.update_attributes!(email: @secondary.email)
+ @user.update!(email: @secondary.email)
@user.reload
expect(@user.emails.count).to eq 1
@@ -425,7 +425,7 @@ describe User do
end
it 'transfers old confirmation values into new secondary' do
- @user.update_attributes!(email: @secondary.email)
+ @user.update!(email: @secondary.email)
@user.reload
expect(@user.emails.count).to eq 1
@@ -494,12 +494,12 @@ describe User do
it 'does nothing when the name is updated' do
expect(user).not_to receive(:update_invalid_gpg_signatures)
- user.update_attributes!(name: 'Bette')
+ user.update!(name: 'Bette')
end
it 'synchronizes the gpg keys when the email is updated' do
expect(user).to receive(:update_invalid_gpg_signatures).at_most(:twice)
- user.update_attributes!(email: 'shawnee.ritchie@denesik.com')
+ user.update!(email: 'shawnee.ritchie@denesik.com')
end
end
end
@@ -617,13 +617,13 @@ describe User do
it 'receives callback when external changes' do
expect(user).to receive(:ensure_user_rights_and_limits)
- user.update_attributes(external: false)
+ user.update(external: false)
end
it 'ensures correct rights and limits for user' do
stub_config_setting(default_can_create_group: true)
- expect { user.update_attributes(external: false) }.to change { user.can_create_group }.to(true)
+ expect { user.update(external: false) }.to change { user.can_create_group }.to(true)
.and change { user.projects_limit }.to(Gitlab::CurrentSettings.default_projects_limit)
end
end
@@ -634,11 +634,11 @@ describe User do
it 'receives callback when external changes' do
expect(user).to receive(:ensure_user_rights_and_limits)
- user.update_attributes(external: true)
+ user.update(external: true)
end
it 'ensures correct rights and limits for user' do
- expect { user.update_attributes(external: true) }.to change { user.can_create_group }.to(false)
+ expect { user.update(external: true) }.to change { user.can_create_group }.to(false)
.and change { user.projects_limit }.to(0)
end
end
@@ -2461,18 +2461,20 @@ describe User do
it 'changes the namespace (just to compare to when username is not changed)' do
expect do
- user.update_attributes!(username: new_username)
+ Timecop.freeze(1.second.from_now) do
+ user.update!(username: new_username)
+ end
end.to change { user.namespace.updated_at }
end
it 'updates the namespace name' do
- user.update_attributes!(username: new_username)
+ user.update!(username: new_username)
expect(user.namespace.name).to eq(new_username)
end
it 'updates the namespace path' do
- user.update_attributes!(username: new_username)
+ user.update!(username: new_username)
expect(user.namespace.path).to eq(new_username)
end
@@ -2481,12 +2483,12 @@ describe User do
let!(:conflicting_namespace) { create(:group, path: new_username) }
it 'causes the user save to fail' do
- expect(user.update_attributes(username: new_username)).to be_falsey
+ expect(user.update(username: new_username)).to be_falsey
expect(user.namespace.errors.messages[:path].first).to eq('has already been taken')
end
it 'adds the namespace errors to the user' do
- user.update_attributes(username: new_username)
+ user.update(username: new_username)
expect(user.errors.full_messages.first).to eq('Username has already been taken')
end
@@ -2496,7 +2498,7 @@ describe User do
context 'when the username is not changed' do
it 'does not change the namespace' do
expect do
- user.update_attributes!(email: 'asdf@asdf.com')
+ user.update!(email: 'asdf@asdf.com')
end.not_to change { user.namespace.updated_at }
end
end
@@ -2526,7 +2528,7 @@ describe User do
expect(system_hook_service).to receive(:execute_hooks_for).with(user, :rename)
expect(user).to receive(:system_hook_service).and_return(system_hook_service)
- user.update_attributes!(username: new_username)
+ user.update!(username: new_username)
end
end
@@ -2534,7 +2536,7 @@ describe User do
it 'does not trigger system hook' do
expect(user).not_to receive(:system_hook_service)
- user.update_attributes!(email: 'asdf@asdf.com')
+ user.update!(email: 'asdf@asdf.com')
end
end
end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 9b5c290b9f9..d6d340bd806 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -9,7 +9,11 @@ describe GroupPolicy do
let(:admin) { create(:admin) }
let(:group) { create(:group, :private) }
- let(:guest_permissions) { [:read_label, :read_group, :upload_file, :read_namespace] }
+ let(:guest_permissions) do
+ [:read_label, :read_group, :upload_file, :read_namespace, :read_group_activity,
+ :read_group_issues, :read_group_boards, :read_group_labels, :read_group_milestones,
+ :read_group_merge_requests]
+ end
let(:reporter_permissions) { [:admin_label] }
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index 24389f28b21..ffca7ab8e45 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -88,7 +88,7 @@ describe API::AccessRequests do
context 'when authenticated as a stranger' do
context "when access request is disabled for the #{source_type}" do
before do
- source.update_attributes(request_access_enabled: false)
+ source.update(request_access_enabled: false)
end
it 'returns 403' do
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index d8a51f36dba..0a789d58fd8 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -201,7 +201,7 @@ describe API::Helpers do
end
it 'does not allow expired tokens' do
- personal_access_token.update_attributes!(expires_at: 1.day.ago)
+ personal_access_token.update!(expires_at: 1.day.ago)
env[Gitlab::Auth::UserAuthFinders::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect { current_user }.to raise_error Gitlab::Auth::ExpiredError
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index eba39bb6ccc..1716d182782 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -306,6 +306,14 @@ describe API::MergeRequests do
expect(json_response['changes_count']).to eq(merge_request.merge_request_diff.real_size)
end
+ it 'exposes description and title html when render_html is true' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), render_html: true
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response).to include('title_html', 'description_html')
+ end
+
context 'merge_request_metrics' do
before do
merge_request.metrics.update!(merged_by: user,
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index dd568c24c72..f4cf5bd0530 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -71,7 +71,7 @@ describe API::Notes do
context "issue is confidential" do
before do
- ext_issue.update_attributes(confidential: true)
+ ext_issue.update(confidential: true)
end
it "returns 404" do
@@ -104,7 +104,7 @@ describe API::Notes do
context "when issue is confidential" do
before do
- issue.update_attributes(confidential: true)
+ issue.update(confidential: true)
end
it "returns 404" do
diff --git a/spec/requests/api/pipeline_schedules_spec.rb b/spec/requests/api/pipeline_schedules_spec.rb
index 91d4d5d3de9..173f2a0dea0 100644
--- a/spec/requests/api/pipeline_schedules_spec.rb
+++ b/spec/requests/api/pipeline_schedules_spec.rb
@@ -22,7 +22,7 @@ describe API::PipelineSchedules do
.each do |pipeline_schedule|
create(:user).tap do |user|
project.add_developer(user)
- pipeline_schedule.update_attributes(owner: user)
+ pipeline_schedule.update(owner: user)
end
pipeline_schedule.pipelines << build(:ci_pipeline, project: project)
end
diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb
index 3834d27d0a9..a4615bd081f 100644
--- a/spec/requests/api/project_export_spec.rb
+++ b/spec/requests/api/project_export_spec.rb
@@ -192,6 +192,13 @@ describe API::ProjectExport do
context 'when upload complete' do
before do
FileUtils.rm_rf(project_after_export.export_path)
+
+ if project_after_export.export_project_object_exists?
+ upload = project_after_export.import_export_upload
+
+ upload.remove_export_file!
+ upload.save
+ end
end
it_behaves_like '404 response' do
@@ -261,6 +268,22 @@ describe API::ProjectExport do
it_behaves_like 'get project export download not found'
end
end
+
+ context 'when an uploader is used' do
+ before do
+ stub_uploads_object_storage(ImportExportUploader)
+
+ [project, project_finished, project_after_export].each do |p|
+ p.add_master(user)
+
+ upload = ImportExportUpload.new(project: p)
+ upload.export_file = fixture_file_upload('spec/fixtures/project_export.tar.gz', "`/tar.gz")
+ upload.save!
+ end
+ end
+
+ it_behaves_like 'get project download by strategy'
+ end
end
describe 'POST /projects/:project_id/export' do
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index 97dffdc9233..24b7835abf5 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -157,7 +157,7 @@ describe API::ProjectImport do
it 'returns the import status and the error if failed' do
project = create(:project, :import_failed)
project.add_master(user)
- project.import_state.update_attributes(last_error: 'error')
+ project.import_state.update(last_error: 'error')
get api("/projects/#{project.id}/import", user)
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index abf9ad738bd..de540ba7a10 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -312,7 +312,7 @@ describe API::Projects do
before do
project_member
- user3.update_attributes(starred_projects: [project, project2, project3, public_project])
+ user3.update(starred_projects: [project, project2, project3, public_project])
end
it 'returns the starred projects viewable by the user' do
@@ -333,7 +333,7 @@ describe API::Projects do
let!(:project9) { create(:project, :public, path: 'gitlab9') }
before do
- user.update_attributes(starred_projects: [project5, project7, project8, project9])
+ user.update(starred_projects: [project5, project7, project8, project9])
end
context 'including owned filter' do
@@ -1451,7 +1451,7 @@ describe API::Projects do
end
it 'updates visibility_level from public to private' do
- project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
+ project3.update({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
project_param = { visibility: 'private' }
put api("/projects/#{project3.id}", user), project_param
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index e7639599874..d57993ab454 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -851,6 +851,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
it 'does not update job status and job trace' do
update_job(state: 'success', trace: 'BUILD TRACE UPDATED')
+ job.reload
expect(response).to have_gitlab_http_status(403)
expect(response.header['Job-Status']).to eq 'failed'
expect(job.trace.raw).to eq 'Job failed'
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index 969710d6613..8df08dd1818 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -109,7 +109,7 @@ describe API::Tags do
before do
release = project.releases.find_or_initialize_by(tag: tag_name)
- release.update_attributes(description: description)
+ release.update(description: description)
end
it 'returns an array of project tags with release info' do
@@ -400,7 +400,7 @@ describe API::Tags do
context 'on tag with existing release' do
before do
release = project.releases.find_or_initialize_by(tag: tag_name)
- release.update_attributes(description: description)
+ release.update(description: description)
end
it 'returns 409 if there is already a release' do
@@ -422,7 +422,7 @@ describe API::Tags do
context 'on tag with existing release' do
before do
release = project.releases.find_or_initialize_by(tag: tag_name)
- release.update_attributes(description: description)
+ release.update(description: description)
end
it 'updates the release description' do
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index decb5d22f59..ecf5d849d3f 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -49,7 +49,7 @@ describe Ci::RetryBuildService do
# Make sure that build has both `stage_id` and `stage` because FactoryBot
# can reset one of the fields when assigning another. We plan to deprecate
# and remove legacy `stage` column in the future.
- build.update_attributes(stage: 'test', stage_id: stage.id)
+ build.update(stage: 'test', stage_id: stage.id)
end
describe 'clone accessors' do
@@ -100,7 +100,11 @@ describe Ci::RetryBuildService do
end
describe '#execute' do
- let(:new_build) { service.execute(build) }
+ let(:new_build) do
+ Timecop.freeze(1.second.from_now) do
+ service.execute(build)
+ end
+ end
context 'when user has ability to execute build' do
before do
@@ -150,7 +154,11 @@ describe Ci::RetryBuildService do
end
describe '#reprocess' do
- let(:new_build) { service.reprocess!(build) }
+ let(:new_build) do
+ Timecop.freeze(1.second.from_now) do
+ service.reprocess!(build)
+ end
+ end
context 'when user has ability to execute build' do
before do
diff --git a/spec/services/import_export_clean_up_service_spec.rb b/spec/services/import_export_clean_up_service_spec.rb
index 1875d0448cd..d5fcef1246f 100644
--- a/spec/services/import_export_clean_up_service_spec.rb
+++ b/spec/services/import_export_clean_up_service_spec.rb
@@ -11,7 +11,6 @@ describe ImportExportCleanUpService do
path = '/invalid/path/'
stub_repository_downloads_path(path)
- expect(File).to receive(:directory?).with(path + tmp_import_export_folder).and_return(false).at_least(:once)
expect(service).not_to receive(:clean_up_export_files)
service.execute
@@ -38,6 +37,24 @@ describe ImportExportCleanUpService do
end
end
+ context 'with uploader exports' do
+ it 'removes old files' do
+ upload = create(:import_export_upload,
+ updated_at: 2.days.ago,
+ export_file: fixture_file_upload('spec/fixtures/project_export.tar.gz'))
+
+ expect { service.execute }.to change { upload.reload.export_file.file.nil? }.to(true)
+ end
+
+ it 'does not remove new files' do
+ upload = create(:import_export_upload,
+ updated_at: 1.hour.ago,
+ export_file: fixture_file_upload('spec/fixtures/project_export.tar.gz'))
+
+ expect { service.execute }.not_to change { upload.reload.export_file.file.nil? }
+ end
+ end
+
def in_directory_with_files(mtime:)
Dir.mktmpdir do |tmpdir|
stub_repository_downloads_path(tmpdir)
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 158541d36e3..4cbf1a52e29 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -501,7 +501,7 @@ describe Issues::UpdateService, :mailer do
let(:params) { { label_ids: [], remove_label_ids: [label.id] } }
before do
- issue.update_attributes(labels: [label, label3])
+ issue.update(labels: [label, label3])
end
it 'ignores the label_ids parameter' do
@@ -517,7 +517,7 @@ describe Issues::UpdateService, :mailer do
let(:params) { { add_label_ids: [label3.id], remove_label_ids: [label.id] } }
before do
- issue.update_attributes(labels: [label])
+ issue.update(labels: [label])
end
it 'adds the passed labels' do
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index 36b6e5a701e..b4e9f6af43a 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -142,8 +142,8 @@ describe Members::DestroyService do
context 'with an access requester' do
before do
- group_project.update_attributes(request_access_enabled: true)
- group.update_attributes(request_access_enabled: true)
+ group_project.update(request_access_enabled: true)
+ group.update(request_access_enabled: true)
group_project.request_access(member_user)
group.request_access(member_user)
end
diff --git a/spec/services/merge_requests/conflicts/list_service_spec.rb b/spec/services/merge_requests/conflicts/list_service_spec.rb
index d57852615d9..a5520e7373e 100644
--- a/spec/services/merge_requests/conflicts/list_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/list_service_spec.rb
@@ -34,7 +34,7 @@ describe MergeRequests::Conflicts::ListService do
it 'returns a falsey value when the MR does not support new diff notes' do
merge_request = create_merge_request('conflict-resolvable')
- merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
+ merge_request.merge_request_diff.update(start_commit_sha: nil)
expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey
end
@@ -84,23 +84,5 @@ describe MergeRequests::Conflicts::ListService do
expect(service.can_be_resolved_in_ui?).to be_falsey
end
-
- context 'with gitaly disabled', :skip_gitaly_mock do
- it 'returns a falsey value when the MR has a missing ref after a force push' do
- merge_request = create_merge_request('conflict-resolvable')
- service = conflicts_service(merge_request)
- allow_any_instance_of(Rugged::Repository).to receive(:merge_commits).and_raise(Rugged::OdbError)
-
- expect(service.can_be_resolved_in_ui?).to be_falsey
- end
-
- it 'returns a falsey value when the MR has a missing revision after a force push' do
- merge_request = create_merge_request('conflict-resolvable')
- service = conflicts_service(merge_request)
- allow(merge_request).to receive_message_chain(:target_branch_head, :raw, :id).and_return(Gitlab::Git::BLANK_SHA)
-
- expect(service.can_be_resolved_in_ui?).to be_falsey
- end
- end
end
end
diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
index cff09237005..7edf8a96c94 100644
--- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
@@ -123,17 +123,6 @@ describe MergeRequests::Conflicts::ResolveService do
expect(merge_request_from_fork.source_branch_head.parents.map(&:id))
.to eq(['404fa3fc7c2c9b5dacff102f353bdf55b1be2813', target_head])
end
-
- context 'when gitaly is disabled', :skip_gitaly_mock do
- it 'gets conflicts from the source project' do
- # REFACTOR NOTE: We used to test that `project.repository.rugged` wasn't
- # used in this case, but since the refactor, for simplification,
- # we always use that repository for read only operations.
- expect(forked_project.repository.rugged).to receive(:merge_commits).and_call_original
-
- subject
- end
- end
end
end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index ef2738ef504..33eea2cf5c3 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -63,7 +63,7 @@ describe MergeRequests::MergeService do
let(:commit) { double('commit', safe_message: "Fixes #{jira_issue.to_reference}") }
before do
- project.update_attributes!(has_external_issue_tracker: true)
+ project.update!(has_external_issue_tracker: true)
jira_service_settings
stub_jira_urls(jira_issue.id)
allow(merge_request).to receive(:commits).and_return([commit])
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 0eadc83bfe3..ab91176737d 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -1314,7 +1314,7 @@ describe NotificationService, :mailer do
describe 'when merge_when_pipeline_succeeds is true' do
before do
- merge_request.update_attributes(
+ merge_request.update(
merge_when_pipeline_succeeds: true,
merge_user: create(:user)
)
diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb
index 64a9559791f..81dc7c57f4a 100644
--- a/spec/services/preview_markdown_service_spec.rb
+++ b/spec/services/preview_markdown_service_spec.rb
@@ -64,4 +64,16 @@ describe PreviewMarkdownService do
expect(result[:commands]).to eq 'Sets time estimate to 2y.'
end
end
+
+ it 'sets correct markdown engine' do
+ service = described_class.new(project, user, { markdown_version: CacheMarkdownField::CACHE_REDCARPET_VERSION })
+ result = service.execute
+
+ expect(result[:markdown_engine]).to eq :redcarpet
+
+ service = described_class.new(project, user, { markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION })
+ result = service.execute
+
+ expect(result[:markdown_engine]).to eq :common_mark
+ end
end
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
index 6fd73a50511..e98df375d48 100644
--- a/spec/services/projects/autocomplete_service_spec.rb
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -131,4 +131,58 @@ describe Projects::AutocompleteService do
end
end
end
+
+ describe '#labels_as_hash' do
+ def expect_labels_to_equal(labels, expected_labels)
+ expect(labels.size).to eq(expected_labels.size)
+ extract_title = lambda { |label| label['title'] }
+ expect(labels.map(&extract_title)).to eq(expected_labels.map(&extract_title))
+ end
+
+ let(:user) { create(:user) }
+ let(:group) { create(:group, :nested) }
+ let!(:sub_group) { create(:group, parent: group) }
+ let(:project) { create(:project, :public, group: group) }
+ let(:issue) { create(:issue, project: project) }
+
+ let!(:label1) { create(:label, project: project) }
+ let!(:label2) { create(:label, project: project) }
+ let!(:sub_group_label) { create(:group_label, group: sub_group) }
+ let!(:parent_group_label) { create(:group_label, group: group.parent) }
+
+ before do
+ create(:group_member, group: group, user: user)
+ end
+
+ it 'returns labels from project and ancestor groups' do
+ service = described_class.new(project, user)
+ results = service.labels_as_hash
+ expected_labels = [label1, label2, parent_group_label]
+
+ expect_labels_to_equal(results, expected_labels)
+ end
+
+ context 'some labels are already assigned' do
+ before do
+ issue.labels << label1
+ end
+
+ it 'marks already assigned as set' do
+ service = described_class.new(project, user)
+ results = service.labels_as_hash(issue)
+ expected_labels = [label1, label2, parent_group_label]
+
+ expect_labels_to_equal(results, expected_labels)
+
+ assigned_label_titles = issue.labels.map(&:title)
+ results.each do |hash|
+ if assigned_label_titles.include?(hash['title'])
+ expect(hash[:set]).to eq(true)
+ else
+ expect(hash.key?(:set)).to eq(false)
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index c15f5120b8a..f89f9b54f53 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -135,7 +135,7 @@ describe Projects::ForkService do
context "when project has restricted visibility level" do
context "and only one visibility level is restricted" do
before do
- @from_project.update_attributes(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ @from_project.update(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL])
end
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index 1bffeee6790..a4c103e6f30 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -24,8 +24,8 @@ describe Projects::UpdatePagesService do
let(:extension) { 'zip' }
before do
- build.update_attributes(legacy_artifacts_file: file)
- build.update_attributes(legacy_artifacts_metadata: metadata)
+ build.update(legacy_artifacts_file: file)
+ build.update(legacy_artifacts_metadata: metadata)
end
describe 'pages artifacts' do
@@ -62,13 +62,13 @@ describe Projects::UpdatePagesService do
end
it 'fails if sha on branch is not latest' do
- build.update_attributes(ref: 'feature')
+ build.update(ref: 'feature')
expect(execute).not_to eq(:success)
end
it 'fails for empty file fails' do
- build.update_attributes(legacy_artifacts_file: empty_file)
+ build.update(legacy_artifacts_file: empty_file)
expect { execute }
.to raise_error(Projects::UpdatePagesService::FailedToExtractError)
@@ -118,7 +118,7 @@ describe Projects::UpdatePagesService do
end
it 'fails if sha on branch is not latest' do
- build.update_attributes(ref: 'feature')
+ build.update(ref: 'feature')
expect(execute).not_to eq(:success)
end
@@ -188,7 +188,7 @@ describe Projects::UpdatePagesService do
end
it 'fails for invalid archive' do
- build.update_attributes(legacy_artifacts_file: invalid_file)
+ build.update(legacy_artifacts_file: invalid_file)
expect(execute).not_to eq(:success)
end
@@ -199,8 +199,8 @@ describe Projects::UpdatePagesService do
file = fixture_file_upload('spec/fixtures/pages.zip')
metafile = fixture_file_upload('spec/fixtures/pages.zip.meta')
- build.update_attributes(legacy_artifacts_file: file)
- build.update_attributes(legacy_artifacts_metadata: metafile)
+ build.update(legacy_artifacts_file: file)
+ build.update(legacy_artifacts_metadata: metafile)
allow(build).to receive(:artifacts_metadata_entry)
.and_return(metadata)
diff --git a/spec/services/reset_project_cache_service_spec.rb b/spec/services/reset_project_cache_service_spec.rb
index de475d16586..1490ad5fe3b 100644
--- a/spec/services/reset_project_cache_service_spec.rb
+++ b/spec/services/reset_project_cache_service_spec.rb
@@ -18,7 +18,7 @@ describe ResetProjectCacheService do
context 'when project cache_index is a numeric value' do
before do
- project.update_attributes(jobs_cache_index: 1)
+ project.update(jobs_cache_index: 1)
end
it 'increments project cache index' do
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index 51396d34f8f..e0335880e8e 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -75,7 +75,7 @@ describe SystemHooksService do
end
it 'handles nil datetime columns' do
- user.update_attributes(created_at: nil, updated_at: nil)
+ user.update(created_at: nil, updated_at: nil)
data = event_data(user, :destroy)
expect(data[:created_at]).to be(nil)
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index fdce8e84620..bd564cc60a6 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -4,7 +4,7 @@ SimpleCovEnv.start!
ENV["RAILS_ENV"] = 'test'
ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true'
-require File.expand_path("../../config/environment", __FILE__)
+require File.expand_path('../config/environment', __dir__)
require 'rspec/rails'
require 'shoulda/matchers'
require 'rspec/retry'
@@ -170,6 +170,17 @@ RSpec.configure do |config|
redis_queues_cleanup!
end
+ config.around(:each, :use_clean_rails_memory_store_fragment_caching) do |example|
+ caching_store = ActionController::Base.cache_store
+ ActionController::Base.cache_store = ActiveSupport::Cache::MemoryStore.new
+ ActionController::Base.perform_caching = true
+
+ example.run
+
+ ActionController::Base.perform_caching = false
+ ActionController::Base.cache_store = caching_store
+ end
+
# The :each scope runs "inside" the example, so this hook ensures the DB is in the
# correct state before any examples' before hooks are called. This prevents a
# problem where `ScheduleIssuesClosedAtTypeChange` (or any migration that depends
diff --git a/spec/support/api/repositories_shared_context.rb b/spec/support/api/repositories_shared_context.rb
index ea38fe4f5b8..f1341804e56 100644
--- a/spec/support/api/repositories_shared_context.rb
+++ b/spec/support/api/repositories_shared_context.rb
@@ -1,6 +1,6 @@
shared_context 'disabled repository' do
before do
- project.project_feature.update_attributes!(
+ project.project_feature.update!(
repository_access_level: ProjectFeature::DISABLED,
merge_requests_access_level: ProjectFeature::DISABLED,
builds_access_level: ProjectFeature::DISABLED
diff --git a/spec/support/api/time_tracking_shared_examples.rb b/spec/support/api/time_tracking_shared_examples.rb
index 52e1bc55191..fee464c15a3 100644
--- a/spec/support/api/time_tracking_shared_examples.rb
+++ b/spec/support/api/time_tracking_shared_examples.rb
@@ -85,7 +85,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
it 'subtracts time of the total spent time' do
Timecop.travel(1.minute.from_now) do
expect do
- issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id })
+ issuable.update!(spend_time: { duration: 7200, user_id: user.id })
end.to change { issuable.reload.updated_at }
end
@@ -99,7 +99,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
context 'when time to subtract is greater than the total spent time' do
it 'does not modify the total time spent' do
- issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id })
+ issuable.update!(spend_time: { duration: 7200, user_id: user.id })
Timecop.travel(1.minute.from_now) do
expect do
@@ -135,8 +135,8 @@ shared_examples 'time tracking endpoints' do |issuable_name|
describe "GET /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_stats" do
it "returns the time stats for #{issuable_name}" do
- issuable.update_attributes!(spend_time: { duration: 1800, user_id: user.id },
- time_estimate: 3600)
+ issuable.update!(spend_time: { duration: 1800, user_id: user.id },
+ time_estimate: 3600)
get api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_stats", user)
diff --git a/spec/support/generate-seed-repo-rb b/spec/support/generate-seed-repo-rb
index 44b3de23b99..bee9d419376 100755
--- a/spec/support/generate-seed-repo-rb
+++ b/spec/support/generate-seed-repo-rb
@@ -15,7 +15,7 @@
require 'erb'
require 'tempfile'
-SOURCE = File.expand_path('../gitlab-git-test.git', __FILE__).freeze
+SOURCE = File.expand_path('gitlab-git-test.git', __dir__).freeze
SCRIPT_NAME = 'generate-seed-repo-rb'.freeze
REPO_NAME = 'gitlab-git-test.git'.freeze
diff --git a/spec/support/helpers/jira_service_helper.rb b/spec/support/helpers/jira_service_helper.rb
index 88a7aeba461..f4d5343c4ed 100644
--- a/spec/support/helpers/jira_service_helper.rb
+++ b/spec/support/helpers/jira_service_helper.rb
@@ -12,7 +12,7 @@ module JiraServiceHelper
jira_issue_transition_id: '1'
}
- jira_tracker.update_attributes(properties: properties, active: true)
+ jira_tracker.update(properties: properties, active: true)
end
def jira_issue_comments
diff --git a/spec/support/helpers/key_generator_helper.rb b/spec/support/helpers/key_generator_helper.rb
index b1c289ffef7..d55d8312c65 100644
--- a/spec/support/helpers/key_generator_helper.rb
+++ b/spec/support/helpers/key_generator_helper.rb
@@ -24,7 +24,7 @@ module Spec
private
# Encodes an openssh-mpi-encoded integer.
- def encode_mpi(n)
+ def encode_mpi(n) # rubocop:disable Naming/UncommunicativeMethodParamName
chars, n = [], n.to_i
chars << (n & 0xff) && n >>= 8 while n != 0
chars << 0 if chars.empty? || chars.last >= 0x80
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index 471b0a74a19..58b5c6a6435 100644
--- a/spec/support/helpers/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -25,6 +25,11 @@ module StubObjectStorage
::Fog::Storage.new(connection_params).tap do |connection|
begin
connection.directories.create(key: remote_directory)
+
+ # Cleanup remaining files
+ connection.directories.each do |directory|
+ directory.files.map(&:destroy)
+ end
rescue Excon::Error::Conflict
end
end
diff --git a/spec/support/matchers/disallow_request_matchers.rb b/spec/support/matchers/disallow_request_matchers.rb
new file mode 100644
index 00000000000..db4d90e4fd0
--- /dev/null
+++ b/spec/support/matchers/disallow_request_matchers.rb
@@ -0,0 +1,15 @@
+RSpec::Matchers.define :disallow_request do
+ match do |middleware|
+ alert = middleware.env['rack.session'].to_hash
+ .dig('flash', 'flashes', 'alert')
+
+ alert&.include?('You cannot perform write operations')
+ end
+end
+
+RSpec::Matchers.define :disallow_request_in_json do
+ match do |response|
+ json_response = JSON.parse(response.body)
+ response.body.include?('You cannot perform write operations') && json_response.key?('message')
+ end
+end
diff --git a/spec/support/shared_examples/ci_trace_shared_examples.rb b/spec/support/shared_examples/ci_trace_shared_examples.rb
index db723a323f8..94e82b8ce90 100644
--- a/spec/support/shared_examples/ci_trace_shared_examples.rb
+++ b/spec/support/shared_examples/ci_trace_shared_examples.rb
@@ -138,6 +138,28 @@ shared_examples_for 'common trace features' do
end
end
+ describe '#write' do
+ subject { trace.send(:write, mode) { } }
+
+ let(:mode) { 'wb' }
+
+ context 'when arhicved trace does not exist yet' do
+ it 'does not raise an error' do
+ expect { subject }.not_to raise_error
+ end
+ end
+
+ context 'when arhicved trace already exists' do
+ before do
+ create(:ci_job_artifact, :trace, job: build)
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Ci::Trace::AlreadyArchivedError)
+ end
+ end
+ end
+
describe '#set' do
before do
trace.set("12")
@@ -574,7 +596,7 @@ shared_examples_for 'trace with disabled live trace feature' do
it 'does not archive' do
expect_any_instance_of(described_class).not_to receive(:archive_stream!)
- expect { subject }.to raise_error('Already archived')
+ expect { subject }.to raise_error(Gitlab::Ci::Trace::AlreadyArchivedError)
expect(build.job_artifacts_trace.file.exists?).to be_truthy
end
end
@@ -589,6 +611,55 @@ shared_examples_for 'trace with disabled live trace feature' do
end
end
end
+
+ describe '#erase!' do
+ subject { trace.erase! }
+
+ context 'when it is a live trace' do
+ context 'when trace is stored in database' do
+ let(:build) { create(:ci_build) }
+
+ before do
+ build.update_column(:trace, 'sample trace')
+ end
+
+ it { expect(trace.raw).not_to be_nil }
+
+ it "removes trace" do
+ subject
+
+ expect(trace.raw).to be_nil
+ end
+ end
+
+ context 'when trace is stored in file storage' do
+ let(:build) { create(:ci_build, :trace_live) }
+
+ it { expect(trace.raw).not_to be_nil }
+
+ it "removes trace" do
+ subject
+
+ expect(trace.raw).to be_nil
+ end
+ end
+ end
+
+ context 'when it is an archived trace' do
+ let(:build) { create(:ci_build, :trace_artifact) }
+
+ it "has trace at first" do
+ expect(trace.raw).not_to be_nil
+ end
+
+ it "removes trace" do
+ subject
+
+ build.reload
+ expect(trace.raw).to be_nil
+ end
+ end
+ end
end
shared_examples_for 'trace with enabled live trace feature' do
@@ -761,7 +832,7 @@ shared_examples_for 'trace with enabled live trace feature' do
it 'does not archive' do
expect_any_instance_of(described_class).not_to receive(:archive_stream!)
- expect { subject }.to raise_error('Already archived')
+ expect { subject }.to raise_error(Gitlab::Ci::Trace::AlreadyArchivedError)
expect(build.job_artifacts_trace.file.exists?).to be_truthy
end
end
@@ -776,4 +847,35 @@ shared_examples_for 'trace with enabled live trace feature' do
end
end
end
+
+ describe '#erase!' do
+ subject { trace.erase! }
+
+ context 'when it is a live trace' do
+ let(:build) { create(:ci_build, :trace_live) }
+
+ it { expect(trace.raw).not_to be_nil }
+
+ it "removes trace" do
+ subject
+
+ expect(trace.raw).to be_nil
+ end
+ end
+
+ context 'when it is an archived trace' do
+ let(:build) { create(:ci_build, :trace_artifact) }
+
+ it "has trace at first" do
+ expect(trace.raw).not_to be_nil
+ end
+
+ it "removes trace" do
+ subject
+
+ build.reload
+ expect(trace.raw).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/support/shared_examples/helm_generated_script.rb b/spec/support/shared_examples/helm_generated_script.rb
index 56e86a87ab9..05d53a13fd3 100644
--- a/spec/support/shared_examples/helm_generated_script.rb
+++ b/spec/support/shared_examples/helm_generated_script.rb
@@ -6,7 +6,7 @@ shared_examples 'helm commands' do
ALPINE_VERSION=$(cat /etc/alpine-release | cut -d '.' -f 1,2)
echo http://mirror.clarkson.edu/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
echo http://mirror1.hs-esslingen.de/pub/Mirrors/alpine/v$ALPINE_VERSION/main >> /etc/apk/repositories
- apk add -U ca-certificates openssl >/dev/null
+ apk add -U wget ca-certificates openssl >/dev/null
wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
mv /tmp/linux-amd64/helm /usr/bin/
EOS
diff --git a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
index 7c34c7b4977..940c24c8d67 100644
--- a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
@@ -130,7 +130,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
context "event channels" do
it "uses the right channel for push event" do
- chat_service.update_attributes(push_channel: "random")
+ chat_service.update(push_channel: "random")
expect(Slack::Notifier).to receive(:new)
.with(webhook_url, channel: "random")
@@ -142,7 +142,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
end
it "uses the right channel for merge request event" do
- chat_service.update_attributes(merge_request_channel: "random")
+ chat_service.update(merge_request_channel: "random")
expect(Slack::Notifier).to receive(:new)
.with(webhook_url, channel: "random")
@@ -154,7 +154,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
end
it "uses the right channel for issue event" do
- chat_service.update_attributes(issue_channel: "random")
+ chat_service.update(issue_channel: "random")
expect(Slack::Notifier).to receive(:new)
.with(webhook_url, channel: "random")
@@ -169,7 +169,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
let(:issue_service_options) { { title: 'Secret', confidential: true } }
it "uses confidential issue channel" do
- chat_service.update_attributes(confidential_issue_channel: 'confidential')
+ chat_service.update(confidential_issue_channel: 'confidential')
expect(Slack::Notifier).to execute_with_options(channel: 'confidential')
@@ -177,7 +177,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
end
it 'falls back to issue channel' do
- chat_service.update_attributes(issue_channel: 'fallback_channel')
+ chat_service.update(issue_channel: 'fallback_channel')
expect(Slack::Notifier).to execute_with_options(channel: 'fallback_channel')
@@ -186,7 +186,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
end
it "uses the right channel for wiki event" do
- chat_service.update_attributes(wiki_page_channel: "random")
+ chat_service.update(wiki_page_channel: "random")
expect(Slack::Notifier).to receive(:new)
.with(webhook_url, channel: "random")
@@ -203,7 +203,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
end
it "uses the right channel" do
- chat_service.update_attributes(note_channel: "random")
+ chat_service.update(note_channel: "random")
note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
@@ -222,7 +222,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
end
it "uses confidential channel" do
- chat_service.update_attributes(confidential_note_channel: "confidential")
+ chat_service.update(confidential_note_channel: "confidential")
note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
@@ -232,7 +232,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
end
it 'falls back to note channel' do
- chat_service.update_attributes(note_channel: "fallback_channel")
+ chat_service.update(note_channel: "fallback_channel")
note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
diff --git a/spec/uploaders/import_export_uploader_spec.rb b/spec/uploaders/import_export_uploader_spec.rb
new file mode 100644
index 00000000000..51b173b682d
--- /dev/null
+++ b/spec/uploaders/import_export_uploader_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe ImportExportUploader do
+ let(:model) { build_stubbed(:import_export_upload) }
+ let(:upload) { create(:upload, model: model) }
+
+ subject { described_class.new(model, :import_file) }
+
+ context "object_store is REMOTE" do
+ before do
+ stub_uploads_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[import_export_upload/import_file/],
+ upload_path: %r[import_export_upload/import_file/]
+ end
+end
diff --git a/spec/views/projects/imports/new.html.haml_spec.rb b/spec/views/projects/imports/new.html.haml_spec.rb
index 32d73d0c5ab..fc389641fcd 100644
--- a/spec/views/projects/imports/new.html.haml_spec.rb
+++ b/spec/views/projects/imports/new.html.haml_spec.rb
@@ -7,7 +7,7 @@ describe "projects/imports/new.html.haml" do
let(:project) { create(:project_empty_repo, :import_failed, import_type: :gitlab_project, import_source: '/var/opt/gitlab/gitlab-rails/shared/tmp/project_exports/uploads/t.tar.gz', import_url: nil) }
before do
- project.import_state.update_attributes(last_error: '<a href="http://googl.com">Foo</a>')
+ project.import_state.update(last_error: '<a href="http://googl.com">Foo</a>')
sign_in(user)
project.add_master(user)
end
diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb
index 264e0ce0b40..fe6ad26a6f6 100644
--- a/spec/views/projects/merge_requests/show.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb
@@ -52,7 +52,7 @@ describe 'projects/merge_requests/show.html.haml' do
context 'when the merge request is open' do
it 'closes the merge request if the source project does not exist' do
- closed_merge_request.update_attributes(state: 'open')
+ closed_merge_request.update(state: 'open')
forked_project.destroy
# Reload merge request so MergeRequest#source_project turns to `nil`
closed_merge_request.reload
diff --git a/spec/workers/ci/archive_traces_cron_worker_spec.rb b/spec/workers/ci/archive_traces_cron_worker_spec.rb
index 9af51b7d4d8..23f5dda298a 100644
--- a/spec/workers/ci/archive_traces_cron_worker_spec.rb
+++ b/spec/workers/ci/archive_traces_cron_worker_spec.rb
@@ -25,24 +25,36 @@ describe Ci::ArchiveTracesCronWorker do
end
end
- context 'when a job was succeeded' do
+ context 'when a job succeeded' do
let!(:build) { create(:ci_build, :success, :trace_live) }
it_behaves_like 'archives trace'
- context 'when archive raised an exception' do
- let!(:build) { create(:ci_build, :success, :trace_artifact, :trace_live) }
+ context 'when a trace had already been archived' do
+ let!(:build) { create(:ci_build, :success, :trace_live, :trace_artifact) }
let!(:build2) { create(:ci_build, :success, :trace_live) }
- it 'archives valid targets' do
- expect(Rails.logger).to receive(:error).with("Failed to archive stale live trace. id: #{build.id} message: Already archived")
-
+ it 'continues to archive live traces' do
subject
build2.reload
expect(build2.job_artifacts_trace).to be_exist
end
end
+
+ context 'when an unexpected exception happened during archiving' do
+ let!(:build) { create(:ci_build, :success, :trace_live) }
+
+ before do
+ allow_any_instance_of(Gitlab::Ci::Trace).to receive(:archive!).and_raise('Unexpected error')
+ end
+
+ it 'puts a log' do
+ expect(Rails.logger).to receive(:error).with("Failed to archive stale live trace. id: #{build.id} message: Unexpected error")
+
+ subject
+ end
+ end
end
context 'when a job was cancelled' do
diff --git a/spec/workers/concerns/waitable_worker_spec.rb b/spec/workers/concerns/waitable_worker_spec.rb
index 199825b5097..ce38cde9208 100644
--- a/spec/workers/concerns/waitable_worker_spec.rb
+++ b/spec/workers/concerns/waitable_worker_spec.rb
@@ -18,8 +18,8 @@ describe WaitableWorker do
def self.bulk_perform_inline(args_list)
end
- def perform(i = 0)
- self.class.counter += i
+ def perform(count = 0)
+ self.class.counter += count
end
end
end
diff --git a/spec/workers/object_storage_upload_worker_spec.rb b/spec/workers/object_storage_upload_worker_spec.rb
deleted file mode 100644
index 32ddcbe9757..00000000000
--- a/spec/workers/object_storage_upload_worker_spec.rb
+++ /dev/null
@@ -1,108 +0,0 @@
-require 'spec_helper'
-
-describe ObjectStorageUploadWorker do
- let(:local) { ObjectStorage::Store::LOCAL }
- let(:remote) { ObjectStorage::Store::REMOTE }
-
- def perform
- described_class.perform_async(uploader_class.name, subject_class, file_field, subject_id)
- end
-
- context 'for LFS' do
- let!(:lfs_object) { create(:lfs_object, :with_file, file_store: local) }
- let(:uploader_class) { LfsObjectUploader }
- let(:subject_class) { LfsObject }
- let(:file_field) { :file }
- let(:subject_id) { lfs_object.id }
-
- context 'when object storage is enabled' do
- before do
- stub_lfs_object_storage(background_upload: true)
- end
-
- it 'uploads object to storage' do
- expect { perform }.to change { lfs_object.reload.file_store }.from(local).to(remote)
- end
-
- context 'when background upload is disabled' do
- before do
- allow(Gitlab.config.lfs.object_store).to receive(:background_upload) { false }
- end
-
- it 'is skipped' do
- expect { perform }.not_to change { lfs_object.reload.file_store }
- end
- end
- end
-
- context 'when object storage is disabled' do
- before do
- stub_lfs_object_storage(enabled: false)
- end
-
- it "doesn't migrate files" do
- perform
-
- expect(lfs_object.reload.file_store).to eq(local)
- end
- end
- end
-
- context 'for legacy artifacts' do
- let(:build) { create(:ci_build, :legacy_artifacts) }
- let(:uploader_class) { LegacyArtifactUploader }
- let(:subject_class) { Ci::Build }
- let(:file_field) { :artifacts_file }
- let(:subject_id) { build.id }
-
- context 'when local storage is used' do
- let(:store) { local }
-
- context 'and remote storage is defined' do
- before do
- stub_artifacts_object_storage(background_upload: true)
- end
-
- it "migrates file to remote storage" do
- perform
-
- expect(build.reload.artifacts_file_store).to eq(remote)
- end
-
- context 'for artifacts_metadata' do
- let(:file_field) { :artifacts_metadata }
-
- it 'migrates metadata to remote storage' do
- perform
-
- expect(build.reload.artifacts_metadata_store).to eq(remote)
- end
- end
- end
- end
- end
-
- context 'for job artifacts' do
- let(:artifact) { create(:ci_job_artifact, :archive) }
- let(:uploader_class) { JobArtifactUploader }
- let(:subject_class) { Ci::JobArtifact }
- let(:file_field) { :file }
- let(:subject_id) { artifact.id }
-
- context 'when local storage is used' do
- let(:store) { local }
-
- context 'and remote storage is defined' do
- before do
- stub_artifacts_object_storage(background_upload: true)
- end
-
- it "migrates file to remote storage" do
- perform
-
- expect(artifact.reload.file_store).to eq(remote)
- end
- end
- end
- end
-end
diff --git a/spec/workers/repository_check/batch_worker_spec.rb b/spec/workers/repository_check/batch_worker_spec.rb
index 6bc551be9ad..ede271b2cdd 100644
--- a/spec/workers/repository_check/batch_worker_spec.rb
+++ b/spec/workers/repository_check/batch_worker_spec.rb
@@ -62,4 +62,12 @@ describe RepositoryCheck::BatchWorker do
expect(subject.perform(shard_name)).to eq([])
end
+
+ it 'does not run if the exclusive lease is taken' do
+ allow(subject).to receive(:try_obtain_lease).and_return(false)
+
+ expect(subject).not_to receive(:perform_repository_checks)
+
+ subject.perform(shard_name)
+ end
end
diff --git a/spec/workers/repository_check/dispatch_worker_spec.rb b/spec/workers/repository_check/dispatch_worker_spec.rb
index 20a4f1f5344..7877429aa8f 100644
--- a/spec/workers/repository_check/dispatch_worker_spec.rb
+++ b/spec/workers/repository_check/dispatch_worker_spec.rb
@@ -11,6 +11,14 @@ describe RepositoryCheck::DispatchWorker do
subject.perform
end
+ it 'does nothing if the exclusive lease is taken' do
+ allow(subject).to receive(:try_obtain_lease).and_return(false)
+
+ expect(RepositoryCheck::BatchWorker).not_to receive(:perform_async)
+
+ subject.perform
+ end
+
it 'dispatches work to RepositoryCheck::BatchWorker' do
expect(RepositoryCheck::BatchWorker).to receive(:perform_async).at_least(:once)
diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
index f0884ad0aff..d07e40377d4 100644
--- a/spec/workers/repository_import_worker_spec.rb
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -51,7 +51,7 @@ describe RepositoryImportWorker do
it 'hide the credentials that were used in the import URL' do
error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
- project.update_attributes(import_jid: '123')
+ project.update(import_jid: '123')
expect_any_instance_of(Projects::ImportService).to receive(:execute).and_return({ status: :error, message: error })
expect do
@@ -63,7 +63,7 @@ describe RepositoryImportWorker do
it 'updates the error on Import/Export' do
error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
- project.update_attributes(import_jid: '123', import_type: 'gitlab_project')
+ project.update(import_jid: '123', import_type: 'gitlab_project')
expect_any_instance_of(Projects::ImportService).to receive(:execute).and_return({ status: :error, message: error })
expect do
diff --git a/spec/workers/repository_update_remote_mirror_worker_spec.rb b/spec/workers/repository_update_remote_mirror_worker_spec.rb
index 152ba2509b9..4f1ad2474f5 100644
--- a/spec/workers/repository_update_remote_mirror_worker_spec.rb
+++ b/spec/workers/repository_update_remote_mirror_worker_spec.rb
@@ -13,7 +13,7 @@ describe RepositoryUpdateRemoteMirrorWorker do
describe '#perform' do
context 'with status none' do
before do
- remote_mirror.update_attributes(update_status: 'none')
+ remote_mirror.update(update_status: 'none')
end
it 'sets status as finished when update remote mirror service executes successfully' do
@@ -34,7 +34,7 @@ describe RepositoryUpdateRemoteMirrorWorker do
end
it 'does nothing if last_update_started_at is higher than the time the job was scheduled in' do
- remote_mirror.update_attributes(last_update_started_at: Time.now)
+ remote_mirror.update(last_update_started_at: Time.now)
expect_any_instance_of(RemoteMirror).to receive(:updated_since?).with(scheduled_time).and_return(true)
expect_any_instance_of(Projects::UpdateRemoteMirrorService).not_to receive(:execute).with(remote_mirror)
@@ -56,7 +56,7 @@ describe RepositoryUpdateRemoteMirrorWorker do
context 'with another worker already running' do
before do
- remote_mirror.update_attributes(update_status: 'started')
+ remote_mirror.update(update_status: 'started')
end
it 'raises RemoteMirrorUpdateAlreadyInProgressError' do
@@ -68,11 +68,11 @@ describe RepositoryUpdateRemoteMirrorWorker do
context 'with status failed' do
before do
- remote_mirror.update_attributes(update_status: 'failed')
+ remote_mirror.update(update_status: 'failed')
end
it 'sets status as finished if last_update_started_at is higher than the time the job was scheduled in' do
- remote_mirror.update_attributes(last_update_started_at: Time.now)
+ remote_mirror.update(last_update_started_at: Time.now)
expect_any_instance_of(RemoteMirror).to receive(:updated_since?).with(scheduled_time).and_return(false)
expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success)
diff --git a/spec/workers/stuck_import_jobs_worker_spec.rb b/spec/workers/stuck_import_jobs_worker_spec.rb
index af7675c8cab..2169c14218b 100644
--- a/spec/workers/stuck_import_jobs_worker_spec.rb
+++ b/spec/workers/stuck_import_jobs_worker_spec.rb
@@ -51,7 +51,7 @@ describe StuckImportJobsWorker do
let(:project) { create(:project, :import_scheduled) }
before do
- project.import_state.update_attributes(jid: '123')
+ project.import_state.update(jid: '123')
end
end
end
@@ -61,7 +61,7 @@ describe StuckImportJobsWorker do
let(:project) { create(:project, :import_started) }
before do
- project.import_state.update_attributes(jid: '123')
+ project.import_state.update(jid: '123')
end
end
end
diff --git a/vendor/gitignore/Autotools.gitignore b/vendor/gitignore/Autotools.gitignore
index ffa6ecc3f9b..96d6ed2cfea 100644
--- a/vendor/gitignore/Autotools.gitignore
+++ b/vendor/gitignore/Autotools.gitignore
@@ -9,7 +9,7 @@ Makefile.in
# http://www.gnu.org/software/autoconf
-/autom4te.cache
+autom4te.cache
/autoscan.log
/autoscan-*.log
/aclocal.m4
@@ -39,4 +39,3 @@ m4/ltoptions.m4
m4/ltsugar.m4
m4/ltversion.m4
m4/lt~obsolete.m4
-autom4te.cache
diff --git a/vendor/gitignore/CraftCMS.gitignore b/vendor/gitignore/CraftCMS.gitignore
index a70d4772c46..0d81b397e35 100644
--- a/vendor/gitignore/CraftCMS.gitignore
+++ b/vendor/gitignore/CraftCMS.gitignore
@@ -1,3 +1,4 @@
-# Craft Storage (cache) [http://buildwithcraft.com/help/craft-storage-gitignore]
+# Craft 2 Storage (https://craftcms.com/support/craft-storage-gitignore)
+# not necessary for Craft 3 (https://github.com/craftcms/craft/issues/26)
/craft/storage/*
-!/craft/storage/logo/* \ No newline at end of file
+!/craft/storage/rebrand
diff --git a/vendor/gitignore/Delphi.gitignore b/vendor/gitignore/Delphi.gitignore
index 19864c6bbef..000ee5f104b 100644
--- a/vendor/gitignore/Delphi.gitignore
+++ b/vendor/gitignore/Delphi.gitignore
@@ -20,7 +20,7 @@
# Deployment Manager configuration file for your project. Added in Delphi XE2.
# Uncomment this if it is not mobile development and you do not use remote debug feature.
#*.deployproj
-#
+#
# C++ object files produced when C/C++ Output file generation is configured.
# Uncomment this if you are not using external objects (zlib library for example).
#*.obj
diff --git a/vendor/gitignore/Eagle.gitignore b/vendor/gitignore/Eagle.gitignore
index 9afc324d6ae..28f0b9715e6 100644
--- a/vendor/gitignore/Eagle.gitignore
+++ b/vendor/gitignore/Eagle.gitignore
@@ -35,7 +35,6 @@ eagle.epf
*.gpi
*.pls
*.ger
-*.gpi
*.xln
*.drd
diff --git a/vendor/gitignore/GWT.gitignore b/vendor/gitignore/GWT.gitignore
index 07704e54bbc..a01e7fcd921 100644
--- a/vendor/gitignore/GWT.gitignore
+++ b/vendor/gitignore/GWT.gitignore
@@ -18,9 +18,6 @@ war/WEB-INF/classes/
#compilation logs
.gwt/
-#caching for already compiled files
-gwt-unitCache/
-
#gwt junit compilation files
www-test/
diff --git a/vendor/gitignore/Global/Backup.gitignore b/vendor/gitignore/Global/Backup.gitignore
new file mode 100644
index 00000000000..825ce52db53
--- /dev/null
+++ b/vendor/gitignore/Global/Backup.gitignore
@@ -0,0 +1,5 @@
+*.bak
+*.gho
+*.ori
+*.orig
+*.tmp
diff --git a/vendor/gitignore/Global/CodeKit.gitignore b/vendor/gitignore/Global/CodeKit.gitignore
index bd9e67fcca2..09b84126cea 100644
--- a/vendor/gitignore/Global/CodeKit.gitignore
+++ b/vendor/gitignore/Global/CodeKit.gitignore
@@ -1,3 +1,4 @@
# General CodeKit files to ignore
config.codekit
+config.codekit3
/min
diff --git a/vendor/gitignore/Global/Eclipse.gitignore b/vendor/gitignore/Global/Eclipse.gitignore
index 0eb8a5e8571..a65649a9ed2 100644
--- a/vendor/gitignore/Global/Eclipse.gitignore
+++ b/vendor/gitignore/Global/Eclipse.gitignore
@@ -23,7 +23,7 @@ local.properties
# CDT-specific (C/C++ Development Tooling)
.cproject
-# CDT- autotools
+# CDT- autotools
.autotools
# Java annotation processor (APT)
@@ -47,6 +47,9 @@ local.properties
# Code Recommenders
.recommenders/
+# Annotation Processing
+.apt_generated/
+
# Scala IDE specific (Scala & Java development for Eclipse)
.cache-main
.scala_dependencies
diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore
index 4d5117a1d9d..0d95b087f19 100644
--- a/vendor/gitignore/Global/JetBrains.gitignore
+++ b/vendor/gitignore/Global/JetBrains.gitignore
@@ -4,6 +4,7 @@
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
@@ -20,9 +21,16 @@
.idea/**/gradle.xml
.idea/**/libraries
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+
# CMake
-cmake-build-debug/
-cmake-build-release/
+cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
diff --git a/vendor/gitignore/Global/Matlab.gitignore b/vendor/gitignore/Global/Matlab.gitignore
index d87a6bdbeeb..46a83d635ba 100644
--- a/vendor/gitignore/Global/Matlab.gitignore
+++ b/vendor/gitignore/Global/Matlab.gitignore
@@ -7,17 +7,20 @@
# Compiled MEX binaries (all platforms)
*.mex*
-# Packaged app and toolbox files
-*.mlappinstall
-*.mltbx
-
-# Generated helpsearch folders
-helpsearch*/
+# Packaged app and toolbox files
+*.mlappinstall
+*.mltbx
+
+# Generated helpsearch folders
+helpsearch*/
# Simulink code generation folders
slprj/
sccprj/
+# Matlab code generation folders
+codegen/
+
# Simulink autosave extension
*.autosave
diff --git a/vendor/gitignore/Global/Patch.gitignore b/vendor/gitignore/Global/Patch.gitignore
new file mode 100644
index 00000000000..6ffab9ad295
--- /dev/null
+++ b/vendor/gitignore/Global/Patch.gitignore
@@ -0,0 +1,2 @@
+*.orig
+*.rej
diff --git a/vendor/gitignore/Global/SynopsysVCS.gitignore b/vendor/gitignore/Global/SynopsysVCS.gitignore
index eed2432fb78..ad751f6bd75 100644
--- a/vendor/gitignore/Global/SynopsysVCS.gitignore
+++ b/vendor/gitignore/Global/SynopsysVCS.gitignore
@@ -4,8 +4,8 @@
*.evcd
*.fsdb
-# Default name of the simulation executable. A different name can be
-# specified with this switch (the associated daidir database name is
+# Default name of the simulation executable. A different name can be
+# specified with this switch (the associated daidir database name is
# also taken from here): -o <path>/<filename>
simv
@@ -13,7 +13,7 @@ simv
simv.daidir/
simv.db.dir/
-# Infrastructure necessary to co-simulate SystemC models with
+# Infrastructure necessary to co-simulate SystemC models with
# Verilog/VHDL models. An alternate directory may be specified with this
# switch: -Mdir=<directory_path>
csrc/
@@ -22,7 +22,7 @@ csrc/
# used to write all messages from simulation: -l <filename>
*.log
-# Coverage results (generated with urg) and database location. The
+# Coverage results (generated with urg) and database location. The
# following switch can also be used: urg -dir <coverage_directory>.vdb
simv.vdb/
urgReport/
diff --git a/vendor/gitignore/Global/Vim.gitignore b/vendor/gitignore/Global/Vim.gitignore
index 19cfe22f583..741518ffd24 100644
--- a/vendor/gitignore/Global/Vim.gitignore
+++ b/vendor/gitignore/Global/Vim.gitignore
@@ -1,7 +1,8 @@
# Swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
-[._]s[a-v][a-z]
+[._]s[a-rt-v][a-z]
+[._]ss[a-gi-z]
[._]sw[a-p]
# Session
diff --git a/vendor/gitignore/LabVIEW.gitignore b/vendor/gitignore/LabVIEW.gitignore
index 122450865cf..31619f59814 100644
--- a/vendor/gitignore/LabVIEW.gitignore
+++ b/vendor/gitignore/LabVIEW.gitignore
@@ -14,3 +14,4 @@
# Metadata
*.aliases
*.lvlps
+.cache/
diff --git a/vendor/gitignore/Maven.gitignore b/vendor/gitignore/Maven.gitignore
index 5f2dbe11df9..e8d57d08088 100644
--- a/vendor/gitignore/Maven.gitignore
+++ b/vendor/gitignore/Maven.gitignore
@@ -7,6 +7,4 @@ release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
-
-# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored)
-!/.mvn/wrapper/maven-wrapper.jar
+.mvn/wrapper/maven-wrapper.jar
diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore
index 4454ba1b5bf..3a4c8581b3a 100644
--- a/vendor/gitignore/Node.gitignore
+++ b/vendor/gitignore/Node.gitignore
@@ -57,9 +57,15 @@ typings/
# dotenv environment variables file
.env
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
# next.js build output
.next
+# nuxt.js build output
+.nuxt
+
# vuepress build output
.vuepress/dist
diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore
index 86de6aa3f5f..a0bd6b453a8 100644
--- a/vendor/gitignore/Objective-C.gitignore
+++ b/vendor/gitignore/Objective-C.gitignore
@@ -35,6 +35,9 @@ xcuserdata/
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
+#
+# Add this line if you want to avoid checking in source code from the Xcode workspace
+# *.xcworkspace
# Carthage
#
diff --git a/vendor/gitignore/Perl6.gitignore b/vendor/gitignore/Perl6.gitignore
new file mode 100644
index 00000000000..7b2c018a562
--- /dev/null
+++ b/vendor/gitignore/Perl6.gitignore
@@ -0,0 +1,7 @@
+# Gitignore for Perl 6 (http://www.perl6.org)
+# As part of https://github.com/github/gitignore
+
+# precompiled files
+.precomp
+lib/.precomp
+
diff --git a/vendor/gitignore/Swift.gitignore b/vendor/gitignore/Swift.gitignore
index 312d1f652c6..b8e04d98e33 100644
--- a/vendor/gitignore/Swift.gitignore
+++ b/vendor/gitignore/Swift.gitignore
@@ -47,6 +47,9 @@ playground.xcworkspace
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
+#
+# Add this line if you want to avoid checking in source code from the Xcode workspace
+# *.xcworkspace
# Carthage
#
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
index 3d12d3f90ad..79a66f9ebfa 100644
--- a/vendor/gitignore/TeX.gitignore
+++ b/vendor/gitignore/TeX.gitignore
@@ -226,6 +226,9 @@ TSWLatexianTemp*
# Texpad
.texpadtmp
+# LyX
+*.lyx~
+
# Kile
*.backup
@@ -241,6 +244,3 @@ TSWLatexianTemp*
# standalone packages
*.sta
-
-# generated if using elsarticle.cls
-*.spl
diff --git a/vendor/gitignore/Typo3.gitignore b/vendor/gitignore/Typo3.gitignore
index cb024fefe99..200c2a2bf79 100644
--- a/vendor/gitignore/Typo3.gitignore
+++ b/vendor/gitignore/Typo3.gitignore
@@ -8,12 +8,15 @@
/typo3conf/temp_CACHED*
/typo3conf/temp_fieldInfo.php
/typo3conf/deprecation_*.log
-/typo3conf/AdditionalConfiguration.php
+/typo3conf/ENABLE_INSTALL_TOOL
+/typo3conf/realurl_autoconf.php
+/FIRST_INSTALL
# Ignore system folders, you should have them symlinked.
# If not comment out the following entries.
/typo3
/typo3_src
/typo3_src-*
+/Packages
/.htaccess
/index.php
# Ignore temp directory.
diff --git a/vendor/gitignore/Umbraco.gitignore b/vendor/gitignore/Umbraco.gitignore
index 10fc2b4d825..cd90af3071a 100644
--- a/vendor/gitignore/Umbraco.gitignore
+++ b/vendor/gitignore/Umbraco.gitignore
@@ -19,7 +19,7 @@
!**/App_Data/[Pp]ackages/*
!**/[Uu]mbraco/[Dd]eveloper/[Pp]ackages/*
-# ImageProcessor DiskCache
+# ImageProcessor DiskCache
**/App_Data/cache/
# Ignore the Models Builder models out of date flag
diff --git a/vendor/gitignore/UnrealEngine.gitignore b/vendor/gitignore/UnrealEngine.gitignore
index 1daca8b50d9..6582eaf9a11 100644
--- a/vendor/gitignore/UnrealEngine.gitignore
+++ b/vendor/gitignore/UnrealEngine.gitignore
@@ -1,9 +1,6 @@
# Visual Studio 2015 user specific files
.vs/
-# Visual Studio 2015 database file
-*.VC.db
-
# Compiled Object files
*.slo
*.lo
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index 1e9abf78d69..f431ddc7cf5 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -220,7 +220,7 @@ ClientBin/
*.publishsettings
orleans.codegen.cs
-# Including strong name files can present a security risk
+# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
@@ -316,7 +316,7 @@ __pycache__/
# OpenCover UI analysis results
OpenCover/
-# Azure Stream Analytics local run output
+# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
@@ -325,5 +325,5 @@ ASALocalRun/
# NVidia Nsight GPU debugger configuration file
*.nvuser
-# MFractors (Xamarin productivity tool) working folder
+# MFractors (Xamarin productivity tool) working folder
.mfractor/
diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
index 0d58a00482a..ee0df7711e7 100644
--- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
@@ -105,11 +105,14 @@ code_quality:
- code_quality
artifacts:
paths: [gl-code-quality-report.json]
+ only:
+ - branches
except:
variables:
- $CODE_QUALITY_DISABLED
license_management:
+ stage: test
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
@@ -121,6 +124,8 @@ license_management:
- license_management
artifacts:
paths: [gl-license-management-report.json]
+ only:
+ - branches
except:
variables:
- $LICENSE_MANAGEMENT_DISABLED
@@ -161,6 +166,8 @@ sast:
- sast
artifacts:
paths: [gl-sast-report.json]
+ only:
+ - branches
except:
variables:
- $SAST_DISABLED
@@ -178,6 +185,8 @@ dependency_scanning:
- dependency_scanning
artifacts:
paths: [gl-dependency-scanning-report.json]
+ only:
+ - branches
except:
variables:
- $DEPENDENCY_SCANNING_DISABLED
@@ -195,6 +204,8 @@ container_scanning:
- container_scanning
artifacts:
paths: [gl-container-scanning-report.json]
+ only:
+ - branches
except:
variables:
- $CONTAINER_SCANNING_DISABLED
@@ -365,6 +376,7 @@ production_manual:
kubernetes: active
variables:
- $STAGING_ENABLED
+ - $CANARY_ENABLED
except:
variables:
- $INCREMENTAL_ROLLOUT_ENABLED
diff --git a/vendor/licenses.csv b/vendor/licenses.csv
index dada0da0b31..7503160baa0 100644
--- a/vendor/licenses.csv
+++ b/vendor/licenses.csv
@@ -7,7 +7,7 @@
@babel/template,7.0.0-beta.44,MIT
@babel/traverse,7.0.0-beta.44,MIT
@babel/types,7.0.0-beta.44,MIT
-@gitlab-org/gitlab-svgs,1.23.0,SEE LICENSE IN LICENSE
+@gitlab-org/gitlab-svgs,1.25.0,SEE LICENSE IN LICENSE
@sindresorhus/is,0.7.0,MIT
@types/jquery,2.0.48,MIT
@vue/component-compiler-utils,1.2.1,MIT
@@ -35,7 +35,7 @@ abbrev,1.1.1,ISC
accepts,1.3.4,MIT
ace-rails-ap,4.1.2,MIT
acorn,3.3.0,MIT
-acorn,5.5.3,MIT
+acorn,5.6.2,MIT
acorn-dynamic-import,3.0.0,MIT
acorn-jsx,3.0.1,MIT
actionmailer,4.2.10,MIT
@@ -51,14 +51,12 @@ addressparser,1.0.1,MIT
aes_key_wrap,1.0.1,MIT
after,0.8.2,MIT
agent-base,2.1.1,MIT
-ajv,4.11.8,MIT
ajv,5.5.2,MIT
ajv,6.1.1,MIT
ajv-keywords,2.1.1,MIT
ajv-keywords,3.1.0,MIT
akismet,2.0.0,MIT
align-text,0.1.4,MIT
-allocations,1.0.5,MIT
alphanum-sort,1.0.2,MIT
amdefine,1.0.1,BSD-3-Clause OR MIT
amqplib,0.5.2,MIT
@@ -208,7 +206,7 @@ base64-js,1.2.3,MIT
base64id,1.0.0,MIT
batch,0.6.1,MIT
batch-loader,1.2.1,MIT
-bcrypt,3.1.11,MIT
+bcrypt,3.1.12,MIT
bcrypt-pbkdf,1.0.1,New BSD
bcrypt_pbkdf,1.0.0,MIT
better-assert,1.0.2,MIT
@@ -220,7 +218,6 @@ bitsyntax,0.0.4,Unknown
bl,1.1.2,MIT
blackst0ne-mermaid,7.1.0-fixed,MIT
blob,0.0.4,MIT*
-block-stream,0.0.9,ISC
bluebird,3.5.1,MIT
bn.js,4.11.8,MIT
body-parser,1.18.2,MIT
@@ -269,7 +266,7 @@ camelcase-keys,2.1.0,MIT
caniuse-api,1.6.1,MIT
caniuse-db,1.0.30000649,CC-BY-4.0
capture-stack-trace,1.0.0,MIT
-carrierwave,1.2.1,MIT
+carrierwave,1.2.3,MIT
caseless,0.11.0,Apache 2.0
caseless,0.12.0,Apache 2.0
cause,0.1,MIT
@@ -399,6 +396,7 @@ dashdash,1.14.1,MIT
data-uri-to-buffer,1.2.0,MIT
date-format,1.2.0,MIT
date-now,0.1.4,MIT
+dateformat,3.0.3,MIT
de-indent,1.0.2,MIT
debug,2.2.0,MIT
debug,2.6.8,MIT
@@ -457,7 +455,7 @@ domelementtype,1.3.0,Simplified BSD
domhandler,2.4.1,Simplified BSD
domutils,1.6.2,Simplified BSD
doorkeeper,4.3.2,MIT
-doorkeeper-openid_connect,1.4.0,MIT
+doorkeeper-openid_connect,1.5.0,MIT
dot-prop,4.2.0,MIT
double-ended-queue,2.1.0-0,MIT
dropzone,4.2.0,MIT
@@ -507,7 +505,7 @@ eslint-plugin-html,4.0.3,ISC
eslint-plugin-import,2.12.0,MIT
eslint-plugin-jasmine,2.2.0,MIT
eslint-plugin-promise,3.8.0,ISC
-eslint-plugin-vue,4.0.1,MIT
+eslint-plugin-vue,4.5.0,MIT
eslint-restricted-globals,0.1.1,MIT
eslint-scope,3.7.1,Simplified BSD
eslint-visitor-keys,1.0.0,Apache 2.0
@@ -515,10 +513,9 @@ espree,3.5.4,Simplified BSD
esprima,2.7.3,Simplified BSD
esprima,3.1.3,Simplified BSD
esprima,4.0.0,Simplified BSD
-esquery,1.0.0,New BSD
-esrecurse,4.1.0,Simplified BSD
+esquery,1.0.1,New BSD
+esrecurse,4.2.1,Simplified BSD
estraverse,1.9.3,Simplified BSD
-estraverse,4.1.1,Simplified BSD
estraverse,4.2.0,Simplified BSD
esutils,2.0.2,Simplified BSD
et-orbi,1.0.3,MIT
@@ -607,11 +604,10 @@ fresh,0.5.2,MIT
from,0.1.7,MIT
from2,2.3.0,MIT
fs-access,1.0.1,MIT
+fs-minipass,1.2.5,ISC
fs-write-stream-atomic,1.0.10,ISC
fs.realpath,1.0.0,ISC
-fsevents,1.1.3,MIT
-fstream,1.0.11,ISC
-fstream-ignore,1.0.5,ISC
+fsevents,1.2.4,MIT
ftp,0.3.10,MIT
function-bind,1.1.1,MIT
functional-red-black-tree,1.0.1,MIT
@@ -630,14 +626,14 @@ get_process_mem,0.2.0,MIT
getpass,0.1.7,MIT
gettext_i18n_rails,1.8.0,MIT
gettext_i18n_rails_js,1.3.0,MIT
-gitaly-proto,0.100.0,MIT
+gitaly-proto,0.105.0,MIT
github-linguist,5.3.3,MIT
github-markup,1.7.0,MIT
gitlab-flowdock-git-hook,1.0.1,MIT
-gitlab-gollum-lib,4.2.7.2,MIT
-gitlab-gollum-rugged_adapter,0.4.4,MIT
+gitlab-gollum-lib,4.2.7.5,MIT
+gitlab-gollum-rugged_adapter,0.4.4.1,MIT
gitlab-grit,2.8.2,MIT
-gitlab-markup,1.6.3,MIT
+gitlab-markup,1.6.4,MIT
gitlab_omniauth-ldap,2.0.4,MIT
glob,5.0.15,ISC
glob,7.1.2,ISC
@@ -664,7 +660,7 @@ gpgme,2.0.13,LGPL-2.1+
graceful-fs,4.1.11,ISC
grape,1.0.3,MIT
grape-entity,0.7.1,MIT
-grape-path-helpers,1.0.4,MIT
+grape-path-helpers,1.0.5,MIT
grape_logging,1.7.0,MIT
graphiql-rails,1.4.10,MIT
graphlib,2.1.1,MIT
@@ -674,10 +670,8 @@ gzip-size,4.1.0,MIT
hamlit,2.6.1,MIT
handle-thing,1.2.5,MIT
handlebars,4.0.6,MIT
-har-schema,1.0.5,ISC
har-schema,2.0.0,ISC
har-validator,2.0.6,ISC
-har-validator,4.2.1,ISC
har-validator,5.0.3,ISC
has,1.0.1,MIT
has-ansi,2.0.0,MIT
@@ -712,7 +706,7 @@ hosted-git-info,2.2.0,ISC
hpack.js,2.1.6,MIT
html-comment-regex,1.1.1,MIT
html-entities,1.2.0,MIT
-html-pipeline,2.7.1,MIT
+html-pipeline,2.8.3,MIT
html2text,0.2.0,MIT
htmlentities,4.3.4,MIT
htmlparser2,3.9.2,MIT
@@ -739,13 +733,14 @@ icalendar,2.4.1,ruby
ice_nine,0.11.2,MIT
iconv-lite,0.4.15,MIT
iconv-lite,0.4.19,MIT
+iconv-lite,0.4.23,MIT
icss-replace-symbols,1.1.0,ISC
icss-utils,2.1.0,ISC
ieee754,1.1.11,New BSD
-ieee754,1.1.8,New BSD
iferr,0.1.5,MIT
ignore,3.3.8,MIT
ignore-by-default,1.0.1,ISC
+ignore-walk,3.0.1,ISC
immediate,3.0.6,MIT
import-lazy,2.1.0,MIT
import-local,1.0.0,MIT
@@ -865,16 +860,14 @@ jsesc,1.3.0,MIT
jsesc,2.5.1,MIT
json,1.8.6,ruby
json-buffer,3.0.0,MIT
-json-jwt,1.9.2,MIT
+json-jwt,1.9.4,MIT
json-parse-better-errors,1.0.2,MIT
json-schema,0.2.3,BSD
json-schema-traverse,0.3.1,MIT
-json-stable-stringify,1.0.1,MIT
json-stable-stringify-without-jsonify,1.0.1,MIT
json-stringify-safe,5.0.1,ISC
json3,3.3.2,MIT
json5,0.5.1,MIT
-jsonify,0.0.0,Public Domain
jsonpointer,4.0.1,MIT
jsprim,1.4.1,MIT
jszip,3.1.3,(MIT OR GPL-3.0)
@@ -992,12 +985,14 @@ minimatch,3.0.4,ISC
minimist,0.0.10,MIT
minimist,0.0.8,MIT
minimist,1.2.0,MIT
+minipass,2.3.3,ISC
+minizlib,1.1.0,MIT
mississippi,2.0.0,Simplified BSD
mixin-deep,1.3.1,MIT
mkdirp,0.5.1,MIT
moment,2.19.2,MIT
monaco-editor,0.13.1,MIT
-monaco-editor-webpack-plugin,1.2.1,MIT
+monaco-editor-webpack-plugin,1.4.0,MIT
mousetrap,1.4.6,Apache 2.0
mousetrap-rails,1.4.6,"MIT,Apache"
move-concurrently,1.0.1,ISC
@@ -1012,9 +1007,10 @@ mustermann,1.0.2,MIT
mustermann-grape,1.0.0,MIT
mute-stream,0.0.7,ISC
mysql2,0.4.10,MIT
-nan,2.8.0,MIT
+nan,2.10.0,MIT
nanomatch,1.2.9,MIT
natural-compare,1.4.0,MIT
+needle,2.2.1,MIT
negotiator,0.6.1,MIT
neo-async,2.5.0,MIT
net-ldap,0.16.0,MIT
@@ -1024,7 +1020,7 @@ netrc,0.11.0,MIT
nice-try,1.0.4,MIT
node-forge,0.6.33,New BSD
node-libs-browser,2.1.0,MIT
-node-pre-gyp,0.6.39,New BSD
+node-pre-gyp,0.10.0,New BSD
node-uuid,1.4.8,MIT
nodemailer,2.7.2,MIT
nodemailer-direct-transport,3.3.2,MIT
@@ -1034,7 +1030,8 @@ nodemailer-smtp-pool,2.8.2,MIT
nodemailer-smtp-transport,2.7.2,MIT
nodemailer-wellknown,0.1.10,MIT
nodemon,1.17.3,MIT
-nokogiri,1.8.2,MIT
+nokogiri,1.8.3,MIT
+nokogumbo,1.5.0,Apache 2.0
nopt,1.0.10,MIT
nopt,3.0.6,ISC
nopt,4.0.1,ISC
@@ -1043,6 +1040,8 @@ normalize-path,2.1.1,MIT
normalize-range,0.1.2,MIT
normalize-url,1.9.1,MIT
normalize-url,2.0.1,MIT
+npm-bundled,1.0.3,ISC
+npm-packlist,1.1.10,ISC
npm-run-path,2.0.2,MIT
npmlog,4.1.2,ISC
null-check,1.0.0,MIT
@@ -1076,7 +1075,7 @@ omniauth-oauth,1.1.0,MIT
omniauth-oauth2,1.5.0,MIT
omniauth-oauth2-generic,0.2.2,MIT
omniauth-saml,1.10.0,MIT
-omniauth-shibboleth,1.2.1,MIT
+omniauth-shibboleth,1.3.0,MIT
omniauth-twitter,1.4.0,MIT
omniauth_crowd,2.2.3,MIT
on-finished,2.3.0,MIT
@@ -1137,7 +1136,6 @@ peek-pg,1.3.0,MIT
peek-rblineprof,0.2.0,MIT
peek-redis,1.2.0,MIT
peek-sidekiq,1.0.3,MIT
-performance-now,0.2.0,MIT
performance-now,2.1.0,MIT
pg,0.18.4,"BSD,ruby,GPL"
pify,2.3.0,MIT
@@ -1194,7 +1192,6 @@ premailer-rails,1.9.7,MIT
prepend-http,1.0.4,MIT
prepend-http,2.0.0,MIT
preserve,0.2.0,MIT
-prettier,1.11.1,MIT
prettier,1.12.1,MIT
prismjs,1.6.0,MIT
private,0.1.8,MIT
@@ -1222,7 +1219,6 @@ q,1.4.1,MIT
q,1.5.0,MIT
qjobs,1.2.0,MIT
qs,6.2.3,New BSD
-qs,6.4.0,New BSD
qs,6.5.1,New BSD
query-string,4.3.2,MIT
query-string,5.1.1,MIT
@@ -1303,11 +1299,9 @@ repeat-string,1.6.1,MIT
repeating,2.0.1,MIT
representable,3.0.4,MIT
request,2.75.0,Apache 2.0
-request,2.81.0,Apache 2.0
request,2.83.0,Apache 2.0
request_store,1.3.1,MIT
requestretry,1.13.0,MIT
-require-all,2.2.0,MIT
require-directory,2.1.1,MIT
require-main-filename,1.0.1,ISC
require-uncached,1.0.3,MIT
@@ -1341,24 +1335,26 @@ ruby_parser,3.9.0,MIT
rubyntlm,0.6.2,MIT
rubypants,0.2.0,BSD
rufus-scheduler,3.4.0,MIT
-rugged,0.27.1,MIT
+rugged,0.27.2,MIT
run-async,2.3.0,MIT
run-queue,1.0.3,ISC
rx-lite,4.0.8,Apache 2.0
rx-lite-aggregates,4.0.8,Apache 2.0
rxjs,5.5.10,Apache 2.0
safe-buffer,5.1.1,MIT
+safe-buffer,5.1.2,MIT
safe-regex,1.1.0,MIT
safe_yaml,1.0.4,MIT
-sanitize,2.1.0,MIT
+safer-buffer,2.1.2,MIT
+sanitize,4.6.5,MIT
sanitize-html,1.16.3,MIT
sass,3.5.5,MIT
sass-listen,4.0.0,MIT
sass-rails,5.0.6,MIT
sawyer,0.8.1,MIT
sax,1.2.2,ISC
+sax,1.2.4,ISC
schema-utils,0.4.5,MIT
-securecompare,1.0.0,MIT
seed-fu,2.3.7,MIT
select,1.1.2,MIT
select-hose,2.0.0,MIT
@@ -1433,7 +1429,7 @@ spdy-transport,2.0.20,MIT
split,0.3.3,MIT
split-string,3.1.0,MIT
sprintf-js,1.0.3,New BSD
-sprockets,3.7.1,MIT
+sprockets,3.7.2,MIT
sprockets-rails,3.2.1,MIT
sql.js,0.4.0,MIT
srcset,1.0.0,MIT
@@ -1479,8 +1475,7 @@ sys-filesystem,1.1.6,Artistic 2.0
table,4.0.2,New BSD
tapable,0.1.10,MIT
tapable,1.0.0,MIT
-tar,2.2.1,ISC
-tar-pack,3.4.1,Simplified BSD
+tar,4.4.4,ISC
temple,0.7.7,MIT
term-size,1.2.0,MIT
test-exclude,4.2.1,ISC
@@ -1535,7 +1530,6 @@ uglify-es,3.3.9,Simplified BSD
uglify-js,2.8.29,Simplified BSD
uglify-to-browserify,1.0.2,MIT
uglifyjs-webpack-plugin,1.2.5,MIT
-uid-number,0.0.6,ISC
ultron,1.1.1,MIT
undefsafe,2.0.2,MIT
underscore,1.7.0,MIT
@@ -1566,7 +1560,6 @@ url-parse,1.1.9,MIT
url-parse-lax,1.0.0,MIT
url-parse-lax,3.0.0,MIT
url-to-options,1.0.1,MIT
-url_safe_base64,0.2.2,MIT
use,2.0.2,MIT
useragent,2.2.1,MIT
util,0.10.3,MIT
@@ -1640,6 +1633,7 @@ xtend,4.0.1,MIT
y18n,3.2.1,ISC
y18n,4.0.0,ISC
yallist,2.1.2,ISC
+yallist,3.0.2,ISC
yargs,11.0.0,MIT
yargs,11.1.0,MIT
yargs,3.10.0,MIT
diff --git a/yarn.lock b/yarn.lock
index 30d49ad276a..d844ae4f8e9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -78,9 +78,9 @@
lodash "^4.2.0"
to-fast-properties "^2.0.0"
-"@gitlab-org/gitlab-svgs@^1.24.0":
- version "1.24.0"
- resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.24.0.tgz#3b2b58c5a1d58ce784f486d648bd87cbbb06cedc"
+"@gitlab-org/gitlab-svgs@^1.25.0":
+ version "1.25.0"
+ resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.25.0.tgz#1a82b1be43e1a46e6b0767ef46f26f5fd6bbd101"
"@sindresorhus/is@^0.7.0":
version "0.7.0"
@@ -5272,9 +5272,9 @@ moment@2.x, moment@^2.18.1:
version "2.19.2"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.2.tgz#8a7f774c95a64550b4c7ebd496683908f9419dbe"
-monaco-editor-webpack-plugin@^1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.2.1.tgz#577ed091420f422bb8f0ff3a8899dd82344da56d"
+monaco-editor-webpack-plugin@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.4.0.tgz#7324258ab3695464cfe3bc12edb2e8c55b80d92f"
monaco-editor@0.13.1:
version "0.13.1"