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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc3
-rw-r--r--.flayignore1
-rw-r--r--.haml-lint.yml18
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile14
-rw-r--r--Gemfile.lock23
-rw-r--r--app/assets/images/new_repo.pngbin0 -> 19292 bytes
-rw-r--r--app/assets/images/old_repo.pngbin0 -> 20668 bytes
-rw-r--r--app/assets/javascripts/api.js16
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js30
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.js5
-rw-r--r--app/assets/javascripts/build.js1
-rw-r--r--app/assets/javascripts/build_variables.js2
-rw-r--r--app/assets/javascripts/commons/bootstrap.js2
-rw-r--r--app/assets/javascripts/commons/jquery.js1
-rw-r--r--app/assets/javascripts/dispatcher.js18
-rw-r--r--app/assets/javascripts/fly_out_nav.js1
-rw-r--r--app/assets/javascripts/gl_dropdown.js67
-rw-r--r--app/assets/javascripts/graphs/graphs_charts.js34
-rw-r--r--app/assets/javascripts/helpers/user_feature_helper.js11
-rw-r--r--app/assets/javascripts/lib/utils/constants.js1
-rw-r--r--app/assets/javascripts/new_sidebar.js42
-rw-r--r--app/assets/javascripts/project.js8
-rw-r--r--app/assets/javascripts/project_select.js17
-rw-r--r--app/assets/javascripts/project_select_combo_button.js85
-rw-r--r--app/assets/javascripts/projects/project_import_gitlab_project.js14
-rw-r--r--app/assets/javascripts/projects/project_new.js20
-rw-r--r--app/assets/javascripts/repo/components/repo.vue63
-rw-r--r--app/assets/javascripts/repo/components/repo_commit_section.vue100
-rw-r--r--app/assets/javascripts/repo/components/repo_edit_button.vue49
-rw-r--r--app/assets/javascripts/repo/components/repo_editor.vue135
-rw-r--r--app/assets/javascripts/repo/components/repo_file.vue66
-rw-r--r--app/assets/javascripts/repo/components/repo_file_buttons.vue42
-rw-r--r--app/assets/javascripts/repo/components/repo_file_options.vue25
-rw-r--r--app/assets/javascripts/repo/components/repo_loading_file.vue51
-rw-r--r--app/assets/javascripts/repo/components/repo_prev_directory.vue26
-rw-r--r--app/assets/javascripts/repo/components/repo_preview.vue32
-rw-r--r--app/assets/javascripts/repo/components/repo_sidebar.vue104
-rw-r--r--app/assets/javascripts/repo/components/repo_tab.vue45
-rw-r--r--app/assets/javascripts/repo/components/repo_tabs.vue43
-rw-r--r--app/assets/javascripts/repo/helpers/monaco_loader_helper.js21
-rw-r--r--app/assets/javascripts/repo/helpers/repo_helper.js303
-rw-r--r--app/assets/javascripts/repo/index.js74
-rw-r--r--app/assets/javascripts/repo/mixins/repo_mixin.js17
-rw-r--r--app/assets/javascripts/repo/monaco_loader.js13
-rw-r--r--app/assets/javascripts/repo/services/repo_service.js82
-rw-r--r--app/assets/javascripts/repo/stores/repo_store.js241
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue82
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form.vue47
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue45
-rw-r--r--app/assets/javascripts/sidebar/sidebar_bundle.js18
-rw-r--r--app/assets/javascripts/test_utils/index.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js100
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js81
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js79
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js27
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js36
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js30
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js25
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js25
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js37
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js30
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js64
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js24
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js95
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js154
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js29
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js24
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js178
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js36
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js36
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js35
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/popup_dialog.vue67
-rw-r--r--app/assets/javascripts/wikis.js2
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/avatar.scss2
-rw-r--r--app/assets/stylesheets/framework/common.scss4
-rw-r--r--app/assets/stylesheets/framework/header.scss2
-rw-r--r--app/assets/stylesheets/framework/layout.scss22
-rw-r--r--app/assets/stylesheets/framework/media_object.scss8
-rw-r--r--app/assets/stylesheets/framework/nav.scss26
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss5
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss23
-rw-r--r--app/assets/stylesheets/new_sidebar.scss185
-rw-r--r--app/assets/stylesheets/pages/boards.scss2
-rw-r--r--app/assets/stylesheets/pages/builds.scss19
-rw-r--r--app/assets/stylesheets/pages/issuable.scss24
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss317
-rw-r--r--app/assets/stylesheets/pages/note_form.scss53
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss6
-rw-r--r--app/assets/stylesheets/pages/projects.scss134
-rw-r--r--app/assets/stylesheets/pages/repo.scss413
-rw-r--r--app/assets/stylesheets/pages/tree.scss5
-rw-r--r--app/assets/stylesheets/pages/wiki.scss12
-rw-r--r--app/controllers/admin/health_check_controller.rb7
-rw-r--r--app/controllers/application_controller.rb22
-rw-r--r--app/controllers/concerns/renders_blob.rb14
-rw-r--r--app/controllers/dashboard/projects_controller.rb6
-rw-r--r--app/controllers/import/gitlab_projects_controller.rb10
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb21
-rw-r--r--app/controllers/projects/blob_controller.rb39
-rw-r--r--app/controllers/projects/merge_requests_controller.rb5
-rw-r--r--app/controllers/projects/tree_controller.rb17
-rw-r--r--app/controllers/projects_controller.rb30
-rw-r--r--app/helpers/application_helper.rb8
-rw-r--r--app/helpers/avatars_helper.rb3
-rw-r--r--app/helpers/blob_helper.rb6
-rw-r--r--app/helpers/diff_helper.rb8
-rw-r--r--app/helpers/dropdowns_helper.rb19
-rw-r--r--app/helpers/gitlab_routing_helper.rb16
-rw-r--r--app/helpers/icons_helper.rb1
-rw-r--r--app/helpers/milestones_routing_helper.rb17
-rw-r--r--app/helpers/nav_helper.rb1
-rw-r--r--app/helpers/projects_helper.rb20
-rw-r--r--app/helpers/storage_health_helper.rb37
-rw-r--r--app/helpers/submodule_helper.rb4
-rw-r--r--app/models/blob_viewer/base.rb2
-rw-r--r--app/models/blob_viewer/server_side.rb2
-rw-r--r--app/models/commit.rb17
-rw-r--r--app/models/concerns/issuable.rb1
-rw-r--r--app/models/conversational_development_index/metric.rb4
-rw-r--r--app/models/merge_request.rb23
-rw-r--r--app/models/merge_request_diff.rb4
-rw-r--r--app/models/milestone.rb12
-rw-r--r--app/models/project.rb8
-rw-r--r--app/models/project_import_data.rb2
-rw-r--r--app/models/project_wiki.rb4
-rw-r--r--app/models/repository.rb78
-rw-r--r--app/models/user.rb24
-rw-r--r--app/models/wiki_page.rb78
-rw-r--r--app/serializers/blob_entity.rb17
-rw-r--r--app/serializers/merge_request_entity.rb2
-rw-r--r--app/serializers/submodule_entity.rb23
-rw-r--r--app/serializers/tree_entity.rb17
-rw-r--r--app/serializers/tree_root_entity.rb8
-rw-r--r--app/serializers/tree_serializer.rb3
-rw-r--r--app/services/auth/container_registry_authentication_service.rb7
-rw-r--r--app/services/ci/register_job_service.rb24
-rw-r--r--app/services/issuable_base_service.rb6
-rw-r--r--app/services/issues/create_service.rb7
-rw-r--r--app/services/merge_requests/create_service.rb8
-rw-r--r--app/services/projects/autocomplete_service.rb10
-rw-r--r--app/services/projects/create_from_template_service.rb15
-rw-r--r--app/services/projects/create_service.rb4
-rw-r--r--app/services/projects/gitlab_projects_import_service.rb36
-rw-r--r--app/services/projects/import_service.rb8
-rw-r--r--app/services/projects/update_pages_service.rb18
-rw-r--r--app/services/quick_actions/interpret_service.rb7
-rw-r--r--app/services/submit_usage_ping_service.rb20
-rw-r--r--app/services/system_note_service.rb3
-rw-r--r--app/services/wiki_pages/update_service.rb2
-rw-r--r--app/views/admin/health_check/_failing_storages.html.haml15
-rw-r--r--app/views/admin/health_check/show.html.haml27
-rw-r--r--app/views/dashboard/projects/index.html.haml4
-rw-r--r--app/views/dashboard/projects/starred.html.haml2
-rw-r--r--app/views/help/_shortcuts.html.haml2
-rw-r--r--app/views/import/gitlab_projects/new.html.haml50
-rw-r--r--app/views/layouts/header/_default.html.haml3
-rw-r--r--app/views/layouts/header/_new.html.haml3
-rw-r--r--app/views/layouts/nav/_new_admin_sidebar.html.haml7
-rw-r--r--app/views/layouts/nav/_new_group_sidebar.html.haml11
-rw-r--r--app/views/layouts/nav/_new_profile_sidebar.html.haml7
-rw-r--r--app/views/layouts/nav/_new_project_sidebar.html.haml13
-rw-r--r--app/views/profiles/preferences/show.html.haml28
-rw-r--r--app/views/projects/_files.html.haml11
-rw-r--r--app/views/projects/_md_preview.html.haml12
-rw-r--r--app/views/projects/_project_templates.html.haml10
-rw-r--r--app/views/projects/artifacts/file.html.haml2
-rw-r--r--app/views/projects/blob/_blob.html.haml2
-rw-r--r--app/views/projects/blob/_upload.html.haml4
-rw-r--r--app/views/projects/blob/_viewer.html.haml1
-rw-r--r--app/views/projects/blob/show.html.haml21
-rw-r--r--app/views/projects/blob/viewers/_balsamiq.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_download.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_image.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_notebook.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_pdf.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_sketch.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_stl.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_video.html.haml2
-rw-r--r--app/views/projects/boards/components/_board.html.haml12
-rw-r--r--app/views/projects/graphs/charts.html.haml6
-rw-r--r--app/views/projects/issues/show.html.haml3
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml168
-rw-r--r--app/views/projects/new.html.haml109
-rw-r--r--app/views/projects/runners/edit.html.haml2
-rw-r--r--app/views/projects/tree/_old_tree_content.html.haml24
-rw-r--r--app/views/projects/tree/_old_tree_header.html.haml70
-rw-r--r--app/views/projects/tree/_tree_content.html.haml29
-rw-r--r--app/views/projects/tree/_tree_header.html.haml82
-rw-r--r--app/views/projects/tree/show.html.haml8
-rw-r--r--app/views/projects/wikis/_form.html.haml5
-rw-r--r--app/views/projects/wikis/_sidebar.html.haml31
-rw-r--r--app/views/shared/_commit_well.html.haml4
-rw-r--r--app/views/shared/_import_form.html.haml26
-rw-r--r--app/views/shared/_new_project_item_select.html.haml9
-rw-r--r--app/views/shared/_sidebar_toggle_button.html.haml8
-rw-r--r--app/views/shared/_target_switcher.html.haml20
-rw-r--r--app/views/shared/_user_dropdown_experimental_features.html.haml1
-rw-r--r--app/views/shared/icons/_java_spring.svg6
-rw-r--r--app/views/shared/icons/_node_express.svg6
-rw-r--r--app/views/shared/icons/_rails.svg6
-rw-r--r--app/views/shared/issuable/_label_page_create.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml4
-rw-r--r--app/views/shared/issuable/_user_dropdown_item.html.haml3
-rw-r--r--app/views/shared/projects/_list.html.haml4
-rw-r--r--app/views/shared/repo/_editable_mode.html.haml2
-rw-r--r--app/views/shared/repo/_repo.html.haml2
-rw-r--r--app/workers/concerns/new_issuable.rb23
-rw-r--r--app/workers/merge_worker.rb2
-rw-r--r--app/workers/new_issue_worker.rb17
-rw-r--r--app/workers/new_merge_request_worker.rb17
-rw-r--r--app/workers/stuck_merge_jobs_worker.rb34
-rw-r--r--changelogs/unreleased/13265-project_events_noteable_iid.yml4
-rw-r--r--changelogs/unreleased/31207-clean-locked-merge-requests.yml4
-rw-r--r--changelogs/unreleased/32844-issuables-performance.yml4
-rw-r--r--changelogs/unreleased/33095-mr-widget-ui.yml4
-rw-r--r--changelogs/unreleased/33874_confi.yml5
-rw-r--r--changelogs/unreleased/34028-collapse-sidebar.yml4
-rw-r--r--changelogs/unreleased/34764-rename-to-overview.yml4
-rw-r--r--changelogs/unreleased/35052-please-select-a-file-when-attempting-to-upload-or-replace-from-the-ui.yml4
-rw-r--r--changelogs/unreleased/35098-raise-encoding-confidence-threshold.yml4
-rw-r--r--changelogs/unreleased/35136-barchart-not-display-label-at-0-hour.yml4
-rw-r--r--changelogs/unreleased/35483-improve-mobile-sidebar.yml4
-rw-r--r--changelogs/unreleased/35761-convdev-perc.yml4
-rw-r--r--changelogs/unreleased/add-star-for-action-scope.yml4
-rw-r--r--changelogs/unreleased/bvl-nfs-circuitbreaker.yml4
-rw-r--r--changelogs/unreleased/dont-use-limit-offset-when-counting-projects.yml4
-rw-r--r--changelogs/unreleased/eager-load-project-creators-for-project-dashboards.yml4
-rw-r--r--changelogs/unreleased/github.yml4
-rw-r--r--changelogs/unreleased/group-milestone-references-system-notes.yml4
-rw-r--r--changelogs/unreleased/group-new-issue.yml4
-rw-r--r--changelogs/unreleased/mattermost_fixes.yml4
-rw-r--r--changelogs/unreleased/memoize-user-personal-projects-count.yml4
-rw-r--r--changelogs/unreleased/pawel-add-sidekiq-metrics-endpoint-32145.yml4
-rw-r--r--changelogs/unreleased/rc-fix-branches-api-endpoint.yml2
-rw-r--r--changelogs/unreleased/rc-fix-commits-api.yml5
-rw-r--r--changelogs/unreleased/rc-fix-tags-api.yml5
-rw-r--r--changelogs/unreleased/remove-redundant-query-when-retrieving-recent-pushes.yml4
-rw-r--r--changelogs/unreleased/restrict-haml-javascript.yml4
-rw-r--r--changelogs/unreleased/wiki_title.yml4
-rw-r--r--changelogs/unreleased/zj-project-templates.yml4
-rw-r--r--config/application.rb4
-rw-r--r--config/dependency_decisions.yml6
-rw-r--r--config/gitlab.yml.example15
-rw-r--r--config/initializers/1_settings.rb19
-rw-r--r--config/initializers/6_validations.rb16
-rw-r--r--config/initializers/7_prometheus_metrics.rb6
-rw-r--r--config/routes/admin.rb4
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--config/webpack.config.js56
-rw-r--r--db/migrate/20170731175128_add_percentages_to_conv_dev.rb32
-rw-r--r--db/migrate/20170731183033_add_merge_jid_to_merge_requests.rb7
-rw-r--r--db/post_migrate/20170803090603_calculate_conv_dev_index_percentages.rb30
-rw-r--r--db/post_migrate/20170807160457_remove_locked_at_column_from_merge_requests.rb11
-rw-r--r--db/schema.rb14
-rw-r--r--doc/administration/img/failing_storage.pngbin0 -> 48281 bytes
-rw-r--r--doc/administration/repository_storage_paths.md77
-rw-r--r--doc/api/commits.md3
-rw-r--r--doc/api/events.md39
-rw-r--r--doc/api/group_milestones.md4
-rw-r--r--doc/api/issues.md6
-rw-r--r--doc/api/merge_requests.md2
-rw-r--r--doc/api/milestones.md4
-rw-r--r--doc/api/notes.md9
-rw-r--r--doc/api/repository_files.md9
-rw-r--r--doc/api/repository_storage_health.md74
-rw-r--r--doc/api/tags.md41
-rw-r--r--doc/development/background_migrations.md9
-rw-r--r--doc/development/migration_style_guide.md15
-rw-r--r--doc/development/rake_tasks.md17
-rw-r--r--doc/update/9.4-to-9.5.md352
-rw-r--r--doc/update/README.md68
-rw-r--r--doc/user/markdown.md6
-rw-r--r--doc/user/project/integrations/webhooks.md1
-rw-r--r--doc/user/project/milestones/index.md3
-rw-r--r--features/steps/project/wiki.rb2
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/circuit_breakers.rb50
-rw-r--r--lib/api/commits.rb40
-rw-r--r--lib/api/entities.rb39
-rw-r--r--lib/api/files.rb7
-rw-r--r--lib/api/tags.rb14
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb81
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb34
-rw-r--r--lib/declarative_policy/runner.rb12
-rw-r--r--lib/github/client.rb36
-rw-r--r--lib/github/import.rb34
-rw-r--r--lib/gitlab/auth.rb3
-rw-r--r--lib/gitlab/conflict/file_collection.rb4
-rw-r--r--lib/gitlab/cycle_analytics/plan_event_fetcher.rb2
-rw-r--r--lib/gitlab/daemon.rb62
-rw-r--r--lib/gitlab/encoding_helper.rb2
-rw-r--r--lib/gitlab/environment.rb7
-rw-r--r--lib/gitlab/git/blame.rb2
-rw-r--r--lib/gitlab/git/blob.rb136
-rw-r--r--lib/gitlab/git/commit.rb124
-rw-r--r--lib/gitlab/git/commit_stats.rb2
-rw-r--r--lib/gitlab/git/diff_collection.rb3
-rw-r--r--lib/gitlab/git/repository.rb143
-rw-r--r--lib/gitlab/git/storage.rb22
-rw-r--r--lib/gitlab/git/storage/circuit_breaker.rb144
-rw-r--r--lib/gitlab/git/storage/forked_storage_check.rb55
-rw-r--r--lib/gitlab/git/storage/health.rb91
-rw-r--r--lib/gitlab/gitaly_client.rb4
-rw-r--r--lib/gitlab/gitaly_client/commit.rb14
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb46
-rw-r--r--lib/gitlab/gitaly_client/ref_service.rb8
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb5
-rw-r--r--lib/gitlab/health_checks/fs_shards_check.rb21
-rw-r--r--lib/gitlab/i18n.rb3
-rw-r--r--lib/gitlab/import_export/import_export.yml1
-rw-r--r--lib/gitlab/import_sources.rb2
-rw-r--r--lib/gitlab/key_fingerprint.rb71
-rw-r--r--lib/gitlab/metrics/base_sampler.rb75
-rw-r--r--lib/gitlab/metrics/sidekiq_metrics_exporter.rb39
-rw-r--r--lib/gitlab/project_template.rb45
-rw-r--r--lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb2
-rw-r--r--lib/gitlab/prometheus/queries/environment_query.rb2
-rw-r--r--lib/gitlab/shell.rb24
-rw-r--r--lib/gitlab/usage_data.rb4
-rw-r--r--lib/haml_lint/inline_javascript.rb16
-rw-r--r--lib/mattermost/session.rb26
-rw-r--r--lib/tasks/gitlab/gitaly.rake3
-rw-r--r--lib/tasks/gitlab/update_templates.rake49
-rw-r--r--lib/tasks/haml-lint.rake1
-rw-r--r--lib/tasks/import.rake3
-rw-r--r--locale/bg/gitlab.po7
-rw-r--r--locale/eo/gitlab.po7
-rw-r--r--locale/fr/gitlab.po9
-rw-r--r--locale/it/gitlab.po30
-rw-r--r--locale/ja/gitlab.po11
-rw-r--r--locale/ko/gitlab.po1207
-rw-r--r--locale/ko/gitlab.po.time_stamp0
-rw-r--r--locale/pt_BR/gitlab.po11
-rw-r--r--locale/ru/gitlab.po25
-rw-r--r--locale/uk/gitlab.po53
-rw-r--r--locale/zh_CN/gitlab.po3
-rw-r--r--locale/zh_HK/gitlab.po3
-rw-r--r--locale/zh_TW/gitlab.po7
-rw-r--r--package.json18
-rw-r--r--spec/controllers/admin/health_check_controller_spec.rb25
-rw-r--r--spec/controllers/application_controller_spec.rb24
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb20
-rw-r--r--spec/controllers/projects_controller_spec.rb14
-rw-r--r--spec/factories/conversational_development_index_metrics.rb10
-rw-r--r--spec/factories/projects.rb6
-rw-r--r--spec/features/admin/admin_health_check_spec.rb24
-rw-r--r--spec/features/boards/boards_spec.rb4
-rw-r--r--spec/features/boards/sidebar_spec.rb6
-rw-r--r--spec/features/dashboard/issues_spec.rb15
-rw-r--r--spec/features/groups/empty_states_spec.rb4
-rw-r--r--spec/features/issues/create_branch_merge_request_spec.rb14
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb8
-rw-r--r--spec/features/issues_spec.rb26
-rw-r--r--spec/features/merge_requests/closes_issues_spec.rb22
-rw-r--r--spec/features/merge_requests/form_spec.rb2
-rw-r--r--spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb10
-rw-r--r--spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb8
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb10
-rw-r--r--spec/features/merge_requests/widget_spec.rb13
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb14
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb2
-rw-r--r--spec/features/projects_spec.rb36
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/comment.json21
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/commit/detail.json16
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/commit_note.json19
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/commit_notes.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/commit_stats.json14
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/commits.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/release.json12
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/tag.json21
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/tags.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/basic.json15
-rw-r--r--spec/fixtures/encoding/Japanese.md42
-rw-r--r--spec/fixtures/markdown.md.erb5
-rw-r--r--spec/helpers/gitlab_routing_helper_spec.rb40
-rw-r--r--spec/helpers/milestones_routing_helper_spec.rb46
-rw-r--r--spec/helpers/projects_helper_spec.rb44
-rw-r--r--spec/helpers/storage_health_helper_spec.rb20
-rw-r--r--spec/initializers/6_validations_spec.rb21
-rw-r--r--spec/initializers/settings_spec.rb11
-rw-r--r--spec/javascripts/blob/blob_file_dropzone_spec.js42
-rw-r--r--spec/javascripts/blob/viewer/index_spec.js4
-rw-r--r--spec/javascripts/boards/issue_card_spec.js28
-rw-r--r--spec/javascripts/build_spec.js1
-rw-r--r--spec/javascripts/fixtures/project_select_combo_button.html.haml6
-rw-r--r--spec/javascripts/fixtures/snippet.rb (renamed from spec/javascripts/fixtures/blob.rb)12
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js1
-rw-r--r--spec/javascripts/project_select_combo_button_spec.js105
-rw-r--r--spec/javascripts/projects/project_import_gitlab_project_spec.js25
-rw-r--r--spec/javascripts/repo/components/repo_commit_section_spec.js158
-rw-r--r--spec/javascripts/repo/components/repo_edit_button_spec.js51
-rw-r--r--spec/javascripts/repo/components/repo_editor_spec.js26
-rw-r--r--spec/javascripts/repo/components/repo_file_buttons_spec.js82
-rw-r--r--spec/javascripts/repo/components/repo_file_options_spec.js33
-rw-r--r--spec/javascripts/repo/components/repo_file_spec.js136
-rw-r--r--spec/javascripts/repo/components/repo_loading_file_spec.js79
-rw-r--r--spec/javascripts/repo/components/repo_prev_directory_spec.js43
-rw-r--r--spec/javascripts/repo/components/repo_preview_spec.js23
-rw-r--r--spec/javascripts/repo/components/repo_sidebar_spec.js61
-rw-r--r--spec/javascripts/repo/components/repo_tab_spec.js88
-rw-r--r--spec/javascripts/repo/components/repo_tabs_spec.js64
-rw-r--r--spec/javascripts/repo/monaco_loader_spec.js17
-rw-r--r--spec/javascripts/repo/services/repo_service_spec.js121
-rw-r--r--spec/javascripts/sidebar/confidential_edit_buttons_spec.js39
-rw-r--r--spec/javascripts/sidebar/confidential_edit_form_buttons_spec.js39
-rw-r--r--spec/javascripts/sidebar/confidential_issue_sidebar_spec.js65
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js10
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js6
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js4
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js73
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js4
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js13
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js4
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js10
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js8
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js16
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js2
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js2
-rw-r--r--spec/lib/banzai/filter/milestone_reference_filter_spec.rb196
-rw-r--r--spec/lib/gitlab/auth_spec.rb3
-rw-r--r--spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb4
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb14
-rw-r--r--spec/lib/gitlab/daemon_spec.rb103
-rw-r--r--spec/lib/gitlab/encoding_helper_spec.rb47
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb71
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb59
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb46
-rw-r--r--spec/lib/gitlab/git/storage/circuit_breaker_spec.rb294
-rw-r--r--spec/lib/gitlab/git/storage/forked_storage_check_spec.rb58
-rw-r--r--spec/lib/gitlab/git/storage/health_spec.rb87
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb28
-rw-r--r--spec/lib/gitlab/health_checks/fs_shards_check_spec.rb15
-rw-r--r--spec/lib/gitlab/import_export/project.json9
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/lib/gitlab/import_sources_spec.rb2
-rw-r--r--spec/lib/gitlab/key_fingerprint_spec.rb84
-rw-r--r--spec/lib/gitlab/metrics/influx_sampler_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/sidekiq_metrics_exporter_spec.rb101
-rw-r--r--spec/lib/gitlab/project_template_spec.rb63
-rw-r--r--spec/lib/gitlab/shell_spec.rb90
-rw-r--r--spec/lib/mattermost/session_spec.rb7
-rw-r--r--spec/migrations/calculate_conv_dev_index_percentages_spec.rb41
-rw-r--r--spec/migrations/migrate_process_commit_worker_jobs_spec.rb2
-rw-r--r--spec/models/commit_spec.rb4
-rw-r--r--spec/models/conversational_development_index/metric_spec.rb11
-rw-r--r--spec/models/issue_spec.rb4
-rw-r--r--spec/models/key_spec.rb10
-rw-r--r--spec/models/merge_request_spec.rb26
-rw-r--r--spec/models/milestone_spec.rb38
-rw-r--r--spec/models/project_wiki_spec.rb14
-rw-r--r--spec/models/repository_spec.rb100
-rw-r--r--spec/models/user_spec.rb24
-rw-r--r--spec/models/wiki_page_spec.rb35
-rw-r--r--spec/presenters/conversational_development_index/metric_presenter_spec.rb6
-rw-r--r--spec/requests/api/circuit_breakers_spec.rb57
-rw-r--r--spec/requests/api/commits_spec.rb634
-rw-r--r--spec/requests/api/environments_spec.rb9
-rw-r--r--spec/requests/api/events_spec.rb35
-rw-r--r--spec/requests/api/projects_spec.rb10
-rw-r--r--spec/requests/api/tags_spec.rb471
-rw-r--r--spec/requests/api/v3/projects_spec.rb10
-rw-r--r--spec/serializers/merge_request_entity_spec.rb2
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb138
-rw-r--r--spec/services/projects/autocomplete_service_spec.rb27
-rw-r--r--spec/services/projects/create_from_template_service_spec.rb26
-rw-r--r--spec/services/projects/import_service_spec.rb32
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb20
-rw-r--r--spec/services/submit_usage_ping_service_spec.rb7
-rw-r--r--spec/services/system_note_service_spec.rb53
-rw-r--r--spec/services/wiki_pages/update_service_spec.rb4
-rw-r--r--spec/spec_helper.rb4
-rw-r--r--spec/support/cycle_analytics_helpers.rb4
-rw-r--r--spec/support/features/issuable_slash_commands_shared_examples.rb11
-rw-r--r--spec/support/issuable_shared_examples.rb6
-rw-r--r--spec/support/markdown_feature.rb6
-rw-r--r--spec/support/matchers/markdown_matchers.rb2
-rw-r--r--spec/support/migrations_helpers.rb10
-rw-r--r--spec/support/stored_repositories.rb12
-rw-r--r--spec/support/test_env.rb41
-rw-r--r--spec/tasks/gitlab/gitaly_rake_spec.rb12
-rw-r--r--spec/workers/merge_worker_spec.rb11
-rw-r--r--spec/workers/new_issue_worker_spec.rb54
-rw-r--r--spec/workers/new_merge_request_worker_spec.rb56
-rw-r--r--spec/workers/stuck_merge_jobs_worker_spec.rb50
-rw-r--r--vendor/assets/javascripts/jquery.nicescroll.js3634
-rw-r--r--vendor/project_templates/rails.tar.gzbin0 -> 899958 bytes
-rw-r--r--yarn.lock737
513 files changed, 14024 insertions, 6754 deletions
diff --git a/.eslintrc b/.eslintrc
index c72a5e0335b..3e07edbccfe 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -30,6 +30,7 @@
"filenames/match-regex": [2, "^[a-z0-9_]+$"],
"import/no-commonjs": "error",
"no-multiple-empty-lines": ["error", { "max": 1 }],
- "promise/catch-or-return": "error"
+ "promise/catch-or-return": "error",
+ "no-underscore-dangle": ["error", { "allow": ["__"]}]
}
}
diff --git a/.flayignore b/.flayignore
index e2d0a2e50c5..b63ce4c4df0 100644
--- a/.flayignore
+++ b/.flayignore
@@ -3,4 +3,5 @@ lib/gitlab/sanitizers/svg/whitelist.rb
lib/gitlab/diff/position_tracer.rb
app/policies/project_policy.rb
app/models/concerns/relative_positioning.rb
+app/workers/stuck_merge_jobs_worker.rb
lib/gitlab/redis/*.rb
diff --git a/.haml-lint.yml b/.haml-lint.yml
index 528f99d08d2..32c7de0fb78 100644
--- a/.haml-lint.yml
+++ b/.haml-lint.yml
@@ -35,9 +35,21 @@ linters:
HtmlAttributes:
enabled: true
+ IdNames:
+ enabled: false
+
ImplicitDiv:
enabled: true
+ InlineJavaScript:
+ enabled: true
+
+ InlineStyles:
+ enabled: false
+
+ InstanceVariables:
+ enabled: false
+
LeadingCommentSpace:
enabled: false
@@ -54,6 +66,9 @@ linters:
ObjectReferenceAttributes:
enabled: true
+ RepeatedId:
+ enabled: false
+
RuboCop:
enabled: false
# These cops are incredibly noisy when it comes to HAML templates, so we
@@ -101,3 +116,6 @@ linters:
UnnecessaryStringOutput:
enabled: true
+
+ ViewLength:
+ enabled: false
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 1b58cc10180..ae6dd4e2032 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.27.0
+0.29.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 42cdd0b540f..11d9efa3d5a 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-5.7.0
+5.8.0
diff --git a/Gemfile b/Gemfile
index 09934bf5654..08564ac0cae 100644
--- a/Gemfile
+++ b/Gemfile
@@ -342,7 +342,7 @@ group :development, :test do
gem 'rubocop', '~> 0.49.1', require: false
gem 'rubocop-rspec', '~> 1.15.1', require: false
gem 'scss_lint', '~> 0.54.0', require: false
- gem 'haml_lint', '~> 0.21.0', require: false
+ gem 'haml_lint', '~> 0.26.0', require: false
gem 'simplecov', '~> 0.14.0', require: false
gem 'flay', '~> 2.8.0', require: false
gem 'bundler-audit', '~> 0.5.0', require: false
@@ -390,8 +390,18 @@ gem 'health_check', '~> 2.6.0'
gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
+# SSH host key support
+gem 'net-ssh', '~> 4.1.0'
+
+# Required for ED25519 SSH host key support
+group :ed25519 do
+ gem 'rbnacl-libsodium'
+ gem 'rbnacl', '~> 3.2'
+ gem 'bcrypt_pbkdf', '~> 1.0'
+end
+
# Gitaly GRPC client
-gem 'gitaly', '~> 0.24.0'
+gem 'gitaly', '~> 0.26.0'
gem 'toml-rb', '~> 0.3.15', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 57cfe0a9de0..948ba02a72c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -75,6 +75,7 @@ GEM
babosa (1.0.2)
base32 (0.3.2)
bcrypt (3.1.11)
+ bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0)
better_errors (2.1.1)
coderay (>= 1.0.0)
@@ -269,7 +270,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly (0.24.0)
+ gitaly (0.26.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
@@ -356,10 +357,11 @@ GEM
googleauth (~> 0.5.1)
haml (4.0.7)
tilt
- haml_lint (0.21.0)
- haml (~> 4.0)
+ haml_lint (0.26.0)
+ haml (>= 4.0, < 5.1)
+ rainbow
rake (>= 10, < 13)
- rubocop (>= 0.47.0)
+ rubocop (>= 0.49.0)
sysexits (~> 1.1)
hamlit (2.6.1)
temple (~> 0.7.6)
@@ -474,6 +476,7 @@ GEM
mustermann (~> 1.0.0)
mysql2 (0.4.5)
net-ldap (0.16.0)
+ net-ssh (4.1.0)
netrc (0.11.0)
nokogiri (1.6.8.1)
mini_portile2 (~> 2.1.0)
@@ -661,6 +664,10 @@ GEM
rake (12.0.0)
rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3)
+ rbnacl (3.4.0)
+ ffi
+ rbnacl-libsodium (1.0.11)
+ rbnacl (>= 3.0.1)
rdoc (4.2.2)
json (~> 1.4)
re2 (1.1.1)
@@ -923,6 +930,7 @@ DEPENDENCIES
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
base32 (~> 0.3.0)
+ bcrypt_pbkdf (~> 1.0)
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2)
@@ -974,7 +982,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
- gitaly (~> 0.24.0)
+ gitaly (~> 0.26.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1)
@@ -987,7 +995,7 @@ DEPENDENCIES
grape (~> 0.19.2)
grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.0.0)
- haml_lint (~> 0.21.0)
+ haml_lint (~> 0.26.0)
hamlit (~> 2.6.1)
hashie-forbidden_attributes
health_check (~> 2.6.0)
@@ -1015,6 +1023,7 @@ DEPENDENCIES
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.4.5)
net-ldap
+ net-ssh (~> 4.1.0)
nokogiri (~> 1.6.7, >= 1.6.7.2)
oauth2 (~> 1.4)
octokit (~> 4.6.2)
@@ -1061,6 +1070,8 @@ DEPENDENCIES
rainbow (~> 2.2)
raindrops (~> 0.18)
rblineprof (~> 0.3.6)
+ rbnacl (~> 3.2)
+ rbnacl-libsodium
rdoc (~> 4.2)
re2 (~> 1.1.1)
recaptcha (~> 3.0)
diff --git a/app/assets/images/new_repo.png b/app/assets/images/new_repo.png
new file mode 100644
index 00000000000..ed3af06ab1d
--- /dev/null
+++ b/app/assets/images/new_repo.png
Binary files differ
diff --git a/app/assets/images/old_repo.png b/app/assets/images/old_repo.png
new file mode 100644
index 00000000000..c3c3b791ad9
--- /dev/null
+++ b/app/assets/images/old_repo.png
Binary files differ
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 56fa0d71a9a..76b724e1bcb 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -13,6 +13,7 @@ const Api = {
dockerfilePath: '/api/:version/templates/dockerfiles/:key',
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json',
+ commitPath: '/api/:version/projects/:id/repository/commits',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath)
@@ -95,6 +96,21 @@ const Api = {
.done(projects => callback(projects));
},
+ commitMultiple(id, data, callback) {
+ // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
+ const url = Api.buildUrl(Api.commitPath)
+ .replace(':id', id);
+ return $.ajax({
+ url,
+ type: 'POST',
+ contentType: 'application/json; charset=utf-8',
+ data: JSON.stringify(data),
+ dataType: 'json',
+ })
+ .done(commitData => callback(commitData))
+ .fail(message => callback(message.responseJSON));
+ },
+
// Return text for a specific license
licenseText(key, data, callback) {
const url = Api.buildUrl(Api.licensePath)
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index dc636050221..26d3419a162 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -1,9 +1,24 @@
/* eslint-disable func-names, object-shorthand, prefer-arrow-callback */
/* global Dropzone */
+import '../lib/utils/url_utility';
+import { HIDDEN_CLASS } from '../lib/utils/constants';
+
+function toggleLoading($el, $icon, loading) {
+ if (loading) {
+ $el.disable();
+ $icon.removeClass(HIDDEN_CLASS);
+ } else {
+ $el.enable();
+ $icon.addClass(HIDDEN_CLASS);
+ }
+}
export default class BlobFileDropzone {
constructor(form, method) {
const formDropzone = form.find('.dropzone');
+ const submitButton = form.find('#submit-all');
+ const submitButtonLoadingIcon = submitButton.find('.js-loading-icon');
+ const dropzoneMessage = form.find('.dz-message');
Dropzone.autoDiscover = false;
const dropzone = formDropzone.dropzone({
@@ -26,12 +41,20 @@ export default class BlobFileDropzone {
},
init: function () {
this.on('addedfile', function () {
+ toggleLoading(submitButton, submitButtonLoadingIcon, false);
+ dropzoneMessage.addClass(HIDDEN_CLASS);
$('.dropzone-alerts').html('').hide();
});
+ this.on('removedfile', function () {
+ toggleLoading(submitButton, submitButtonLoadingIcon, false);
+ dropzoneMessage.removeClass(HIDDEN_CLASS);
+ });
this.on('success', function (header, response) {
- window.location.href = response.filePath;
+ $('#modal-upload-blob').modal('hide');
+ window.gl.utils.visitUrl(response.filePath);
});
this.on('maxfilesexceeded', function (file) {
+ dropzoneMessage.addClass(HIDDEN_CLASS);
this.removeFile(file);
});
this.on('sending', function (file, xhr, formData) {
@@ -48,14 +71,15 @@ export default class BlobFileDropzone {
},
});
- const submitButton = form.find('#submit-all')[0];
- submitButton.addEventListener('click', function (e) {
+ submitButton.on('click', (e) => {
e.preventDefault();
e.stopPropagation();
if (dropzone[0].dropzone.getQueuedFiles().length === 0) {
// eslint-disable-next-line no-alert
alert('Please select a file');
+ return false;
}
+ toggleLoading(submitButton, submitButtonLoadingIcon, true);
dropzone[0].dropzone.processQueue();
return false;
});
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
index daef01bc93d..d3de1830895 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -97,9 +97,8 @@ gl.issueBoards.IssueCardInner = Vue.extend({
return `Avatar for ${assignee.name}`;
},
showLabel(label) {
- if (!this.list) return true;
-
- return !this.list.label || label.id !== this.list.label.id;
+ if (!this.list || !label) return true;
+ return true;
},
filterByLabel(label, e) {
if (!this.updateFilters) return;
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index b3d3bbcf84f..940326dcd33 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -164,7 +164,6 @@ window.Build = (function () {
Build.prototype.initSidebar = function () {
this.$sidebar = $('.js-build-sidebar');
- this.$sidebar.niceScroll();
};
Build.prototype.getBuildTrace = function () {
diff --git a/app/assets/javascripts/build_variables.js b/app/assets/javascripts/build_variables.js
index 99082b412e2..c955a9ac2ea 100644
--- a/app/assets/javascripts/build_variables.js
+++ b/app/assets/javascripts/build_variables.js
@@ -2,7 +2,7 @@
$(function() {
$('.reveal-variables').off('click').on('click', function() {
- $('.js-build').toggle().niceScroll();
+ $('.js-build-variables').toggle();
$(this).hide();
});
});
diff --git a/app/assets/javascripts/commons/bootstrap.js b/app/assets/javascripts/commons/bootstrap.js
index 389587a2596..c11b7d5f340 100644
--- a/app/assets/javascripts/commons/bootstrap.js
+++ b/app/assets/javascripts/commons/bootstrap.js
@@ -3,13 +3,13 @@ import $ from 'jquery';
// bootstrap jQuery plugins
import 'bootstrap-sass/assets/javascripts/bootstrap/affix';
import 'bootstrap-sass/assets/javascripts/bootstrap/alert';
+import 'bootstrap-sass/assets/javascripts/bootstrap/button';
import 'bootstrap-sass/assets/javascripts/bootstrap/dropdown';
import 'bootstrap-sass/assets/javascripts/bootstrap/modal';
import 'bootstrap-sass/assets/javascripts/bootstrap/tab';
import 'bootstrap-sass/assets/javascripts/bootstrap/transition';
import 'bootstrap-sass/assets/javascripts/bootstrap/tooltip';
import 'bootstrap-sass/assets/javascripts/bootstrap/popover';
-import 'bootstrap-sass/assets/javascripts/bootstrap/button';
// custom jQuery functions
$.fn.extend({
diff --git a/app/assets/javascripts/commons/jquery.js b/app/assets/javascripts/commons/jquery.js
index b53f6284afc..b93e94a3c97 100644
--- a/app/assets/javascripts/commons/jquery.js
+++ b/app/assets/javascripts/commons/jquery.js
@@ -6,6 +6,5 @@ import 'vendor/jquery.endless-scroll';
import 'vendor/jquery.caret';
import 'vendor/jquery.atwho';
import 'vendor/jquery.scrollTo';
-import 'vendor/jquery.nicescroll';
import 'vendor/jquery.waitforimages';
import 'select2/select2';
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index d1aa9a84292..de3c30772d2 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -75,6 +75,7 @@ import initNotes from './init_notes';
import initLegacyFilters from './init_legacy_filters';
import initIssuableSidebar from './init_issuable_sidebar';
import GpgBadges from './gpg_badges';
+import UserFeatureHelper from './helpers/user_feature_helper';
(function() {
var Dispatcher;
@@ -92,6 +93,7 @@ import GpgBadges from './gpg_badges';
if (!page) {
return false;
}
+
path = page.split(':');
shortcut_handler = null;
@@ -331,19 +333,16 @@ import GpgBadges from './gpg_badges';
break;
case 'projects:commits:show':
CommitsList.init(document.querySelector('.js-project-commits-show').dataset.commitsLimit);
- new gl.Activities();
shortcut_handler = new ShortcutsNavigation();
GpgBadges.fetch();
break;
case 'projects:show':
shortcut_handler = new ShortcutsNavigation();
new NotificationsForm();
- if ($('#tree-slider').length) {
- new TreeView();
- }
- if ($('.blob-viewer').length) {
- new BlobViewer();
- }
+
+ if ($('#tree-slider').length) new TreeView();
+ if ($('.blob-viewer').length) new BlobViewer();
+ if ($('.project-show-activity').length) new gl.Activities();
break;
case 'projects:edit':
setupProjectEdit();
@@ -407,6 +406,9 @@ import GpgBadges from './gpg_badges';
break;
case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation();
+
+ if (UserFeatureHelper.isNewRepo()) break;
+
new TreeView();
new BlobViewer();
new NewCommitForm($('.js-create-dir-form'));
@@ -425,6 +427,7 @@ import GpgBadges from './gpg_badges';
shortcut_handler = true;
break;
case 'projects:blob:show':
+ if (UserFeatureHelper.isNewRepo()) break;
new BlobViewer();
initBlob();
break;
@@ -577,7 +580,6 @@ import GpgBadges from './gpg_badges';
shortcut_handler = new ShortcutsWiki();
new ZenMode();
new gl.GLForm($('.wiki-form'), true);
- new Sidebar();
break;
case 'snippets':
shortcut_handler = new ShortcutsNavigation();
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
index 8e9a97fe207..301e82f4610 100644
--- a/app/assets/javascripts/fly_out_nav.js
+++ b/app/assets/javascripts/fly_out_nav.js
@@ -23,6 +23,7 @@ export const showSubLevelItems = (el) => {
const top = calculateTop(boundingRect, subItems.offsetHeight);
const isAbove = top < boundingRect.top;
+ subItems.classList.add('fly-out-list');
subItems.style.transform = `translate3d(0, ${Math.floor(top)}px, 0)`;
if (isAbove) {
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 7d11cd0b6b2..b62acfcd445 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,9 +1,53 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
+/* eslint-disable func-names, no-underscore-dangle, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */
/* global fuzzaldrinPlus */
import _ from 'underscore';
import { isObject } from './lib/utils/type_utility';
-var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote;
+var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput;
+
+GitLabDropdownInput = (function() {
+ function GitLabDropdownInput(input, options) {
+ var $inputContainer, $clearButton;
+ var _this = this;
+ this.input = input;
+ this.options = options;
+ this.fieldName = this.options.fieldName || 'field-name';
+ $inputContainer = this.input.parent();
+ $clearButton = $inputContainer.find('.js-dropdown-input-clear');
+ $clearButton.on('click', (function(_this) {
+ // Clear click
+ return function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ return _this.input.val('').trigger('input').focus();
+ };
+ })(this));
+
+ this.input
+ .on('keydown', function (e) {
+ var keyCode = e.which;
+ if (keyCode === 13 && !options.elIsInput) {
+ e.preventDefault();
+ }
+ })
+ .on('input', function(e) {
+ var val = e.currentTarget.value || _this.options.inputFieldName;
+ val = val.split(' ').join('-') // replaces space with dash
+ .replace(/[^a-zA-Z0-9 -]/g, '').toLowerCase() // replace non alphanumeric
+ .replace(/(-)\1+/g, '-'); // replace repeated dashes
+ _this.cb(_this.options.fieldName, val, {}, true);
+ _this.input.closest('.dropdown')
+ .find('.dropdown-toggle-text')
+ .text(val);
+ });
+ }
+
+ GitLabDropdownInput.prototype.onInput = function(cb) {
+ this.cb = cb;
+ };
+
+ return GitLabDropdownInput;
+})();
GitLabDropdownFilter = (function() {
var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS;
@@ -191,7 +235,7 @@ GitLabDropdownRemote = (function() {
})();
GitLabDropdown = (function() {
- var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex;
+ var ACTIVE_CLASS, FILTER_INPUT, NO_FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex;
LOADING_CLASS = "is-loading";
@@ -209,7 +253,9 @@ GitLabDropdown = (function() {
CURSOR_SELECT_SCROLL_PADDING = 5;
- FILTER_INPUT = '.dropdown-input .dropdown-input-field';
+ FILTER_INPUT = '.dropdown-input .dropdown-input-field:not(.dropdown-no-filter)';
+
+ NO_FILTER_INPUT = '.dropdown-input .dropdown-input-field.dropdown-no-filter';
function GitLabDropdown(el1, options) {
var searchFields, selector, self;
@@ -224,6 +270,7 @@ GitLabDropdown = (function() {
this.dropdown = selector != null ? $(selector) : $(this.el).parent();
// Set Defaults
this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
+ this.noFilterInput = this.options.noFilterInput || this.getElement(NO_FILTER_INPUT);
this.highlight = !!this.options.highlight;
this.filterInputBlur = this.options.filterInputBlur != null
? this.options.filterInputBlur
@@ -262,6 +309,10 @@ GitLabDropdown = (function() {
});
}
}
+ if (this.noFilterInput.length) {
+ this.plainInput = new GitLabDropdownInput(this.noFilterInput, this.options);
+ this.plainInput.onInput(this.addInput.bind(this));
+ }
// Init filterable
if (this.options.filterable) {
this.filter = new GitLabDropdownFilter(this.filterInput, {
@@ -753,9 +804,13 @@ GitLabDropdown = (function() {
}
};
- GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
+ GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject, single) {
var $input;
// Create hidden input for form
+ if (single) {
+ $('input[name="' + fieldName + '"]').remove();
+ }
+
$input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value);
if (this.options.inputId != null) {
$input.attr('id', this.options.inputId);
@@ -771,7 +826,7 @@ GitLabDropdown = (function() {
$input.attr('data-meta', selectedObject[this.options.inputMeta]);
}
- return this.dropdown.before($input);
+ this.dropdown.before($input).trigger('change');
};
GitLabDropdown.prototype.selectRowAtIndex = function(index) {
diff --git a/app/assets/javascripts/graphs/graphs_charts.js b/app/assets/javascripts/graphs/graphs_charts.js
index 279ffef770f..ec6eab34989 100644
--- a/app/assets/javascripts/graphs/graphs_charts.js
+++ b/app/assets/javascripts/graphs/graphs_charts.js
@@ -1,4 +1,5 @@
import Chart from 'vendor/Chart';
+import _ from 'underscore';
document.addEventListener('DOMContentLoaded', () => {
const projectChartData = JSON.parse(document.getElementById('projectChartData').innerHTML);
@@ -27,28 +28,25 @@ document.addEventListener('DOMContentLoaded', () => {
return generateChart();
};
- const chartData = (keys, values) => {
- const data = {
- labels: keys,
- datasets: [{
- fillColor: 'rgba(220,220,220,0.5)',
- strokeColor: 'rgba(220,220,220,1)',
- barStrokeWidth: 1,
- barValueSpacing: 1,
- barDatasetSpacing: 1,
- data: values,
- }],
- };
- return data;
- };
-
- const hourData = chartData(projectChartData.hour.keys, projectChartData.hour.values);
+ const chartData = data => ({
+ labels: Object.keys(data),
+ datasets: [{
+ fillColor: 'rgba(220,220,220,0.5)',
+ strokeColor: 'rgba(220,220,220,1)',
+ barStrokeWidth: 1,
+ barValueSpacing: 1,
+ barDatasetSpacing: 1,
+ data: _.values(data),
+ }],
+ });
+
+ const hourData = chartData(projectChartData.hour);
responsiveChart($('#hour-chart'), hourData);
- const dayData = chartData(projectChartData.weekDays.keys, projectChartData.weekDays.values);
+ const dayData = chartData(projectChartData.weekDays);
responsiveChart($('#weekday-chart'), dayData);
- const monthData = chartData(projectChartData.month.keys, projectChartData.month.values);
+ const monthData = chartData(projectChartData.month);
responsiveChart($('#month-chart'), monthData);
const data = projectChartData.languages;
diff --git a/app/assets/javascripts/helpers/user_feature_helper.js b/app/assets/javascripts/helpers/user_feature_helper.js
new file mode 100644
index 00000000000..fcd8569819c
--- /dev/null
+++ b/app/assets/javascripts/helpers/user_feature_helper.js
@@ -0,0 +1,11 @@
+import Cookies from 'js-cookie';
+
+function isNewRepo() {
+ return Cookies.get('new_repo') === 'true';
+}
+
+const UserFeatureHelper = {
+ isNewRepo,
+};
+
+export default UserFeatureHelper;
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index 1e96c7ab5cd..7a72509d234 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -1,2 +1,3 @@
/* eslint-disable import/prefer-default-export */
export const BYTES_IN_KIB = 1024;
+export const HIDDEN_CLASS = 'hidden';
diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/new_sidebar.js
index 5f98aff8ced..930218dd1f5 100644
--- a/app/assets/javascripts/new_sidebar.js
+++ b/app/assets/javascripts/new_sidebar.js
@@ -1,23 +1,65 @@
+import Cookies from 'js-cookie';
+import _ from 'underscore';
+/* global bp */
+import './breakpoints';
+
export default class NewNavSidebar {
constructor() {
this.initDomElements();
+ this.render();
}
initDomElements() {
+ this.$page = $('.page-with-sidebar');
this.$sidebar = $('.nav-sidebar');
this.$overlay = $('.mobile-overlay');
this.$openSidebar = $('.toggle-mobile-nav');
this.$closeSidebar = $('.close-nav-button');
+ this.$sidebarToggle = $('.js-toggle-sidebar');
}
bindEvents() {
this.$openSidebar.on('click', () => this.toggleSidebarNav(true));
this.$closeSidebar.on('click', () => this.toggleSidebarNav(false));
this.$overlay.on('click', () => this.toggleSidebarNav(false));
+ this.$sidebarToggle.on('click', () => {
+ const value = !this.$sidebar.hasClass('sidebar-icons-only');
+ this.toggleCollapsedSidebar(value);
+ });
+
+ $(window).on('resize', () => _.debounce(this.render(), 100));
+ }
+
+ static setCollapsedCookie(value) {
+ if (bp.getBreakpointSize() !== 'lg') {
+ return;
+ }
+ Cookies.set('sidebar_collapsed', value, { expires: 365 * 10 });
}
toggleSidebarNav(show) {
this.$sidebar.toggleClass('nav-sidebar-expanded', show);
this.$overlay.toggleClass('mobile-nav-open', show);
+ this.$sidebar.removeClass('sidebar-icons-only');
+ }
+
+ toggleCollapsedSidebar(collapsed) {
+ this.$sidebar.toggleClass('sidebar-icons-only', collapsed);
+ if (this.$sidebar.length) {
+ this.$page.toggleClass('page-with-new-sidebar', !collapsed);
+ this.$page.toggleClass('page-with-icon-sidebar', collapsed);
+ }
+ NewNavSidebar.setCollapsedCookie(collapsed);
+ }
+
+ render() {
+ const breakpoint = bp.getBreakpointSize();
+
+ if (breakpoint === 'sm' || breakpoint === 'md') {
+ this.toggleCollapsedSidebar(true);
+ } else if (breakpoint === 'lg') {
+ const collapse = Cookies.get('sidebar_collapsed') === 'true';
+ this.toggleCollapsedSidebar(collapse);
+ }
}
}
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 6e1744e8e72..1c2100a1c25 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -90,6 +90,7 @@ import Cookies from 'js-cookie';
filterable: true,
filterRemote: true,
filterByText: true,
+ inputFieldName: $dropdown.data('input-field-name'),
fieldName: $dropdown.data('field-name'),
renderRow: function(ref) {
var li = refListItem.cloneNode(false);
@@ -123,9 +124,14 @@ import Cookies from 'js-cookie';
e.preventDefault();
if ($('input[name="ref"]').length) {
var $form = $dropdown.closest('form');
+
+ var $visit = $dropdown.data('visit');
+ var shouldVisit = typeof $visit === 'undefined' ? true : $visit;
var action = $form.attr('action');
var divider = action.indexOf('?') === -1 ? '?' : '&';
- gl.utils.visitUrl(action + '' + divider + '' + $form.serialize());
+ if (shouldVisit) {
+ gl.utils.visitUrl(action + '' + divider + '' + $form.serialize());
+ }
}
}
});
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index ebcefc819f5..1b4ed6be90a 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -1,5 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */
import Api from './api';
+import ProjectSelectComboButton from './project_select_combo_button';
(function() {
this.ProjectSelect = (function() {
@@ -58,7 +59,8 @@ import Api from './api';
if (this.includeGroups) {
placeholder += " or group";
}
- return $(select).select2({
+
+ $(select).select2({
placeholder: placeholder,
minimumInputLength: 0,
query: (function(_this) {
@@ -96,21 +98,18 @@ import Api from './api';
};
})(this),
id: function(project) {
- return project.web_url;
+ return JSON.stringify({
+ name: project.name,
+ url: project.web_url,
+ });
},
text: function(project) {
return project.name_with_namespace || project.name;
},
dropdownCssClass: "ajax-project-dropdown"
});
- });
-
- $('.new-project-item-select-button').on('click', function() {
- $('.project-item-select', this.parentNode).select2('open');
- });
- $('.project-item-select').on('click', function() {
- window.location = `${$(this).val()}/${this.dataset.relativePath}`;
+ return new ProjectSelectComboButton(select);
});
}
diff --git a/app/assets/javascripts/project_select_combo_button.js b/app/assets/javascripts/project_select_combo_button.js
new file mode 100644
index 00000000000..f799d9d619a
--- /dev/null
+++ b/app/assets/javascripts/project_select_combo_button.js
@@ -0,0 +1,85 @@
+import AccessorUtilities from './lib/utils/accessor';
+
+export default class ProjectSelectComboButton {
+ constructor(select) {
+ this.projectSelectInput = $(select);
+ this.newItemBtn = $('.new-project-item-link');
+ this.newItemBtnBaseText = this.newItemBtn.data('label');
+ this.itemType = this.deriveItemTypeFromLabel();
+ this.groupId = this.projectSelectInput.data('groupId');
+
+ this.bindEvents();
+ this.initLocalStorage();
+ }
+
+ bindEvents() {
+ this.projectSelectInput.siblings('.new-project-item-select-button')
+ .on('click', this.openDropdown);
+
+ this.projectSelectInput.on('change', () => this.selectProject());
+ }
+
+ initLocalStorage() {
+ const localStorageIsSafe = AccessorUtilities.isLocalStorageAccessSafe();
+
+ if (localStorageIsSafe) {
+ const itemTypeKebabed = this.newItemBtnBaseText.toLowerCase().split(' ').join('-');
+
+ this.localStorageKey = ['group', this.groupId, itemTypeKebabed, 'recent-project'].join('-');
+ this.setBtnTextFromLocalStorage();
+ }
+ }
+
+ openDropdown() {
+ $(this).siblings('.project-item-select').select2('open');
+ }
+
+ selectProject() {
+ const selectedProjectData = JSON.parse(this.projectSelectInput.val());
+ const projectUrl = `${selectedProjectData.url}/${this.projectSelectInput.data('relativePath')}`;
+ const projectName = selectedProjectData.name;
+
+ const projectMeta = {
+ url: projectUrl,
+ name: projectName,
+ };
+
+ this.setNewItemBtnAttributes(projectMeta);
+ this.setProjectInLocalStorage(projectMeta);
+ }
+
+ setBtnTextFromLocalStorage() {
+ const cachedProjectData = this.getProjectFromLocalStorage();
+
+ this.setNewItemBtnAttributes(cachedProjectData);
+ }
+
+ setNewItemBtnAttributes(project) {
+ if (project) {
+ this.newItemBtn.attr('href', project.url);
+ this.newItemBtn.text(`${this.newItemBtnBaseText} in ${project.name}`);
+ this.newItemBtn.enable();
+ } else {
+ this.newItemBtn.text(`Select project to create ${this.itemType}`);
+ this.newItemBtn.disable();
+ }
+ }
+
+ deriveItemTypeFromLabel() {
+ // label is either 'New issue' or 'New merge request'
+ return this.newItemBtnBaseText.split(' ').slice(1).join(' ');
+ }
+
+ getProjectFromLocalStorage() {
+ const projectString = localStorage.getItem(this.localStorageKey);
+
+ return JSON.parse(projectString);
+ }
+
+ setProjectInLocalStorage(projectMeta) {
+ const projectString = JSON.stringify(projectMeta);
+
+ localStorage.setItem(this.localStorageKey, projectString);
+ }
+}
+
diff --git a/app/assets/javascripts/projects/project_import_gitlab_project.js b/app/assets/javascripts/projects/project_import_gitlab_project.js
new file mode 100644
index 00000000000..c34927499fc
--- /dev/null
+++ b/app/assets/javascripts/projects/project_import_gitlab_project.js
@@ -0,0 +1,14 @@
+import '../lib/utils/url_utility';
+
+const bindEvents = () => {
+ const path = gl.utils.getParameterValues('path')[0];
+
+ // get the path url and append it in the inputS
+ $('.js-path-name').val(path);
+};
+
+document.addEventListener('DOMContentLoaded', bindEvents);
+
+export default {
+ bindEvents,
+};
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 1dc1dbf356d..985521aef34 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -1,7 +1,7 @@
let hasUserDefinedProjectPath = false;
const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => {
- if ($projectImportUrl.attr('disabled') || hasUserDefinedProjectPath) {
+ if (hasUserDefinedProjectPath) {
return;
}
@@ -27,8 +27,6 @@ const deriveProjectPathFromUrl = ($projectImportUrl, $projectPath) => {
const bindEvents = () => {
const $newProjectForm = $('#new_project');
- const importBtnTooltip = 'Please enter a valid project name.';
- const $importBtnWrapper = $('.import_gitlab_project');
const $projectImportUrl = $('#project_import_url');
const $projectPath = $('#project_path');
@@ -50,31 +48,15 @@ const bindEvents = () => {
$('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$projectPath.val()}`);
});
- $('.btn_import_gitlab_project').attr('disabled', !$projectPath.val().trim().length);
- $importBtnWrapper.attr('title', importBtnTooltip);
-
$newProjectForm.on('submit', () => {
$projectPath.val($projectPath.val().trim());
});
$projectPath.on('keyup', () => {
hasUserDefinedProjectPath = $projectPath.val().trim().length > 0;
- if (hasUserDefinedProjectPath) {
- $('.btn_import_gitlab_project').attr('disabled', false);
- $importBtnWrapper.attr('title', '');
- $importBtnWrapper.removeClass('has-tooltip');
- } else {
- $('.btn_import_gitlab_project').attr('disabled', true);
- $importBtnWrapper.addClass('has-tooltip');
- }
});
- $projectImportUrl.disable();
$projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl, $projectPath));
-
- $('.import_git').on('click', () => {
- $projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled'));
- });
};
document.addEventListener('DOMContentLoaded', bindEvents);
diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue
new file mode 100644
index 00000000000..703da749ad3
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo.vue
@@ -0,0 +1,63 @@
+<script>
+import RepoSidebar from './repo_sidebar.vue';
+import RepoCommitSection from './repo_commit_section.vue';
+import RepoTabs from './repo_tabs.vue';
+import RepoFileButtons from './repo_file_buttons.vue';
+import RepoPreview from './repo_preview.vue';
+import RepoMixin from '../mixins/repo_mixin';
+import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
+import Store from '../stores/repo_store';
+import Helper from '../helpers/repo_helper';
+import MonacoLoaderHelper from '../helpers/monaco_loader_helper';
+
+export default {
+ data: () => Store,
+ mixins: [RepoMixin],
+ components: {
+ 'repo-sidebar': RepoSidebar,
+ 'repo-tabs': RepoTabs,
+ 'repo-file-buttons': RepoFileButtons,
+ 'repo-editor': MonacoLoaderHelper.repoEditorLoader,
+ 'repo-commit-section': RepoCommitSection,
+ 'popup-dialog': PopupDialog,
+ 'repo-preview': RepoPreview,
+ },
+
+ mounted() {
+ Helper.getContent().catch(Helper.loadingError);
+ },
+
+ methods: {
+ dialogToggled(toggle) {
+ this.dialog.open = toggle;
+ },
+
+ dialogSubmitted(status) {
+ this.dialog.open = false;
+ this.dialog.status = status;
+ },
+
+ toggleBlobView: Store.toggleBlobView,
+ },
+};
+</script>
+
+<template>
+<div class="repository-view tree-content-holder">
+ <repo-sidebar/><div class="panel-right" :class="{'edit-mode': editMode}">
+ <repo-tabs/>
+ <component :is="currentBlobView" class="blob-viewer-container"></component>
+ <repo-file-buttons/>
+ </div>
+ <repo-commit-section/>
+ <popup-dialog
+ :primary-button-label="__('Discard changes')"
+ :open="dialog.open"
+ kind="warning"
+ :title="__('Are you sure?')"
+ :body="__('Are you sure you want to discard your changes?')"
+ @toggle="dialogToggled"
+ @submit="dialogSubmitted"
+ />
+</div>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue
new file mode 100644
index 00000000000..bd83f80c928
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_commit_section.vue
@@ -0,0 +1,100 @@
+<script>
+/* global Flash */
+import Store from '../stores/repo_store';
+import RepoMixin from '../mixins/repo_mixin';
+import Helper from '../helpers/repo_helper';
+import Service from '../services/repo_service';
+
+const RepoCommitSection = {
+ data: () => Store,
+
+ mixins: [RepoMixin],
+
+ computed: {
+ branchPaths() {
+ const branch = Helper.getBranch();
+ return this.changedFiles.map(f => Helper.getFilePathFromFullPath(f.url, branch));
+ },
+
+ cantCommitYet() {
+ return !this.commitMessage || this.submitCommitsLoading;
+ },
+
+ filePluralize() {
+ return this.changedFiles.length > 1 ? 'files' : 'file';
+ },
+ },
+
+ methods: {
+ makeCommit() {
+ // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
+ const branch = Helper.getBranch();
+ const commitMessage = this.commitMessage;
+ const actions = this.changedFiles.map(f => ({
+ action: 'update',
+ file_path: Helper.getFilePathFromFullPath(f.url, branch),
+ content: f.newContent,
+ }));
+ const payload = {
+ branch: Store.targetBranch,
+ commit_message: commitMessage,
+ actions,
+ };
+ Store.submitCommitsLoading = true;
+ Service.commitFiles(payload, this.resetCommitState);
+ },
+
+ resetCommitState() {
+ this.submitCommitsLoading = false;
+ this.changedFiles = [];
+ this.openedFiles = [];
+ this.commitMessage = '';
+ this.editMode = false;
+ $('html, body').animate({ scrollTop: 0 }, 'fast');
+ },
+ },
+};
+
+export default RepoCommitSection;
+</script>
+
+<template>
+<div id="commit-area" v-if="isCommitable && changedFiles.length" >
+ <form class="form-horizontal">
+ <fieldset>
+ <div class="form-group">
+ <label class="col-md-4 control-label staged-files">Staged files ({{changedFiles.length}})</label>
+ <div class="col-md-4">
+ <ul class="list-unstyled changed-files">
+ <li v-for="file in branchPaths" :key="file.id">
+ <span class="help-block">{{file}}</span>
+ </li>
+ </ul>
+ </div>
+ </div>
+ <!-- Textarea
+ -->
+ <div class="form-group">
+ <label class="col-md-4 control-label" for="commit-message">Commit message</label>
+ <div class="col-md-4">
+ <textarea class="form-control" id="commit-message" name="commit-message" v-model="commitMessage"></textarea>
+ </div>
+ </div>
+ <!-- Button Drop Down
+ -->
+ <div class="form-group target-branch">
+ <label class="col-md-4 control-label" for="target-branch">Target branch</label>
+ <div class="col-md-4">
+ <span class="help-block">{{targetBranch}}</span>
+ </div>
+ </div>
+ <div class="col-md-offset-4 col-md-4">
+ <button type="submit" :disabled="cantCommitYet" class="btn btn-success submit-commit" @click.prevent="makeCommit">
+ <i class="fa fa-spinner fa-spin" v-if="submitCommitsLoading"></i>
+ <span class="commit-summary">Commit {{changedFiles.length}} {{filePluralize}}</span>
+ </button>
+ </div>
+ </fieldset>
+ </form>
+</div>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_edit_button.vue b/app/assets/javascripts/repo/components/repo_edit_button.vue
new file mode 100644
index 00000000000..e954fd38fc9
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_edit_button.vue
@@ -0,0 +1,49 @@
+<script>
+import Store from '../stores/repo_store';
+import RepoMixin from '../mixins/repo_mixin';
+
+export default {
+ data: () => Store,
+ mixins: [RepoMixin],
+ computed: {
+ buttonLabel() {
+ return this.editMode ? this.__('Cancel edit') : this.__('Edit');
+ },
+
+ buttonIcon() {
+ return this.editMode ? [] : ['fa', 'fa-pencil'];
+ },
+ },
+ methods: {
+ editClicked() {
+ if (this.changedFiles.length) {
+ this.dialog.open = true;
+ return;
+ }
+ this.editMode = !this.editMode;
+ Store.toggleBlobView();
+ },
+ },
+
+ watch: {
+ editMode() {
+ if (this.editMode) {
+ $('.project-refs-form').addClass('disabled');
+ $('.fa-long-arrow-right').show();
+ $('.project-refs-target-form').show();
+ } else {
+ $('.project-refs-form').removeClass('disabled');
+ $('.fa-long-arrow-right').hide();
+ $('.project-refs-target-form').hide();
+ }
+ },
+ },
+};
+</script>
+
+<template>
+<button class="btn btn-default" @click.prevent="editClicked" v-cloak v-if="isCommitable && !activeFile.render_error" :disabled="binary">
+ <i :class="buttonIcon"></i>
+ <span>{{buttonLabel}}</span>
+</button>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/repo/components/repo_editor.vue
new file mode 100644
index 00000000000..fd1a21e15b4
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_editor.vue
@@ -0,0 +1,135 @@
+<script>
+/* global monaco */
+import Store from '../stores/repo_store';
+import Service from '../services/repo_service';
+import Helper from '../helpers/repo_helper';
+
+const RepoEditor = {
+ data: () => Store,
+
+ destroyed() {
+ // this.monacoInstance.getModels().forEach((m) => {
+ // m.dispose();
+ // });
+ this.monacoInstance.destroy();
+ },
+
+ mounted() {
+ Service.getRaw(this.activeFile.raw_path)
+ .then((rawResponse) => {
+ Store.blobRaw = rawResponse.data;
+ Helper.findOpenedFileFromActive().plain = rawResponse.data;
+
+ const monacoInstance = this.monaco.editor.create(this.$el, {
+ model: null,
+ readOnly: false,
+ contextmenu: false,
+ });
+
+ Store.monacoInstance = monacoInstance;
+
+ this.addMonacoEvents();
+
+ const languages = this.monaco.languages.getLanguages();
+ const languageID = Helper.getLanguageIDForFile(this.activeFile, languages);
+ this.showHide();
+ const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
+
+ this.monacoInstance.setModel(newModel);
+ }).catch(Helper.loadingError);
+ },
+
+ methods: {
+ showHide() {
+ if (!this.openedFiles.length || (this.binary && !this.activeFile.raw)) {
+ this.$el.style.display = 'none';
+ } else {
+ this.$el.style.display = 'inline-block';
+ }
+ },
+
+ addMonacoEvents() {
+ this.monacoInstance.onMouseUp(this.onMonacoEditorMouseUp);
+ this.monacoInstance.onKeyUp(this.onMonacoEditorKeysPressed.bind(this));
+ },
+
+ onMonacoEditorKeysPressed() {
+ Store.setActiveFileContents(this.monacoInstance.getValue());
+ },
+
+ onMonacoEditorMouseUp(e) {
+ const lineNumber = e.target.position.lineNumber;
+ if (e.target.element.className === 'line-numbers') {
+ location.hash = `L${lineNumber}`;
+ Store.activeLine = lineNumber;
+ }
+ },
+ },
+
+ watch: {
+ activeLine() {
+ this.monacoInstance.setPosition({
+ lineNumber: this.activeLine,
+ column: 1,
+ });
+ },
+
+ activeFileLabel() {
+ this.showHide();
+ },
+
+ dialog: {
+ handler(obj) {
+ const newObj = obj;
+ if (newObj.status) {
+ newObj.status = false;
+ this.openedFiles.map((file) => {
+ const f = file;
+ if (f.active) {
+ this.blobRaw = f.plain;
+ }
+ f.changed = false;
+ delete f.newContent;
+
+ return f;
+ });
+ this.editMode = false;
+ }
+ },
+ deep: true,
+ },
+
+ isTree() {
+ this.showHide();
+ },
+
+ openedFiles() {
+ this.showHide();
+ },
+
+ binary() {
+ this.showHide();
+ },
+
+ blobRaw() {
+ this.showHide();
+
+ if (this.isTree) return;
+
+ this.monacoInstance.setModel(null);
+
+ const languages = this.monaco.languages.getLanguages();
+ const languageID = Helper.getLanguageIDForFile(this.activeFile, languages);
+ const newModel = this.monaco.editor.createModel(this.blobRaw, languageID);
+
+ this.monacoInstance.setModel(newModel);
+ },
+ },
+};
+
+export default RepoEditor;
+</script>
+
+<template>
+<div id="ide"></div>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/repo/components/repo_file.vue
new file mode 100644
index 00000000000..f604bc22a26
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_file.vue
@@ -0,0 +1,66 @@
+<script>
+import TimeAgoMixin from '../../vue_shared/mixins/timeago';
+
+const RepoFile = {
+ mixins: [TimeAgoMixin],
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ isMini: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ loading: {
+ type: Object,
+ required: false,
+ default() { return { tree: false }; },
+ },
+ hasFiles: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ activeFile: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ computed: {
+ canShowFile() {
+ return !this.loading.tree || this.hasFiles;
+ },
+ },
+
+ methods: {
+ linkClicked(file) {
+ this.$emit('linkclicked', file);
+ },
+ },
+};
+
+export default RepoFile;
+</script>
+
+<template>
+<tr class="file" v-if="canShowFile" :class="{'active': activeFile.url === file.url}">
+ <td @click.prevent="linkClicked(file)">
+ <i class="fa file-icon" v-if="!file.loading" :class="file.icon" :style="{'margin-left': file.level * 10 + 'px'}"></i>
+ <i class="fa fa-spinner fa-spin" v-if="file.loading" :style="{'margin-left': file.level * 10 + 'px'}"></i>
+ <a :href="file.url" class="repo-file-name" :title="file.url">{{file.name}}</a>
+ </td>
+
+ <td v-if="!isMini" class="hidden-sm hidden-xs">
+ <div class="commit-message">
+ <a :href="file.lastCommitUrl">{{file.lastCommitMessage}}</a>
+ </div>
+ </td>
+
+ <td v-if="!isMini" class="hidden-xs">
+ <span class="commit-update" :title="tooltipTitle(file.lastCommitUpdate)">{{timeFormated(file.lastCommitUpdate)}}</span>
+ </td>
+</tr>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/repo/components/repo_file_buttons.vue
new file mode 100644
index 00000000000..628d02ca704
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_file_buttons.vue
@@ -0,0 +1,42 @@
+<script>
+import Store from '../stores/repo_store';
+import Helper from '../helpers/repo_helper';
+import RepoMixin from '../mixins/repo_mixin';
+
+const RepoFileButtons = {
+ data: () => Store,
+
+ mixins: [RepoMixin],
+
+ computed: {
+
+ rawDownloadButtonLabel() {
+ return this.binary ? 'Download' : 'Raw';
+ },
+
+ canPreview() {
+ return Helper.isKindaBinary();
+ },
+ },
+
+ methods: {
+ rawPreviewToggle: Store.toggleRawPreview,
+ },
+};
+
+export default RepoFileButtons;
+</script>
+
+<template>
+<div id="repo-file-buttons" v-if="isMini">
+ <a :href="activeFile.raw_path" target="_blank" class="btn btn-default raw" rel="noopener noreferrer">{{rawDownloadButtonLabel}}</a>
+
+ <div class="btn-group" role="group" aria-label="File actions">
+ <a :href="activeFile.blame_path" class="btn btn-default blame">Blame</a>
+ <a :href="activeFile.commits_path" class="btn btn-default history">History</a>
+ <a :href="activeFile.permalink" class="btn btn-default permalink">Permalink</a>
+ </div>
+
+ <a href="#" v-if="canPreview" @click.prevent="rawPreviewToggle" class="btn btn-default preview">{{activeFileLabel}}</a>
+</div>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_file_options.vue b/app/assets/javascripts/repo/components/repo_file_options.vue
new file mode 100644
index 00000000000..ba53ce0eecc
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_file_options.vue
@@ -0,0 +1,25 @@
+<script>
+const RepoFileOptions = {
+ props: {
+ isMini: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ projectName: {
+ type: String,
+ required: true,
+ },
+ },
+};
+
+export default RepoFileOptions;
+</script>
+
+<template>
+<tr v-if="isMini" class="repo-file-options">
+ <td>
+ <span class="title">{{projectName}}</span>
+ </td>
+ </tr>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_loading_file.vue b/app/assets/javascripts/repo/components/repo_loading_file.vue
new file mode 100644
index 00000000000..38e9f16d041
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_loading_file.vue
@@ -0,0 +1,51 @@
+<script>
+const RepoLoadingFile = {
+ props: {
+ loading: {
+ type: Object,
+ required: false,
+ default: {},
+ },
+ hasFiles: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isMini: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+
+ methods: {
+ lineOfCode(n) {
+ return `line-of-code-${n}`;
+ },
+ },
+};
+
+export default RepoLoadingFile;
+</script>
+
+<template>
+<tr v-if="loading.tree && !hasFiles" class="loading-file">
+ <td>
+ <div class="animation-container animation-container-small">
+ <div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
+ </div>
+ </td>
+
+ <td v-if="!isMini" class="hidden-sm hidden-xs">
+ <div class="animation-container">
+ <div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
+ </div>
+ </td>
+
+ <td v-if="!isMini" class="hidden-xs">
+ <div class="animation-container animation-container-small">
+ <div v-for="n in 6" :class="lineOfCode(n)" :key="n"></div>
+ </div>
+ </td>
+</tr>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_prev_directory.vue b/app/assets/javascripts/repo/components/repo_prev_directory.vue
new file mode 100644
index 00000000000..6a0d684052f
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_prev_directory.vue
@@ -0,0 +1,26 @@
+<script>
+const RepoPreviousDirectory = {
+ props: {
+ prevUrl: {
+ type: String,
+ required: true,
+ },
+ },
+
+ methods: {
+ linkClicked(file) {
+ this.$emit('linkclicked', file);
+ },
+ },
+};
+
+export default RepoPreviousDirectory;
+</script>
+
+<template>
+<tr class="prev-directory">
+ <td colspan="3">
+ <a :href="prevUrl" @click.prevent="linkClicked(prevUrl)">..</a>
+ </td>
+</tr>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue
new file mode 100644
index 00000000000..d8de022335b
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_preview.vue
@@ -0,0 +1,32 @@
+<script>
+import Store from '../stores/repo_store';
+
+export default {
+ data: () => Store,
+ mounted() {
+ $(this.$el).find('.file-content').syntaxHighlight();
+ },
+ computed: {
+ html() {
+ return this.activeFile.html;
+ },
+ },
+
+ watch: {
+ html() {
+ this.$nextTick(() => {
+ $(this.$el).find('.file-content').syntaxHighlight();
+ });
+ },
+ },
+};
+</script>
+
+<template>
+<div>
+ <div v-if="!activeFile.render_error" v-html="activeFile.html"></div>
+ <div v-if="activeFile.render_error" class="vertical-center render-error">
+ <p class="text-center">The source could not be displayed because it is too large. You can <a :href="activeFile.raw_path">download</a> it instead.</p>
+ </div>
+</div>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue
new file mode 100644
index 00000000000..d6d832efc49
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_sidebar.vue
@@ -0,0 +1,104 @@
+<script>
+import Service from '../services/repo_service';
+import Helper from '../helpers/repo_helper';
+import Store from '../stores/repo_store';
+import RepoPreviousDirectory from './repo_prev_directory.vue';
+import RepoFileOptions from './repo_file_options.vue';
+import RepoFile from './repo_file.vue';
+import RepoLoadingFile from './repo_loading_file.vue';
+import RepoMixin from '../mixins/repo_mixin';
+
+const RepoSidebar = {
+ mixins: [RepoMixin],
+ components: {
+ 'repo-file-options': RepoFileOptions,
+ 'repo-previous-directory': RepoPreviousDirectory,
+ 'repo-file': RepoFile,
+ 'repo-loading-file': RepoLoadingFile,
+ },
+
+ created() {
+ this.addPopEventListener();
+ },
+
+ data: () => Store,
+
+ methods: {
+ addPopEventListener() {
+ window.addEventListener('popstate', () => {
+ if (location.href.indexOf('#') > -1) return;
+ this.linkClicked({
+ url: location.href,
+ });
+ });
+ },
+
+ linkClicked(clickedFile) {
+ let url = '';
+ let file = clickedFile;
+ if (typeof file === 'object') {
+ file.loading = true;
+ if (file.type === 'tree' && file.opened) {
+ file = Store.removeChildFilesOfTree(file);
+ file.loading = false;
+ } else {
+ url = file.url;
+ Service.url = url;
+ // I need to refactor this to do the `then` here.
+ // Not a callback. For now this is good enough.
+ // it works.
+ Helper.getContent(file, () => {
+ file.loading = false;
+ Helper.scrollTabsRight();
+ });
+ }
+ } else if (typeof file === 'string') {
+ // go back
+ url = file;
+ Service.url = url;
+ Helper.getContent(null, () => Helper.scrollTabsRight());
+ }
+ },
+ },
+};
+
+export default RepoSidebar;
+</script>
+
+<template>
+<div id="sidebar" :class="{'sidebar-mini' : isMini}" v-cloak>
+ <table class="table">
+ <thead v-if="!isMini">
+ <tr>
+ <th class="name">Name</th>
+ <th class="hidden-sm hidden-xs last-commit">Last Commit</th>
+ <th class="hidden-xs last-update">Last Update</th>
+ </tr>
+ </thead>
+ <tbody>
+ <repo-file-options
+ :is-mini="isMini"
+ :project-name="projectName"/>
+ <repo-previous-directory
+ v-if="isRoot"
+ :prev-url="prevURL"
+ @linkclicked="linkClicked(prevURL)"/>
+ <repo-loading-file
+ v-for="n in 5"
+ :key="n"
+ :loading="loading"
+ :has-files="!!files.length"
+ :is-mini="isMini"/>
+ <repo-file
+ v-for="file in files"
+ :key="file.id"
+ :file="file"
+ :is-mini="isMini"
+ @linkclicked="linkClicked(file)"
+ :is-tree="isTree"
+ :has-files="!!files.length"
+ :active-file="activeFile"/>
+ </tbody>
+ </table>
+</div>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/repo/components/repo_tab.vue
new file mode 100644
index 00000000000..712d64c236f
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_tab.vue
@@ -0,0 +1,45 @@
+<script>
+import Store from '../stores/repo_store';
+
+const RepoTab = {
+ props: {
+ tab: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ computed: {
+ changedClass() {
+ const tabChangedObj = {
+ 'fa-times': !this.tab.changed,
+ 'fa-circle': this.tab.changed,
+ };
+ return tabChangedObj;
+ },
+ },
+
+ methods: {
+ tabClicked: Store.setActiveFiles,
+
+ xClicked(file) {
+ if (file.changed) return;
+ this.$emit('xclicked', file);
+ },
+ },
+};
+
+export default RepoTab;
+</script>
+
+<template>
+<li>
+ <a href="#" class="close" @click.prevent="xClicked(tab)" v-if="!tab.loading">
+ <i class="fa" :class="changedClass"></i>
+ </a>
+
+ <a href="#" class="repo-tab" v-if="!tab.loading" :title="tab.url" @click.prevent="tabClicked(tab)">{{tab.name}}</a>
+
+ <i v-if="tab.loading" class="fa fa-spinner fa-spin"></i>
+</li>
+</template>
diff --git a/app/assets/javascripts/repo/components/repo_tabs.vue b/app/assets/javascripts/repo/components/repo_tabs.vue
new file mode 100644
index 00000000000..907a03e1601
--- /dev/null
+++ b/app/assets/javascripts/repo/components/repo_tabs.vue
@@ -0,0 +1,43 @@
+<script>
+import Vue from 'vue';
+import Store from '../stores/repo_store';
+import RepoTab from './repo_tab.vue';
+import RepoMixin from '../mixins/repo_mixin';
+
+const RepoTabs = {
+ mixins: [RepoMixin],
+
+ components: {
+ 'repo-tab': RepoTab,
+ },
+
+ data: () => Store,
+
+ methods: {
+ isOverflow() {
+ return this.$el.scrollWidth > this.$el.offsetWidth;
+ },
+
+ xClicked(file) {
+ Store.removeFromOpenedFiles(file);
+ },
+ },
+
+ watch: {
+ openedFiles() {
+ Vue.nextTick(() => {
+ this.tabsOverflow = this.isOverflow();
+ });
+ },
+ },
+};
+
+export default RepoTabs;
+</script>
+
+<template>
+<ul id="tabs" v-if="isMini" v-cloak :class="{'overflown': tabsOverflow}">
+ <repo-tab v-for="tab in openedFiles" :key="tab.id" :tab="tab" :class="{'active' : tab.active}" @xclicked="xClicked"/>
+ <li class="tabs-divider" />
+</ul>
+</template>
diff --git a/app/assets/javascripts/repo/helpers/monaco_loader_helper.js b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js
new file mode 100644
index 00000000000..8ee2df5c879
--- /dev/null
+++ b/app/assets/javascripts/repo/helpers/monaco_loader_helper.js
@@ -0,0 +1,21 @@
+/* global monaco */
+import RepoEditor from '../components/repo_editor.vue';
+import Store from '../stores/repo_store';
+import monacoLoader from '../monaco_loader';
+
+function repoEditorLoader() {
+ Store.monacoLoading = true;
+ return new Promise((resolve, reject) => {
+ monacoLoader(['vs/editor/editor.main'], () => {
+ Store.monaco = monaco;
+ Store.monacoLoading = false;
+ resolve(RepoEditor);
+ }, reject);
+ });
+}
+
+const MonacoLoaderHelper = {
+ repoEditorLoader,
+};
+
+export default MonacoLoaderHelper;
diff --git a/app/assets/javascripts/repo/helpers/repo_helper.js b/app/assets/javascripts/repo/helpers/repo_helper.js
new file mode 100644
index 00000000000..fee98c12592
--- /dev/null
+++ b/app/assets/javascripts/repo/helpers/repo_helper.js
@@ -0,0 +1,303 @@
+/* global Flash */
+import Service from '../services/repo_service';
+import Store from '../stores/repo_store';
+import '../../flash';
+
+const RepoHelper = {
+ getDefaultActiveFile() {
+ return {
+ active: true,
+ binary: false,
+ extension: '',
+ html: '',
+ mime_type: '',
+ name: '',
+ plain: '',
+ size: 0,
+ url: '',
+ raw: false,
+ newContent: '',
+ changed: false,
+ loading: false,
+ };
+ },
+
+ key: '',
+
+ isTree(data) {
+ return Object.hasOwnProperty.call(data, 'blobs');
+ },
+
+ Time: window.performance
+ && window.performance.now
+ ? window.performance
+ : Date,
+
+ getBranch() {
+ return $('button.dropdown-menu-toggle').attr('data-ref');
+ },
+
+ getLanguageIDForFile(file, langs) {
+ const ext = file.name.split('.').pop();
+ const foundLang = RepoHelper.findLanguage(ext, langs);
+
+ return foundLang ? foundLang.id : 'plaintext';
+ },
+
+ getFilePathFromFullPath(fullPath, branch) {
+ return fullPath.split(`${Store.projectUrl}/blob/${branch}`)[1];
+ },
+
+ findLanguage(ext, langs) {
+ return langs.find(lang => lang.extensions && lang.extensions.indexOf(`.${ext}`) > -1);
+ },
+
+ setDirectoryOpen(tree) {
+ const file = tree;
+ if (!file) return undefined;
+
+ file.opened = true;
+ file.icon = 'fa-folder-open';
+ RepoHelper.toURL(file.url, file.name);
+ return file;
+ },
+
+ isKindaBinary() {
+ const okExts = ['md', 'svg'];
+ return okExts.indexOf(Store.activeFile.extension) > -1;
+ },
+
+ setBinaryDataAsBase64(file) {
+ Service.getBase64Content(file.raw_path)
+ .then((response) => {
+ Store.blobRaw = response;
+ file.base64 = response; // eslint-disable-line no-param-reassign
+ })
+ .catch(RepoHelper.loadingError);
+ },
+
+ toggleFakeTab(loading, file) {
+ if (loading) return Store.addPlaceholderFile();
+ return Store.removeFromOpenedFiles(file);
+ },
+
+ setLoading(loading, file) {
+ if (Service.url.indexOf('blob') > -1) {
+ Store.loading.blob = loading;
+ return RepoHelper.toggleFakeTab(loading, file);
+ }
+
+ if (Service.url.indexOf('tree') > -1) Store.loading.tree = loading;
+
+ return undefined;
+ },
+
+ getNewMergedList(inDirectory, currentList, newList) {
+ const newListSorted = newList.sort(this.compareFilesCaseInsensitive);
+ if (!inDirectory) return newListSorted;
+ const indexOfFile = currentList.findIndex(file => file.url === inDirectory.url);
+ if (!indexOfFile) return newListSorted;
+ return RepoHelper.mergeNewListToOldList(newListSorted, currentList, inDirectory, indexOfFile);
+ },
+
+ mergeNewListToOldList(newList, oldList, inDirectory, indexOfFile) {
+ newList.reverse().forEach((newFile) => {
+ const fileIndex = indexOfFile + 1;
+ const file = newFile;
+ file.level = inDirectory.level + 1;
+ oldList.splice(fileIndex, 0, file);
+ });
+
+ return oldList;
+ },
+
+ compareFilesCaseInsensitive(a, b) {
+ const aName = a.name.toLowerCase();
+ const bName = b.name.toLowerCase();
+ if (a.level > 0) return 0;
+ if (aName < bName) { return -1; }
+ if (aName > bName) { return 1; }
+ return 0;
+ },
+
+ isRoot(url) {
+ // the url we are requesting -> split by the project URL. Grab the right side.
+ const isRoot = !!url.split(Store.projectUrl)[1]
+ // remove the first "/"
+ .slice(1)
+ // split this by "/"
+ .split('/')
+ // remove the first two items of the array... usually /tree/master.
+ .slice(2)
+ // we want to know the length of the array.
+ // If greater than 0 not root.
+ .length;
+ return isRoot;
+ },
+
+ getContent(treeOrFile, cb) {
+ let file = treeOrFile;
+ // const loadingData = RepoHelper.setLoading(true);
+ return Service.getContent()
+ .then((response) => {
+ const data = response.data;
+ // RepoHelper.setLoading(false, loadingData);
+ if (cb) cb();
+ Store.isTree = RepoHelper.isTree(data);
+ if (!Store.isTree) {
+ if (!file) file = data;
+ Store.binary = data.binary;
+
+ if (data.binary) {
+ Store.binaryMimeType = data.mime_type;
+ // file might be undefined
+ RepoHelper.setBinaryDataAsBase64(data);
+ Store.setViewToPreview();
+ } else if (!Store.isPreviewView()) {
+ if (!data.render_error) {
+ Service.getRaw(data.raw_path)
+ .then((rawResponse) => {
+ Store.blobRaw = rawResponse.data;
+ data.plain = rawResponse.data;
+ RepoHelper.setFile(data, file);
+ }).catch(RepoHelper.loadingError);
+ }
+ }
+
+ if (Store.isPreviewView()) {
+ RepoHelper.setFile(data, file);
+ }
+
+ // if the file tree is empty
+ if (Store.files.length === 0) {
+ const parentURL = Service.blobURLtoParentTree(Service.url);
+ Service.url = parentURL;
+ RepoHelper.getContent();
+ }
+ } else {
+ // it's a tree
+ if (!file) Store.isRoot = RepoHelper.isRoot(Service.url);
+ file = RepoHelper.setDirectoryOpen(file);
+ const newDirectory = RepoHelper.dataToListOfFiles(data);
+ Store.addFilesToDirectory(file, Store.files, newDirectory);
+ Store.prevURL = Service.blobURLtoParentTree(Service.url);
+ }
+ }).catch(RepoHelper.loadingError);
+ },
+
+ setFile(data, file) {
+ const newFile = data;
+
+ newFile.url = file.url || location.pathname;
+ newFile.url = file.url;
+ if (newFile.render_error === 'too_large') {
+ newFile.tooLarge = true;
+ }
+ newFile.newContent = '';
+
+ Store.addToOpenedFiles(newFile);
+ Store.setActiveFiles(newFile);
+ },
+
+ toFA(icon) {
+ return `fa-${icon}`;
+ },
+
+ serializeBlob(blob) {
+ const simpleBlob = RepoHelper.serializeRepoEntity('blob', blob);
+ simpleBlob.lastCommitMessage = blob.last_commit.message;
+ simpleBlob.lastCommitUpdate = blob.last_commit.committed_date;
+ simpleBlob.loading = false;
+
+ return simpleBlob;
+ },
+
+ serializeTree(tree) {
+ return RepoHelper.serializeRepoEntity('tree', tree);
+ },
+
+ serializeSubmodule(submodule) {
+ return RepoHelper.serializeRepoEntity('submodule', submodule);
+ },
+
+ serializeRepoEntity(type, entity) {
+ const { url, name, icon, last_commit } = entity;
+ const returnObj = {
+ type,
+ name,
+ url,
+ icon: RepoHelper.toFA(icon),
+ level: 0,
+ loading: false,
+ };
+
+ if (entity.last_commit) {
+ returnObj.lastCommitUrl = `${Store.projectUrl}/commit/${last_commit.id}`;
+ } else {
+ returnObj.lastCommitUrl = '';
+ }
+ return returnObj;
+ },
+
+ scrollTabsRight() {
+ // wait for the transition. 0.1 seconds.
+ setTimeout(() => {
+ const tabs = document.getElementById('tabs');
+ if (!tabs) return;
+ tabs.scrollLeft = 12000;
+ }, 200);
+ },
+
+ dataToListOfFiles(data) {
+ const a = [];
+
+ // push in blobs
+ data.blobs.forEach((blob) => {
+ a.push(RepoHelper.serializeBlob(blob));
+ });
+
+ data.trees.forEach((tree) => {
+ a.push(RepoHelper.serializeTree(tree));
+ });
+
+ data.submodules.forEach((submodule) => {
+ a.push(RepoHelper.serializeSubmodule(submodule));
+ });
+
+ return a;
+ },
+
+ genKey() {
+ return RepoHelper.Time.now().toFixed(3);
+ },
+
+ getStateKey() {
+ return RepoHelper.key;
+ },
+
+ setStateKey(key) {
+ RepoHelper.key = key;
+ },
+
+ toURL(url, title) {
+ const history = window.history;
+
+ RepoHelper.key = RepoHelper.genKey();
+
+ history.pushState({ key: RepoHelper.key }, '', url);
+
+ if (title) {
+ document.title = `${title} · GitLab`;
+ }
+ },
+
+ findOpenedFileFromActive() {
+ return Store.openedFiles.find(openedFile => Store.activeFile.url === openedFile.url);
+ },
+
+ loadingError() {
+ Flash('Unable to load the file at this time.');
+ },
+};
+
+export default RepoHelper;
diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js
new file mode 100644
index 00000000000..67c03680fca
--- /dev/null
+++ b/app/assets/javascripts/repo/index.js
@@ -0,0 +1,74 @@
+import $ from 'jquery';
+import Vue from 'vue';
+import Service from './services/repo_service';
+import Store from './stores/repo_store';
+import Repo from './components/repo.vue';
+import RepoEditButton from './components/repo_edit_button.vue';
+import Translate from '../vue_shared/translate';
+
+function initDropdowns() {
+ $('.project-refs-target-form').hide();
+ $('.fa-long-arrow-right').hide();
+}
+
+function addEventsForNonVueEls() {
+ $(document).on('change', '.dropdown', () => {
+ Store.targetBranch = $('.project-refs-target-form input[name="ref"]').val();
+ });
+
+ window.onbeforeunload = function confirmUnload(e) {
+ const hasChanged = Store.openedFiles
+ .some(file => file.changed);
+ if (!hasChanged) return undefined;
+ const event = e || window.event;
+ if (event) event.returnValue = 'Are you sure you want to lose unsaved changes?';
+ // For Safari
+ return 'Are you sure you want to lose unsaved changes?';
+ };
+}
+
+function setInitialStore(data) {
+ Store.service = Service;
+ Store.service.url = data.url;
+ Store.service.refsUrl = data.refsUrl;
+ Store.projectId = data.projectId;
+ Store.projectName = data.projectName;
+ Store.projectUrl = data.projectUrl;
+ Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
+ Store.checkIsCommitable();
+}
+
+function initRepo(el) {
+ return new Vue({
+ el,
+ components: {
+ repo: Repo,
+ },
+ });
+}
+
+function initRepoEditButton(el) {
+ return new Vue({
+ el,
+ components: {
+ repoEditButton: RepoEditButton,
+ },
+ });
+}
+
+function initRepoBundle() {
+ const repo = document.getElementById('repo');
+ const editButton = document.querySelector('.editable-mode');
+ setInitialStore(repo.dataset);
+ addEventsForNonVueEls();
+ initDropdowns();
+
+ Vue.use(Translate);
+
+ initRepo(repo);
+ initRepoEditButton(editButton);
+}
+
+$(initRepoBundle);
+
+export default initRepoBundle;
diff --git a/app/assets/javascripts/repo/mixins/repo_mixin.js b/app/assets/javascripts/repo/mixins/repo_mixin.js
new file mode 100644
index 00000000000..c8e8238a0d3
--- /dev/null
+++ b/app/assets/javascripts/repo/mixins/repo_mixin.js
@@ -0,0 +1,17 @@
+import Store from '../stores/repo_store';
+
+const RepoMixin = {
+ computed: {
+ isMini() {
+ return !!Store.openedFiles.length;
+ },
+
+ changedFiles() {
+ const changedFileList = this.openedFiles
+ .filter(file => file.changed);
+ return changedFileList;
+ },
+ },
+};
+
+export default RepoMixin;
diff --git a/app/assets/javascripts/repo/monaco_loader.js b/app/assets/javascripts/repo/monaco_loader.js
new file mode 100644
index 00000000000..ad1370a7730
--- /dev/null
+++ b/app/assets/javascripts/repo/monaco_loader.js
@@ -0,0 +1,13 @@
+/* eslint-disable no-underscore-dangle, camelcase */
+/* global __webpack_public_path__ */
+
+import monacoContext from 'monaco-editor/dev/vs/loader';
+
+monacoContext.require.config({
+ paths: {
+ vs: `${__webpack_public_path__}monaco-editor/vs`,
+ },
+});
+
+window.__monaco_context__ = monacoContext;
+export default monacoContext.require;
diff --git a/app/assets/javascripts/repo/services/repo_service.js b/app/assets/javascripts/repo/services/repo_service.js
new file mode 100644
index 00000000000..8fba928e456
--- /dev/null
+++ b/app/assets/javascripts/repo/services/repo_service.js
@@ -0,0 +1,82 @@
+/* global Flash */
+import axios from 'axios';
+import Store from '../stores/repo_store';
+import Api from '../../api';
+
+const RepoService = {
+ url: '',
+ options: {
+ params: {
+ format: 'json',
+ },
+ },
+ richExtensionRegExp: /md/,
+
+ checkCurrentBranchIsCommitable() {
+ const url = Store.service.refsUrl;
+ return axios.get(url, { params: {
+ ref: Store.currentBranch,
+ search: Store.currentBranch,
+ } });
+ },
+
+ getRaw(url) {
+ return axios.get(url, {
+ transformResponse: [res => res],
+ });
+ },
+
+ buildParams(url = this.url) {
+ // shallow clone object without reference
+ const params = Object.assign({}, this.options.params);
+
+ if (this.urlIsRichBlob(url)) params.viewer = 'rich';
+
+ return params;
+ },
+
+ urlIsRichBlob(url = this.url) {
+ const extension = url.split('.').pop();
+
+ return this.richExtensionRegExp.test(extension);
+ },
+
+ getContent(url = this.url) {
+ const params = this.buildParams(url);
+
+ return axios.get(url, {
+ params,
+ });
+ },
+
+ getBase64Content(url = this.url) {
+ const request = axios.get(url, {
+ responseType: 'arraybuffer',
+ });
+
+ return request.then(response => this.bufferToBase64(response.data));
+ },
+
+ bufferToBase64(data) {
+ return new Buffer(data, 'binary').toString('base64');
+ },
+
+ blobURLtoParentTree(url) {
+ const urlArray = url.split('/');
+ urlArray.pop();
+ const blobIndex = urlArray.lastIndexOf('blob');
+
+ if (blobIndex > -1) urlArray[blobIndex] = 'tree';
+
+ return urlArray.join('/');
+ },
+
+ commitFiles(payload, cb) {
+ Api.commitMultiple(Store.projectId, payload, (data) => {
+ Flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
+ cb();
+ });
+ },
+};
+
+export default RepoService;
diff --git a/app/assets/javascripts/repo/stores/repo_store.js b/app/assets/javascripts/repo/stores/repo_store.js
new file mode 100644
index 00000000000..06ca391ed0c
--- /dev/null
+++ b/app/assets/javascripts/repo/stores/repo_store.js
@@ -0,0 +1,241 @@
+/* global Flash */
+import Helper from '../helpers/repo_helper';
+import Service from '../services/repo_service';
+
+const RepoStore = {
+ ideEl: {},
+ monaco: {},
+ monacoLoading: false,
+ monacoInstance: {},
+ service: '',
+ editor: '',
+ sidebar: '',
+ editMode: false,
+ isTree: false,
+ isRoot: false,
+ prevURL: '',
+ projectId: '',
+ projectName: '',
+ projectUrl: '',
+ trees: [],
+ blobs: [],
+ submodules: [],
+ blobRaw: '',
+ blobRendered: '',
+ currentBlobView: 'repo-preview',
+ openedFiles: [],
+ tabSize: 100,
+ defaultTabSize: 100,
+ minTabSize: 30,
+ tabsOverflow: 41,
+ submitCommitsLoading: false,
+ binaryLoaded: false,
+ dialog: {
+ open: false,
+ title: '',
+ status: false,
+ },
+ activeFile: Helper.getDefaultActiveFile(),
+ activeFileIndex: 0,
+ activeLine: 0,
+ activeFileLabel: 'Raw',
+ files: [],
+ isCommitable: false,
+ binary: false,
+ currentBranch: '',
+ targetBranch: 'new-branch',
+ commitMessage: '',
+ binaryMimeType: '',
+ // scroll bar space for windows
+ scrollWidth: 0,
+ binaryTypes: {
+ png: false,
+ md: false,
+ svg: false,
+ unknown: false,
+ },
+ loading: {
+ tree: false,
+ blob: false,
+ },
+ readOnly: true,
+
+ resetBinaryTypes() {
+ Object.keys(RepoStore.binaryTypes).forEach((key) => {
+ RepoStore.binaryTypes[key] = false;
+ });
+ },
+
+ // mutations
+ checkIsCommitable() {
+ RepoStore.service.checkCurrentBranchIsCommitable()
+ .then((data) => {
+ // you shouldn't be able to make commits on commits or tags.
+ const { Branches, Commits, Tags } = data.data;
+ if (Branches && Branches.length) RepoStore.isCommitable = true;
+ if (Commits && Commits.length) RepoStore.isCommitable = false;
+ if (Tags && Tags.length) RepoStore.isCommitable = false;
+ }).catch(() => Flash('Failed to check if branch can be committed to.'));
+ },
+
+ addFilesToDirectory(inDirectory, currentList, newList) {
+ RepoStore.files = Helper.getNewMergedList(inDirectory, currentList, newList);
+ },
+
+ toggleRawPreview() {
+ RepoStore.activeFile.raw = !RepoStore.activeFile.raw;
+ RepoStore.activeFileLabel = RepoStore.activeFile.raw ? 'Display rendered file' : 'Display source';
+ },
+
+ setActiveFiles(file) {
+ if (RepoStore.isActiveFile(file)) return;
+ RepoStore.openedFiles = RepoStore.openedFiles
+ .map((openedFile, i) => RepoStore.setFileActivity(file, openedFile, i));
+
+ RepoStore.setActiveToRaw();
+
+ if (file.binary) {
+ RepoStore.blobRaw = file.base64;
+ RepoStore.binaryMimeType = file.mime_type;
+ } else if (file.newContent || file.plain) {
+ RepoStore.blobRaw = file.newContent || file.plain;
+ } else {
+ Service.getRaw(file.raw_path)
+ .then((rawResponse) => {
+ RepoStore.blobRaw = rawResponse.data;
+ Helper.findOpenedFileFromActive().plain = rawResponse.data;
+ }).catch(Helper.loadingError);
+ }
+
+ if (!file.loading) Helper.toURL(file.url, file.name);
+ RepoStore.binary = file.binary;
+ },
+
+ setFileActivity(file, openedFile, i) {
+ const activeFile = openedFile;
+ activeFile.active = file.url === activeFile.url;
+
+ if (activeFile.active) RepoStore.setActiveFile(activeFile, i);
+
+ return activeFile;
+ },
+
+ setActiveFile(activeFile, i) {
+ RepoStore.activeFile = Object.assign({}, RepoStore.activeFile, activeFile);
+ RepoStore.activeFileIndex = i;
+ },
+
+ setActiveToRaw() {
+ RepoStore.activeFile.raw = false;
+ // can't get vue to listen to raw for some reason so RepoStore for now.
+ RepoStore.activeFileLabel = 'Display source';
+ },
+
+ removeChildFilesOfTree(tree) {
+ let foundTree = false;
+ const treeToClose = tree;
+ let wereDone = false;
+ RepoStore.files = RepoStore.files.filter((file) => {
+ const isItTheTreeWeWant = file.url === treeToClose.url;
+ // if it's the next tree
+ if (foundTree && file.type === 'tree' && !isItTheTreeWeWant && file.level === treeToClose.level) {
+ wereDone = true;
+ return true;
+ }
+ if (wereDone) return true;
+
+ if (isItTheTreeWeWant) foundTree = true;
+
+ if (foundTree) return file.level <= treeToClose.level;
+ return true;
+ });
+
+ treeToClose.opened = false;
+ treeToClose.icon = 'fa-folder';
+ return treeToClose;
+ },
+
+ removeFromOpenedFiles(file) {
+ if (file.type === 'tree') return;
+ let foundIndex;
+ RepoStore.openedFiles = RepoStore.openedFiles.filter((openedFile, i) => {
+ if (openedFile.url === file.url) foundIndex = i;
+ return openedFile.url !== file.url;
+ });
+
+ // now activate the right tab based on what you closed.
+ if (RepoStore.openedFiles.length === 0) {
+ RepoStore.activeFile = {};
+ return;
+ }
+
+ if (RepoStore.openedFiles.length === 1 || foundIndex === 0) {
+ RepoStore.setActiveFiles(RepoStore.openedFiles[0]);
+ return;
+ }
+
+ if (foundIndex) {
+ if (foundIndex > 0) {
+ RepoStore.setActiveFiles(RepoStore.openedFiles[foundIndex - 1]);
+ }
+ }
+ },
+
+ addPlaceholderFile() {
+ const randomURL = Helper.Time.now();
+ const newFakeFile = {
+ active: false,
+ binary: true,
+ type: 'blob',
+ loading: true,
+ mime_type: 'loading',
+ name: 'loading',
+ url: randomURL,
+ fake: true,
+ };
+
+ RepoStore.openedFiles.push(newFakeFile);
+
+ return newFakeFile;
+ },
+
+ addToOpenedFiles(file) {
+ const openFile = file;
+
+ const openedFilesAlreadyExists = RepoStore.openedFiles
+ .some(openedFile => openedFile.url === openFile.url);
+
+ if (openedFilesAlreadyExists) return;
+
+ openFile.changed = false;
+ RepoStore.openedFiles.push(openFile);
+ },
+
+ setActiveFileContents(contents) {
+ if (!RepoStore.editMode) return;
+ const currentFile = RepoStore.openedFiles[RepoStore.activeFileIndex];
+ RepoStore.activeFile.newContent = contents;
+ RepoStore.activeFile.changed = RepoStore.activeFile.plain !== RepoStore.activeFile.newContent;
+ currentFile.changed = RepoStore.activeFile.changed;
+ currentFile.newContent = contents;
+ },
+
+ toggleBlobView() {
+ RepoStore.currentBlobView = RepoStore.isPreviewView() ? 'repo-editor' : 'repo-preview';
+ },
+
+ setViewToPreview() {
+ RepoStore.currentBlobView = 'repo-preview';
+ },
+
+ // getters
+
+ isActiveFile(file) {
+ return file && file.url === RepoStore.activeFile.url;
+ },
+
+ isPreviewView() {
+ return RepoStore.currentBlobView === 'repo-preview';
+ },
+};
+export default RepoStore;
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
new file mode 100644
index 00000000000..422c02c7b7e
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -0,0 +1,82 @@
+<script>
+/* global Flash */
+import editForm from './edit_form.vue';
+
+export default {
+ components: {
+ editForm,
+ },
+ props: {
+ isConfidential: {
+ required: true,
+ type: Boolean,
+ },
+ isEditable: {
+ required: true,
+ type: Boolean,
+ },
+ service: {
+ required: true,
+ type: Object,
+ },
+ },
+ data() {
+ return {
+ edit: false,
+ };
+ },
+ computed: {
+ faEye() {
+ const eye = this.isConfidential ? 'fa-eye-slash' : 'fa-eye';
+ return {
+ [eye]: true,
+ };
+ },
+ },
+ methods: {
+ toggleForm() {
+ this.edit = !this.edit;
+ },
+ updateConfidentialAttribute(confidential) {
+ this.service.update('issue', { confidential })
+ .then(() => location.reload())
+ .catch(() => new Flash('Something went wrong trying to change the confidentiality of this issue'));
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="block confidentiality">
+ <div class="sidebar-collapsed-icon">
+ <i class="fa" :class="faEye" aria-hidden="true" data-hidden="true"></i>
+ </div>
+ <div class="title hide-collapsed">
+ Confidentiality
+ <a
+ v-if="isEditable"
+ class="pull-right confidential-edit"
+ href="#"
+ @click.prevent="toggleForm"
+ >
+ Edit
+ </a>
+ </div>
+ <div class="value confidential-value hide-collapsed">
+ <editForm
+ v-if="edit"
+ :toggle-form="toggleForm"
+ :is-confidential="isConfidential"
+ :update-confidential-attribute="updateConfidentialAttribute"
+ />
+ <div v-if="!isConfidential" class="no-value confidential-value">
+ <i class="fa fa-eye is-not-confidential"></i>
+ None
+ </div>
+ <div v-else class="value confidential-value hide-collapsed">
+ <i aria-hidden="true" data-hidden="true" class="fa fa-eye-slash is-confidential"></i>
+ This issue is confidential
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
new file mode 100644
index 00000000000..d578b663a54
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
@@ -0,0 +1,47 @@
+<script>
+import editFormButtons from './edit_form_buttons.vue';
+
+export default {
+ components: {
+ editFormButtons,
+ },
+ props: {
+ isConfidential: {
+ required: true,
+ type: Boolean,
+ },
+ toggleForm: {
+ required: true,
+ type: Function,
+ },
+ updateConfidentialAttribute: {
+ required: true,
+ type: Function,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown open">
+ <div class="dropdown-menu confidential-warning-message">
+ <div>
+ <p v-if="!isConfidential">
+ You are going to turn on the confidentiality. This means that only team members with
+ <strong>at least Reporter access</strong>
+ are able to see and leave comments on the issue.
+ </p>
+ <p v-else>
+ You are going to turn off the confidentiality. This means
+ <strong>everyone</strong>
+ will be able to see and leave a comment on this issue.
+ </p>
+ <edit-form-buttons
+ :is-confidential="isConfidential"
+ :toggle-form="toggleForm"
+ :update-confidential-attribute="updateConfidentialAttribute"
+ />
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
new file mode 100644
index 00000000000..97af4a3f505
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
@@ -0,0 +1,45 @@
+<script>
+export default {
+ props: {
+ isConfidential: {
+ required: true,
+ type: Boolean,
+ },
+ toggleForm: {
+ required: true,
+ type: Function,
+ },
+ updateConfidentialAttribute: {
+ required: true,
+ type: Function,
+ },
+ },
+ computed: {
+ onOrOff() {
+ return this.isConfidential ? 'Turn Off' : 'Turn On';
+ },
+ updateConfidentialBool() {
+ return !this.isConfidential;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="confidential-warning-message-actions">
+ <button
+ type="button"
+ class="btn btn-default append-right-10"
+ @click="toggleForm"
+ >
+ Cancel
+ </button>
+ <button
+ type="button"
+ class="btn btn-close"
+ @click.prevent="updateConfidentialAttribute(updateConfidentialBool)"
+ >
+ {{ onOrOff }}
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js
index a9df66748c5..9edded3ead6 100644
--- a/app/assets/javascripts/sidebar/sidebar_bundle.js
+++ b/app/assets/javascripts/sidebar/sidebar_bundle.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import sidebarTimeTracking from './components/time_tracking/sidebar_time_tracking';
import sidebarAssignees from './components/assignees/sidebar_assignees';
+import confidential from './components/confidential/confidential_issue_sidebar.vue';
import Mediator from './sidebar_mediator';
@@ -10,13 +11,28 @@ function domContentLoaded() {
mediator.fetch();
const sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees');
-
+ const confidentialEl = document.querySelector('#js-confidential-entry-point');
// Only create the sidebarAssignees vue app if it is found in the DOM
// We currently do not use sidebarAssignees for the MR page
if (sidebarAssigneesEl) {
new Vue(sidebarAssignees).$mount(sidebarAssigneesEl);
}
+ if (confidentialEl) {
+ const dataNode = document.getElementById('js-confidential-issue-data');
+ const initialData = JSON.parse(dataNode.innerHTML);
+
+ const ConfidentialComp = Vue.extend(confidential);
+
+ new ConfidentialComp({
+ propsData: {
+ isConfidential: initialData.is_confidential,
+ isEditable: initialData.is_editable,
+ service: mediator.service,
+ },
+ }).$mount(confidentialEl);
+ }
+
new Vue(sidebarTimeTracking).$mount('#issuable-time-tracker');
}
diff --git a/app/assets/javascripts/test_utils/index.js b/app/assets/javascripts/test_utils/index.js
index ef401abce2d..8875590f0f2 100644
--- a/app/assets/javascripts/test_utils/index.js
+++ b/app/assets/javascripts/test_utils/index.js
@@ -1,3 +1,5 @@
+import 'core-js/es6/map';
+import 'core-js/es6/set';
import simulateDrag from './simulate_drag';
// Export to global space for rspec to use
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js
index a01cb8cc202..982b5e8e373 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js
@@ -1,3 +1,5 @@
+import tooltip from '../../vue_shared/directives/tooltip';
+
export default {
name: 'MRWidgetAuthor',
props: {
@@ -5,11 +7,14 @@ export default {
showAuthorName: { type: Boolean, required: false, default: true },
showAuthorTooltip: { type: Boolean, required: false, default: false },
},
+ directives: {
+ tooltip,
+ },
template: `
<a
:href="author.webUrl || author.web_url"
- class="author-link"
- :class="{ 'has-tooltip': showAuthorTooltip }"
+ class="author-link inline"
+ :v-tooltip="showAuthorTooltip"
:title="author.name">
<img
:src="author.avatarUrl || author.avatar_url"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
index 744a1cd24fa..e98d147733c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
@@ -1,8 +1,8 @@
/* global Flash */
import '~/lib/utils/datetime_utility';
-import { statusIconEntityMap } from '../../vue_shared/ci_status_icons';
import MemoryUsage from './mr_widget_memory_usage';
+import StatusIcon from './mr_widget_status_icon';
import MRWidgetService from '../services/mr_widget_service';
export default {
@@ -13,11 +13,7 @@ export default {
},
components: {
'mr-widget-memory-usage': MemoryUsage,
- },
- computed: {
- svg() {
- return statusIconEntityMap.icon_status_success;
- },
+ 'status-icon': StatusIcon,
},
methods: {
formatDate(date) {
@@ -51,51 +47,51 @@ export default {
},
},
template: `
- <div class="mr-widget-heading">
+ <div class="mr-widget-heading deploy-heading">
<div v-for="deployment in mr.deployments">
- <div class="ci-widget">
+ <div class="ci-widget media">
<div class="ci-status-icon ci-status-icon-success">
<span class="js-icon-link icon-link">
- <span class="ci-status-icon"
- v-html="svg"
- aria-hidden="true"></span>
+ <status-icon status="success" />
</span>
</div>
- <span>
- <span
- v-if="hasDeploymentMeta(deployment)">
- Deployed to
- </span>
- <a
- v-if="hasDeploymentMeta(deployment)"
- :href="deployment.url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="js-deploy-meta">
- {{deployment.name}}
- </a>
- <span
- v-if="hasExternalUrls(deployment)">
- on
- </span>
- <a
- v-if="hasExternalUrls(deployment)"
- :href="deployment.external_url"
- target="_blank"
- rel="noopener noreferrer nofollow"
- class="js-deploy-url">
- <i
- class="fa fa-external-link"
- aria-hidden="true" />
- {{deployment.external_url_formatted}}
- </a>
- <span
- v-if="hasDeploymentTime(deployment)"
- :data-title="deployment.deployed_at_formatted"
- class="js-deploy-time"
- data-toggle="tooltip"
- data-placement="top">
- {{formatDate(deployment.deployed_at)}}
+ <div class="media-body space-children">
+ <span>
+ <span
+ v-if="hasDeploymentMeta(deployment)">
+ Deployed to
+ </span>
+ <a
+ v-if="hasDeploymentMeta(deployment)"
+ :href="deployment.url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="js-deploy-meta inline">
+ {{deployment.name}}
+ </a>
+ <span
+ v-if="hasExternalUrls(deployment)">
+ on
+ </span>
+ <a
+ v-if="hasExternalUrls(deployment)"
+ :href="deployment.external_url"
+ target="_blank"
+ rel="noopener noreferrer nofollow"
+ class="js-deploy-url inline">
+ <i
+ class="fa fa-external-link"
+ aria-hidden="true" />
+ {{deployment.external_url_formatted}}
+ </a>
+ <span
+ v-if="hasDeploymentTime(deployment)"
+ :data-title="deployment.deployed_at_formatted"
+ class="js-deploy-time"
+ data-toggle="tooltip"
+ data-placement="top">
+ {{formatDate(deployment.deployed_at)}}
+ </span>
</span>
<button
type="button"
@@ -104,13 +100,13 @@ export default {
class="btn btn-default btn-xs">
Stop environment
</button>
- </span>
+ <mr-widget-memory-usage
+ v-if="deployment.metrics_url"
+ :metrics-url="deployment.metrics_url"
+ :metrics-monitoring-url="deployment.metrics_monitoring_url"
+ />
+ </div>
</div>
- <mr-widget-memory-usage
- v-if="deployment.metrics_url"
- :metrics-url="deployment.metrics_url"
- :metrics-monitoring-url="deployment.metrics_monitoring_url"
- />
</div>
</div>
`,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
index 8430548903c..c05a76a3b4a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
@@ -1,3 +1,4 @@
+import tooltip from '../../vue_shared/directives/tooltip';
import '../../lib/utils/text_utility';
export default {
@@ -5,6 +6,9 @@ export default {
props: {
mr: { type: Object, required: true },
},
+ directives: {
+ tooltip,
+ },
computed: {
shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
@@ -29,18 +33,51 @@ export default {
},
template: `
<div class="mr-source-target">
- <div
- v-if="mr.isOpen"
- class="pull-right">
+ <div class="normal">
+ <strong>
+ Request to merge
+ <span
+ class="label-branch"
+ :class="{'label-truncated': isBranchTitleLong(mr.sourceBranch)}"
+ :title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
+ data-placement="bottom"
+ :v-tooltip="isBranchTitleLong(mr.sourceBranch)"
+ v-html="mr.sourceBranchLink"></span>
+ <button
+ v-tooltip
+ class="btn btn-transparent btn-clipboard"
+ data-title="Copy branch name to clipboard"
+ :data-clipboard-text="branchNameClipboardData">
+ <i
+ aria-hidden="true"
+ class="fa fa-clipboard"></i>
+ </button>
+ into
+ <span
+ class="label-branch"
+ :v-tooltip="isBranchTitleLong(mr.sourceBranch)"
+ :class="{'label-truncatedtooltip': isBranchTitleLong(mr.targetBranch)}"
+ :title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
+ data-placement="bottom">
+ <a :href="mr.targetBranchTreePath">{{mr.targetBranch}}</a>
+ </span>
+ </strong>
+ <span
+ v-if="shouldShowCommitsBehindText"
+ class="diverged-commits-count">
+ (<a :href="mr.targetBranchPath">{{mr.divergedCommitsCount}} {{commitsText}} behind</a>)
+ </span>
+ </div>
+ <div v-if="mr.isOpen">
<a
href="#modal_merge_info"
data-toggle="modal"
- class="btn inline btn-grouped btn-sm">
+ class="btn btn-small inline">
Check out branch
</a>
- <span class="dropdown inline prepend-left-5">
+ <span class="dropdown inline prepend-left-10">
<a
- class="btn btn-sm dropdown-toggle"
+ class="btn btn-xs dropdown-toggle"
data-toggle="dropdown"
aria-label="Download as"
role="button">
@@ -69,38 +106,6 @@ export default {
</ul>
</span>
</div>
- <div class="normal">
- <strong>
- Request to merge
- <span
- class="label-branch"
- :class="{'label-truncated has-tooltip': isBranchTitleLong(mr.sourceBranch)}"
- :title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
- data-placement="bottom"
- v-html="mr.sourceBranchLink"></span>
- <button
- class="btn btn-transparent btn-clipboard has-tooltip"
- data-title="Copy branch name to clipboard"
- :data-clipboard-text="branchNameClipboardData">
- <i
- aria-hidden="true"
- class="fa fa-clipboard"></i>
- </button>
- into
- <span
- class="label-branch"
- :class="{'label-truncated has-tooltip': isBranchTitleLong(mr.targetBranch)}"
- :title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
- data-placement="bottom">
- <a :href="mr.targetBranchTreePath">{{mr.targetBranch}}</a>
- </span>
- </strong>
- <span
- v-if="shouldShowCommitsBehindText"
- class="diverged-commits-count">
- (<a :href="mr.targetBranchPath">{{mr.divergedCommitsCount}} {{commitsText}} behind</a>)
- </span>
- </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
index 534e2a88eff..a4e34116c33 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
@@ -120,13 +120,12 @@ export default {
},
template: `
<div class="mr-info-list clearfix mr-memory-usage js-mr-memory-usage">
- <div class="legend"></div>
<p
v-if="shouldShowLoading"
class="usage-info js-usage-info usage-info-loading">
<i
class="fa fa-spinner fa-spin usage-info-load-spinner"
- aria-hidden="true" />Loading deployment statistics.
+ aria-hidden="true" />Loading deployment statistics
</p>
<p
v-if="shouldShowMemoryGraph"
@@ -136,12 +135,12 @@ export default {
<p
v-if="shouldShowLoadFailure"
class="usage-info js-usage-info usage-info-failed">
- Failed to load deployment statistics.
+ Failed to load deployment statistics
</p>
<p
v-if="shouldShowMetricsUnavailable"
class="usage-info js-usage-info usage-info-unavailable">
- Deployment statistics are not available currently.
+ Deployment statistics are not available currently
</p>
<mr-memory-graph
v-if="shouldShowMemoryGraph"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js
index 2fecebce7a0..1d9f9863dd9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js
@@ -16,7 +16,7 @@ export default {
<a
data-toggle="modal"
href="#modal_merge_info">
- command line.
+ command line
</a>
</section>
`,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
index c02e10128e2..6c2e9ba1d30 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
@@ -29,58 +29,55 @@ export default {
},
template: `
<div class="mr-widget-heading">
- <div class="ci-widget">
+ <div class="ci-widget media">
<template v-if="hasCIError">
- <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error">
- <span class="js-icon-link icon-link">
- <span
- v-html="svg"
- aria-hidden="true"></span>
- </span>
+ <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-10">
+ <span
+ v-html="svg"
+ aria-hidden="true"></span>
+ </div>
+ <div class="media-body">
+ Could not connect to the CI server. Please check your settings and try again
</div>
- <span>Could not connect to the CI server. Please check your settings and try again.</span>
</template>
<template v-else>
- <div>
+ <div class="ci-status-icon append-right-10">
<a
class="icon-link"
:href="this.status.details_path">
<ci-icon :status="status" />
</a>
</div>
- <span>
- Pipeline
- <a
- :href="mr.pipeline.path"
- class="pipeline-id">#{{mr.pipeline.id}}</a>
- {{mr.pipeline.details.status.label}}
- </span>
- <span
- v-if="mr.pipeline.details.stages.length > 0">
- with {{stageText}}
- </span>
- <div class="mr-widget-pipeline-graph">
- <div class="stage-cell">
- <div
- v-if="mr.pipeline.details.stages.length > 0"
- v-for="stage in mr.pipeline.details.stages"
- class="stage-container dropdown js-mini-pipeline-graph">
- <pipeline-stage :stage="stage" />
- </div>
- </div>
+ <div class="media-body">
+ <span>
+ Pipeline
+ <a
+ :href="mr.pipeline.path"
+ class="pipeline-id">#{{mr.pipeline.id}}</a>
+ </span>
+ <span class="mr-widget-pipeline-graph">
+ <span class="stage-cell">
+ <div
+ v-if="mr.pipeline.details.stages.length > 0"
+ v-for="stage in mr.pipeline.details.stages"
+ class="stage-container dropdown js-mini-pipeline-graph">
+ <pipeline-stage :stage="stage" />
+ </div>
+ </span>
+ </span>
+ <span>
+ {{mr.pipeline.details.status.label}} for
+ <a
+ :href="mr.pipeline.commit.commit_path"
+ class="commit-sha js-commit-link">
+ {{mr.pipeline.commit.short_id}}</a>.
+ </span>
+ <span
+ v-if="mr.pipeline.coverage"
+ class="js-mr-coverage">
+ Coverage {{mr.pipeline.coverage}}%
+ </span>
</div>
- <span>
- for
- <a
- :href="mr.pipeline.commit.commit_path"
- class="commit-sha js-commit-link">
- {{mr.pipeline.commit.short_id}}</a>.
- </span>
- <span
- v-if="mr.pipeline.coverage"
- class="js-mr-coverage">
- Coverage {{mr.pipeline.coverage}}%.
- </span>
</template>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js
index 205804670fa..563267ad044 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js
@@ -2,37 +2,32 @@ export default {
name: 'MRWidgetRelatedLinks',
props: {
relatedLinks: { type: Object, required: true },
+ state: { type: String, required: false },
},
computed: {
hasLinks() {
const { closing, mentioned, assignToMe } = this.relatedLinks;
return closing || mentioned || assignToMe;
},
- },
- methods: {
- hasMultipleIssues(text) {
- return !text ? false : text.match(/<\/a> and <a/);
- },
- issueLabel(field) {
- return this.hasMultipleIssues(this.relatedLinks[field]) ? 'issues' : 'issue';
- },
- verbLabel(field) {
- return this.hasMultipleIssues(this.relatedLinks[field]) ? 'are' : 'is';
+ closesText() {
+ if (this.state === 'merged') {
+ return 'Closed';
+ }
+ if (this.state === 'closed') {
+ return 'Did not close';
+ }
+ return 'Closes';
},
},
template: `
<section
v-if="hasLinks"
class="mr-info-list mr-links">
- <div class="legend"></div>
<p v-if="relatedLinks.closing">
- Closes {{issueLabel('closing')}}
- <span v-html="relatedLinks.closing"></span>.
+ {{closesText}} <span v-html="relatedLinks.closing"></span>
</p>
<p v-if="relatedLinks.mentioned">
- <span class="capitalize">{{issueLabel('mentioned')}}</span>
- <span v-html="relatedLinks.mentioned"></span>
- {{verbLabel('mentioned')}} mentioned but will not be closed.
+ Mentions <span v-html="relatedLinks.mentioned"></span>
</p>
<p v-if="relatedLinks.assignToMe">
<span v-html="relatedLinks.assignToMe"></span>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
new file mode 100644
index 00000000000..b01c923311b
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.js
@@ -0,0 +1,36 @@
+import ciIcon from '../../vue_shared/components/ci_icon.vue';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+
+export default {
+ props: {
+ status: { type: String, required: true },
+ showDisabledButton: { type: Boolean, required: false },
+ },
+ components: {
+ ciIcon,
+ loadingIcon,
+ },
+ computed: {
+ statusObj() {
+ return {
+ group: this.status,
+ icon: `icon_status_${this.status}`,
+ };
+ },
+ },
+ template: `
+ <div class="space-children flex-container-block append-right-10">
+ <div v-if="status === 'loading'" class="mr-widget-icon">
+ <loading-icon />
+ </div>
+ <ci-icon v-else :status="statusObj" />
+ <button
+ v-if="showDisabledButton"
+ type="button"
+ class="btn btn-success btn-small"
+ disabled="true">
+ Merge
+ </button>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js
index c7f25a1697c..2b16a2d6817 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js
@@ -1,16 +1,26 @@
+import statusIcon from '../mr_widget_status_icon';
+
export default {
name: 'MRWidgetArchived',
+ components: {
+ statusIcon,
+ },
template: `
- <div class="mr-widget-body">
- <button
- type="button"
- class="btn btn-success btn-small"
- disabled="true">
- Merge
- </button>
- <span class="bold">
- This project is archived, write access has been disabled.
- </span>
+ <div class="mr-widget-body media">
+ <div class="space-children">
+ <status-icon status="failed" />
+ <button
+ type="button"
+ class="btn btn-success btn-small"
+ disabled="true">
+ Merge
+ </button>
+ </div>
+ <div class="media-body">
+ <span class="bold">
+ This project is archived, write access has been disabled
+ </span>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js
index 4063859d5d0..5648208f7b1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js
@@ -1,4 +1,5 @@
import eventHub from '../../event_hub';
+import statusIcon from '../mr_widget_status_icon';
export default {
name: 'MRWidgetAutoMergeFailed',
@@ -10,6 +11,9 @@ export default {
isRefreshing: false,
};
},
+ components: {
+ statusIcon,
+ },
methods: {
refreshWidget() {
this.isRefreshing = true;
@@ -19,18 +23,16 @@ export default {
},
},
template: `
- <div class="mr-widget-body">
- <button
- class="btn btn-success btn-small"
- disabled="true"
- type="button">
- Merge
- </button>
- <span class="bold danger">
- This merge request failed to be merged automatically.
+ <div class="mr-widget-body media">
+ <status-icon status="failed" />
+ <div class="media-body space-children">
+ <span class="bold">
+ <template v-if="mr.mergeError">{{mr.mergeError}}.</template>
+ This merge request failed to be merged automatically
+ </span>
<button
@click="refreshWidget"
- :class="{ disabled: isRefreshing }"
+ :disabled="isRefreshing"
type="button"
class="btn btn-xs btn-default">
<i
@@ -39,9 +41,6 @@ export default {
aria-hidden="true" />
Refresh
</button>
- </span>
- <div class="merge-error-text danger bold">
- {{mr.mergeError}}
</div>
</div>
`,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js
index 8515b54e62d..aaf9d3304a4 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js
@@ -1,19 +1,18 @@
+import statusIcon from '../mr_widget_status_icon';
+
export default {
name: 'MRWidgetChecking',
+ components: {
+ statusIcon,
+ },
template: `
- <div class="mr-widget-body">
- <button
- type="button"
- class="btn btn-success btn-small"
- disabled="true">
- Merge
- </button>
- <span class="bold">
- Checking ability to merge automatically.
- <i
- class="fa fa-spinner fa-spin"
- aria-hidden="true" />
- </span>
+ <div class="mr-widget-body media">
+ <status-icon status="loading" showDisabledButton />
+ <div class="media-body space-children">
+ <span class="bold">
+ Checking ability to merge automatically
+ </span>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
index fc2e42c6821..4078aad7f83 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
@@ -1,4 +1,5 @@
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
+import statusIcon from '../mr_widget_status_icon';
export default {
name: 'MRWidgetClosed',
@@ -7,24 +8,28 @@ export default {
},
components: {
'mr-widget-author-and-time': mrWidgetAuthorTime,
+ statusIcon,
},
template: `
- <div class="mr-widget-body">
- <mr-widget-author-and-time
- actionText="Closed by"
- :author="mr.closedBy"
- :dateTitle="mr.updatedAt"
- :dateReadable="mr.closedAt"
- />
- <section>
- <p>
- The changes were not merged into
- <a
- :href="mr.targetBranchPath"
- class="label-branch">
- {{mr.targetBranch}}</a>.
- </p>
- </section>
+ <div class="mr-widget-body media">
+ <status-icon status="failed" />
+ <div class="media-body">
+ <mr-widget-author-and-time
+ actionText="Closed by"
+ :author="mr.closedBy"
+ :dateTitle="mr.updatedAt"
+ :dateReadable="mr.closedAt"
+ />
+ <section class="mr-info-list">
+ <p>
+ The changes were not merged into
+ <a
+ :href="mr.targetBranchPath"
+ class="label-branch">
+ {{mr.targetBranch}}</a>
+ </p>
+ </section>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
index 36596c6f37e..f9cb79a0bc1 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
@@ -1,27 +1,25 @@
+import statusIcon from '../mr_widget_status_icon';
+
export default {
name: 'MRWidgetConflicts',
props: {
mr: { type: Object, required: true },
},
+ components: {
+ statusIcon,
+ },
template: `
- <div class="mr-widget-body">
- <button
- type="button"
- class="btn btn-success btn-small"
- disabled="true">
- Merge
- </button>
- <span class="bold">
- There are merge conflicts.
- <span v-if="!mr.canMerge">
- Resolve these conflicts or ask someone with write access to this repository to merge it locally.
+ <div class="mr-widget-body media">
+ <status-icon status="failed" showDisabledButton />
+ <div class="media-body space-children">
+ <span class="bold">
+ There are merge conflicts<span v-if="!mr.canMerge">.</span>
+ <span v-if="!mr.canMerge">
+ Resolve these conflicts or ask someone with write access to this repository to merge it locally
+ </span>
</span>
- </span>
- <div
- v-if="mr.canMerge"
- class="btn-group">
<a
- v-if="mr.conflictResolutionPath"
+ v-if="mr.canMerge && mr.conflictResolutionPath"
:href="mr.conflictResolutionPath"
class="btn btn-default btn-xs js-resolve-conflicts-button">
Resolve conflicts
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
index 600b4d42e3d..1cb24549d53 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.js
@@ -1,3 +1,4 @@
+import statusIcon from '../mr_widget_status_icon';
import eventHub from '../../event_hub';
export default {
@@ -38,39 +39,40 @@ export default {
}
},
},
+ components: {
+ statusIcon,
+ },
template: `
- <div class="mr-widget-body">
- <button
- class="btn btn-success btn-small"
- disabled="true"
- type="button">
- Merge
- </button>
- <span
- v-if="!isRefreshing"
- class="bold danger">
- <span
- class="has-error-message"
- v-if="mr.mergeError">
- {{mr.mergeError}}
- </span>
- <span v-else>Merge failed.</span>
- <span
- :class="{ 'has-custom-error': mr.mergeError }">
- Refreshing in {{timerText}} to show the updated status...
+ <div class="mr-widget-body media">
+ <template v-if="isRefreshing">
+ <status-icon status="loading" />
+ <span class="media-body bold js-refresh-label">
+ Refreshing now
</span>
- <button
- @click="refresh"
- class="btn btn-default btn-xs js-refresh-button"
- type="button">
- Refresh now
- </button>
- </span>
- <span
- v-if="isRefreshing"
- class="bold js-refresh-label">
- Refreshing now...
- </span>
+ </template>
+ <template v-else>
+ <status-icon status="failed" showDisabledButton />
+ <div class="media-body space-children">
+ <span class="bold">
+ <span
+ class="has-error-message"
+ v-if="mr.mergeError">
+ {{mr.mergeError}}.
+ </span>
+ <span v-else>Merge failed.</span>
+ <span
+ :class="{ 'has-custom-error': mr.mergeError }">
+ Refreshing in {{timerText}} to show the updated status...
+ </span>
+ </span>
+ <button
+ @click="refresh"
+ class="btn btn-default btn-xs js-refresh-button"
+ type="button">
+ Refresh now
+ </button>
+ </div>
+ </template>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js
deleted file mode 100644
index 0bd31731a0b..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js
+++ /dev/null
@@ -1,24 +0,0 @@
-export default {
- name: 'MRWidgetLocked',
- props: {
- mr: { type: Object, required: true },
- },
- template: `
- <div class="mr-widget-body mr-state-locked">
- <span class="state-label">Locked</span>
- This merge request is in the process of being merged, during which time it is locked and cannot be closed.
- <i
- class="fa fa-spinner fa-spin"
- aria-hidden="true" />
- <section class="mr-info-list mr-links">
- <div class="legend"></div>
- <p>
- The changes will be merged into
- <span class="label-branch">
- <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
- </span>.
- </p>
- </section>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
index 419d174f3ff..bdfd4d9667c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
@@ -1,5 +1,5 @@
/* global Flash */
-
+import statusIcon from '../mr_widget_status_icon';
import MRWidgetAuthor from '../../components/mr_widget_author';
import eventHub from '../../event_hub';
@@ -11,6 +11,7 @@ export default {
},
components: {
'mr-widget-author': MRWidgetAuthor,
+ statusIcon,
},
data() {
return {
@@ -61,56 +62,56 @@ export default {
},
},
template: `
- <div class="mr-widget-body">
- <h4>
- Set by
- <mr-widget-author :author="mr.setToMWPSBy" />
- to be merged automatically when the pipeline succeeds.
- <a
- v-if="mr.canCancelAutomaticMerge"
- @click.prevent="cancelAutomaticMerge"
- :disabled="isCancellingAutoMerge"
- role="button"
- href="#"
- class="btn btn-xs btn-default js-cancel-auto-merge">
- <i
- v-if="isCancellingAutoMerge"
- class="fa fa-spinner fa-spin"
- aria-hidden="true" />
- Cancel automatic merge
- </a>
- </h4>
- <section class="mr-info-list">
- <div class="legend"></div>
- <p>The changes will be merged into
- <a
- :href="mr.targetBranchPath"
- class="label-branch">
- {{mr.targetBranch}}
- </a>.
- </p>
- <p v-if="mr.shouldRemoveSourceBranch">
- The source branch will be removed.
- </p>
- <p
- v-else
- class="with-button">
- The source branch will not be removed.
+ <div class="mr-widget-body media">
+ <status-icon status="success" />
+ <div class="media-body">
+ <h4>
+ Set by
+ <mr-widget-author :author="mr.setToMWPSBy" />
+ to be merged automatically when the pipeline succeeds
<a
- v-if="canRemoveSourceBranch"
- :disabled="isRemovingSourceBranch"
- @click.prevent="removeSourceBranch"
+ v-if="mr.canCancelAutomaticMerge"
+ @click.prevent="cancelAutomaticMerge"
+ :disabled="isCancellingAutoMerge"
role="button"
- class="btn btn-xs btn-default js-remove-source-branch"
- href="#">
+ href="#"
+ class="btn btn-xs btn-default js-cancel-auto-merge">
<i
- v-if="isRemovingSourceBranch"
- class="fa fa-spinner fa-spin"
- aria-hidden="true" />
- Remove source branch
+ v-if="isCancellingAutoMerge"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ Cancel automatic merge
</a>
- </p>
- </section>
+ </h4>
+ <section class="mr-info-list">
+ <p>The changes will be merged into
+ <a
+ :href="mr.targetBranchPath"
+ class="label-branch">
+ {{mr.targetBranch}}
+ </a>
+ </p>
+ <p v-if="mr.shouldRemoveSourceBranch">
+ The source branch will be removed
+ </p>
+ <p v-else>
+ The source branch will not be removed
+ <a
+ v-if="canRemoveSourceBranch"
+ :disabled="isRemovingSourceBranch"
+ @click.prevent="removeSourceBranch"
+ role="button"
+ class="btn btn-xs btn-default js-remove-source-branch"
+ href="#">
+ <i
+ v-if="isRemovingSourceBranch"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ Remove source branch
+ </a>
+ </p>
+ </section>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
index c7d32d18141..e452260a4d0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
@@ -1,6 +1,9 @@
/* global Flash */
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
+import tooltip from '../../../vue_shared/directives/tooltip';
+import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
+import statusIcon from '../mr_widget_status_icon';
import eventHub from '../../event_hub';
export default {
@@ -9,14 +12,19 @@ export default {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
- components: {
- 'mr-widget-author-and-time': mrWidgetAuthorTime,
- },
data() {
return {
isMakingRequest: false,
};
},
+ directives: {
+ tooltip,
+ },
+ components: {
+ 'mr-widget-author-and-time': mrWidgetAuthorTime,
+ loadingIcon,
+ statusIcon,
+ },
computed: {
shouldShowRemoveSourceBranch() {
const { sourceBranchRemoved, isRemovingSourceBranch, canRemoveSourceBranch } = this.mr;
@@ -55,75 +63,77 @@ export default {
},
},
template: `
- <div class="mr-widget-body">
- <mr-widget-author-and-time
- actionText="Merged by"
- :author="mr.mergedBy"
- :dateTitle="mr.updatedAt"
- :dateReadable="mr.mergedAt" />
- <section class="mr-info-list">
- <div class="legend"></div>
- <p>
- The changes were merged into
- <span class="label-branch">
- <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
- </span>
- </p>
- <p v-if="mr.sourceBranchRemoved">The source branch has been removed.</p>
- <p v-if="shouldShowRemoveSourceBranch">
- You can remove source branch now.
- <button
- @click="removeSourceBranch"
- :class="{ disabled: isMakingRequest }"
- type="button"
- class="btn btn-xs btn-default js-remove-branch-button">
- Remove Source Branch
- </button>
- </p>
- <p v-if="shouldShowSourceBranchRemoving">
- <i
- class="fa fa-spinner fa-spin"
- aria-hidden="true" />
- The source branch is being removed.
- </p>
- </section>
- <div
- v-if="shouldShowMergedButtons"
- class="merged-buttons clearfix">
- <a
- v-if="mr.canRevertInCurrentMR"
- class="btn btn-close btn-sm has-tooltip"
- href="#modal-revert-commit"
- data-toggle="modal"
- data-container="body"
- title="Revert this merge request in a new merge request">
- Revert
- </a>
- <a
- v-else-if="mr.revertInForkPath"
- class="btn btn-close btn-sm has-tooltip"
- data-method="post"
- :href="mr.revertInForkPath"
- title="Revert this merge request in a new merge request">
- Revert
- </a>
- <a
- v-if="mr.canCherryPickInCurrentMR"
- class="btn btn-default btn-sm has-tooltip"
- href="#modal-cherry-pick-commit"
- data-toggle="modal"
- data-container="body"
- title="Cherry-pick this merge request in a new merge request">
- Cherry-pick
- </a>
- <a
- v-else-if="mr.cherryPickInForkPath"
- class="btn btn-default btn-sm has-tooltip"
- data-method="post"
- :href="mr.cherryPickInForkPath"
- title="Cherry-pick this merge request in a new merge request">
- Cherry-pick
- </a>
+ <div class="mr-widget-body media">
+ <status-icon status="success" />
+ <div class="media-body">
+ <div class="space-children">
+ <mr-widget-author-and-time
+ actionText="Merged by"
+ :author="mr.mergedBy"
+ :dateTitle="mr.updatedAt"
+ :dateReadable="mr.mergedAt" />
+ <a
+ v-if="mr.canRevertInCurrentMR"
+ v-tooltip
+ class="btn btn-close btn-xs"
+ href="#modal-revert-commit"
+ data-toggle="modal"
+ data-container="body"
+ title="Revert this merge request in a new merge request">
+ Revert
+ </a>
+ <a
+ v-else-if="mr.revertInForkPath"
+ v-tooltip
+ class="btn btn-close btn-xs"
+ data-method="post"
+ :href="mr.revertInForkPath"
+ title="Revert this merge request in a new merge request">
+ Revert
+ </a>
+ <a
+ v-if="mr.canCherryPickInCurrentMR"
+ v-tooltip
+ class="btn btn-default btn-xs"
+ href="#modal-cherry-pick-commit"
+ data-toggle="modal"
+ data-container="body"
+ title="Cherry-pick this merge request in a new merge request">
+ Cherry-pick
+ </a>
+ <a
+ v-else-if="mr.cherryPickInForkPath"
+ v-tooltip
+ class="btn btn-default btn-xs"
+ data-method="post"
+ :href="mr.cherryPickInForkPath"
+ title="Cherry-pick this merge request in a new merge request">
+ Cherry-pick
+ </a>
+ </div>
+ <section class="mr-info-list">
+ <p>
+ The changes were merged into
+ <span class="label-branch">
+ <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
+ </span>
+ </p>
+ <p v-if="mr.sourceBranchRemoved">The source branch has been removed</p>
+ <p v-if="shouldShowRemoveSourceBranch" class="space-children">
+ <span>You can remove source branch now</span>
+ <button
+ @click="removeSourceBranch"
+ :disabled="isMakingRequest"
+ type="button"
+ class="btn btn-xs btn-default js-remove-branch-button">
+ Remove Source Branch
+ </button>
+ </p>
+ <p v-if="shouldShowSourceBranchRemoving">
+ <loading-icon inline />
+ <span>The source branch is being removed</span>
+ </p>
+ </section>
</div>
</div>
`,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js
new file mode 100644
index 00000000000..f6d1a4feeb2
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.js
@@ -0,0 +1,29 @@
+import statusIcon from '../mr_widget_status_icon';
+
+export default {
+ name: 'MRWidgetMerging',
+ props: {
+ mr: { type: Object, required: true },
+ },
+ components: {
+ statusIcon,
+ },
+ template: `
+ <div class="mr-widget-body mr-state-locked media">
+ <status-icon status="loading" />
+ <div class="media-body">
+ <h4>
+ This merge request is in the process of being merged
+ </h4>
+ <section class="mr-info-list">
+ <p>
+ The changes will be merged into
+ <span class="label-branch">
+ <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
+ </span>
+ </p>
+ </section>
+ </div>
+ </div>
+ `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
index 328382485f6..9f0a359d01a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
@@ -1,3 +1,5 @@
+import statusIcon from '../mr_widget_status_icon';
+import tooltip from '../../../vue_shared/directives/tooltip';
import mrWidgetMergeHelp from '../../components/mr_widget_merge_help';
export default {
@@ -5,30 +7,37 @@ export default {
props: {
mr: { type: Object, required: true },
},
+ directives: {
+ tooltip,
+ },
components: {
'mr-widget-merge-help': mrWidgetMergeHelp,
+ statusIcon,
},
computed: {
missingBranchName() {
return this.mr.sourceBranchRemoved ? 'source' : 'target';
},
+ message() {
+ return `If the ${this.missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line`;
+ },
},
template: `
- <div class="mr-widget-body">
- <button
- type="button"
- class="btn btn-success btn-small"
- disabled="true">
- Merge
- </button>
- <span class="bold js-branch-text">
- <span class="capitalize">
- {{missingBranchName}}
- </span> branch does not exist.
- Please restore the {{missingBranchName}} branch or use a different {{missingBranchName}} branch.
- </span>
- <mr-widget-merge-help
- :missing-branch="missingBranchName" />
+ <div class="mr-widget-body media">
+ <status-icon status="failed" showDisabledButton />
+ <div class="media-body space-children">
+ <span class="bold js-branch-text">
+ <span class="capitalize">
+ {{missingBranchName}}
+ </span> branch does not exist.
+ Please restore it or use a different {{missingBranchName}} branch
+ <i
+ v-tooltip
+ class="fa fa-question-circle"
+ :title="message"
+ :aria-label="message"></i>
+ </span>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
index 07169b349be..797511d4e3a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
@@ -1,17 +1,19 @@
+import statusIcon from '../mr_widget_status_icon';
+
export default {
name: 'MRWidgetNotAllowed',
+ components: {
+ statusIcon,
+ },
template: `
- <div class="mr-widget-body">
- <button
- type="button"
- class="btn btn-success btn-small"
- disabled="true">
- Merge
- </button>
- <span class="bold">
- Ready to be merged automatically.
- Ask someone with write access to this repository to merge this request.
- </span>
+ <div class="mr-widget-body media">
+ <status-icon status="success" showDisabledButton />
+ <div class="media-body space-children">
+ <span class="bold">
+ Ready to be merged automatically.
+ Ask someone with write access to this repository to merge this request
+ </span>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js
index 375a382615a..ebfd6765934 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js
@@ -12,7 +12,7 @@ export default {
return { emptyStateSVG };
},
template: `
- <div class="mr-widget-body empty-state">
+ <div class="mr-widget-body mr-widget-empty-state">
<div class="row">
<div class="artwork col-sm-5 col-sm-push-7 col-xs-12 text-center">
<span v-html="emptyStateSVG"></span>
@@ -29,12 +29,14 @@ export default {
Currently there are no changes in this merge request's source branch.
Please push new commits or use a different branch.
</p>
- <a
- v-if="mr.newBlobPath"
- :href="mr.newBlobPath"
- class="btn btn-inverted btn-save">
- Create file
- </a>
+ <div>
+ <a
+ v-if="mr.newBlobPath"
+ :href="mr.newBlobPath"
+ class="btn btn-inverted btn-save">
+ Create file
+ </a>
+ </div>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
index 31c53b679ed..167a0d4613a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
@@ -1,16 +1,18 @@
+import statusIcon from '../mr_widget_status_icon';
+
export default {
name: 'MRWidgetPipelineBlocked',
+ components: {
+ statusIcon,
+ },
template: `
- <div class="mr-widget-body">
- <button
- type="button"
- class="btn btn-success btn-small"
- disabled="true">
- Merge
- </button>
- <span class="bold">
- Pipeline blocked. The pipeline for this merge request requires a manual action to proceed.
- </span>
+ <div class="mr-widget-body media">
+ <status-icon status="failed" showDisabledButton />
+ <div class="media-body space-children">
+ <span class="bold">
+ Pipeline blocked. The pipeline for this merge request requires a manual action to proceed
+ </span>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
index 002820123ca..c5be9a0530a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
@@ -1,16 +1,18 @@
+import statusIcon from '../mr_widget_status_icon';
+
export default {
name: 'MRWidgetPipelineBlocked',
+ components: {
+ statusIcon,
+ },
template: `
- <div class="mr-widget-body">
- <button
- class="btn btn-success btn-small"
- disabled="true"
- type="button">
- Merge
- </button>
- <span class="bold">
- The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure.
- </span>
+ <div class="mr-widget-body media">
+ <status-icon status="failed" showDisabledButton />
+ <div class="media-body space-children">
+ <span class="bold">
+ The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
+ </span>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
index fcd4fdaf09f..65187754009 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
@@ -1,8 +1,8 @@
/* global Flash */
-
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll';
+import statusIcon from '../mr_widget_status_icon';
import eventHub from '../../event_hub';
export default {
@@ -25,6 +25,9 @@ export default {
warningSvg,
};
},
+ components: {
+ statusIcon,
+ },
computed: {
commitMessageLinkTitle() {
const withDesc = 'Include description in commit message';
@@ -196,84 +199,98 @@ export default {
},
},
template: `
- <div class="mr-widget-body">
- <span class="btn-group">
- <button
- @click="handleMergeButtonClick()"
- :disabled="isMergeButtonDisabled"
- :class="mergeButtonClass"
- type="button">
- <i
- v-if="isMakingRequest"
- class="fa fa-spinner fa-spin"
- aria-hidden="true" />
- {{mergeButtonText}}
- </button>
- <button
- v-if="shouldShowMergeOptionsDropdown"
- :disabled="isMergeButtonDisabled"
- type="button"
- class="btn btn-small btn-info dropdown-toggle"
- data-toggle="dropdown">
- <i
- class="fa fa-caret-down"
- aria-hidden="true" />
- <span class="sr-only">
- Select merge moment
+ <div class="mr-widget-body media">
+ <status-icon status="success" />
+ <div class="media-body">
+ <div class="media space-children">
+ <span class="btn-group">
+ <button
+ @click="handleMergeButtonClick()"
+ :disabled="isMergeButtonDisabled"
+ :class="mergeButtonClass"
+ type="button">
+ <i
+ v-if="isMakingRequest"
+ class="fa fa-spinner fa-spin"
+ aria-hidden="true" />
+ {{mergeButtonText}}
+ </button>
+ <button
+ v-if="shouldShowMergeOptionsDropdown"
+ :disabled="isMergeButtonDisabled"
+ type="button"
+ class="btn btn-small btn-info dropdown-toggle js-merge-moment"
+ data-toggle="dropdown"
+ aria-label="Select merge moment">
+ <i
+ class="fa fa-chevron-down"
+ aria-hidden="true" />
+ </button>
+ <ul
+ v-if="shouldShowMergeOptionsDropdown"
+ class="dropdown-menu dropdown-menu-right"
+ role="menu">
+ <li>
+ <a
+ @click.prevent="handleMergeButtonClick(true)"
+ class="merge_when_pipeline_succeeds"
+ href="#">
+ <span class="media">
+ <span
+ v-html="successSvg"
+ class="merge-opt-icon"
+ aria-hidden="true"></span>
+ <span class="media-body merge-opt-title">Merge when pipeline succeeds</span>
+ </span>
+ </a>
+ </li>
+ <li>
+ <a
+ @click.prevent="handleMergeButtonClick(false, true)"
+ class="accept-merge-request"
+ href="#">
+ <span class="media">
+ <span
+ v-html="warningSvg"
+ class="merge-opt-icon"
+ aria-hidden="true"></span>
+ <span class="media-body merge-opt-title">Merge immediately</span>
+ </span>
+ </a>
+ </li>
+ </ul>
</span>
- </button>
- <ul
- v-if="shouldShowMergeOptionsDropdown"
- class="dropdown-menu dropdown-menu-right"
- role="menu">
- <li>
- <a
- @click.prevent="handleMergeButtonClick(true)"
- class="merge_when_pipeline_succeeds"
- href="#">
- <span
- v-html="successSvg"
- class="merge-opt-icon"
- aria-hidden="true"></span>
- <span class="merge-opt-title">Merge when pipeline succeeds</span>
- </a>
- </li>
- <li>
- <a
- @click.prevent="handleMergeButtonClick(false, true)"
- class="accept-merge-request"
- href="#">
- <span
- v-html="warningSvg"
- class="merge-opt-icon"
- aria-hidden="true"></span>
- <span class="merge-opt-title">Merge immediately</span>
- </a>
- </li>
- </ul>
- </span>
- <template v-if="isMergeAllowed()">
- <label class="spacing">
- <input
- id="remove-source-branch-input"
- v-model="removeSourceBranch"
- :disabled="isRemoveSourceBranchButtonDisabled"
- type="checkbox"/> Remove source branch
- </label>
+ <div class="media-body space-children">
+ <template v-if="isMergeAllowed()">
+ <label>
+ <input
+ id="remove-source-branch-input"
+ v-model="removeSourceBranch"
+ :disabled="isRemoveSourceBranchButtonDisabled"
+ type="checkbox"/> Remove source branch
+ </label>
- <!-- Placeholder for EE extension of this component -->
- <squash-before-merge
- v-if="shouldShowSquashBeforeMerge"
- :mr="mr"
- :is-merge-button-disabled="isMergeButtonDisabled" />
+ <!-- Placeholder for EE extension of this component -->
+ <squash-before-merge
+ v-if="shouldShowSquashBeforeMerge"
+ :mr="mr"
+ :is-merge-button-disabled="isMergeButtonDisabled" />
- <button
- @click="toggleCommitMessageEditor"
- :disabled="isMergeButtonDisabled"
- class="btn btn-default btn-xs"
- type="button">
- Modify commit message
- </button>
+ <button
+ @click="toggleCommitMessageEditor"
+ :disabled="isMergeButtonDisabled"
+ class="btn btn-default btn-xs"
+ type="button">
+ Modify commit message
+ </button>
+ </template>
+ <template v-else>
+ <span class="bold">
+ The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure
+ </span>
+ </template>
+ </div>
+ </div>
<div
v-if="showCommitMessageEditor"
class="prepend-top-default commit-message-editor">
@@ -293,7 +310,7 @@ export default {
rows="14"
name="Commit message"></textarea>
</div>
- <p class="hint">Try to keep the first line under 52 characters and the others under 72.</p>
+ <p class="hint">Try to keep the first line under 52 characters and the others under 72</p>
<div class="hint">
<a
@click.prevent="updateCommitMessage"
@@ -302,12 +319,7 @@ export default {
</div>
</div>
</div>
- </template>
- <template v-else>
- <span class="bold">
- The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure.
- </span>
- </template>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
index 79f8ef408e6..89f38e5bd2a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_sha_mismatch.js
@@ -1,16 +1,18 @@
+import statusIcon from '../mr_widget_status_icon';
+
export default {
name: 'MRWidgetSHAMismatch',
+ components: {
+ statusIcon,
+ },
template: `
- <div class="mr-widget-body">
- <button
- type="button"
- class="btn btn-success btn-small"
- disabled="true">
- Merge
- </button>
- <span class="bold">
- The source branch HEAD has recently changed. Please reload the page and review the changes before merging.
- </span>
+ <div class="mr-widget-body media">
+ <status-icon status="failed" showDisabledButton />
+ <div class="media-body space-children">
+ <span class="bold">
+ The source branch HEAD has recently changed. Please reload the page and review the changes before merging
+ </span>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
index f4ab2d9fa58..d762ca6e640 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
@@ -1,27 +1,27 @@
+import statusIcon from '../mr_widget_status_icon';
+
export default {
name: 'MRWidgetUnresolvedDiscussions',
props: {
mr: { type: Object, required: true },
},
+ components: {
+ statusIcon,
+ },
template: `
- <div class="mr-widget-body">
- <button
- type="button"
- class="btn btn-success btn-small"
- disabled="true">
- Merge
- </button>
- <span class="bold">
- There are unresolved discussions. Please resolve these discussions
- <span v-if="mr.canCreateIssue">or</span>
- <span v-else>.</span>
- </span>
- <a
- v-if="mr.createIssueToResolveDiscussionsPath"
- :href="mr.createIssueToResolveDiscussionsPath"
- class="btn btn-default btn-xs js-create-issue">
- Create an issue to resolve them later
- </a>
+ <div class="mr-widget-body media">
+ <status-icon status="failed" showDisabledButton />
+ <div class="media-body space-children">
+ <span class="bold">
+ There are unresolved discussions. Please resolve these discussions
+ </span>
+ <a
+ v-if="mr.createIssueToResolveDiscussionsPath"
+ :href="mr.createIssueToResolveDiscussionsPath"
+ class="btn btn-default btn-xs js-create-issue">
+ Create an issue to resolve them later
+ </a>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
index cb02ffe93bd..b11a06899cf 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
@@ -1,4 +1,6 @@
/* global Flash */
+import statusIcon from '../mr_widget_status_icon';
+import tooltip from '../../../vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
export default {
@@ -7,11 +9,17 @@ export default {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
+ directives: {
+ tooltip,
+ },
data() {
return {
isMakingRequest: false,
};
},
+ components: {
+ statusIcon,
+ },
methods: {
removeWIP() {
this.isMakingRequest = true;
@@ -29,20 +37,20 @@ export default {
},
},
template: `
- <div class="mr-widget-body">
- <button
- type="button"
- class="btn btn-success btn-small"
- disabled="true">
- Merge</button>
- <span class="bold">
- This merge request is currently Work In Progress and therefore unable to merge
- </span>
- <template v-if="mr.removeWIPPath">
- <i
- class="fa fa-question-circle has-tooltip"
- title="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged." />
+ <div class="mr-widget-body media">
+ <status-icon status="failed" :showDisabledButton="Boolean(mr.removeWIPPath)" />
+ <div class="media-body space-children">
+ <span class="bold">
+ This is a Work in Progress
+ <i
+ v-tooltip
+ class="fa fa-question-circle"
+ title="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged"
+ aria-label="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged">
+ </i>
+ </span>
<button
+ v-if="mr.removeWIPPath"
@click="removeWIP"
:disabled="isMakingRequest"
type="button"
@@ -53,7 +61,7 @@ export default {
aria-hidden="true" />
Resolve WIP status
</button>
- </template>
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index 546a3f625c7..49340c232c8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -19,7 +19,7 @@ export { default as WidgetRelatedLinks } from './components/mr_widget_related_li
export { default as MergedState } from './components/states/mr_widget_merged';
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge';
export { default as ClosedState } from './components/states/mr_widget_closed';
-export { default as LockedState } from './components/states/mr_widget_locked';
+export { default as MergingState } from './components/states/mr_widget_merging';
export { default as WipState } from './components/states/mr_widget_wip';
export { default as ArchivedState } from './components/states/mr_widget_archived';
export { default as ConflictsState } from './components/states/mr_widget_conflicts';
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
index 2339a00ddd0..0042c48816f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js
@@ -8,7 +8,7 @@ import {
WidgetRelatedLinks,
MergedState,
ClosedState,
- LockedState,
+ MergingState,
WipState,
ArchivedState,
ConflictsState,
@@ -35,8 +35,14 @@ import {
export default {
el: '#js-vue-mr-widget',
name: 'MRWidget',
+ props: {
+ mrData: {
+ type: Object,
+ required: false,
+ },
+ },
data() {
- const store = new MRWidgetStore(gl.mrWidgetData);
+ const store = new MRWidgetStore(this.mrData || window.gl.mrWidgetData);
const service = this.createService(store);
return {
mr: store,
@@ -206,7 +212,7 @@ export default {
'mr-widget-related-links': WidgetRelatedLinks,
'mr-widget-merged': MergedState,
'mr-widget-closed': ClosedState,
- 'mr-widget-locked': LockedState,
+ 'mr-widget-merging': MergingState,
'mr-widget-failed-to-merge': FailedToMerge,
'mr-widget-wip': WipState,
'mr-widget-archived': ArchivedState,
@@ -234,14 +240,21 @@ export default {
v-if="shouldRenderDeployments"
:mr="mr"
:service="service" />
- <component
- :is="componentName"
- :mr="mr"
- :service="service" />
- <mr-widget-related-links
- v-if="shouldRenderRelatedLinks"
- :related-links="mr.relatedLinks" />
- <mr-widget-merge-help v-if="shouldRenderMergeHelp" />
+ <div class="mr-widget-section">
+ <component
+ :is="componentName"
+ :mr="mr"
+ :service="service" />
+ <mr-widget-related-links
+ v-if="shouldRenderRelatedLinks"
+ :state="mr.state"
+ :related-links="mr.relatedLinks" />
+ </div>
+ <div
+ class="mr-widget-footer"
+ v-if="shouldRenderMergeHelp">
+ <mr-widget-merge-help />
+ </div>
</div>
`,
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
index fddafb0ddfa..fbea764b739 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -73,6 +73,7 @@ export default class MergeRequestStore {
this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path;
this.hasSHAChanged = this.sha !== data.diff_head_sha;
this.canBeMerged = data.can_be_merged || false;
+ this.mergeOngoing = data.merge_ongoing;
// Cherry-pick and Revert actions related
this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
@@ -94,6 +95,11 @@ export default class MergeRequestStore {
}
setState(data) {
+ if (this.mergeOngoing) {
+ this.state = 'merging';
+ return;
+ }
+
if (this.isOpen) {
this.state = getStateKey.call(this, data);
} else {
@@ -104,9 +110,6 @@ export default class MergeRequestStore {
case 'closed':
this.state = 'closed';
break;
- case 'locked':
- this.state = 'locked';
- break;
default:
this.state = null;
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
index 605dd3a1ff4..9074a064a6d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
@@ -1,7 +1,7 @@
const stateToComponentMap = {
merged: 'mr-widget-merged',
closed: 'mr-widget-closed',
- locked: 'mr-widget-locked',
+ merging: 'mr-widget-merging',
conflicts: 'mr-widget-conflicts',
missingBranch: 'mr-widget-missing-branch',
workInProgress: 'mr-widget-wip',
@@ -20,7 +20,7 @@ const stateToComponentMap = {
};
const statesToShowHelpWidget = [
- 'locked',
+ 'merging',
'conflicts',
'workInProgress',
'readyToMerge',
diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue
new file mode 100644
index 00000000000..7d339c0e753
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue
@@ -0,0 +1,67 @@
+<script>
+const PopupDialog = {
+ name: 'popup-dialog',
+
+ props: {
+ open: Boolean,
+ title: String,
+ body: String,
+ kind: {
+ type: String,
+ default: 'primary',
+ },
+ closeButtonLabel: {
+ type: String,
+ default: 'Cancel',
+ },
+ primaryButtonLabel: {
+ type: String,
+ default: 'Save changes',
+ },
+ },
+
+ computed: {
+ typeOfClass() {
+ const className = `btn-${this.kind}`;
+ const returnObj = {};
+ returnObj[className] = true;
+ return returnObj;
+ },
+ },
+
+ methods: {
+ close() {
+ this.$emit('toggle', false);
+ },
+
+ yesClick() {
+ this.$emit('submit', true);
+ },
+
+ noClick() {
+ this.$emit('submit', false);
+ },
+ },
+};
+
+export default PopupDialog;
+</script>
+<template>
+<div class="modal popup-dialog" tabindex="-1" v-show="open" role="dialog">
+ <div class="modal-dialog" role="document">
+ <div class="modal-content">
+ <div class="modal-header">
+ <button type="button" class="close" @click="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
+ <h4 class="modal-title">{{this.title}}</h4>
+ </div>
+ <div class="modal-body">
+ <p>{{this.body}}</p>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-default" data-dismiss="modal" @click="noClick">{{closeButtonLabel}}</button>
+ <button type="button" class="btn" :class="typeOfClass" @click="yesClick">{{primaryButtonLabel}}</button>
+ </div>
+ </div>
+ </div>
+</div>
+</template>
diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js
index 00676bcb0b3..51ed2b4fd15 100644
--- a/app/assets/javascripts/wikis.js
+++ b/app/assets/javascripts/wikis.js
@@ -1,6 +1,5 @@
/* global Breakpoints */
-import 'vendor/jquery.nicescroll';
import './breakpoints';
export default class Wikis {
@@ -8,7 +7,6 @@ export default class Wikis {
this.bp = Breakpoints.get();
this.sidebarEl = document.querySelector('.js-wiki-sidebar');
this.sidebarExpanded = false;
- $(this.sidebarEl).niceScroll();
const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle');
for (let i = 0; i < sidebarToggles.length; i += 1) {
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 6ce331a9129..b2b3297e880 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -26,6 +26,7 @@
@import "framework/lists";
@import "framework/logo";
@import "framework/markdown_area";
+@import "framework/media_object";
@import "framework/mobile";
@import "framework/modal";
@import "framework/nav";
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index cb41df8a88d..486d88efbc5 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -100,6 +100,8 @@
margin: 0;
align-self: center;
}
+
+ &.s40 { min-width: 40px; min-height: 40px; }
}
.avatar-counter {
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 5e374360359..293aa194528 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -372,6 +372,10 @@ table {
background: $gl-success !important;
}
+.dz-message {
+ margin: 0;
+}
+
.space-right {
margin-right: 10px;
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 4a69c14fa7e..b677882eba4 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -26,7 +26,7 @@ header {
&.navbar-gitlab {
padding: 0 16px;
- z-index: 400;
+ z-index: 1000;
margin-bottom: 0;
min-height: $header-height;
background-color: $gray-light;
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 67c3287ed74..bd0367f86dd 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -109,18 +109,20 @@ body {
}
}
-
-/* The following prevents side effects related to iOS Safari's implementation of -webkit-overflow-scrolling: touch,
-which is applied to the body by jquery.nicescroling plugin to force hardware acceleration for momentum scrolling. Side
-effects are commonly related to inconsisent z-index behavior (e.g. tooltips). By applying the following to direct children
-of the body element here, we negate cascading side effects but allow momentum scrolling to be applied to the body */
-
-.navbar,
-.page-gutter,
-.page-with-sidebar {
- -webkit-overflow-scrolling: auto;
+.page-with-sidebar > .content-wrapper {
+ min-height: calc(100vh - #{$header-height});
}
.with-performance-bar .page-with-sidebar {
margin-top: $header-height + $performance-bar-height;
}
+
+[v-cloak] {
+ display: none;
+}
+
+.vertical-center {
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+}
diff --git a/app/assets/stylesheets/framework/media_object.scss b/app/assets/stylesheets/framework/media_object.scss
new file mode 100644
index 00000000000..b573052c14a
--- /dev/null
+++ b/app/assets/stylesheets/framework/media_object.scss
@@ -0,0 +1,8 @@
+.media {
+ display: flex;
+ align-items: flex-start;
+}
+
+.media-body {
+ flex: 1;
+}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 88e7ba117d5..d386ac5ba9c 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -251,7 +251,6 @@
// Applies on /dashboard/issues
.project-item-select-holder {
- display: block;
margin: 0;
}
}
@@ -283,6 +282,31 @@
}
}
+.project-item-select-holder.btn-group {
+ display: flex;
+ max-width: 350px;
+ overflow: hidden;
+
+ @media(max-width: $screen-xs-max) {
+ width: 100%;
+ max-width: none;
+ }
+
+ .new-project-item-link {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .new-project-item-select-button {
+ width: 32px;
+ }
+}
+
+.new-project-item-select-button .fa-caret-down {
+ margin-left: 2px;
+}
+
.layout-nav {
width: 100%;
background: $gray-light;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 09b60ad1676..40e8a928e6e 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -78,15 +78,12 @@
.right-sidebar {
border-left: 1px solid $border-color;
+ height: calc(100% - #{$header-height});
&.affix {
position: fixed;
top: $header-height;
}
-
- &:not(.affix-top) {
- min-height: 100%;
- }
}
.with-performance-bar .right-sidebar.affix {
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index b666223b120..4c35e3a9c3c 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -162,3 +162,5 @@ $pre-color: $gl-text-color !default;
$pre-border-color: $border-color;
$table-bg-accent: $gray-light;
+
+$zindex-popover: 900;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 0df6f24bfe6..3c109a5a929 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -88,6 +88,7 @@ $indigo-950: #1a1a40;
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
+$almost-black: #242424;
$border-white-light: darken($white-light, $darken-border-factor);
$border-white-normal: darken($white-normal, $darken-border-factor);
@@ -206,7 +207,6 @@ $general-hover-transition-curve: linear;
$highlight-changes-color: rgb(235, 255, 232);
$performance-bar-height: 35px;
-
/*
* Common component specific colors
*/
@@ -316,6 +316,12 @@ $badge-bg: rgba(0, 0, 0, 0.07);
$badge-color: $gl-text-color-secondary;
/*
+ * Status icons
+ */
+$status-icon-size: 22px;
+$status-icon-margin: $gl-btn-padding;
+
+/*
* Award emoji
*/
$award-emoji-menu-shadow: rgba(0, 0, 0, .175);
@@ -614,6 +620,13 @@ $color-average-score: $orange-400;
$color-low-score: $red-400;
/*
+Repo editor
+*/
+$repo-editor-grey: #f6f7f9;
+$repo-editor-grey-darker: #e9ebee;
+$repo-editor-linear-gradient: linear-gradient(to right, $repo-editor-grey 0%, $repo-editor-grey-darker, 20%, $repo-editor-grey 40%, $repo-editor-grey 100%);
+
+/*
Performance Bar
*/
$perf-bar-text: #999;
@@ -624,3 +637,11 @@ $perf-bar-bucket-bg: #111;
$perf-bar-bucket-color: #ccc;
$perf-bar-bucket-box-shadow-from: rgba($white-light, .2);
$perf-bar-bucket-box-shadow-to: rgba($black, .25);
+
+
+/*
+Project Templates Icons
+*/
+$rails: #c00;
+$node: #353535;
+$java: #70ad51;
diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss
index 3d202183c82..609bc9a7dfc 100644
--- a/app/assets/stylesheets/new_sidebar.scss
+++ b/app/assets/stylesheets/new_sidebar.scss
@@ -8,20 +8,25 @@ $active-color: $indigo-700;
$active-hover-background: $active-background;
$active-hover-color: $gl-text-color;
$inactive-badge-background: rgba(0, 0, 0, .08);
-$hover-background: $indigo-700;
-$hover-color: $white-light;
+$hover-background: $white-light;
+$hover-color: $gl-text-color;
$inactive-color: $gl-text-color-secondary;
$new-sidebar-width: 220px;
+$new-sidebar-collapsed-width: 50px;
.page-with-new-sidebar {
- @media (min-width: $screen-sm-min) {
+ @media (min-width: $screen-md-min) {
+ padding-left: $new-sidebar-collapsed-width;
+ }
+
+ @media (min-width: $screen-lg-min) {
padding-left: $new-sidebar-width;
}
// Override position: absolute
.right-sidebar {
position: fixed;
- height: 100%;
+ height: calc(100% - #{$header-height});
}
.issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
@@ -29,8 +34,15 @@ $new-sidebar-width: 220px;
}
}
+.page-with-icon-sidebar {
+ @media (min-width: $screen-sm-min) {
+ padding-left: $new-sidebar-collapsed-width;
+ }
+}
+
.context-header {
position: relative;
+ margin-right: 2px;
a {
border-bottom: 1px solid $border-color;
@@ -39,26 +51,16 @@ $new-sidebar-width: 220px;
align-items: center;
padding: 10px 16px 10px 10px;
color: $gl-text-color;
+ }
- @media (max-width: $screen-xs-max) {
- padding-right: 30px;
- }
-
- &:hover {
- background-color: $hover-background;
- color: $hover-color;
- border-color: $hover-background;
-
- .avatar-container {
- border-color: transparent;
- }
-
- .settings-avatar {
- background-color: $indigo-500;
+ &:hover,
+ a:hover {
+ background-color: $hover-background;
+ color: $hover-color;
- i {
- color: $hover-color;
- }
+ .settings-avatar {
+ i {
+ color: $hover-color;
}
}
}
@@ -73,32 +75,6 @@ $new-sidebar-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
}
-
-
- &:hover {
- .close-nav-button {
- color: $white-light;
- }
- }
-
- .close-nav-button {
- display: none;
- position: absolute;
- top: 0;
- right: 0;
- height: 100%;
- background-color: transparent;
- border: 0;
- padding: 0 10px;
-
- @media (max-width: $screen-xs-max) {
- display: block;
- }
-
- &:hover {
- color: $gl-text-color;
- }
- }
}
.settings-avatar {
@@ -125,6 +101,16 @@ $new-sidebar-width: 220px;
background-color: $gray-normal;
box-shadow: inset -2px 0 0 $border-color;
+ &.sidebar-icons-only {
+ width: $new-sidebar-collapsed-width;
+
+ .nav-item-name,
+ .badge,
+ .project-title {
+ display: none;
+ }
+ }
+
&.nav-sidebar-expanded {
left: 0;
}
@@ -219,6 +205,8 @@ $new-sidebar-width: 220px;
}
.sidebar-top-level-items {
+ margin-bottom: 60px;
+
> li {
> a {
@media (min-width: $screen-sm-min) {
@@ -233,15 +221,15 @@ $new-sidebar-width: 220px;
&:not(.active) {
> a {
margin-left: 1px;
- margin-right: 3px;
+ margin-right: 2px;
}
.sidebar-sub-level-items {
@media (min-width: $screen-sm-min) {
position: fixed;
top: 0;
- left: 220px;
- width: 150px;
+ left: $new-sidebar-width;
+ min-width: 150px;
margin-top: -1px;
padding: 8px 1px;
background-color: $white-light;
@@ -326,6 +314,95 @@ $new-sidebar-width: 220px;
}
}
+
+// Collapsed nav
+
+.toggle-sidebar-button,
+.close-nav-button {
+ width: $new-sidebar-width - 2px;
+ position: fixed;
+ bottom: 0;
+ padding: 16px;
+ background-color: $gray-normal;
+ border: 0;
+ border-top: 2px solid $border-color;
+ color: $gl-text-color-secondary;
+ display: flex;
+ align-items: center;
+
+ i {
+ font-size: 20px;
+ margin-right: 8px;
+ }
+
+ .fa-angle-double-right {
+ display: none;
+ }
+
+ &:hover {
+ background-color: $border-color;
+ color: $gl-text-color;
+ }
+}
+
+.toggle-sidebar-button {
+ @media (max-width: $screen-xs-max) {
+ display: none;
+ }
+}
+
+
+.sidebar-icons-only {
+ .context-header {
+ height: 60px;
+
+ a {
+ padding: 10px 4px;
+ }
+ }
+
+ li a {
+ padding: 12px 15px;
+ }
+
+ .sidebar-top-level-items > li {
+ &.active a {
+ padding-left: 12px;
+ }
+
+ .sidebar-sub-level-items {
+ @media (min-width: $screen-sm-min) {
+ left: $new-sidebar-collapsed-width;
+ }
+
+ &:not(.flyout-list) {
+ display: none;
+ }
+ }
+ }
+
+ .toggle-sidebar-button {
+ width: $new-sidebar-collapsed-width - 2px;
+ padding: 16px 18px;
+
+ .collapse-text,
+ .fa-angle-double-left {
+ display: none;
+ }
+
+ .fa-angle-double-right {
+ display: block;
+ }
+ }
+}
+
+
+// Mobile nav
+
+.close-nav-button {
+ display: none;
+}
+
.toggle-mobile-nav {
display: none;
background-color: transparent;
@@ -345,6 +422,12 @@ $new-sidebar-width: 220px;
}
}
+@media (max-width: $screen-xs-max) {
+ .close-nav-button {
+ display: flex;
+ }
+}
+
.mobile-overlay {
display: none;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 6039cda96d8..e5b467a2691 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -165,6 +165,7 @@
.board-title {
padding-top: ($gl-padding - 3px);
+ padding-bottom: $gl-padding;
}
}
}
@@ -178,6 +179,7 @@
position: relative;
margin: 0;
padding: $gl-padding;
+ padding-bottom: ($gl-padding + 3px);
font-size: 1em;
border-bottom: 1px solid $border-color;
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 28c99d8e57c..486424fb729 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -235,8 +235,18 @@
display: none;
}
+ .sidebar-container {
+ width: calc(100% + 100px);
+ padding-right: 100px;
+ height: 100%;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch;
+ }
+
.blocks-container {
padding: 0 $gl-padding;
+ width: 289px;
}
.block {
@@ -259,7 +269,15 @@
padding: 16px 0;
}
+ .trigger-build-variables {
+ margin: 0;
+ overflow-x: auto;
+ -ms-overflow-style: scrollbar;
+ -webkit-overflow-scrolling: touch;
+ }
+
.trigger-build-variable {
+ font-weight: normal;
color: $code-color;
}
@@ -326,6 +344,7 @@
border-top: 1px solid $border-color;
border-bottom: 1px solid $border-color;
max-height: 300px;
+ width: 289px;
overflow: auto;
svg {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index ceb8cb70500..e165a583ecb 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -5,6 +5,30 @@
margin-right: auto;
}
+.is-confidential {
+ color: $orange-600;
+ background-color: $orange-50;
+ border-radius: 3px;
+ padding: 5px;
+ margin: 0 3px 0 -4px;
+}
+
+.is-not-confidential {
+ border-radius: 3px;
+ padding: 5px;
+ margin: 0 3px 0 -4px;
+}
+
+.confidentiality {
+ .is-not-confidential {
+ margin: auto;
+ }
+
+ .is-confidential {
+ margin: auto;
+ }
+}
+
.limit-container-width {
.detail-page-header,
.page-content-header,
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index a4e19094508..6bb013cca85 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -2,10 +2,35 @@
* MR -> show: Automerge widget
*
*/
+
+.space-children {
+ @include clearfix;
+
+ > * {
+ float: left;
+ }
+
+ > *:not(:last-child) {
+ margin-right: 10px;
+ }
+}
+
.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;
+ }
+
+ .mr-widget-footer {
+ padding: 0;
+ }
form {
margin-bottom: 0;
@@ -15,15 +40,35 @@
}
}
+ label {
+ margin-bottom: 0;
+ }
+
+ .btn {
+ font-size: $gl-font-size;
+
+ &[disabled] {
+ opacity: 0.3;
+ }
+
+ &.btn-xs {
+ line-height: 1;
+ padding: 5px 10px;
+ margin-top: 1px;
+ }
+
+ &.dropdown-toggle {
+ .fa {
+ color: inherit;
+ }
+ }
+ }
+
.accept-merge-holder {
.accept-action {
display: inline-block;
float: left;
- .btn-success.dropdown-toggle .fa {
- color: inherit;
- }
-
.accept-merge-request {
&.ci-pending,
&.ci-running {
@@ -84,77 +129,64 @@
.ci-widget {
color: $gl-text-color;
- display: -webkit-flex;
display: flex;
- -webkit-align-items: center;
- align-items: center;
- padding: $gl-padding-top $gl-padding 0;
-
- svg {
- position: relative;
- top: 1px;
- overflow: visible;
- }
-
- > span {
- padding-right: 4px;
- }
@media (max-width: $screen-xs-max) {
flex-wrap: wrap;
}
+ }
- .icon-link > .ci-status-icon > svg {
- width: 22px;
- height: 22px;
- margin-right: 8px;
- }
+ .mr-widget-icon {
+ font-size: 22px;
+ margin-right: $status-icon-margin;
+ }
- .ci-error {
- margin-right: $btn-side-margin;
- }
+ .ci-status-icon svg {
+ width: $status-icon-size;
+ height: $status-icon-size;
+ margin: 3px 0;
+ position: relative;
+ overflow: visible;
+ display: block;
}
- .mr-widget-body,
- .mr-widget-footer {
- margin: 16px;
+ .mr-widget-body {
+ @include clearfix;
+
+ &.media > *:first-child {
+ margin-right: 10px;
+ }
}
.mr-widget-pipeline-graph {
- flex-shrink: 0;
+ padding: 0 4px;
.dropdown-menu {
- margin-top: 11px;
z-index: 300;
}
.ci-action-icon-wrapper {
line-height: 16px;
}
+ }
- @media (max-width: $screen-xs-max) {
- order: 1;
- margin-top: $gl-padding-top;
- border-radius: 3px;
- background-color: $white-light;
- border: 1px solid $gray-darker;
- width: 100%;
- text-align: center;
+ .mini-pipeline-graph-dropdown-toggle {
+ vertical-align: top;
+ }
- .dropdown-menu {
- margin-left: -97.5px;
- }
+ .mini-pipeline-graph-dropdown-menu .mini-pipeline-graph-dropdown-item {
+ display: flex;
+ align-items: center;
- .arrow-up::before,
- .arrow-up::after, {
- margin-left: 97.5px;
- }
+ .ci-status-text,
+ .ci-status-icon {
+ top: 0;
+ margin-right: 10px;
}
}
.normal {
- color: $gl-text-color;
- font-size: 15px;
+ line-height: 28px;
}
.capitalize {
@@ -165,9 +197,8 @@
@extend .ref-name;
color: $gl-text-color;
- font-weight: bold;
+ font-weight: 600;
overflow: hidden;
- margin: 0 3px;
word-break: break-all;
&.label-truncated {
@@ -189,52 +220,19 @@
}
}
- .js-deployment-link {
- display: inline-block;
- }
-
.mr-widget-help {
- margin: $gl-padding;
- color: $ci-skipped-color;
- }
-
- .mr-info-list {
-
- &.mr-links {
- margin-left: 28px;
- }
-
- &.mr-memory-usage {
- margin: 5px 0 10px 25px;
- }
- }
-
- .mr-widget-heading {
- .btn-default.btn-xs {
- margin-left: 5px;
- }
- }
-
- .mr-widget-body {
- .btn {
- font-size: 15px;
- }
-
- .btn-group .btn {
- padding: 5px 10px;
-
- &.dropdown-toggle {
- padding: 5px 7px;
- }
- }
+ padding: 10px 16px 10px 48px;
+ font-style: italic;
}
.mr-widget-body {
h4 {
- font-weight: bold;
- font-size: 15px;
- margin: 5px 0;
- color: $gl-text-color;
+ float: left;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: inherit;
+ margin-top: 0;
+ margin-bottom: 0;
&.has-conflicts .fa-exclamation-triangle {
color: $gl-warning;
@@ -255,18 +253,16 @@
}
.spacing {
- margin: 0 $gl-padding;
+ margin: 0 0 0 10px;
}
.bold {
- font-weight: bold;
- font-size: 15px;
+ font-weight: 600;
color: $gl-gray-light;
}
.state-label {
- font-size: 16px;
- font-weight: bold;
+ font-weight: 600;
padding-right: 10px;
}
@@ -274,16 +270,6 @@
color: $gl-danger;
}
- .mr-widget-help {
- margin: $gl-padding 0;
- }
-
- .with-button {
- position: relative;
- top: 6px;
- margin-bottom: 24px;
- }
-
.spacing,
.bold {
vertical-align: middle;
@@ -294,15 +280,8 @@
padding: 5px;
}
- .merge-opt-icon,
- .merge-opt-title {
- display: inline-block;
- float: left;
- }
-
- .merge-opt-icon svg {
- height: 15px;
- width: 15px;
+ .merge-opt-icon {
+ line-height: 1.5;
}
.merge-opt-title {
@@ -316,34 +295,15 @@
}
}
- .has-error-message + .has-custom-error {
- margin-left: 0;
- }
-
.has-custom-error {
display: inline-block;
- margin-left: 70px;
- }
-
- .merge-error-text {
- margin-left: 70px;
}
@media (max-width: $screen-xs-max) {
- h4 {
- font-size: 14px;
- }
-
p {
font-size: 13px;
}
- .btn,
- .btn-group,
- .accept-action {
- margin-bottom: 4px;
- }
-
.btn-grouped {
float: none;
margin-right: 0;
@@ -367,19 +327,16 @@
}
}
- &.mr-state-locked .mr-info-list {
- margin-top: 10px;
- margin-left: 12px;
- }
+ &.mr-widget-empty-state {
+ line-height: 20px;
- &.empty-state {
.artwork {
margin-bottom: $gl-padding;
}
.text {
span {
- font-weight: bold;
+ font-weight: 600;
}
p {
@@ -389,10 +346,6 @@
}
}
- .mr-widget-footer {
- border-top: 1px solid $gray-darker;
- }
-
.ci-coverage {
float: right;
}
@@ -497,8 +450,6 @@
}
.btn-clipboard {
- @extend .pull-right;
-
margin-right: 20px;
margin-top: 5px;
position: absolute;
@@ -506,56 +457,29 @@
}
}
+.mr-links {
+ padding-left: $status-icon-size + $status-icon-margin;
+}
+
.mr-info-list {
+ clear: left;
position: relative;
- margin: 10px 0 $gl-padding 12px;
+ padding-top: 4px;
p {
- margin: 6px 0;
+ margin: 0;
position: relative;
- padding-left: 15px;
-
- &::before {
- content: '';
- position: absolute;
- border-top: 2px solid $border-color;
- height: 1px;
- top: 9px;
- width: 8px;
- left: 0;
- }
+ padding: 4px 0;
&:last-child {
- margin-bottom: 0;
+ padding-bottom: 0;
}
}
-
- .legend {
- height: 100%;
- width: 2px;
- background: $border-color;
- position: absolute;
- top: -9px;
- }
}
.mr-info-list.mr-memory-usage {
- .legend {
- height: 65%;
- top: 0;
-
- @media (max-width: $screen-xs-max) {
- height: 20px;
- }
- }
-
p {
float: left;
- padding-left: 21px;
-
- &::before {
- top: 13px;
- }
}
.memory-graph-container {
@@ -565,12 +489,13 @@
}
.mr-source-target {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ align-items: center;
background-color: $gray-light;
- border-radius: 3px 3px 0 0;
- border-bottom: 1px solid $border-color;
- padding: 0 $gl-padding;
- margin-bottom: 6px;
- line-height: 44px;
+ border-radius: $border-radius-default $border-radius-default 0 0;
+ padding: $gl-padding / 2 $gl-padding;
.dropdown-toggle .fa {
color: $gl-text-color;
@@ -679,14 +604,8 @@
}
.merged-buttons {
- margin-top: 20px;
-
.btn {
float: left;
-
- &:not(:last-child) {
- margin-right: 10px;
- }
}
}
@@ -803,20 +722,8 @@
}
.mr-memory-usage {
- p.usage-info-loading,
- p.usage-info-unavailable,
- p.usage-info-failed {
- margin-bottom: 5px;
- }
-
p.usage-info-loading .usage-info-load-spinner {
margin-right: 10px;
font-size: 16px;
}
-
- @media (max-width: $screen-md-min) {
- .mr-info-list.mr-memory-usage .legend {
- height: 80%;
- }
- }
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index cdb1e65e4be..c90642178fc 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -104,40 +104,51 @@
}
.confidential-issue-warning {
- background-color: $gray-normal;
- border-radius: 3px;
+ color: $orange-600;
+ background-color: $orange-50;
+ border-radius: $border-radius-default $border-radius-default 0 0;
+ border: 1px solid $border-gray-normal;
padding: 3px 12px;
margin: auto;
- margin-top: 0;
- text-align: center;
- font-size: 12px;
align-items: center;
+}
- @media (max-width: $screen-md-max) {
- // On smaller devices the warning becomes the fourth item in the list,
- // rather than centering, and grows to span the full width of the
- // comment area.
- order: 4;
- margin: 6px auto;
- width: 100%;
+.confidential-value {
+ .fa {
+ background-color: inherit;
}
+}
- .fa {
- margin-right: 8px;
+.confidential-warning-message {
+ line-height: 1.5;
+ padding: 16px;
+
+ .confidential-warning-message-actions {
+ display: flex;
+
+ button {
+ flex-grow: 1;
+ }
}
}
+.not-confidential {
+ padding: 0;
+ border-top: none;
+}
+
.right-sidebar-expanded {
- .confidential-issue-warning {
- // When the sidebar is open the warning becomes the fourth item in the list,
- // rather than centering, and grows to span the full width of the
- // comment area.
- order: 4;
- margin: 6px auto;
- width: 100%;
+ .md-area {
+ border-radius: 0;
+ border-top: none;
}
}
+.right-sidebar-collapsed {
+ .confidential-issue-warning {
+ border-bottom: none;
+ }
+}
.discussion-form {
padding: $gl-padding-top $gl-padding $gl-padding;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index d3862df20d3..6185342b495 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -220,7 +220,11 @@
position: relative;
vertical-align: middle;
height: 22px;
- margin: 3px 6px 3px 0;
+ margin: 3px 0;
+
+ + .stage-container {
+ margin-left: 6px;
+ }
// Hack to show a button tooltip inline
button.has-tooltip + .tooltip {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 73603f20ef6..276465488e7 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -7,7 +7,8 @@
}
.new_project,
-.edit-project {
+.edit-project,
+.import-project {
.sharing-and-permissions {
.header {
@@ -457,6 +458,7 @@ a.deploy-project-label {
}
}
+.project-template,
.project-import {
.form-group {
margin-bottom: 5px;
@@ -471,7 +473,44 @@ a.deploy-project-label {
.btn {
padding: 8px;
- margin-left: 10px;
+ margin-right: 10px;
+ }
+
+ .blank-option {
+ min-width: 70px;
+ }
+
+ .btn-template-icon {
+ height: 24px;
+ width: inherit;
+ display: block;
+ margin: 0 auto 4px;
+ font-size: 24px;
+
+ @media (min-width: $screen-xs-max) {
+ top: 0;
+ }
+ }
+
+ @media (max-width: $screen-xs-max) {
+ .btn-template-icon {
+ display: inline-block;
+ height: 14px;
+ font-size: 14px;
+ margin: 0;
+ }
+ }
+
+ .icon-rails path {
+ fill: $rails;
+ }
+
+ .icon-node-express path {
+ fill: $node;
+ }
+
+ .icon-java-spring path {
+ fill: $java;
}
> div {
@@ -481,6 +520,97 @@ a.deploy-project-label {
}
}
+.project-templates-buttons .btn:last-child {
+ margin-right: 0;
+}
+
+.create-project-options {
+ display: flex;
+
+ @media (max-width: $screen-xs-max) {
+ display: block;
+ }
+
+ .first-column {
+ @media(min-width: $screen-xs-min) {
+ max-width: 50%;
+ padding-right: 30px;
+ }
+
+ @media(max-width: $screen-xs-max) {
+ max-width: 100%;
+ width: 100%;
+ }
+ }
+
+ .second-column {
+ @media(min-width: $screen-xs-min) {
+ width: 50%;
+ flex: 1;
+ padding-left: 30px;
+ position: relative;
+ }
+
+ @media(max-width: $screen-xs-max) {
+ max-width: 100%;
+ width: 100%;
+ padding-left: 0;
+ position: relative;
+ }
+
+ // Mobile
+ @media (max-width: $screen-xs-max) {
+ padding-top: 30px;
+ }
+
+ &::before {
+ content: "OR";
+ position: absolute;
+ left: 0;
+ top: 40%;
+ z-index: 10;
+ padding: 8px 0;
+ text-align: center;
+ background-color: $white-light;
+ color: $gl-text-color-tertiary;
+ transform: translateX(-50%);
+ font-size: 12px;
+ font-weight: bold;
+ line-height: 20px;
+
+ // Mobile
+ @media (max-width: $screen-xs-max) {
+ left: 50%;
+ top: 10px;
+ transform: translateY(-50%);
+ padding: 0 8px;
+ }
+ }
+
+ &::after {
+ content: "";
+ position: absolute;
+ background-color: $border-color;
+ bottom: 0;
+ left: 0;
+ right: auto;
+ height: 100%;
+ width: 1px;
+ top: 0;
+
+ // Mobile
+ @media (max-width: $screen-xs-max) {
+ top: 10px;
+ left: 10px;
+ right: 10px;
+ height: 1px;
+ width: auto;
+ }
+ }
+ }
+}
+
+
.project-stats {
font-size: 0;
text-align: center;
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
new file mode 100644
index 00000000000..ad17078c98a
--- /dev/null
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -0,0 +1,413 @@
+.fade-enter-active,
+.fade-leave-active {
+ transition: opacity .5s;
+}
+
+.monaco-loader {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: $black-transparent;
+}
+
+.modal.popup-dialog {
+ display: block;
+ background-color: $black-transparent;
+ z-index: 2100;
+
+ @media (min-width: $screen-md-min) {
+ .modal-dialog {
+ width: 600px;
+ margin: 30px auto;
+ }
+ }
+}
+
+.project-refs-form,
+.project-refs-target-form {
+ display: inline-block;
+
+ &.disabled {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+}
+
+.fade-enter,
+.fade-leave-to {
+ opacity: 0;
+}
+
+.commit-message {
+ @include str-truncated(250px);
+}
+
+.editable-mode {
+ display: inline-block;
+}
+
+.blob-viewer[data-type="rich"] {
+ margin: 20px;
+}
+
+.repository-view.tree-content-holder {
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+ color: $almost-black;
+
+ .panel-right {
+ display: inline-block;
+ width: 80%;
+
+ .monaco-editor.vs {
+ .line-numbers {
+ cursor: pointer;
+
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
+ .cursor {
+ display: none !important;
+ }
+ }
+
+ &.edit-mode {
+ .blob-viewer-container {
+ overflow: hidden;
+ }
+
+ .monaco-editor.vs {
+ .cursor {
+ background: $black;
+ border-color: $black;
+ display: block !important;
+ }
+ }
+ }
+
+ .blob-viewer-container {
+ height: calc(100vh - 63px);
+ overflow: auto;
+ }
+
+ #tabs {
+ padding-left: 0;
+ margin-bottom: 0;
+ display: flex;
+ white-space: nowrap;
+ width: 100%;
+ overflow-y: hidden;
+ overflow-x: auto;
+
+ li {
+ animation: swipeRightAppear ease-in 0.1s;
+ animation-iteration-count: 1;
+ transform-origin: 0% 50%;
+ list-style-type: none;
+ background: $gray-normal;
+ display: inline-block;
+ padding: 10px 18px;
+ border-right: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
+ white-space: nowrap;
+
+ &.remove {
+ animation: swipeRightDissapear ease-in 0.1s;
+ animation-iteration-count: 1;
+ transform-origin: 0% 50%;
+
+ a {
+ width: 0;
+ }
+ }
+
+ &.active {
+ background: $white-light;
+ border-bottom: none;
+ }
+
+ a {
+ @include str-truncated(100px);
+ color: $black;
+ display: inline-block;
+ width: 100px;
+ text-align: center;
+ vertical-align: middle;
+
+ &.close {
+ width: auto;
+ font-size: 15px;
+ opacity: 1;
+ margin-right: -6px;
+ }
+ }
+
+ i.fa.fa-times,
+ i.fa.fa-circle {
+ float: right;
+ margin-top: 3px;
+ margin-left: 15px;
+ color: $gray-darkest;
+ }
+
+ i.fa.fa-circle {
+ color: $brand-success;
+ }
+
+ &.tabs-divider {
+ width: 100%;
+ background-color: $white-light;
+ border-right: none;
+ border-top-right-radius: 2px;
+ }
+ }
+ }
+
+ #repo-file-buttons {
+ background-color: $white-light;
+ border-bottom: 1px solid $white-normal;
+ padding: 5px 10px;
+ position: relative;
+ border-top: 1px solid $white-normal;
+ margin-top: -5px;
+ }
+
+ #binary-viewer {
+ height: 80vh;
+ overflow: auto;
+ margin: 0;
+
+ .blob-viewer {
+ padding-top: 20px;
+ padding-left: 20px;
+ }
+
+ .binary-unknown {
+ text-align: center;
+ padding-top: 100px;
+ background: $gray-light;
+ height: 100%;
+ font-size: 17px;
+
+ span {
+ display: block;
+ }
+ }
+ }
+ }
+
+ #commit-area {
+ background: $gray-light;
+ padding: 20px;
+
+ span.help-block {
+ padding-top: 7px;
+ margin-top: 0;
+ }
+ }
+
+ #view-toggler {
+ height: 41px;
+ position: relative;
+ display: block;
+ border-bottom: 1px solid $white-normal;
+ background: $white-light;
+ margin-top: -5px;
+ }
+
+ #binary-viewer {
+ img {
+ max-width: 100%;
+ }
+ }
+
+ #sidebar {
+
+ &.sidebar-mini {
+ display: inline-block;
+ vertical-align: top;
+ width: 20%;
+ border-right: 1px solid $white-normal;
+ height: calc(100vh + 20px);
+ overflow: auto;
+ }
+
+ table {
+ margin-bottom: 0;
+ }
+
+ tr {
+ animation: fadein 0.5s;
+ cursor: pointer;
+
+ &.repo-file-options td {
+ padding: 0;
+ border-top: none;
+ background: $gray-light;
+ width: 100%;
+ display: inline-block;
+
+ &:first-child {
+ border-top-left-radius: 2px;
+ }
+
+ .title {
+ display: inline-block;
+ font-size: 10px;
+ text-transform: uppercase;
+ font-weight: bold;
+ color: $gray-darkest;
+ width: 185px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: middle;
+ padding: 2px 16px;
+ }
+ }
+
+ .fa {
+ margin-right: 5px;
+ }
+
+ td {
+ white-space: nowrap;
+ }
+ }
+
+ a {
+ color: $almost-black;
+ display: inline-block;
+ vertical-align: middle;
+ }
+
+ ul {
+ list-style-type: none;
+ padding: 0;
+
+ li {
+ border-bottom: 1px solid $border-gray-normal;
+ padding: 10px 20px;
+
+ a {
+ color: $almost-black;
+ }
+
+ .fa {
+ font-size: $code_font_size;
+ margin-right: 5px;
+ }
+ }
+ }
+ }
+
+}
+
+.animation-container {
+ background: $repo-editor-grey;
+ height: 40px;
+ overflow: hidden;
+ position: relative;
+
+ &.animation-container-small {
+ height: 12px;
+ }
+
+ &::before {
+ animation-duration: 1s;
+ animation-fill-mode: forwards;
+ animation-iteration-count: infinite;
+ animation-name: blockTextShine;
+ animation-timing-function: linear;
+ background-image: $repo-editor-linear-gradient;
+ background-repeat: no-repeat;
+ background-size: 800px 45px;
+ content: ' ';
+ display: block;
+ height: 100%;
+ position: relative;
+ }
+
+ div {
+ background: $white-light;
+ height: 6px;
+ left: 0;
+ position: absolute;
+ right: 0;
+ }
+
+ .line-of-code-1 {
+ left: 0;
+ top: 8px;
+ }
+
+ .line-of-code-2 {
+ left: 150px;
+ top: 0;
+ height: 10px;
+ }
+
+ .line-of-code-3 {
+ left: 0;
+ top: 23px;
+ }
+
+ .line-of-code-4 {
+ left: 0;
+ top: 38px;
+ }
+
+ .line-of-code-5 {
+ left: 200px;
+ top: 28px;
+ height: 10px;
+ }
+
+ .line-of-code-6 {
+ top: 14px;
+ left: 230px;
+ height: 10px;
+ }
+}
+
+.render-error {
+ min-height: calc(100vh - 63px);
+
+ p {
+ width: 100%;
+ }
+}
+
+@keyframes blockTextShine {
+ 0% {
+ transform: translateX(-468px);
+ }
+
+ 100% {
+ transform: translateX(468px);
+ }
+}
+
+@keyframes swipeRightAppear {
+ 0% {
+ transform: scaleX(0.00);
+ }
+
+ 100% {
+ transform: scaleX(1.00);
+ }
+}
+
+@keyframes swipeRightDissapear {
+ 0% {
+ transform: scaleX(1.00);
+ }
+
+ 100% {
+ transform: scaleX(0.00);
+ }
+}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 44ab07a4367..11236cbf2e7 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -87,7 +87,7 @@
}
.add-to-tree {
- vertical-align: top;
+ vertical-align: middle;
padding: 6px 10px;
}
@@ -216,6 +216,9 @@
}
.blob-upload-dropzone-previews {
+ display: flex;
+ justify-content: center;
+ align-items: center;
text-align: center;
border: 2px;
border-style: dashed;
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index 45c21c5d274..fa6bdd297eb 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -95,12 +95,22 @@
}
.right-sidebar.wiki-sidebar {
- padding: $gl-padding 0;
+ padding: 0;
&.right-sidebar-collapsed {
display: none;
}
+ .sidebar-container {
+ padding: $gl-padding 0;
+ width: calc(100% + 100px);
+ padding-right: 100px;
+ height: 100%;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch;
+ }
+
.blocks-container {
padding: 0 $gl-padding;
}
diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb
index caf4c138da8..65a17828feb 100644
--- a/app/controllers/admin/health_check_controller.rb
+++ b/app/controllers/admin/health_check_controller.rb
@@ -1,5 +1,12 @@
class Admin::HealthCheckController < Admin::ApplicationController
def show
@errors = HealthCheck::Utils.process_checks(['standard'])
+ @failing_storage_statuses = Gitlab::Git::Storage::Health.for_failing_storages
+ end
+
+ def reset_storage_health
+ Gitlab::Git::Storage::CircuitBreaker.reset_all!
+ redirect_to admin_health_check_path,
+ notice: _('Git storage health information has been reset')
end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index d14b1dbecf6..5b448008a1b 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -52,6 +52,15 @@ class ApplicationController < ActionController::Base
head :forbidden, retry_after: Gitlab::Auth::UniqueIpsLimiter.config.unique_ips_limit_time_window
end
+ rescue_from Gitlab::Git::Storage::Inaccessible, GRPC::Unavailable, Gitlab::Git::CommandError do |exception|
+ Raven.capture_exception(exception) if sentry_enabled?
+ log_exception(exception)
+
+ headers['Retry-After'] = exception.retry_after if exception.respond_to?(:retry_after)
+
+ render_503
+ end
+
def redirect_back_or_default(default: root_path, options: {})
redirect_to request.referer.present? ? :back : default, options
end
@@ -152,6 +161,19 @@ class ApplicationController < ActionController::Base
head :unprocessable_entity
end
+ def render_503
+ respond_to do |format|
+ format.html do
+ render(
+ file: Rails.root.join("public", "503"),
+ layout: false,
+ status: :service_unavailable
+ )
+ end
+ format.any { head :service_unavailable }
+ end
+ end
+
def no_cache_headers
response.headers["Cache-Control"] = "no-cache, no-store, max-age=0, must-revalidate"
response.headers["Pragma"] = "no-cache"
diff --git a/app/controllers/concerns/renders_blob.rb b/app/controllers/concerns/renders_blob.rb
index 54dcd7c61ce..ba7adcfea86 100644
--- a/app/controllers/concerns/renders_blob.rb
+++ b/app/controllers/concerns/renders_blob.rb
@@ -1,7 +1,7 @@
module RendersBlob
extend ActiveSupport::Concern
- def render_blob_json(blob)
+ def blob_json(blob)
viewer =
case params[:viewer]
when 'rich'
@@ -11,13 +11,21 @@ module RendersBlob
else
blob.simple_viewer
end
- return render_404 unless viewer
- render json: {
+ return unless viewer
+
+ {
html: view_to_html_string("projects/blob/_viewer", viewer: viewer, load_async: false)
}
end
+ def render_blob_json(blob)
+ json = blob_json(blob)
+ return render_404 unless json
+
+ render json: json
+ end
+
def conditionally_expand_blob(blob)
blob.expand! if params[:expanded] == 'true'
end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 91c1e4dff79..74fe45e1ff6 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -45,8 +45,10 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
def load_projects(finder_params)
- ProjectsFinder.new(params: finder_params, current_user: current_user)
- .execute.includes(:route, namespace: :route)
+ ProjectsFinder
+ .new(params: finder_params, current_user: current_user)
+ .execute
+ .includes(:route, :creator, namespace: :route)
end
def load_events
diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb
index 36d246d185b..510813846a4 100644
--- a/app/controllers/import/gitlab_projects_controller.rb
+++ b/app/controllers/import/gitlab_projects_controller.rb
@@ -12,15 +12,7 @@ class Import::GitlabProjectsController < Import::BaseController
return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive." })
end
- import_upload_path = Gitlab::ImportExport.import_upload_path(filename: project_params[:file].original_filename)
-
- FileUtils.mkdir_p(File.dirname(import_upload_path))
- FileUtils.copy_entry(project_params[:file].path, import_upload_path)
-
- @project = Gitlab::ImportExport::ProjectCreator.new(project_params[:namespace_id],
- current_user,
- import_upload_path,
- project_params[:path]).execute
+ @project = ::Projects::GitlabProjectsImportService.new(current_user, project_params).execute
if @project.saved?
redirect_to(
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 323d5d26eb6..b4213574561 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -34,12 +34,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
if @user.two_factor_enabled?
prompt_for_two_factor(@user)
else
- log_audit_event(@user, with: :ldap)
+ log_audit_event(@user, with: oauth['provider'])
sign_in_and_redirect(@user)
end
else
- flash[:alert] = "Access denied for your LDAP account."
- redirect_to new_user_session_path
+ fail_ldap_login
end
end
@@ -123,9 +122,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
sign_in_and_redirect(@user)
end
else
- error_message = @user.errors.full_messages.to_sentence
-
- return redirect_to omniauth_error_path(oauth['provider'], error: error_message)
+ fail_login
end
end
@@ -145,6 +142,18 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def oauth
@oauth ||= request.env['omniauth.auth']
end
+
+ def fail_login
+ error_message = @user.errors.full_messages.to_sentence
+
+ return redirect_to omniauth_error_path(oauth['provider'], error: error_message)
+ end
+
+ def fail_ldap_login
+ flash[:alert] = 'Access denied for your LDAP account.'
+
+ redirect_to new_user_session_path
+ end
def log_audit_event(user, options = {})
AuditEventService.new(user, user, options)
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 49ea2945675..a2e8c10857d 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -37,16 +37,11 @@ class Projects::BlobController < Projects::ApplicationController
respond_to do |format|
format.html do
- environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
- @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
-
- @last_commit = @repository.last_commit_for_path(@commit.id, @blob.path)
-
- render 'show'
+ show_html
end
format.json do
- render_blob_json(@blob)
+ show_json
end
end
end
@@ -190,4 +185,34 @@ class Projects::BlobController < Projects::ApplicationController
@last_commit_sha = Gitlab::Git::Commit
.last_for_path(@repository, @ref, @path).sha
end
+
+ def show_html
+ environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
+ @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
+ @last_commit = @repository.last_commit_for_path(@commit.id, @blob.path)
+
+ render 'show'
+ end
+
+ def show_json
+ json = blob_json(@blob)
+ return render_404 unless json
+
+ render json: json.merge(
+ path: blob.path,
+ name: blob.name,
+ extension: blob.extension,
+ size: blob.raw_size,
+ mime_type: blob.mime_type,
+ binary: blob.raw_binary?,
+ simple_viewer: blob.simple_viewer&.class&.partial_name,
+ rich_viewer: blob.rich_viewer&.class&.partial_name,
+ show_viewer_switcher: !!blob.show_viewer_switcher?,
+ render_error: blob.simple_viewer&.render_error || blob.rich_viewer&.render_error,
+ raw_path: project_raw_path(project, @id),
+ blame_path: project_blame_path(project, @id),
+ commits_path: project_commits_path(project, @id),
+ permalink: project_blob_path(project, File.join(@commit.id, @path))
+ )
+ end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index d361e661d0e..4de814d0ca8 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -67,11 +67,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@noteable = @merge_request
@commits_count = @merge_request.commits_count
- if @merge_request.locked_long_ago?
- @merge_request.unlock_mr
- @merge_request.close
- end
-
labels
set_pipeline_variables
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index 30181ac3bdf..1fc276b8c03 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -24,12 +24,19 @@ class Projects::TreeController < Projects::ApplicationController
end
end
- @last_commit = @repository.last_commit_for_path(@commit.id, @tree.path) || @commit
-
respond_to do |format|
- format.html
- # Disable cache so browser history works
- format.js { no_cache_headers }
+ format.html do
+ @last_commit = @repository.last_commit_for_path(@commit.id, @tree.path) || @commit
+ end
+
+ format.js do
+ # Disable cache so browser history works
+ no_cache_headers
+ end
+
+ format.json do
+ render json: TreeSerializer.new(project: @project, repository: @repository, ref: @ref).represent(@tree)
+ end
end
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 2d7cbd4614e..8dfe0f51709 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -220,21 +220,34 @@ class ProjectsController < Projects::ApplicationController
end
def refs
- branches = BranchesFinder.new(@repository, params).execute.map(&:name)
+ find_refs = params['find']
- options = {
- s_('RefSwitcher|Branches') => branches.take(100)
- }
+ find_branches = true
+ find_tags = true
+ find_commits = true
+
+ unless find_refs.nil?
+ find_branches = find_refs.include?('branches')
+ find_tags = find_refs.include?('tags')
+ find_commits = find_refs.include?('commits')
+ end
+
+ options = {}
+
+ if find_branches
+ branches = BranchesFinder.new(@repository, params).execute.take(100).map(&:name)
+ options[s_('RefSwitcher|Branches')] = branches
+ end
- unless @repository.tag_count.zero?
- tags = TagsFinder.new(@repository, params).execute.map(&:name)
+ if find_tags && @repository.tag_count.nonzero?
+ tags = TagsFinder.new(@repository, params).execute.take(100).map(&:name)
- options[s_('RefSwitcher|Tags')] = tags.take(100)
+ options[s_('RefSwitcher|Tags')] = tags
end
# If reference is commit id - we should add it to branch/tag selectbox
ref = Addressable::URI.unescape(params[:ref])
- if ref && options.flatten(2).exclude?(ref) && ref =~ /\A[0-9a-zA-Z]{6,52}\z/
+ if find_commits && ref && options.flatten(2).exclude?(ref) && ref =~ /\A[0-9a-zA-Z]{6,52}\z/
options['Commits'] = [ref]
end
@@ -324,6 +337,7 @@ class ProjectsController < Projects::ApplicationController
:runners_token,
:tag_list,
:visibility_level,
+ :template_name,
project_feature_attributes: %i[
builds_access_level
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 14dc9bd9d62..bcee81bdc15 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -305,4 +305,12 @@ module ApplicationHelper
def show_new_nav?
cookies["new_nav"] == "true"
end
+
+ def collapsed_sidebar?
+ cookies["sidebar_collapsed"] == "true"
+ end
+
+ def show_new_repo?
+ cookies["new_repo"] == "true" && body_data_page != 'projects:show'
+ end
end
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index 0e068d4b51c..4b51269533c 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -19,7 +19,8 @@ module AvatarsHelper
class: %W[avatar has-tooltip s#{avatar_size}].push(*options[:css_class]),
alt: "#{user_name}'s avatar",
title: user_name,
- data: data_attributes
+ data: data_attributes,
+ lazy: true
)
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index e964d7a5e16..18075ee8be7 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -118,7 +118,7 @@ module BlobHelper
icon("#{file_type_icon_class('file', mode, name)} fw")
end
- def blob_raw_url
+ def blob_raw_path
if @build && @entry
raw_project_job_artifacts_path(@project, @build, path: @entry.path)
elsif @snippet
@@ -235,7 +235,7 @@ module BlobHelper
title = 'Open raw'
end
- link_to icon, blob_raw_url, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' }
+ link_to icon, blob_raw_path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' }
end
def blob_render_error_reason(viewer)
@@ -270,7 +270,7 @@ module BlobHelper
options << link_to('view the source', '#', class: 'js-blob-viewer-switch-btn', data: { viewer: 'simple' })
end
- options << link_to('download it', blob_raw_url, target: '_blank', rel: 'noopener noreferrer')
+ options << link_to('download it', blob_raw_path, target: '_blank', rel: 'noopener noreferrer')
options
end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 087f7f88fb5..28f591a4e22 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -88,15 +88,15 @@ module DiffHelper
end
def submodule_link(blob, ref, repository = @repository)
- tree, commit = submodule_links(blob, ref, repository)
- commit_id = if commit.nil?
+ project_url, tree_url = submodule_links(blob, ref, repository)
+ commit_id = if tree_url.nil?
Commit.truncate_sha(blob.id)
else
- link_to Commit.truncate_sha(blob.id), commit
+ link_to Commit.truncate_sha(blob.id), tree_url
end
[
- content_tag(:span, link_to(truncate(blob.name, length: 40), tree)),
+ content_tag(:span, link_to(truncate(blob.name, length: 40), project_url)),
'@',
content_tag(:span, commit_id, class: 'commit-sha')
].join(' ').html_safe
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index ac8c518ac84..ff305fa39b4 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -48,11 +48,11 @@ module DropdownsHelper
end
end
- def dropdown_title(title, back: false)
+ def dropdown_title(title, options: {})
content_tag :div, class: "dropdown-title" do
title_output = ""
- if back
+ if options.fetch(:back, false)
title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-back", aria: { label: "Go back" }, type: "button") do
icon('arrow-left')
end
@@ -60,14 +60,25 @@ module DropdownsHelper
title_output << content_tag(:span, title)
- title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do
- icon('times', class: 'dropdown-menu-close-icon')
+ if options.fetch(:close, true)
+ title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close", aria: { label: "Close" }, type: "button") do
+ icon('times', class: 'dropdown-menu-close-icon')
+ end
end
title_output.html_safe
end
end
+ def dropdown_input(placeholder, input_id: nil)
+ content_tag :div, class: "dropdown-input" do
+ filter_output = text_field_tag input_id, nil, class: "dropdown-input-field dropdown-no-filter", placeholder: placeholder, autocomplete: 'off'
+ filter_output << icon('times', class: "dropdown-input-clear js-dropdown-input-clear", role: "button")
+
+ filter_output.html_safe
+ end
+ end
+
def dropdown_filter(placeholder, search_id: nil)
content_tag :div, class: "dropdown-input" do
filter_output = search_field_tag search_id, nil, class: "dropdown-input-field", placeholder: placeholder, autocomplete: 'off'
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 1f7db9b2eb8..d4a91e533c1 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -47,14 +47,6 @@ module GitlabRoutingHelper
project_pipeline_path(pipeline.project, pipeline.id, *args)
end
- def milestone_path(entity, *args)
- if entity.is_group_milestone?
- group_milestone_path(entity.group, entity, *args)
- elsif entity.is_project_milestone?
- project_milestone_path(entity.project, entity, *args)
- end
- end
-
def issue_url(entity, *args)
project_issue_url(entity.project, entity, *args)
end
@@ -67,14 +59,6 @@ module GitlabRoutingHelper
project_pipeline_url(pipeline.project, pipeline.id, *args)
end
- def milestone_url(entity, *args)
- if entity.is_group_milestone?
- group_milestone_url(entity.group, entity, *args)
- elsif entity.is_project_milestone?
- project_milestone_url(entity.project, entity, *args)
- end
- end
-
def pipeline_job_url(pipeline, build, *args)
project_job_url(pipeline.project, build.id, *args)
end
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index f29faeca22d..9a404832423 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -1,4 +1,5 @@
module IconsHelper
+ extend self
include FontAwesome::Rails::IconHelper
# Creates an icon tag given icon name(s) and possible icon modifiers.
diff --git a/app/helpers/milestones_routing_helper.rb b/app/helpers/milestones_routing_helper.rb
new file mode 100644
index 00000000000..766d5262018
--- /dev/null
+++ b/app/helpers/milestones_routing_helper.rb
@@ -0,0 +1,17 @@
+module MilestonesRoutingHelper
+ def milestone_path(milestone, *args)
+ if milestone.is_group_milestone?
+ group_milestone_path(milestone.group, milestone, *args)
+ elsif milestone.is_project_milestone?
+ project_milestone_path(milestone.project, milestone, *args)
+ end
+ end
+
+ def milestone_url(milestone, *args)
+ if milestone.is_group_milestone?
+ group_milestone_url(milestone.group, milestone, *args)
+ elsif milestone.is_project_milestone?
+ project_milestone_url(milestone.project, milestone, *args)
+ end
+ end
+end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index b1205b8529b..b63b3b70903 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -2,6 +2,7 @@ module NavHelper
def page_with_sidebar_class
class_name = page_gutter_class
class_name << 'page-with-new-sidebar' if defined?(@new_sidebar) && @new_sidebar
+ class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @new_sidebar
class_name
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 34ff6107eab..a268413e84f 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -225,6 +225,26 @@ module ProjectsHelper
end
end
+ # Returns true if any projects are present.
+ #
+ # If the relation has a LIMIT applied we'll cast the relation to an Array
+ # since repeated any? checks would otherwise result in multiple COUNT queries
+ # being executed.
+ #
+ # If no limit is applied we'll just issue a COUNT since the result set could
+ # be too large to load into memory.
+ def any_projects?(projects)
+ if projects.limit_value
+ projects.to_a.any?
+ else
+ projects.except(:offset).any?
+ end
+ end
+
+ def has_projects_or_name?(projects, params)
+ !!(params[:name] || any_projects?(projects))
+ end
+
private
def repo_children_classes(field)
diff --git a/app/helpers/storage_health_helper.rb b/app/helpers/storage_health_helper.rb
new file mode 100644
index 00000000000..544c9efb845
--- /dev/null
+++ b/app/helpers/storage_health_helper.rb
@@ -0,0 +1,37 @@
+module StorageHealthHelper
+ def failing_storage_health_message(storage_health)
+ storage_name = content_tag(:strong, h(storage_health.storage_name))
+ host_names = h(storage_health.failing_on_hosts.to_sentence)
+ translation_params = { storage_name: storage_name,
+ host_names: host_names,
+ failed_attempts: storage_health.total_failures }
+
+ translation = n_('%{storage_name}: failed storage access attempt on host:',
+ '%{storage_name}: %{failed_attempts} failed storage access attempts:',
+ storage_health.total_failures) % translation_params
+
+ translation.html_safe
+ end
+
+ def message_for_circuit_breaker(circuit_breaker)
+ maximum_failures = circuit_breaker.failure_count_threshold
+ current_failures = circuit_breaker.failure_count
+ permanently_broken = circuit_breaker.circuit_broken? && current_failures >= maximum_failures
+
+ translation_params = { number_of_failures: current_failures,
+ maximum_failures: maximum_failures,
+ number_of_seconds: circuit_breaker.failure_wait_time }
+
+ if permanently_broken
+ s_("%{number_of_failures} of %{maximum_failures} failures. GitLab will not "\
+ "retry automatically. Reset storage information when the problem is "\
+ "resolved.") % translation_params
+ elsif circuit_breaker.circuit_broken?
+ _("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\
+ "block access for %{number_of_seconds} seconds.") % translation_params
+ else
+ _("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\
+ "allow access on the next attempt.") % translation_params
+ end
+ end
+end
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index b24039fb349..88f7702db1e 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -1,5 +1,5 @@
module SubmoduleHelper
- include Gitlab::ShellAdapter
+ extend self
VALID_SUBMODULE_PROTOCOLS = %w[http https git ssh].freeze
@@ -59,7 +59,7 @@ module SubmoduleHelper
return true if url_no_dotgit == [Gitlab.config.gitlab.url, '/', namespace, '/',
project].join('')
url_with_dotgit = url_no_dotgit + '.git'
- url_with_dotgit == gitlab_shell.url_to_repo([namespace, '/', project].join(''))
+ url_with_dotgit == Gitlab::Shell.new.url_to_repo([namespace, '/', project].join(''))
end
def relative_self_url?(url)
diff --git a/app/models/blob_viewer/base.rb b/app/models/blob_viewer/base.rb
index 35965d01692..bf3453b3063 100644
--- a/app/models/blob_viewer/base.rb
+++ b/app/models/blob_viewer/base.rb
@@ -82,7 +82,7 @@ module BlobViewer
# format of the blob.
#
# Prefer to implement a client-side viewer, where the JS component loads the
- # binary from `blob_raw_url` and does its own format validation and error
+ # binary from `blob_raw_path` and does its own format validation and error
# rendering, especially for potentially large binary formats.
def render_error
if too_large?
diff --git a/app/models/blob_viewer/server_side.rb b/app/models/blob_viewer/server_side.rb
index fbc1b520c01..86afcc86aa0 100644
--- a/app/models/blob_viewer/server_side.rb
+++ b/app/models/blob_viewer/server_side.rb
@@ -17,7 +17,7 @@ module BlobViewer
# build artifacts, can only be rendered using a client-side viewer,
# since we do not want to read large amounts of data into memory on the
# server side. Client-side viewers use JS and can fetch the file from
- # `blob_raw_url` using AJAX.
+ # `blob_raw_path` using AJAX.
return :server_side_but_stored_externally if blob.stored_externally?
super
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 7940733f557..638fddc5d3d 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -55,7 +55,8 @@ class Commit
end
def from_hash(hash, project)
- new(Gitlab::Git::Commit.new(hash), project)
+ raw_commit = Gitlab::Git::Commit.new(project.repository.raw, hash)
+ new(raw_commit, project)
end
def valid_hash?(key)
@@ -320,21 +321,11 @@ class Commit
end
def raw_diffs(*args)
- if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
- Gitlab::GitalyClient::CommitService.new(project.repository).diff_from_parent(self, *args)
- else
- raw.diffs(*args)
- end
+ raw.diffs(*args)
end
def raw_deltas
- @deltas ||= Gitlab::GitalyClient.migrate(:commit_deltas) do |is_enabled|
- if is_enabled
- Gitlab::GitalyClient::CommitService.new(project.repository).commit_deltas(self)
- else
- raw.deltas
- end
- end
+ @deltas ||= raw.deltas
end
def diffs(diff_options = nil)
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 935ffe343ff..3731b7c8577 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -16,6 +16,7 @@ module Issuable
include TimeTrackable
include Importable
include Editable
+ include AfterCommitQueue
# This object is used to gather issuable meta data for displaying
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests
diff --git a/app/models/conversational_development_index/metric.rb b/app/models/conversational_development_index/metric.rb
index f42f516f99a..0bee62f954f 100644
--- a/app/models/conversational_development_index/metric.rb
+++ b/app/models/conversational_development_index/metric.rb
@@ -13,9 +13,7 @@ module ConversationalDevelopmentIndex
end
def percentage_score(feature)
- return 100 if leader_score(feature).zero?
-
- 100 * instance_score(feature) / leader_score(feature)
+ self["percentage_#{feature}"]
end
end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 8ca850b6d96..e83b11f7668 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -8,6 +8,7 @@ class MergeRequest < ActiveRecord::Base
include CreatedAtFilterable
ignore_column :position
+ ignore_column :locked_at
belongs_to :target_project, class_name: "Project"
belongs_to :source_project, class_name: "Project"
@@ -61,16 +62,6 @@ class MergeRequest < ActiveRecord::Base
transition locked: :opened
end
- after_transition any => :locked do |merge_request, transition|
- merge_request.locked_at = Time.now
- merge_request.save
- end
-
- after_transition locked: (any - :locked) do |merge_request, transition|
- merge_request.locked_at = nil
- merge_request.save
- end
-
state :opened
state :closed
state :merged
@@ -392,6 +383,12 @@ class MergeRequest < ActiveRecord::Base
'Source project is not a fork of the target project'
end
+ def merge_ongoing?
+ return false unless merge_jid
+
+ Gitlab::SidekiqStatus.num_running([merge_jid]) > 0
+ end
+
def closed_without_fork?
closed? && source_project_missing?
end
@@ -725,12 +722,6 @@ class MergeRequest < ActiveRecord::Base
end
end
- def locked_long_ago?
- return false unless locked?
-
- locked_at.nil? || locked_at < (Time.now - 1.day)
- end
-
def has_ci?
has_ci_integration = source_project.try(:ci_service)
uses_gitlab_ci = all_pipelines.any?
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index d9d746ccf41..58050e1f438 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -282,9 +282,7 @@ class MergeRequestDiff < ActiveRecord::Base
def load_commits
commits = st_commits.presence || merge_request_diff_commits
- commits.map do |commit|
- Commit.new(Gitlab::Git::Commit.new(commit.to_hash), merge_request.source_project)
- end
+ commits.map { |commit| Commit.from_hash(commit.to_hash, project) }
end
def save_diffs
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 48d00764965..01e0d0155a3 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -149,7 +149,9 @@ class Milestone < ActiveRecord::Base
end
##
- # Returns the String necessary to reference this Milestone in Markdown
+ # Returns the String necessary to reference this Milestone in Markdown. Group
+ # milestones only support name references, and do not support cross-project
+ # references.
#
# format - Symbol format to use (default: :iid, optional: :name)
#
@@ -161,12 +163,16 @@ class Milestone < ActiveRecord::Base
# Milestone.first.to_reference(same_namespace_project) # => "gitlab-ce%1"
#
def to_reference(from_project = nil, format: :iid, full: false)
- return if is_group_milestone?
+ return if is_group_milestone? && format != :name
format_reference = milestone_format_reference(format)
reference = "#{self.class.reference_prefix}#{format_reference}"
- "#{project.to_reference(from_project, full: full)}#{reference}"
+ if project
+ "#{project.to_reference(from_project, full: full)}#{reference}"
+ else
+ reference
+ end
end
def reference_link_text(from_project = nil)
diff --git a/app/models/project.rb b/app/models/project.rb
index d85782782aa..e7baba2ef08 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -75,6 +75,7 @@ class Project < ActiveRecord::Base
attr_accessor :new_default_branch
attr_accessor :old_path_with_namespace
+ attr_accessor :template_name
attr_writer :pipeline_status
alias_attribute :title, :name
@@ -163,7 +164,7 @@ class Project < ActiveRecord::Base
has_many :todos
has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
- has_one :import_data, class_name: 'ProjectImportData'
+ has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
has_one :project_feature
has_one :statistics, class_name: 'ProjectStatistics'
@@ -192,6 +193,7 @@ class Project < ActiveRecord::Base
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature
+ accepts_nested_attributes_for :import_data
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :count, to: :forks, prefix: true
@@ -588,8 +590,6 @@ class Project < ActiveRecord::Base
project_import_data.credentials ||= {}
project_import_data.credentials = project_import_data.credentials.merge(credentials)
end
-
- project_import_data.save
end
def import?
@@ -941,7 +941,7 @@ class Project < ActiveRecord::Base
end
def repo
- repository.raw
+ repository.rugged
end
def url_to_repo
diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb
index 37730474324..6da6632f4f2 100644
--- a/app/models/project_import_data.rb
+++ b/app/models/project_import_data.rb
@@ -1,7 +1,7 @@
require 'carrierwave/orm/activerecord'
class ProjectImportData < ActiveRecord::Base
- belongs_to :project
+ belongs_to :project, inverse_of: :import_data
attr_encrypted :credentials,
key: Gitlab::Application.secrets.db_key_base,
marshal: true,
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index e8929a35836..698fdf7a20c 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -113,10 +113,10 @@ class ProjectWiki
return false
end
- def update_page(page, content, format = :markdown, message = nil)
+ def update_page(page, content:, title: nil, format: :markdown, message: nil)
commit = commit_details(:updated, message, page.title)
- wiki.update_page(page, page.name, format.to_sym, content, commit)
+ wiki.update_page(page, title || page.name, format.to_sym, content, commit)
update_project_activity
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 2dd48290e58..ff82b958255 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -64,6 +64,8 @@ class Repository
@raw_repository ||= initialize_raw_repository
end
+ alias_method :raw, :raw_repository
+
# Return absolute path to repository
def path_to_repo
@path_to_repo ||= File.expand_path(
@@ -130,16 +132,13 @@ class Repository
return []
end
- ref ||= root_ref
-
- args = %W(
- #{Gitlab.config.git.bin_path} 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 = Gitlab::Popen.popen(args, path_to_repo).first.lines
- git_log_results.map { |c| commit(c.chomp) }.compact
+ raw_repository.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
+ end
end
def find_branch(name, fresh_repo: true)
@@ -687,8 +686,8 @@ class Repository
end
def refs_contains_sha(ref_type, sha)
- args = %W(#{Gitlab.config.git.bin_path} #{ref_type} --contains #{sha})
- names = Gitlab::Popen.popen(args, path_to_repo).first
+ args = %W(#{ref_type} --contains #{sha})
+ names = run_git(args).first
if names.respond_to?(:split)
names = names.split("\n").map(&:strip)
@@ -766,7 +765,7 @@ class Repository
index = Gitlab::Git::Index.new(raw_repository)
if start_commit
- index.read_tree(start_commit.raw_commit.tree)
+ index.read_tree(start_commit.rugged_commit.tree)
parents = [start_commit.sha]
else
parents = []
@@ -966,15 +965,17 @@ class Repository
return [] if empty_repo? || query.blank?
offset = 2
- args = %W(#{Gitlab.config.git.bin_path} grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
- Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
+ args = %W(grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref})
+
+ run_git(args).first.scrub.split(/^--$/)
end
def search_files_by_name(query, ref)
return [] if empty_repo? || query.blank?
- args = %W(#{Gitlab.config.git.bin_path} ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)})
- Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:strip)
+ args = %W(ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)})
+
+ run_git(args).first.lines.map(&:strip)
end
def with_repo_branch_commit(start_repository, start_branch_name)
@@ -1019,8 +1020,8 @@ class Repository
end
def fetch_ref(source_path, source_ref, target_ref)
- args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
- Gitlab::Popen.popen(args, path_to_repo)
+ args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
+ run_git(args)
end
def create_ref(ref, ref_path)
@@ -1101,6 +1102,12 @@ class Repository
private
+ def run_git(args)
+ circuit_breaker.perform do
+ Gitlab::Popen.popen([Gitlab.config.git.bin_path, *args], path_to_repo)
+ end
+ end
+
def blob_data_at(sha, path)
blob = blob_at(sha, path)
return unless blob
@@ -1110,7 +1117,9 @@ class Repository
end
def refs_directory_exists?
- File.exist?(File.join(path_to_repo, 'refs'))
+ circuit_breaker.perform do
+ File.exist?(File.join(path_to_repo, 'refs'))
+ end
end
def cache
@@ -1158,8 +1167,8 @@ class Repository
end
def last_commit_id_for_path_by_shelling_out(sha, path)
- args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path})
- Gitlab::Popen.popen(args, path_to_repo).first.strip
+ args = %W(rev-list --max-count=1 #{sha} -- #{path})
+ run_git(args).first.strip
end
def repository_storage_path
@@ -1169,4 +1178,29 @@ class Repository
def initialize_raw_repository
Gitlab::Git::Repository.new(project.repository_storage, disk_path + '.git')
end
+
+ def circuit_breaker
+ @circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(project.repository_storage)
+ 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)
+ raw_repository
+ .gitaly_commit_client
+ .commits_by_message(query, revision: ref, path: path, limit: limit, offset: offset)
+ .map { |c| commit(c) }
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 267eebb42ff..5148886eed7 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -632,7 +632,11 @@ class User < ActiveRecord::Base
end
def projects_limit_left
- projects_limit - personal_projects.count
+ projects_limit - personal_projects_count
+ end
+
+ def personal_projects_count
+ @personal_projects_count ||= personal_projects.count
end
def projects_limit_percent
@@ -646,16 +650,14 @@ class User < ActiveRecord::Base
events = events.where(project_id: project_ids) if project_ids
# Use the latest event that has not been pushed or merged recently
- events.recent.find do |event|
- project = Project.find_by_id(event.project_id)
- next unless project
-
- if project.repository.branch_exists?(event.branch_name)
- merge_requests = MergeRequest.where("created_at >= ?", event.created_at)
- .where(source_project_id: project.id,
- source_branch: event.branch_name)
- merge_requests.empty?
- end
+ events.includes(:project).recent.find do |event|
+ next unless event.project.repository.branch_exists?(event.branch_name)
+
+ merge_requests = MergeRequest.where("created_at >= ?", event.created_at)
+ .where(source_project_id: event.project.id,
+ source_branch: event.branch_name)
+
+ merge_requests.empty?
end
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 148998bc9be..5c7c2204374 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -180,31 +180,50 @@ class WikiPage
#
# Returns the String SHA1 of the newly created page
# or False if the save was unsuccessful.
- def create(attr = {})
- @attributes.merge!(attr)
+ def create(attrs = {})
+ @attributes.merge!(attrs)
- save :create_page, title, content, format, message
+ save(page_details: title) do
+ wiki.create_page(title, content, format, message)
+ end
end
# Updates an existing Wiki Page, creating a new version.
#
- # new_content - The raw markup content to replace the existing.
- # format - Optional symbol representing the content format.
- # See ProjectWiki::MARKUPS Hash for available formats.
- # message - Optional commit message to set on the new version.
- # last_commit_sha - Optional last commit sha to validate the page unchanged.
+ # attrs - Hash of attributes to be updated on the page.
+ # :content - The raw markup content to replace the existing.
+ # :format - Optional symbol representing the content format.
+ # See ProjectWiki::MARKUPS Hash for available formats.
+ # :message - Optional commit message to set on the new version.
+ # :last_commit_sha - Optional last commit sha to validate the page unchanged.
+ # :title - The Title to replace existing title
#
# Returns the String SHA1 of the newly created page
# or False if the save was unsuccessful.
- def update(new_content, format: :markdown, message: nil, last_commit_sha: nil)
- @attributes[:content] = new_content
- @attributes[:format] = format
-
+ def update(attrs = {})
+ last_commit_sha = attrs.delete(:last_commit_sha)
if last_commit_sha && last_commit_sha != self.last_commit_sha
raise PageChangedError.new("You are attempting to update a page that has changed since you started editing it.")
end
- save :update_page, @page, content, format, message
+ attrs.slice!(:content, :format, :message, :title)
+ @attributes.merge!(attrs)
+ page_details =
+ if title.present? && @page.title != title
+ title
+ else
+ @page.url_path
+ end
+
+ save(page_details: page_details) do
+ wiki.update_page(
+ @page,
+ content: content,
+ format: format,
+ message: attrs[:message],
+ title: title
+ )
+ end
end
# Destroys the Wiki Page.
@@ -236,30 +255,19 @@ class WikiPage
attributes[:format] = @page.format
end
- def save(method, *args)
- saved = false
+ def save(page_details:)
+ return unless valid?
- project_wiki = wiki
- if valid? && project_wiki.send(method, *args)
-
- page_details = if method == :update_page
- # Use url_path instead of path to omit format extension
- @page.url_path
- else
- title
- end
-
- page_title, page_dir = project_wiki.page_title_and_dir(page_details)
- gollum_wiki = project_wiki.wiki
- @page = gollum_wiki.paged(page_title, page_dir)
+ unless yield
+ errors.add(:base, wiki.error_message)
+ return false
+ end
- set_attributes
+ page_title, page_dir = wiki.page_title_and_dir(page_details)
+ gollum_wiki = wiki.wiki
+ @page = gollum_wiki.paged(page_title, page_dir)
- @persisted = true
- saved = true
- else
- errors.add(:base, project_wiki.error_message) if project_wiki.error_message
- end
- saved
+ set_attributes
+ @persisted = errors.blank?
end
end
diff --git a/app/serializers/blob_entity.rb b/app/serializers/blob_entity.rb
new file mode 100644
index 00000000000..56f173e5a27
--- /dev/null
+++ b/app/serializers/blob_entity.rb
@@ -0,0 +1,17 @@
+class BlobEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id, :path, :name, :mode
+
+ expose :last_commit do |blob|
+ request.project.repository.last_commit_for_path(blob.commit_id, blob.path)
+ end
+
+ expose :icon do |blob|
+ IconsHelper.file_type_icon_class('file', blob.mode, blob.name)
+ end
+
+ expose :url do |blob|
+ project_blob_path(request.project, File.join(request.ref, blob.path))
+ end
+end
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
index 7f17f2bf604..07650ce6f20 100644
--- a/app/serializers/merge_request_entity.rb
+++ b/app/serializers/merge_request_entity.rb
@@ -2,7 +2,6 @@ class MergeRequestEntity < IssuableEntity
include RequestAwareEntity
expose :in_progress_merge_commit_sha
- expose :locked_at
expose :merge_commit_sha
expose :merge_error
expose :merge_params
@@ -32,6 +31,7 @@ class MergeRequestEntity < IssuableEntity
expose :head_pipeline, with: PipelineDetailsEntity, as: :pipeline
# Booleans
+ expose :merge_ongoing?, as: :merge_ongoing
expose :work_in_progress?, as: :work_in_progress
expose :source_branch_exists?, as: :source_branch_exists
expose :mergeable_discussions_state?, as: :mergeable_discussions_state
diff --git a/app/serializers/submodule_entity.rb b/app/serializers/submodule_entity.rb
new file mode 100644
index 00000000000..9a7eb5e7880
--- /dev/null
+++ b/app/serializers/submodule_entity.rb
@@ -0,0 +1,23 @@
+class SubmoduleEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id, :path, :name, :mode
+
+ expose :icon do |blob|
+ 'archive'
+ end
+
+ expose :project_url do |blob|
+ submodule_links(blob, request).first
+ end
+
+ expose :tree_url do |blob|
+ submodule_links(blob, request).last
+ end
+
+ private
+
+ def submodule_links(blob, request)
+ @submodule_links ||= SubmoduleHelper.submodule_links(blob, request.ref, request.repository)
+ end
+end
diff --git a/app/serializers/tree_entity.rb b/app/serializers/tree_entity.rb
new file mode 100644
index 00000000000..555e5cf83bd
--- /dev/null
+++ b/app/serializers/tree_entity.rb
@@ -0,0 +1,17 @@
+class TreeEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :id, :path, :name, :mode
+
+ expose :last_commit do |tree|
+ request.project.repository.last_commit_for_path(tree.commit_id, tree.path)
+ end
+
+ expose :icon do |tree|
+ IconsHelper.file_type_icon_class('folder', tree.mode, tree.name)
+ end
+
+ expose :url do |tree|
+ project_tree_path(request.project, File.join(request.ref, tree.path))
+ end
+end
diff --git a/app/serializers/tree_root_entity.rb b/app/serializers/tree_root_entity.rb
new file mode 100644
index 00000000000..23b65aa4a4c
--- /dev/null
+++ b/app/serializers/tree_root_entity.rb
@@ -0,0 +1,8 @@
+# TODO: Inherit from TreeEntity, when `Tree` implements `id` and `name` like `Gitlab::Git::Tree`.
+class TreeRootEntity < Grape::Entity
+ expose :path
+
+ expose :trees, using: TreeEntity
+ expose :blobs, using: BlobEntity
+ expose :submodules, using: SubmoduleEntity
+end
diff --git a/app/serializers/tree_serializer.rb b/app/serializers/tree_serializer.rb
new file mode 100644
index 00000000000..713ade23bc9
--- /dev/null
+++ b/app/serializers/tree_serializer.rb
@@ -0,0 +1,3 @@
+class TreeSerializer < BaseSerializer
+ entity TreeRootEntity
+end
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 5e151b0f044..7dae5880931 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -103,6 +103,8 @@ module Auth
build_can_pull?(requested_project) || user_can_pull?(requested_project)
when 'push'
build_can_push?(requested_project) || user_can_push?(requested_project)
+ when '*'
+ user_can_admin?(requested_project)
else
false
end
@@ -120,6 +122,11 @@ module Auth
(requested_project == project || can?(current_user, :build_read_container_image, requested_project))
end
+ def user_can_admin?(requested_project)
+ has_authentication_ability?(:admin_container_image) &&
+ can?(current_user, :admin_container_image, requested_project)
+ end
+
def user_can_pull?(requested_project)
has_authentication_ability?(:read_container_image) &&
can?(current_user, :read_container_image, requested_project)
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index b951e8d0c9f..fc87bd6a659 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -30,6 +30,7 @@ module Ci
# with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
build.runner_id = runner.id
build.run!
+ register_success(build)
return Result.new(build, true)
rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError
@@ -46,6 +47,7 @@ module Ci
end
end
+ register_failure
Result.new(nil, valid)
end
@@ -81,5 +83,27 @@ module Ci
def shared_runner_build_limits_feature_enabled?
ENV['DISABLE_SHARED_RUNNER_BUILD_MINUTES_LIMIT'].to_s != 'true'
end
+
+ def register_failure
+ failed_attempt_counter.increase
+ attempt_counter.increase
+ end
+
+ def register_success(job)
+ job_queue_duration_seconds.observe({ shared_runner: @runner.shared? }, Time.now - job.created_at)
+ attempt_counter.increase
+ end
+
+ def failed_attempt_counter
+ @failed_attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_failed_total, "Counts the times a runner tries to register a job")
+ end
+
+ def attempt_counter
+ @attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_total, "Counts the times a runner tries to register a job")
+ end
+
+ def job_queue_duration_seconds
+ @job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time')
+ end
end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 760a15e3ed0..b84a6fd2b7d 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -2,11 +2,8 @@ class IssuableBaseService < BaseService
private
def create_milestone_note(issuable)
- milestone = issuable.milestone
- return if milestone && milestone.is_group_milestone?
-
SystemNoteService.change_milestone(
- issuable, issuable.project, current_user, milestone)
+ issuable, issuable.project, current_user, issuable.milestone)
end
def create_labels_note(issuable, old_labels)
@@ -182,7 +179,6 @@ class IssuableBaseService < BaseService
if params.present? && create_issuable(issuable, params, label_ids: label_ids)
after_create(issuable)
- issuable.create_cross_references!(current_user)
execute_hooks(issuable)
invalidate_cache_counts(issuable, users: issuable.assignees)
end
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index 718a7ac1f22..9114f0ccc81 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -15,11 +15,14 @@ module Issues
def before_create(issue)
spam_check(issue, current_user)
issue.move_to_end
+
+ user = current_user
+ issue.run_after_commit do
+ NewIssueWorker.perform_async(issue.id, user.id)
+ end
end
def after_create(issuable)
- event_service.open_issue(issuable, current_user)
- notification_service.new_issue(issuable, current_user)
todo_service.new_issue(issuable, current_user)
user_agent_detail_service.create
resolve_discussions_with_issue(issuable)
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 5414fa79def..7d539fa49e6 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -16,9 +16,15 @@ module MergeRequests
create(merge_request)
end
+ def before_create(merge_request)
+ user = current_user
+ merge_request.run_after_commit do
+ NewMergeRequestWorker.perform_async(merge_request.id, user.id)
+ end
+ end
+
def after_create(issuable)
event_service.open_mr(issuable, current_user)
- notification_service.new_merge_request(issuable, current_user)
todo_service.new_merge_request(issuable, current_user)
issuable.cache_merge_request_closes_issues!(current_user)
update_merge_requests_head_pipeline(issuable)
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index fc85f398935..724a77c873a 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -5,7 +5,15 @@ module Projects
end
def milestones
- @project.milestones.active.reorder(due_date: :asc, title: :asc).select([:iid, :title])
+ finder_params = {
+ project_ids: [@project.id],
+ state: :active,
+ order: { due_date: :asc, title: :asc }
+ }
+
+ finder_params[:group_ids] = [@project.group.id] if @project.group
+
+ MilestonesFinder.new(finder_params).execute.select([:iid, :title])
end
def merge_requests
diff --git a/app/services/projects/create_from_template_service.rb b/app/services/projects/create_from_template_service.rb
new file mode 100644
index 00000000000..87d9ed7a0e6
--- /dev/null
+++ b/app/services/projects/create_from_template_service.rb
@@ -0,0 +1,15 @@
+module Projects
+ class CreateFromTemplateService < BaseService
+ def initialize(user, params)
+ @current_user, @params = user, params.dup
+ end
+
+ def execute
+ params[:file] = Gitlab::ProjectTemplate.find(params[:template_name]).file
+
+ GitlabProjectsImportService.new(@current_user, @params).execute
+ ensure
+ params[:file]&.close
+ end
+ end
+end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index e874a2d8789..48578b6d9e5 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -5,6 +5,10 @@ module Projects
end
def execute
+ if @params[:template_name]&.present?
+ return ::Projects::CreateFromTemplateService.new(current_user, params).execute
+ end
+
forked_from_project_id = params.delete(:forked_from_project_id)
import_data = params.delete(:import_data)
@skip_wiki = params.delete(:skip_wiki)
diff --git a/app/services/projects/gitlab_projects_import_service.rb b/app/services/projects/gitlab_projects_import_service.rb
new file mode 100644
index 00000000000..4ca6414b73b
--- /dev/null
+++ b/app/services/projects/gitlab_projects_import_service.rb
@@ -0,0 +1,36 @@
+# This service is an adapter used to for the GitLab Import feature, and
+# creating a project from a template.
+# The latter will under the hood just import an archive supplied by GitLab.
+module Projects
+ class GitlabProjectsImportService
+ attr_reader :current_user, :params
+
+ def initialize(user, params)
+ @current_user, @params = user, params.dup
+ end
+
+ def execute
+ FileUtils.mkdir_p(File.dirname(import_upload_path))
+ FileUtils.copy_entry(file.path, import_upload_path)
+
+ Gitlab::ImportExport::ProjectCreator.new(params[:namespace_id],
+ current_user,
+ import_upload_path,
+ params[:path]).execute
+ end
+
+ private
+
+ def import_upload_path
+ @import_upload_path ||= Gitlab::ImportExport.import_upload_path(filename: tmp_filename)
+ end
+
+ def tmp_filename
+ "#{SecureRandom.hex}_#{params[:path]}"
+ end
+
+ def file
+ params[:file]
+ end
+ end
+end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index 50ec3651515..c3bf0031409 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -34,8 +34,12 @@ module Projects
def import_repository
raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url)
+ # We should return early for a GitHub import because the new GitHub
+ # importer fetch the project repositories for us.
+ return if project.github_import?
+
begin
- if project.github_import? || project.gitea_import?
+ if project.gitea_import?
fetch_repository
else
clone_repository
@@ -55,7 +59,7 @@ module Projects
end
def fetch_repository
- project.create_repository
+ project.ensure_repository
project.repository.add_remote(project.import_type, project.import_url)
project.repository.set_remote_as_mirror(project.import_type)
project.repository.fetch_remote(project.import_type, forced: true)
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 749a1cc56d8..5038155ca31 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -33,8 +33,10 @@ module Projects
success
end
rescue => e
+ register_failure
error(e.message)
ensure
+ register_attempt
build.erase_artifacts! unless build.has_expiring_artifacts?
end
@@ -168,5 +170,21 @@ module Projects
def sha
build.sha
end
+
+ def register_attempt
+ pages_deployments_total_counter.increase
+ end
+
+ def register_failure
+ pages_deployments_failed_total_counter.increase
+ end
+
+ def pages_deployments_total_counter
+ @pages_deployments_total_counter ||= Gitlab::Metrics.counter(:pages_deployments_total, "Counter of GitLab Pages deployments triggered")
+ end
+
+ def pages_deployments_failed_total_counter
+ @pages_deployments_failed_total_counter ||= Gitlab::Metrics.counter(:pages_deployments_failed_total, "Counter of GitLab Pages deployments which failed")
+ end
end
end
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index c22bf7498bb..c7832c47e1a 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -511,7 +511,12 @@ module QuickActions
users = extract_references(params, :user)
if users.empty?
- users = User.where(username: params.split(' ').map(&:strip))
+ users =
+ if params == 'me'
+ [current_user]
+ else
+ User.where(username: params.split(' ').map(&:strip))
+ end
end
users
diff --git a/app/services/submit_usage_ping_service.rb b/app/services/submit_usage_ping_service.rb
index 17857ca62f2..14171bce782 100644
--- a/app/services/submit_usage_ping_service.rb
+++ b/app/services/submit_usage_ping_service.rb
@@ -1,6 +1,16 @@
class SubmitUsagePingService
URL = 'https://version.gitlab.com/usage_data'.freeze
+ METRICS = %w[leader_issues instance_issues percentage_issues leader_notes instance_notes
+ percentage_notes leader_milestones instance_milestones percentage_milestones
+ leader_boards instance_boards percentage_boards leader_merge_requests
+ instance_merge_requests percentage_merge_requests leader_ci_pipelines
+ instance_ci_pipelines percentage_ci_pipelines leader_environments instance_environments
+ percentage_environments leader_deployments instance_deployments percentage_deployments
+ leader_projects_prometheus_active instance_projects_prometheus_active
+ percentage_projects_prometheus_active leader_service_desk_issues instance_service_desk_issues
+ percentage_service_desk_issues].freeze
+
include Gitlab::CurrentSettings
def execute
@@ -27,15 +37,7 @@ class SubmitUsagePingService
return unless response['conv_index'].present?
ConversationalDevelopmentIndex::Metric.create!(
- response['conv_index'].slice(
- 'leader_issues', 'instance_issues', 'leader_notes', 'instance_notes',
- 'leader_milestones', 'instance_milestones', 'leader_boards', 'instance_boards',
- 'leader_merge_requests', 'instance_merge_requests', 'leader_ci_pipelines',
- 'instance_ci_pipelines', 'leader_environments', 'instance_environments',
- 'leader_deployments', 'instance_deployments', 'leader_projects_prometheus_active',
- 'instance_projects_prometheus_active', 'leader_service_desk_issues',
- 'instance_service_desk_issues'
- )
+ response['conv_index'].slice(*METRICS)
)
end
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 2dbee9c246e..1763f64a4e4 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -142,7 +142,8 @@ module SystemNoteService
#
# Returns the created Note object
def change_milestone(noteable, project, author, milestone)
- body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project)}"
+ format = milestone&.is_group_milestone? ? :name : :iid
+ body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project, format: format)}"
create_note(NoteSummary.new(noteable, project, author, body, action: 'milestone'))
end
diff --git a/app/services/wiki_pages/update_service.rb b/app/services/wiki_pages/update_service.rb
index c628e6781af..93cbd9a509f 100644
--- a/app/services/wiki_pages/update_service.rb
+++ b/app/services/wiki_pages/update_service.rb
@@ -1,7 +1,7 @@
module WikiPages
class UpdateService < WikiPages::BaseService
def execute(page)
- if page.update(@params[:content], format: @params[:format], message: @params[:message], last_commit_sha: @params[:last_commit_sha])
+ if page.update(@params)
execute_hooks(page, 'update')
end
diff --git a/app/views/admin/health_check/_failing_storages.html.haml b/app/views/admin/health_check/_failing_storages.html.haml
new file mode 100644
index 00000000000..6830201538d
--- /dev/null
+++ b/app/views/admin/health_check/_failing_storages.html.haml
@@ -0,0 +1,15 @@
+- if failing_storages.any?
+ = _('There are problems accessing Git storage: ')
+ %ul
+ - failing_storages.each do |storage_health|
+ %li
+ = failing_storage_health_message(storage_health)
+ %ul
+ - storage_health.failing_circuit_breakers.each do |circuit_breaker|
+ %li
+ #{circuit_breaker.hostname}: #{message_for_circuit_breaker(circuit_breaker)}
+
+ = _("Access to failing storages has been temporarily disabled to allow the mount to recover. Reset storage information after the issue has been resolved to allow access again.")
+ .prepend-top-10
+ = button_to _("Reset git storage health information"), reset_storage_health_admin_health_check_path,
+ method: :post, class: 'btn btn-default'
diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml
index f16f59623f7..517db50b97f 100644
--- a/app/views/admin/health_check/show.html.haml
+++ b/app/views/admin/health_check/show.html.haml
@@ -1,22 +1,22 @@
- @no_container = true
-- page_title "Health Check"
+- page_title _('Health Check')
+- no_errors = @errors.blank? && @failing_storage_statuses.blank?
= render 'admin/monitoring/head'
%div{ class: container_class }
- %h3.page-title
- Health Check
+ %h3.page-title= page_title
.bs-callout.clearfix
.pull-left
%p
- Access token is
+ #{ s_('HealthCheck|Access token is') }
%code#health-check-token= current_application_settings.health_check_access_token
.prepend-top-10
- = button_to "Reset health check access token", reset_health_check_token_admin_application_settings_path,
+ = button_to _("Reset health check access token"), reset_health_check_token_admin_application_settings_path,
method: :put, class: 'btn btn-default',
- data: { confirm: 'Are you sure you want to reset the health check token?' }
+ data: { confirm: _('Are you sure you want to reset the health check token?') }
%p.light
- Health information can be retrieved from the following endpoints. More information is available
- = link_to 'here', help_page_path('user/admin_area/monitoring/health_check')
+ #{ _('Health information can be retrieved from the following endpoints. More information is available') }
+ = link_to s_('More information is available|here'), help_page_path('user/admin_area/monitoring/health_check')
%ul
%li
%code= readiness_url(token: current_application_settings.health_check_access_token)
@@ -29,14 +29,15 @@
.panel.panel-default
.panel-heading
Current Status:
- - if @errors.blank?
+ - if no_errors
= icon('circle', class: 'cgreen')
- Healthy
+ #{ s_('HealthCheck|Healthy') }
- else
= icon('warning', class: 'cred')
- Unhealthy
+ #{ s_('HealthCheck|Unhealthy') }
.panel-body
- - if @errors.blank?
- No Health Problems Detected
+ - if no_errors
+ #{ s_('HealthCheck|No Health Problems Detected') }
- else
= @errors
+ = render partial: 'failing_storages', object: @failing_storage_statuses
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index ec6cb1a9624..c546252455a 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -13,10 +13,8 @@
- if show_callout?('user_callout_dismissed')
= render 'shared/user_callout'
- - if @projects.any? || params[:name]
+ - if has_projects_or_name?(@projects, params)
= render 'dashboard/projects_head'
-
- - if @projects.any? || params[:name]
= render 'projects'
- else
= render "zero_authorized_projects"
diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml
index ae1d733a516..14f9f8cd70a 100644
--- a/app/views/dashboard/projects/starred.html.haml
+++ b/app/views/dashboard/projects/starred.html.haml
@@ -9,7 +9,7 @@
%div{ class: container_class }
= render 'dashboard/projects_head'
- - if @projects.any? || params[:filter_projects]
+ - if params[:filter_projects] || any_projects?(@projects)
= render 'projects'
- else
%h3 You don't have starred projects yet
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 56e628a2b74..b18b3dd5766 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -121,7 +121,7 @@
.key g
.key p
%td
- Go to the project's home page
+ Go to the project's overview page
%tr
%td.shortcut
.key g
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index 767dffb5589..008e8287aa3 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -1,25 +1,43 @@
- page_title "GitLab Import"
- header_title "Projects", root_path
+- content_for :page_specific_javascripts do
+ = webpack_bundle_tag 'project_import_gl'
+
%h3.page-title
= icon('gitlab')
Import an exported GitLab project
%hr
-= form_tag import_gitlab_project_path, class: 'form-horizontal', multipart: true do
- %p
- Project will be imported as
- %strong
- #{@namespace.name}/#{@path}
+= form_tag import_gitlab_project_path, class: 'new_project', multipart: true do
+ .row
+ .form-group.col-xs-12.col-sm-6
+ = label_tag :namespace_id, 'Project path', class: 'label-light'
+ .form-group
+ .input-group
+ - if current_user.can_select_namespace?
+ .input-group-addon
+ = root_url
+ = select_tag :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), class: 'select2 js-select-namespace', tabindex: 1
- %p
- To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here.
- .form-group
- = hidden_field_tag :namespace_id, @namespace.id
- = hidden_field_tag :path, @path
- = label_tag :file, class: 'control-label' do
- %span GitLab project export
- .col-sm-10
- = file_field_tag :file, class: ''
+ - else
+ .input-group-addon.static-namespace
+ #{root_url}#{current_user.username}/
+ = hidden_field_tag :namespace_id, value: current_user.namespace_id
+ .form-group.col-xs-12.col-sm-6.project-path
+ = label_tag :path, 'Project name', class: 'label-light'
+ = text_field_tag :path, nil, placeholder: "my-awesome-project", class: "js-path-name form-control", tabindex: 2, autofocus: true, required: true
- .form-actions
- = submit_tag 'Import project', class: 'btn btn-create'
+ .row
+ .form-group.col-md-12
+ To move or copy an entire GitLab project from another GitLab installation to this one, navigate to the original project's settings page, generate an export file, and upload it here.
+ .row
+ .form-group.col-sm-12
+ = hidden_field_tag :namespace_id, @namespace.id
+ = hidden_field_tag :path, @path
+ = label_tag :file, 'GitLab project export', class: 'label-light'
+ .form-group
+ = file_field_tag :file, class: ''
+ .row
+ .form-actions
+ = submit_tag 'Import project', class: 'btn btn-create'
+ = link_to 'Cancel', new_project_path, class: 'btn btn-cancel'
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index b32cfe158bb..df1dc736571 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -74,8 +74,7 @@
= link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
%li
= link_to "Settings", profile_path
- %li
- = link_to "Turn on new navigation", profile_preferences_path(anchor: "new-navigation")
+ = render 'shared/user_dropdown_experimental_features'
%li.divider
%li
= link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link"
diff --git a/app/views/layouts/header/_new.html.haml b/app/views/layouts/header/_new.html.haml
index 2c1c23d6ea9..fa94925d537 100644
--- a/app/views/layouts/header/_new.html.haml
+++ b/app/views/layouts/header/_new.html.haml
@@ -68,8 +68,7 @@
= link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
%li
= link_to "Settings", profile_path
- %li
- = link_to "Turn off new navigation", profile_preferences_path(anchor: "new-navigation")
+ = render 'shared/user_dropdown_experimental_features'
%li.divider
%li
= link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link"
diff --git a/app/views/layouts/nav/_new_admin_sidebar.html.haml b/app/views/layouts/nav/_new_admin_sidebar.html.haml
index 54ea39a2d36..0b4a9d92bea 100644
--- a/app/views/layouts/nav/_new_admin_sidebar.html.haml
+++ b/app/views/layouts/nav/_new_admin_sidebar.html.haml
@@ -1,12 +1,9 @@
-.nav-sidebar
+.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
.context-header
= link_to admin_root_path, title: 'Admin Overview' do
.avatar-container.s40.settings-avatar
= icon('wrench')
.project-title Admin Area
- = button_tag class: 'close-nav-button', type: 'button' do
- %span.sr-only Close sidebar
- = icon ('times')
%ul.sidebar-top-level-items
= nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: {class: 'home'}) do
= link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
@@ -149,3 +146,5 @@
= custom_icon('settings')
%span.nav-item-name
Settings
+
+ = render 'shared/sidebar_toggle_button'
diff --git a/app/views/layouts/nav/_new_group_sidebar.html.haml b/app/views/layouts/nav/_new_group_sidebar.html.haml
index 33a83866cbf..c7dabbd8237 100644
--- a/app/views/layouts/nav/_new_group_sidebar.html.haml
+++ b/app/views/layouts/nav/_new_group_sidebar.html.haml
@@ -1,20 +1,17 @@
-.nav-sidebar
+.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
.context-header
= link_to group_path(@group), title: @group.name do
.avatar-container.s40.group-avatar
= image_tag group_icon(@group), class: "avatar s40 avatar-tile"
.group-title
= @group.name
- = button_tag class: 'close-nav-button', type: 'button' do
- %span.sr-only Close sidebar
- = icon ('times')
%ul.sidebar-top-level-items
= nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do
- = link_to group_path(@group), title: 'About group' do
+ = link_to group_path(@group), title: 'Group overview' do
.nav-icon-container
= custom_icon('project')
%span.nav-item-name
- About
+ Overview
%ul.sidebar-sub-level-items
= nav_link(path: ['groups#show', 'groups#subgroups'], html_options: { class: 'home' }) do
@@ -88,3 +85,5 @@
= link_to group_settings_ci_cd_path(@group), title: 'CI / CD' do
%span
CI / CD
+
+ = render 'shared/sidebar_toggle_button'
diff --git a/app/views/layouts/nav/_new_profile_sidebar.html.haml b/app/views/layouts/nav/_new_profile_sidebar.html.haml
index f715d8a63f9..edae009a28e 100644
--- a/app/views/layouts/nav/_new_profile_sidebar.html.haml
+++ b/app/views/layouts/nav/_new_profile_sidebar.html.haml
@@ -1,12 +1,9 @@
-.nav-sidebar
+.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
.context-header
= link_to profile_path, title: 'Profile Settings' do
.avatar-container.s40.settings-avatar
= icon('user')
.project-title User Settings
- = button_tag class: 'close-nav-button', type: 'button' do
- %span.sr-only Close sidebar
- = icon ('times')
%ul.sidebar-top-level-items
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
= link_to profile_path, title: 'Profile Settings' do
@@ -83,3 +80,5 @@
= custom_icon('authentication_log')
%span.nav-item-name
Authentication log
+
+ = render 'shared/sidebar_toggle_button'
diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml
index 673febbc798..e0477c29ebe 100644
--- a/app/views/layouts/nav/_new_project_sidebar.html.haml
+++ b/app/views/layouts/nav/_new_project_sidebar.html.haml
@@ -1,4 +1,4 @@
-.nav-sidebar
+.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) }
- can_edit = can?(current_user, :admin_project, @project)
.context-header
= link_to project_path(@project), title: @project.name do
@@ -6,16 +6,13 @@
= project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile')
.project-title
= @project.name
- = button_tag class: 'close-nav-button', type: 'button' do
- %span.sr-only Close sidebar
- = icon ('times')
%ul.sidebar-top-level-items
= nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do
- = link_to project_path(@project), title: 'About project', class: 'shortcuts-project' do
+ = link_to project_path(@project), title: 'Project overview', class: 'shortcuts-project' do
.nav-icon-container
= custom_icon('project')
%span.nav-item-name
- About
+ Overview
%ul.sidebar-sub-level-items
= nav_link(path: 'projects#show') do
@@ -219,9 +216,11 @@
= link_to project_settings_members_path(@project), title: 'Members', class: 'shortcuts-tree' do
.nav-icon-container
= custom_icon('members')
- %span
+ %span.nav-item-name
Members
+ = render 'shared/sidebar_toggle_button'
+
-# Shortcut to Project > Activity
%li.hidden
= link_to activity_project_path(@project), title: 'Activity', class: 'shortcuts-project-activity' do
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 9aed498a8a0..9bd8bf91d1c 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -18,6 +18,8 @@
= scheme.name
.col-sm-12
%hr
+ %h3#experimental-features Experimental features
+ %hr
.col-lg-4.profile-settings-sidebar#new-navigation
%h4.prepend-top-0
New Navigation
@@ -40,6 +42,28 @@
New
.col-sm-12
%hr
+ .col-lg-4.profile-settings-sidebar#new-repository
+ %h4.prepend-top-0
+ New Repository
+ %p
+ This setting allows you to turn on or off the new upcoming repository concept.
+ .col-lg-8.syntax-theme
+ .nav-wip
+ %p
+ The new repository is currently a work-in-progress concept and only usable on wide-screens. There are a number of improvements that we are working on in order to further refine the repository view.
+ %p
+ %a{ href: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/31890', target: 'blank' } Learn more
+ about the improvements that are coming soon!
+ = label_tag do
+ .preview= image_tag "old_repo.png"
+ %input.js-experiment-feature-toggle{ type: "radio", value: "false", name: "new_repo", checked: !show_new_repo? }
+ Old
+ = label_tag do
+ .preview= image_tag "new_repo.png"
+ %input.js-experiment-feature-toggle{ type: "radio", value: "true", name: "new_repo", checked: show_new_repo? }
+ New
+ .col-sm-12
+ %hr
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Behavior
@@ -60,9 +84,9 @@
= f.select :dashboard, dashboard_choices, {}, class: 'form-control'
.form-group
= f.label :project_view, class: 'label-light' do
- Project home page content
+ Project overview content
= f.select :project_view, project_view_choices, {}, class: 'form-control'
.help-block
- Choose what content you want to see on a project’s home page
+ Choose what content you want to see on a project’s overview page
.form-group
= f.submit 'Save changes', class: 'btn btn-save'
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index 426085b3e1c..3a7a99462a6 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -1,14 +1,13 @@
- commit = local_assigns.fetch(:commit) { @repository.commit }
- ref = local_assigns.fetch(:ref) { current_ref }
- project = local_assigns.fetch(:project) { @project }
+- content_url = local_assigns.fetch(:content_url) { @tree.readme ? project_blob_path(@project, tree_join(@ref, @tree.readme.path)) : project_tree_path(@project, @ref) }
+
#tree-holder.tree-holder.clearfix
.nav-block
= render 'projects/tree/tree_header', tree: @tree
- - if commit
- .info-well.hidden-xs.project-last-commit.append-bottom-default
- .well-segment
- %ul.blob-commit-info
- = render 'projects/commits/commit', commit: commit, ref: ref, project: project
+ - if !show_new_repo? && commit
+ = render 'shared/commit_well', commit: commit, ref: ref, project: project
- = render 'projects/tree/tree_content', tree: @tree
+ = render 'projects/tree/tree_content', tree: @tree, content_url: content_url
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index d0698285f84..6e13bf47ff6 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -1,5 +1,12 @@
- referenced_users = local_assigns.fetch(:referenced_users, nil)
+- if defined?(@issue) && @issue.confidential?
+ %li.confidential-issue-warning
+ = confidential_icon(@issue)
+ %span This is a confidential issue. Your comment will not be visible to the public.
+- else
+ %li.confidential-issue-warning.not-confidential
+
.md-area
.md-header
%ul.nav-links.clearfix
@@ -10,11 +17,6 @@
%a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 }
Preview
- - if defined?(@issue) && @issue.confidential?
- %li.confidential-issue-warning
- = icon('warning')
- %span This is a confidential issue. Your comment will not be visible to the public.
-
%li.pull-right
.toolbar-group
= markdown_toolbar_button({ icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" })
diff --git a/app/views/projects/_project_templates.html.haml b/app/views/projects/_project_templates.html.haml
new file mode 100644
index 00000000000..21baf35f2ac
--- /dev/null
+++ b/app/views/projects/_project_templates.html.haml
@@ -0,0 +1,10 @@
+.project-templates-buttons.import-buttons{ data: { toggle: "buttons" } }
+ .btn.blank-option.active
+ %input{ type: "radio", autocomplete: "off", name: "project_templates", id: "blank", checked: "true" }
+ = icon('file-o', class: 'btn-template-icon')
+ Blank
+ - Gitlab::ProjectTemplate.all.each do |template|
+ .btn
+ %input{ type: "radio", autocomplete: "off", name: "project_templates", id: template.name }
+ = custom_icon(template.logo)
+ = template.title
diff --git a/app/views/projects/artifacts/file.html.haml b/app/views/projects/artifacts/file.html.haml
index 18e86ac5a92..b85bbcb980e 100644
--- a/app/views/projects/artifacts/file.html.haml
+++ b/app/views/projects/artifacts/file.html.haml
@@ -3,7 +3,7 @@
= render "projects/jobs/header", show_controls: false
-#tree-holder.tree-holder
+.tree-holder
.nav-block
%ul.breadcrumb.repo-breadcrumb
%li
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 8bd336269ff..849716a679b 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -9,5 +9,5 @@
#blob-content-holder.blob-content-holder
%article.file-holder
- = render "projects/blob/header", blob: blob
+ = render 'projects/blob/header', blob: blob
= render 'projects/blob/content', blob: blob
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
index 32dbc1b3417..05b7dfe2872 100644
--- a/app/views/projects/blob/_upload.html.haml
+++ b/app/views/projects/blob/_upload.html.haml
@@ -19,7 +19,9 @@
= render 'shared/new_commit_form', placeholder: placeholder
.form-actions
- = button_tag button_title, class: 'btn btn-small btn-create btn-upload-file', id: 'submit-all'
+ = button_tag class: 'btn btn-create btn-upload-file', id: 'submit-all', type: 'button' do
+ = icon('spin spinner', class: 'js-loading-icon hidden' )
+ = button_title
= link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal"
- unless can?(current_user, :push_code, @project)
diff --git a/app/views/projects/blob/_viewer.html.haml b/app/views/projects/blob/_viewer.html.haml
index 013f1c267c8..cc85e5de40f 100644
--- a/app/views/projects/blob/_viewer.html.haml
+++ b/app/views/projects/blob/_viewer.html.haml
@@ -17,3 +17,4 @@
- viewer = BlobViewer::Download.new(viewer.blob) if viewer.binary_detected_after_load?
= render viewer.partial_path, viewer: viewer
+
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index 7dd834e84b5..240e62d5ac5 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -5,16 +5,23 @@
= render "projects/commits/head"
- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('blob')
+ = webpack_bundle_tag 'blob'
+
+ - if show_new_repo?
+ = webpack_bundle_tag 'common_vue'
+ = webpack_bundle_tag 'repo'
= render 'projects/last_push'
%div{ class: container_class }
- #tree-holder.tree-holder
- = render 'blob', blob: @blob
+ - if show_new_repo?
+ = render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_blob_path(@project, @id)
+ - else
+ #tree-holder.tree-holder
+ = render 'blob', blob: @blob
- - if can_modify_blob?(@blob)
- = render 'projects/blob/remove'
+ - if can_modify_blob?(@blob)
+ = render 'projects/blob/remove'
- - title = "Replace #{@blob.name}"
- = render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: project_update_blob_path(@project, @id), method: :put
+ - title = "Replace #{@blob.name}"
+ = render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: project_update_blob_path(@project, @id), method: :put
diff --git a/app/views/projects/blob/viewers/_balsamiq.html.haml b/app/views/projects/blob/viewers/_balsamiq.html.haml
index 28670e7de97..1e7c461f02e 100644
--- a/app/views/projects/blob/viewers/_balsamiq.html.haml
+++ b/app/views/projects/blob/viewers/_balsamiq.html.haml
@@ -1,4 +1,4 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('balsamiq_viewer')
-.file-content.balsamiq-viewer#js-balsamiq-viewer{ data: { endpoint: blob_raw_url } }
+.file-content.balsamiq-viewer#js-balsamiq-viewer{ data: { endpoint: blob_raw_path } }
diff --git a/app/views/projects/blob/viewers/_download.html.haml b/app/views/projects/blob/viewers/_download.html.haml
index 684240d02c7..6d1138f7959 100644
--- a/app/views/projects/blob/viewers/_download.html.haml
+++ b/app/views/projects/blob/viewers/_download.html.haml
@@ -1,6 +1,6 @@
.file-content.blob_file.blob-no-preview
.center
- = link_to blob_raw_url do
+ = link_to blob_raw_path do
%h1.light
= icon('download')
%h4
diff --git a/app/views/projects/blob/viewers/_image.html.haml b/app/views/projects/blob/viewers/_image.html.haml
index 5fd22a59217..26ea028c5d7 100644
--- a/app/views/projects/blob/viewers/_image.html.haml
+++ b/app/views/projects/blob/viewers/_image.html.haml
@@ -1,2 +1,2 @@
.file-content.image_file
- = image_tag(blob_raw_url, alt: viewer.blob.name)
+ = image_tag(blob_raw_path, alt: viewer.blob.name)
diff --git a/app/views/projects/blob/viewers/_notebook.html.haml b/app/views/projects/blob/viewers/_notebook.html.haml
index 2399fb16265..8a41bc53004 100644
--- a/app/views/projects/blob/viewers/_notebook.html.haml
+++ b/app/views/projects/blob/viewers/_notebook.html.haml
@@ -2,4 +2,4 @@
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('notebook_viewer')
-.file-content#js-notebook-viewer{ data: { endpoint: blob_raw_url } }
+.file-content#js-notebook-viewer{ data: { endpoint: blob_raw_path } }
diff --git a/app/views/projects/blob/viewers/_pdf.html.haml b/app/views/projects/blob/viewers/_pdf.html.haml
index 1dd179c4fdc..ec2b18bd4ab 100644
--- a/app/views/projects/blob/viewers/_pdf.html.haml
+++ b/app/views/projects/blob/viewers/_pdf.html.haml
@@ -2,4 +2,4 @@
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('pdf_viewer')
-.file-content#js-pdf-viewer{ data: { endpoint: blob_raw_url } }
+.file-content#js-pdf-viewer{ data: { endpoint: blob_raw_path } }
diff --git a/app/views/projects/blob/viewers/_sketch.html.haml b/app/views/projects/blob/viewers/_sketch.html.haml
index 49f716c2c59..775e4584f77 100644
--- a/app/views/projects/blob/viewers/_sketch.html.haml
+++ b/app/views/projects/blob/viewers/_sketch.html.haml
@@ -2,6 +2,6 @@
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('sketch_viewer')
-.file-content#js-sketch-viewer{ data: { endpoint: blob_raw_url } }
+.file-content#js-sketch-viewer{ data: { endpoint: blob_raw_path } }
.js-loading-icon.text-center.prepend-top-default.append-bottom-default.js-loading-icon{ 'aria-label' => 'Loading Sketch preview' }
= icon('spinner spin 2x', 'aria-hidden' => 'true');
diff --git a/app/views/projects/blob/viewers/_stl.html.haml b/app/views/projects/blob/viewers/_stl.html.haml
index e4e9d746176..6578d826ace 100644
--- a/app/views/projects/blob/viewers/_stl.html.haml
+++ b/app/views/projects/blob/viewers/_stl.html.haml
@@ -2,7 +2,7 @@
= page_specific_javascript_bundle_tag('stl_viewer')
.file-content.is-stl-loading
- .text-center#js-stl-viewer{ data: { endpoint: blob_raw_url } }
+ .text-center#js-stl-viewer{ data: { endpoint: blob_raw_path } }
= icon('spinner spin 2x', class: 'prepend-top-default append-bottom-default', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
.text-center.prepend-top-default.append-bottom-default.stl-controls
.btn-group
diff --git a/app/views/projects/blob/viewers/_video.html.haml b/app/views/projects/blob/viewers/_video.html.haml
index 595a890a27d..36039c08d52 100644
--- a/app/views/projects/blob/viewers/_video.html.haml
+++ b/app/views/projects/blob/viewers/_video.html.haml
@@ -1,2 +1,2 @@
.file-content.video
- %video{ src: blob_raw_url, controls: true, data: { setup: '{}' } }
+ %video{ src: blob_raw_path, controls: true, data: { setup: '{}' } }
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index 539ee087b14..64f5f6d7ba0 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -6,8 +6,16 @@
%i.fa.fa-fw.board-title-expandable-toggle{ "v-if": "list.isExpandable",
":class": "{ \"fa-caret-down\": list.isExpanded, \"fa-caret-right\": !list.isExpanded && list.position === -1, \"fa-caret-left\": !list.isExpanded && list.position !== -1 }",
"aria-hidden": "true" }
- %span.has-tooltip{ ":title" => '(list.label ? list.label.description : "")',
- data: { container: "body", placement: "bottom" } }
+
+ %span.has-tooltip{ "v-if": "list.type !== \"label\"",
+ ":title" => '(list.label ? list.label.description : "")' }
+ {{ list.title }}
+
+ %span.has-tooltip{ "v-if": "list.type === \"label\"",
+ ":title" => '(list.label ? list.label.description : "")',
+ data: { container: "body", placement: "bottom" },
+ class: "label color-label title",
+ ":style" => "{ backgroundColor: (list.label && list.label.color ? list.label.color : null), color: (list.label && list.label.color ? list.label.text_color : \"#2e2e2e\") }" }
{{ list.title }}
.issue-count-badge.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' }
%span.issue-count-badge-count.pull-left{ ":class" => '{ "has-btn": list.type !== "closed" && !disabled }' }
diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml
index 228c8c84792..9f5a1239a82 100644
--- a/app/views/projects/graphs/charts.html.haml
+++ b/app/views/projects/graphs/charts.html.haml
@@ -78,8 +78,8 @@
%script#projectChartData{ type: "application/json" }
- projectChartData = {};
- - projectChartData['hour'] = { 'keys' => @commits_per_time.keys, 'values' => @commits_per_time.values }
- - projectChartData['weekDays'] = { 'keys' => @commits_per_week_days.keys, 'values' => @commits_per_week_days.values }
- - projectChartData['month'] = { 'keys' => @commits_per_month.keys, 'values' => @commits_per_month.values }
+ - projectChartData['hour'] = @commits_per_time
+ - projectChartData['weekDays'] = @commits_per_week_days
+ - projectChartData['month'] = @commits_per_month
- projectChartData['languages'] = @languages
= projectChartData.to_json.html_safe
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 043f862f552..f2141b84e6d 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -19,7 +19,8 @@
= icon('angle-double-left')
.issuable-meta
- = confidential_icon(@issue)
+ - if @issue.confidential
+ = icon('eye-slash', class: 'is-confidential')
= issuable_meta(@issue, @project, "Issue")
.issuable-actions.js-issuable-actions
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index f2db71e8838..99f4b30d085 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -1,101 +1,101 @@
- builds = @build.pipeline.builds.to_a
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } }
- .blocks-container
- .block
- %strong
- = @build.name
- %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-build-toggle{ href: "#", 'aria-label': 'Toggle Sidebar', role: 'button' }
- = icon('angle-double-right')
-
- #js-details-block-vue
-
- - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
+ .sidebar-container
+ .blocks-container
.block
- .title
- Job artifacts
- - if @build.artifacts_expired?
- %p.build-detail-row
- The artifacts were removed
- #{time_ago_with_tooltip(@build.artifacts_expire_at)}
- - elsif @build.has_expiring_artifacts?
- %p.build-detail-row
- The artifacts will be removed in
- %span.js-artifacts-remove= @build.artifacts_expire_at
+ %strong
+ = @build.name
+ %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-build-toggle{ href: "#", 'aria-label': 'Toggle Sidebar', role: 'button' }
+ = icon('angle-double-right')
- - if @build.artifacts?
- .btn-group.btn-group-justified{ role: :group }
- - if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build)
- = link_to keep_project_job_artifacts_path(@project, @build), class: 'btn btn-sm btn-default', method: :post do
- Keep
+ #js-details-block-vue
- = link_to download_project_job_artifacts_path(@project, @build), rel: 'nofollow', download: '', class: 'btn btn-sm btn-default' do
- Download
+ - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
+ .block
+ .title
+ Job artifacts
+ - if @build.artifacts_expired?
+ %p.build-detail-row
+ The artifacts were removed
+ #{time_ago_with_tooltip(@build.artifacts_expire_at)}
+ - elsif @build.has_expiring_artifacts?
+ %p.build-detail-row
+ The artifacts will be removed in
+ %span.js-artifacts-remove= @build.artifacts_expire_at
- - if @build.artifacts_metadata?
- = link_to browse_project_job_artifacts_path(@project, @build), class: 'btn btn-sm btn-default' do
- Browse
+ - if @build.artifacts?
+ .btn-group.btn-group-justified{ role: :group }
+ - if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build)
+ = link_to keep_project_job_artifacts_path(@project, @build), class: 'btn btn-sm btn-default', method: :post do
+ Keep
- - if @build.trigger_request
- .build-widget.block
- %h4.title
- Trigger
+ = link_to download_project_job_artifacts_path(@project, @build), rel: 'nofollow', download: '', class: 'btn btn-sm btn-default' do
+ Download
- %p
- %span.build-light-text Token:
- #{@build.trigger_request.trigger.short_token}
+ - if @build.artifacts_metadata?
+ = link_to browse_project_job_artifacts_path(@project, @build), class: 'btn btn-sm btn-default' do
+ Browse
+
+ - if @build.trigger_request
+ .build-widget.block
+ %h4.title
+ Trigger
- - if @build.trigger_request.variables
%p
- %button.btn.group.btn-group-justified.reveal-variables Reveal Variables
+ %span.build-light-text Token:
+ #{@build.trigger_request.trigger.short_token}
+ - if @build.trigger_request.variables
+ %p
+ %button.btn.group.btn-group-justified.reveal-variables Reveal Variables
- - @build.trigger_request.variables.each do |key, value|
- .hide.js-build
- .js-build-variable.trigger-build-variable= key
- .js-build-value.trigger-build-value= value
+ %dl.js-build-variables.trigger-build-variables.hide
+ - @build.trigger_request.variables.each do |key, value|
+ %dt.js-build-variable.trigger-build-variable= key
+ %dd.js-build-value.trigger-build-value= value
- %div{ class: (@build.pipeline.stages_count > 1 ? "block" : "block-last") }
- %p
- Commit
- = link_to @build.pipeline.short_sha, project_commit_path(@project, @build.pipeline.sha), class: 'commit-sha link-commit'
- = clipboard_button(text: @build.pipeline.short_sha, title: "Copy commit SHA to clipboard")
- - if @build.merge_request
- in
- = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request), class: 'link-commit'
+ %div{ class: (@build.pipeline.stages_count > 1 ? "block" : "block-last") }
+ %p
+ Commit
+ = link_to @build.pipeline.short_sha, project_commit_path(@project, @build.pipeline.sha), class: 'commit-sha link-commit'
+ = clipboard_button(text: @build.pipeline.short_sha, title: "Copy commit SHA to clipboard")
+ - if @build.merge_request
+ in
+ = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request), class: 'link-commit'
- %p.build-light-text.append-bottom-0
- #{@build.pipeline.git_commit_title}
+ %p.build-light-text.append-bottom-0
+ #{@build.pipeline.git_commit_title}
- - if @build.pipeline.stages_count > 1
- .dropdown.build-dropdown
- %div
- %span{ class: "ci-status-icon-#{@build.pipeline.status}" }
- = ci_icon_for_status(@build.pipeline.status)
- Pipeline
- = link_to "##{@build.pipeline.id}", project_pipeline_path(@project, @build.pipeline), class: 'link-commit'
- from
- = link_to "#{@build.pipeline.ref}", project_branch_path(@project, @build.pipeline.ref), class: 'link-commit'
- %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
- %span.stage-selection More
- = icon('chevron-down')
- %ul.dropdown-menu
- - @build.pipeline.legacy_stages.each do |stage|
- %li
- %a.stage-item= stage.name
+ - if @build.pipeline.stages_count > 1
+ .block-last.dropdown.build-dropdown
+ %div
+ %span{ class: "ci-status-icon-#{@build.pipeline.status}" }
+ = ci_icon_for_status(@build.pipeline.status)
+ Pipeline
+ = link_to "##{@build.pipeline.id}", project_pipeline_path(@project, @build.pipeline), class: 'link-commit'
+ from
+ = link_to "#{@build.pipeline.ref}", project_branch_path(@project, @build.pipeline.ref), class: 'link-commit'
+ %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
+ %span.stage-selection More
+ = icon('chevron-down')
+ %ul.dropdown-menu
+ - @build.pipeline.legacy_stages.each do |stage|
+ %li
+ %a.stage-item= stage.name
- .builds-container
- - HasStatus::ORDERED_STATUSES.each do |build_status|
- - builds.select{|build| build.status == build_status}.each do |build|
- .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }
- = link_to project_job_path(@project, build) do
- = icon('arrow-right')
- %span{ class: "ci-status-icon-#{build.status}" }
- = ci_icon_for_status(build.status)
- %span
- - if build.name
- = build.name
- - else
- = build.id
- - if build.retried?
- %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
+ .builds-container
+ - HasStatus::ORDERED_STATUSES.each do |build_status|
+ - builds.select{|build| build.status == build_status}.each do |build|
+ .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } }
+ = link_to project_job_path(@project, build) do
+ = icon('arrow-right')
+ %span{ class: "ci-status-icon-#{build.status}" }
+ = ci_icon_for_status(build.status)
+ %span
+ - if build.name
+ = build.name
+ - else
+ = build.id
+ - if build.retried?
+ %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 25109f0f414..e3bbebbcf4c 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -17,8 +17,68 @@
- if import_sources_enabled?
%p
Create or Import your project from popular Git services
- .col-lg-9
+ .col-lg-9.js-toggle-container
= form_for @project, html: { class: 'new_project' } do |f|
+ .create-project-options
+ .first-column
+ .project-template
+ .form-group
+ = f.label :template_project, class: 'label-light' do
+ Create from template
+ = link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: "What’s included in a template?" }, title: "What’s included in a template?", class: 'has-tooltip', data: { placement: 'top'}
+ %div
+ = render 'project_templates', f: f
+ .second-column
+ - if import_sources_enabled?
+ .project-import
+ .form-group.clearfix
+ = f.label :visibility_level, class: 'label-light' do #the label here seems wrong
+ Import project from
+ .col-sm-12.import-buttons
+ %div
+ - if github_import_enabled?
+ = link_to new_import_github_path, class: 'btn import_github' do
+ = icon('github', text: 'GitHub')
+ %div
+ - if bitbucket_import_enabled?
+ = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
+ = icon('bitbucket', text: 'Bitbucket')
+ - unless bitbucket_import_configured?
+ = render 'bitbucket_import_modal'
+ %div
+ - if gitlab_import_enabled?
+ = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
+ = icon('gitlab', text: 'GitLab.com')
+ - unless gitlab_import_configured?
+ = render 'gitlab_import_modal'
+ %div
+ - if google_code_import_enabled?
+ = link_to new_import_google_code_path, class: 'btn import_google_code' do
+ = icon('google', text: 'Google Code')
+ %div
+ - if fogbugz_import_enabled?
+ = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
+ = icon('bug', text: 'Fogbugz')
+ %div
+ - if gitea_import_enabled?
+ = link_to new_import_gitea_url, class: 'btn import_gitea' do
+ = custom_icon('go_logo')
+ Gitea
+ %div
+ - if git_import_enabled?
+ %button.btn.js-toggle-button.import_git{ type: "button" }
+ = icon('git', text: 'Repo by URL')
+ .import_gitlab_project.has-tooltip{ data: { container: 'body' } }
+ = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
+ = icon('gitlab', text: 'GitLab export')
+
+ .row
+ .col-lg-12
+ .js-toggle-content.hide
+ %hr
+ = render "shared/import_form", f: f
+ %hr
+
.row
.form-group.col-xs-12.col-sm-6
= f.label :namespace_id, class: 'label-light' do
@@ -45,53 +105,6 @@
Want to house several dependent projects under the same namespace?
= link_to "Create a group", new_group_path
- - if import_sources_enabled?
- .project-import.js-toggle-container
- .form-group.clearfix
- = f.label :visibility_level, class: 'label-light' do
- Import project from
- .col-sm-12.import-buttons
- %div
- - if github_import_enabled?
- = link_to new_import_github_path, class: 'btn import_github' do
- = icon('github', text: 'GitHub')
- %div
- - if bitbucket_import_enabled?
- = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do
- = icon('bitbucket', text: 'Bitbucket')
- - unless bitbucket_import_configured?
- = render 'bitbucket_import_modal'
- %div
- - if gitlab_import_enabled?
- = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
- = icon('gitlab', text: 'GitLab.com')
- - unless gitlab_import_configured?
- = render 'gitlab_import_modal'
- %div
- - if google_code_import_enabled?
- = link_to new_import_google_code_path, class: 'btn import_google_code' do
- = icon('google', text: 'Google Code')
- %div
- - if fogbugz_import_enabled?
- = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
- = icon('bug', text: 'FogBugz')
- %div
- - if gitea_import_enabled?
- = link_to new_import_gitea_url, class: 'btn import_gitea' do
- = custom_icon('go_logo')
- Gitea
- %div
- - if git_import_enabled?
- %button.btn.js-toggle-button.import_git{ type: "button" }
- = icon('git', text: 'Repo by URL')
- .import_gitlab_project.has-tooltip{ data: { container: 'body' } }
- - if gitlab_project_import_enabled?
- = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
- = icon('gitlab', text: 'GitLab export')
-
- .js-toggle-content.hide
- = render "shared/import_form", f: f
-
.form-group
= f.label :description, class: 'label-light' do
Project description
diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml
index 95706888655..78dc4817ed7 100644
--- a/app/views/projects/runners/edit.html.haml
+++ b/app/views/projects/runners/edit.html.haml
@@ -3,4 +3,4 @@
%h4 Runner ##{@runner.id}
%hr
- = render 'form', runner: @runner, runner_form_url: runner_path(@runner)
+ = render 'form', runner: @runner, runner_form_url: runner_path(@runner)
diff --git a/app/views/projects/tree/_old_tree_content.html.haml b/app/views/projects/tree/_old_tree_content.html.haml
new file mode 100644
index 00000000000..820b947804e
--- /dev/null
+++ b/app/views/projects/tree/_old_tree_content.html.haml
@@ -0,0 +1,24 @@
+.tree-content-holder.js-tree-content{ 'data-logs-path': @logs_path }
+ .table-holder
+ %table.table#tree-slider{ class: "table_#{@hex_path} tree-table" }
+ %thead
+ %tr
+ %th= s_('ProjectFileTree|Name')
+ %th.hidden-xs
+ .pull-left= _('Last commit')
+ %th.text-right= _('Last Update')
+ - if @path.present?
+ %tr.tree-item
+ %td.tree-item-file-name
+ = link_to "..", project_tree_path(@project, up_dir_path), class: 'prepend-left-10'
+ %td
+ %td.hidden-xs
+
+ = render_tree(tree)
+
+ - if tree.readme
+ = render "projects/tree/readme", readme: tree.readme
+
+- if can_edit_tree?
+ = render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, @id), method: :post
+ = render 'projects/blob/new_dir'
diff --git a/app/views/projects/tree/_old_tree_header.html.haml b/app/views/projects/tree/_old_tree_header.html.haml
new file mode 100644
index 00000000000..13705ca303b
--- /dev/null
+++ b/app/views/projects/tree/_old_tree_header.html.haml
@@ -0,0 +1,70 @@
+%ul.breadcrumb.repo-breadcrumb
+ %li
+ = link_to project_tree_path(@project, @ref) do
+ = @project.path
+ - path_breadcrumbs do |title, path|
+ %li
+ = link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path))
+
+ - if current_user
+ %li
+ - if !on_top_of_branch?
+ %span.btn.add-to-tree.disabled.has-tooltip{ title: _("You can only add files when you are on a branch"), data: { container: 'body' } }
+ = icon('plus')
+ - else
+ %span.dropdown
+ %a.dropdown-toggle.btn.add-to-tree{ href: '#', "data-toggle" => "dropdown", "data-target" => ".add-to-tree-dropdown" }
+ = icon('plus')
+ .add-to-tree-dropdown
+ %ul.dropdown-menu
+ - if can_edit_tree?
+ %li
+ = link_to project_new_blob_path(@project, @id) do
+ = icon('pencil fw')
+ #{ _('New file') }
+ %li
+ = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do
+ = icon('file fw')
+ #{ _('Upload file') }
+ %li
+ = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
+ = icon('folder fw')
+ #{ _('New directory') }
+ - elsif can?(current_user, :fork_project, @project)
+ %li
+ - continue_params = { to: project_new_blob_path(@project, @id),
+ notice: edit_in_new_fork_notice,
+ notice_now: edit_in_new_fork_notice_now }
+ - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
+ continue: continue_params)
+ = link_to fork_path, method: :post do
+ = icon('pencil fw')
+ #{ _('New file') }
+ %li
+ - continue_params = { to: request.fullpath,
+ notice: edit_in_new_fork_notice + " Try to upload a file again.",
+ notice_now: edit_in_new_fork_notice_now }
+ - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
+ continue: continue_params)
+ = link_to fork_path, method: :post do
+ = icon('file fw')
+ #{ _('Upload file') }
+ %li
+ - continue_params = { to: request.fullpath,
+ notice: edit_in_new_fork_notice + " Try to create a new directory again.",
+ notice_now: edit_in_new_fork_notice_now }
+ - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
+ continue: continue_params)
+ = link_to fork_path, method: :post do
+ = icon('folder fw')
+ #{ _('New directory') }
+
+ %li.divider
+ %li
+ = link_to new_project_branch_path(@project) do
+ = icon('code-fork fw')
+ #{ _('New branch') }
+ %li
+ = link_to new_project_tag_path(@project) do
+ = icon('tags fw')
+ #{ _('New tag') }
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index 820b947804e..a4bdd67209d 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -1,24 +1,5 @@
-.tree-content-holder.js-tree-content{ 'data-logs-path': @logs_path }
- .table-holder
- %table.table#tree-slider{ class: "table_#{@hex_path} tree-table" }
- %thead
- %tr
- %th= s_('ProjectFileTree|Name')
- %th.hidden-xs
- .pull-left= _('Last commit')
- %th.text-right= _('Last Update')
- - if @path.present?
- %tr.tree-item
- %td.tree-item-file-name
- = link_to "..", project_tree_path(@project, up_dir_path), class: 'prepend-left-10'
- %td
- %td.hidden-xs
-
- = render_tree(tree)
-
- - if tree.readme
- = render "projects/tree/readme", readme: tree.readme
-
-- if can_edit_tree?
- = render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, @id), method: :post
- = render 'projects/blob/new_dir'
+- content_url = local_assigns.fetch(:content_url, nil)
+- if show_new_repo?
+ = render 'shared/repo/repo', project: @project, content_url: content_url
+- else
+ = render 'projects/tree/old_tree_content', tree: tree
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 858418ff8df..427b059cb82 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -1,81 +1,19 @@
.tree-ref-container
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path
+ - if show_new_repo?
+ = icon('long-arrow-right', title: 'to target branch')
+ = render 'shared/target_switcher', destination: 'tree', path: @path
- %ul.breadcrumb.repo-breadcrumb
- %li
- = link_to project_tree_path(@project, @ref) do
- = @project.path
- - path_breadcrumbs do |title, path|
- %li
- = link_to truncate(title, length: 40), project_tree_path(@project, tree_join(@ref, path))
-
- - if current_user
- %li
- - if !on_top_of_branch?
- %span.btn.add-to-tree.disabled.has-tooltip{ title: _("You can only add files when you are on a branch"), data: { container: 'body' } }
- = icon('plus')
- - else
- %span.dropdown
- %a.dropdown-toggle.btn.add-to-tree{ href: '#', "data-toggle" => "dropdown", "data-target" => ".add-to-tree-dropdown" }
- = icon('plus')
- .add-to-tree-dropdown
- %ul.dropdown-menu
- - if can_edit_tree?
- %li
- = link_to project_new_blob_path(@project, @id) do
- = icon('pencil fw')
- #{ _('New file') }
- %li
- = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do
- = icon('file fw')
- #{ _('Upload file') }
- %li
- = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
- = icon('folder fw')
- #{ _('New directory') }
- - elsif can?(current_user, :fork_project, @project)
- %li
- - continue_params = { to: project_new_blob_path(@project, @id),
- notice: edit_in_new_fork_notice,
- notice_now: edit_in_new_fork_notice_now }
- - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
- continue: continue_params)
- = link_to fork_path, method: :post do
- = icon('pencil fw')
- #{ _('New file') }
- %li
- - continue_params = { to: request.fullpath,
- notice: edit_in_new_fork_notice + " Try to upload a file again.",
- notice_now: edit_in_new_fork_notice_now }
- - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
- continue: continue_params)
- = link_to fork_path, method: :post do
- = icon('file fw')
- #{ _('Upload file') }
- %li
- - continue_params = { to: request.fullpath,
- notice: edit_in_new_fork_notice + " Try to create a new directory again.",
- notice_now: edit_in_new_fork_notice_now }
- - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id,
- continue: continue_params)
- = link_to fork_path, method: :post do
- = icon('folder fw')
- #{ _('New directory') }
-
- %li.divider
- %li
- = link_to new_project_branch_path(@project) do
- = icon('code-fork fw')
- #{ _('New branch') }
- %li
- = link_to new_project_tag_path(@project) do
- = icon('tags fw')
- #{ _('New tag') }
+ - unless show_new_repo?
+ = render 'projects/tree/old_tree_header'
.tree-controls
- = render 'projects/find_file_link'
+ - if show_new_repo?
+ = render 'shared/repo/editable_mode'
+ - else
+ = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
- = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
+ = render 'projects/find_file_link'
= render 'projects/buttons/download', project: @project, ref: @ref
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index c8587245f88..375e6764add 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -5,8 +5,14 @@
- page_title @path.presence || _("Files"), @ref
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
+
+- if show_new_repo?
+ - content_for :page_specific_javascripts do
+ = webpack_bundle_tag 'common_vue'
+ = webpack_bundle_tag 'repo'
+
= render "projects/commits/head"
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
= render 'projects/last_push'
- = render 'projects/files', commit: @last_commit, project: @project, ref: @ref
+ = render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index adb8d5aaecb..e5a1fccf9ba 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -3,9 +3,12 @@
= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f|
= form_errors(@page)
- = f.hidden_field :title, value: @page.title
- if @page.persisted?
= f.hidden_field :last_commit_sha, value: @page.last_commit_sha
+
+ .form-group
+ .col-sm-12= f.label :title, class: 'control-label-full-width'
+ .col-sm-12= f.text_field :title, class: 'form-control', value: @page.title
.form-group
.col-sm-12= f.label :format, class: 'control-label-full-width'
.col-sm-12
diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml
index e71ce1f357f..f7283ae4739 100644
--- a/app/views/projects/wikis/_sidebar.html.haml
+++ b/app/views/projects/wikis/_sidebar.html.haml
@@ -1,21 +1,22 @@
%aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "50", "spy" => "affix" } }
- .block.wiki-sidebar-header.append-bottom-default
- %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" }
- = icon('angle-double-right')
+ .sidebar-container
+ .block.wiki-sidebar-header.append-bottom-default
+ %a.gutter-toggle.pull-right.visible-xs-block.visible-sm-block.js-sidebar-wiki-toggle{ href: "#" }
+ = icon('angle-double-right')
- - git_access_url = project_wikis_git_access_path(@project)
- = link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '' do
- = succeed '&nbsp;' do
- = icon('cloud-download')
- Clone repository
+ - git_access_url = project_wikis_git_access_path(@project)
+ = link_to git_access_url, class: active_nav_link?(path: 'wikis#git_access') ? 'active' : '' do
+ = succeed '&nbsp;' do
+ = icon('cloud-download')
+ Clone repository
- .blocks-container
- .block.block-first
- %ul.wiki-pages
- = render @sidebar_wiki_entries, context: 'sidebar'
+ .blocks-container
+ .block.block-first
+ %ul.wiki-pages
+ = render @sidebar_wiki_entries, context: 'sidebar'
- .block
- = link_to project_wikis_pages_path(@project), class: 'btn btn-block' do
- More Pages
+ .block
+ = link_to project_wikis_pages_path(@project), class: 'btn btn-block' do
+ More Pages
= render 'projects/wikis/new'
diff --git a/app/views/shared/_commit_well.html.haml b/app/views/shared/_commit_well.html.haml
new file mode 100644
index 00000000000..50e3d80a84d
--- /dev/null
+++ b/app/views/shared/_commit_well.html.haml
@@ -0,0 +1,4 @@
+.info-well.hidden-xs.project-last-commit.append-bottom-default
+ .well-segment
+ %ul.blob-commit-info
+ = render 'projects/commits/commit', commit: commit, ref: ref, project: project
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 1c7c73be933..873179339dc 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -1,16 +1,16 @@
.form-group.import-url-data
- = f.label :import_url, class: 'control-label' do
+ = f.label :import_url, class: 'label-light' do
%span Git repository URL
- .col-sm-10
- = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
- .well.prepend-top-20
- %ul
- %li
- The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.
- %li
- If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.
- %li
- The import will time out after 15 minutes. For repositories that take longer, use a clone/push combination.
- %li
- To migrate an SVN repository, check out #{link_to "this document", help_page_path('workflow/importing/migrating_from_svn')}.
+ = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
+
+ .well.prepend-top-20
+ %ul
+ %li
+ The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.
+ %li
+ If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.
+ %li
+ The import will time out after 15 minutes. For repositories that take longer, use a clone/push combination.
+ %li
+ To migrate an SVN repository, check out #{link_to "this document", help_page_path('workflow/importing/migrating_from_svn')}.
diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml
index 5f3cdaefd54..96502d7ce93 100644
--- a/app/views/shared/_new_project_item_select.html.haml
+++ b/app/views/shared/_new_project_item_select.html.haml
@@ -1,6 +1,7 @@
-- if @projects.any?
- .project-item-select-holder
+- if any_projects?(@projects)
+ .project-item-select-holder.btn-group.pull-right
+ %a.btn.btn-new.new-project-item-link{ href: '', data: { label: local_assigns[:label] } }
+ = icon('spinner spin')
= project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path] }, with_feature_enabled: local_assigns[:with_feature_enabled]
- %a.btn.btn-new.new-project-item-select-button
- = local_assigns[:label]
+ %button.btn.btn-new.new-project-item-select-button
= icon('caret-down')
diff --git a/app/views/shared/_sidebar_toggle_button.html.haml b/app/views/shared/_sidebar_toggle_button.html.haml
new file mode 100644
index 00000000000..eb5ddb0dde4
--- /dev/null
+++ b/app/views/shared/_sidebar_toggle_button.html.haml
@@ -0,0 +1,8 @@
+%a.toggle-sidebar-button.js-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" }
+ = icon('angle-double-left')
+ = icon('angle-double-right')
+ %span.collapse-text Collapse sidebar
+
+= button_tag class: 'close-nav-button', type: 'button' do
+ = icon ('times')
+ %span.collapse-text Close sidebar
diff --git a/app/views/shared/_target_switcher.html.haml b/app/views/shared/_target_switcher.html.haml
new file mode 100644
index 00000000000..3672b552f10
--- /dev/null
+++ b/app/views/shared/_target_switcher.html.haml
@@ -0,0 +1,20 @@
+- dropdown_toggle_text = @ref || @project.default_branch
+= form_tag nil, method: :get, class: "project-refs-target-form" do
+ = hidden_field_tag :destination, destination
+ - if defined?(path)
+ = hidden_field_tag :path, path
+ - @options && @options.each do |key, value|
+ = hidden_field_tag key, value, id: nil
+ .dropdown
+ = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_project_path(@project, find: ['branches']), field_name: 'ref', input_field_name: 'new-branch', submit_form_on_click: true, visit: false }, { toggle_class: "js-project-refs-dropdown" }
+ %ul.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
+ %li
+ = dropdown_title _("Create a new branch")
+ %li
+ = dropdown_input _("Create a new branch")
+ %li
+ = dropdown_title _("Select existing branch"), options: {close: false}
+ %li
+ = dropdown_filter _("Search branches and tags")
+ = dropdown_content
+ = dropdown_loading
diff --git a/app/views/shared/_user_dropdown_experimental_features.html.haml b/app/views/shared/_user_dropdown_experimental_features.html.haml
new file mode 100644
index 00000000000..8e71407b748
--- /dev/null
+++ b/app/views/shared/_user_dropdown_experimental_features.html.haml
@@ -0,0 +1 @@
+%li= link_to 'Experimental features', profile_preferences_path(anchor: 'experimental-features')
diff --git a/app/views/shared/icons/_java_spring.svg b/app/views/shared/icons/_java_spring.svg
new file mode 100644
index 00000000000..508349aa456
--- /dev/null
+++ b/app/views/shared/icons/_java_spring.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" class="btn-template-icon icon-java-spring">
+ <g fill="none" fill-rule="evenodd">
+ <rect width="32" height="32"/>
+ <path fill="#70AD51" d="M5.46647617,27.9932117 C6.0517027,28.4658996 6.91159892,28.3777063 7.38425926,27.7914452 C7.85922261,27.2048452 7.76991326,26.3449044 7.18398981,25.8699411 C6.59874295,25.3956543 5.74015536,25.4869934 5.26383884,26.0722403 C4.81393367,26.6267596 4.87238621,27.4284565 5.37913494,27.9159868 L5.11431334,27.6818383 C1.97157151,24.7616933 0,20.5966301 0,15.9782542 C0,7.16842834 7.16775175,0 15.9796074,0 C20.4586065,0 24.5113565,1.8565519 27.4145869,4.8362365 C28.0749348,3.93840692 28.6466499,2.93435335 29.115524,1.82069284 C31.1513712,7.93770658 32.3482517,13.0811131 31.909824,17.1311567 C31.3178113,25.4044499 24.4017495,31.9585382 15.9796074,31.9585382 C12.0682639,31.9585382 8.48438805,30.5444735 5.7042963,28.2034861 L5.46647617,27.9932117 Z M29.0471888,23.0106888 C33.0546075,17.6737787 30.8211972,9.04527781 28.9612624,3.529749 C27.3029502,6.98304378 23.2217836,9.62375882 19.6981239,10.4613722 C16.3950312,11.2482417 13.4715032,10.6021021 10.4153644,11.7780085 C3.44517575,14.457289 3.55613585,22.7698242 7.39373146,24.6365249 C7.39711439,24.6392312 7.62444728,24.7616933 7.62174094,24.7576338 C7.62309411,24.7562806 13.2658211,23.6358542 16.3862356,22.4843049 C20.9450718,20.7996058 25.9524846,16.6494275 27.5986182,11.8273993 C26.723116,16.8415779 22.4179995,21.6669891 18.093262,23.8828081 C15.7908399,25.0648038 14.0005934,25.3279957 10.2123886,26.6385428 C9.74892722,26.798217 9.38492397,26.9538318 9.38492397,26.9538318 C10.3463526,26.7948341 11.301692,26.7420604 11.301692,26.7420604 C16.6954354,26.4869875 25.1087819,28.2582896 29.0471888,23.0106888 Z"/>
+ </g>
+</svg>
diff --git a/app/views/shared/icons/_node_express.svg b/app/views/shared/icons/_node_express.svg
new file mode 100644
index 00000000000..f2c94319f19
--- /dev/null
+++ b/app/views/shared/icons/_node_express.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="27" height="32" viewBox="0 0 27 32" class="btn-template-icon icon-node-express">
+ <g fill="none" fill-rule="evenodd" transform="translate(-3)">
+ <rect width="32" height="32"/>
+ <path fill="#353535" d="M4.19170065,16.2667139 C4.23142421,18.3323387 4.47969269,20.2489714 4.93651356,22.0166696 C5.39333443,23.7843677 6.09841693,25.3236323 7.05178222,26.6345096 C8.00514751,27.9453869 9.23655921,28.9781838 10.7460543,29.7329313 C12.2555493,30.4876788 14.1026668,30.8650469 16.2874623,30.8650469 C19.5050701,30.8650469 22.1764391,30.0209341 24.3016492,28.3326831 C26.4268593,26.644432 27.7476477,24.1120935 28.2640539,20.7355914 L29.4557545,20.7355914 C29.0187954,24.3107112 27.6086304,27.0813875 25.2252172,29.0477034 C22.841804,31.0140194 19.9023051,31.9971626 16.4066324,31.9971626 C14.0232191,32.0368861 11.9874175,31.659518 10.2991665,30.8650469 C8.61091547,30.0705759 7.23054269,28.9484023 6.15800673,27.4984926 C5.08547078,26.0485829 4.29101162,24.3404957 3.77460543,22.3741798 C3.25819923,20.4078639 3,18.2926164 3,16.0283738 C3,13.4860664 3.3773681,11.2218578 4.13211562,9.23568007 C4.88686314,7.24950238 5.87993709,5.57120741 7.11136726,4.20074481 C8.34279742,2.8302822 9.77282391,1.78755456 11.4014896,1.07253059 C13.0301553,0.357506621 14.6985195,0 16.4066324,0 C18.7900456,0 20.8457087,0.456814016 22.5736832,1.37045575 C24.3016578,2.28409749 25.7118228,3.4956477 26.8042206,5.00514275 C27.8966183,6.51463779 28.6910775,8.24258646 29.1876219,10.1890406 C29.6841663,12.1354947 29.8927118,14.1613656 29.8132647,16.2667139 L4.19170065,16.2667139 Z M28.6215641,15.0750133 C28.6215641,13.2080062 28.3633648,11.4304039 27.8469586,9.74215285 C27.3305524,8.05390181 26.5658855,6.57422163 25.5529349,5.30306791 C24.5399843,4.03191419 23.2787803,3.0289095 21.7692853,2.29402376 C20.2597903,1.55913801 18.5119801,1.19170065 16.5258024,1.19170065 C14.8574132,1.19170065 13.2982871,1.50948432 11.8483774,2.14506118 C10.3984676,2.78063804 9.12733299,3.70419681 8.03493526,4.9157652 C6.94253754,6.12733359 6.05870172,7.58715229 5.38340131,9.2952651 C4.70810089,11.0033779 4.31087132,12.9299414 4.19170065,15.0750133 L28.6215641,15.0750133 Z"/>
+ </g>
+</svg>
diff --git a/app/views/shared/icons/_rails.svg b/app/views/shared/icons/_rails.svg
new file mode 100644
index 00000000000..0bb09a705df
--- /dev/null
+++ b/app/views/shared/icons/_rails.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="32" height="20" viewBox="0 0 32 20" class="btn-template-icon icon-rails">
+ <g fill="none" fill-rule="evenodd" transform="translate(0 -6)">
+ <rect width="32" height="32"/>
+ <path fill="#C00" fill-rule="nonzero" d="M0.984615385,25.636044 C0.984615385,25.636044 1.40659341,21.4725275 4.36043956,16.5494505 C7.31428571,11.6263736 12.3498901,7.8989011 16.4430769,7.53318681 C24.5872527,6.71736264 31.9015385,14.0175824 31.9015385,14.0175824 C31.9015385,14.0175824 31.6624176,14.1863736 31.4092308,14.3973626 C23.4197802,8.48967033 18.5389011,11.2747253 17.0057143,12.0202198 C9.97274725,15.9446154 12.0967033,25.636044 12.0967033,25.636044 L0.984615385,25.636044 Z M24.1371429,8.32087912 C23.687033,8.13802198 23.2369231,7.96923077 22.7727473,7.81450549 L22.829011,6.88615385 C23.7151648,7.13934066 24.0668132,7.30813187 24.1934066,7.37846154 L24.1371429,8.32087912 Z M22.8008791,11.3028571 C23.250989,11.330989 23.7151648,11.3872527 24.1934066,11.4857143 L24.1371429,12.3578022 C23.672967,12.2593407 23.2087912,12.2030769 22.7446154,12.189011 L22.8008791,11.3028571 Z M17.5964835,6.91428571 C17.1885714,6.91428571 16.7806593,6.92835165 16.3727473,6.97054945 L16.1054945,6.14065934 C16.5696703,6.0843956 17.0197802,6.05626374 17.4558242,6.05626374 L17.7371429,6.91428571 C17.6949451,6.91428571 17.6386813,6.91428571 17.5964835,6.91428571 Z M18.2716484,12.0905495 C18.6232967,11.9358242 19.0312088,11.7810989 19.5094505,11.6404396 L19.8189011,12.5687912 C19.410989,12.6953846 19.0030769,12.8641758 18.5951648,13.0610989 L18.2716484,12.0905495 Z M11.8857143,8.39120879 C11.52,8.57406593 11.1683516,8.78505495 10.8026374,9.01010989 L10.1556044,8.02549451 C10.5353846,7.80043956 10.9010989,7.60351648 11.2527473,7.42065934 L11.8857143,8.39120879 Z M14.7692308,14.7208791 C15.0224176,14.3973626 15.3178022,14.0738462 15.6413187,13.7784615 L16.2742857,14.7349451 C15.9648352,15.0584615 15.6835165,15.381978 15.4443956,15.7336264 L14.7692308,14.7208791 Z M12.7296703,19.2501099 C12.8421978,18.7437363 12.9687912,18.2232967 13.1516484,17.7028571 L14.1643956,18.5046154 C14.0237363,19.0531868 13.9252747,19.6017582 13.869011,20.1503297 L12.7296703,19.2501099 Z M6.56879121,12.5687912 C6.23120879,12.9204396 5.90769231,13.3002198 5.61230769,13.68 L4.52923077,12.7516484 C4.85274725,12.4 5.2043956,12.0483516 5.57010989,11.6967033 L6.56879121,12.5687912 Z M2.32087912,18.8562637 C2.09582418,19.3767033 1.80043956,20.0659341 1.61758242,20.5441758 L0,19.9534066 C0.140659341,19.5736264 0.436043956,18.8703297 0.703296703,18.2654945 L2.32087912,18.8562637 Z M12.5186813,22.8228571 L14.0378022,23.3714286 C14.1221978,24.0325275 14.2487912,24.6514286 14.3753846,25.2 L12.6874725,24.5951648 C12.6171429,24.1731868 12.5468132,23.5683516 12.5186813,22.8228571 Z"/>
+ </g>
+</svg>
diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml
index bd66f39fa59..0a692d9653f 100644
--- a/app/views/shared/issuable/_label_page_create.html.haml
+++ b/app/views/shared/issuable/_label_page_create.html.haml
@@ -1,5 +1,5 @@
.dropdown-page-two.dropdown-new-label
- = dropdown_title("Create new label", back: true)
+ = dropdown_title("Create new label", options: { back: true })
= dropdown_content do
.dropdown-labels-error.js-label-error
%input#new_label_name.default-dropdown-input{ type: "text", placeholder: "Name new label" }
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index e7510c1d1ec..c2de6926460 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -115,6 +115,10 @@
- if can? current_user, :admin_label, @project and @project
= render partial: "shared/issuable/label_page_create"
+ - if issuable.has_attribute?(:confidential)
+ %script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: @issue.confidential, is_editable: can_edit_issuable }.to_json.html_safe
+ #js-confidential-entry-point
+
= render "shared/issuable/participants", participants: issuable.participants(current_user)
- if current_user
- subscribed = issuable.subscribed?(current_user, @project)
diff --git a/app/views/shared/issuable/_user_dropdown_item.html.haml b/app/views/shared/issuable/_user_dropdown_item.html.haml
index a82c01c6dc2..c18e4975bb8 100644
--- a/app/views/shared/issuable/_user_dropdown_item.html.haml
+++ b/app/views/shared/issuable/_user_dropdown_item.html.haml
@@ -3,7 +3,8 @@
%li.filter-dropdown-item{ class: ('js-current-user' if user == current_user) }
%button.btn.btn-link.dropdown-user{ type: :button }
- = user_avatar_without_link(user: user, lazy: avatar[:lazy], url: avatar[:url], size: 30)
+ .avatar-container.s40
+ = user_avatar_without_link(user: user, lazy: avatar[:lazy], url: avatar[:url], size: 40).gsub('/images/{{avatar_url}}','{{avatar_url}}').html_safe
.dropdown-user-details
%span
= user.name
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 7ed6c622558..914506bf0ce 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -10,7 +10,7 @@
- load_pipeline_status(projects)
.js-projects-list-holder
- - if projects.any?
+ - if any_projects?(projects)
%ul.projects-list
- projects.each_with_index do |project, i|
- css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil
@@ -22,7 +22,7 @@
%li.project-row.private-forks-notice
= icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon')
%strong= pluralize(@private_forks_count, 'private fork')
- %span you have no access to.
+ %span &nbsp;you have no access to.
= paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages
- else
.nothing-here-block No projects found
diff --git a/app/views/shared/repo/_editable_mode.html.haml b/app/views/shared/repo/_editable_mode.html.haml
new file mode 100644
index 00000000000..73fdb8b523f
--- /dev/null
+++ b/app/views/shared/repo/_editable_mode.html.haml
@@ -0,0 +1,2 @@
+.editable-mode
+ %repo-edit-button
diff --git a/app/views/shared/repo/_repo.html.haml b/app/views/shared/repo/_repo.html.haml
new file mode 100644
index 00000000000..0fc40cf0801
--- /dev/null
+++ b/app/views/shared/repo/_repo.html.haml
@@ -0,0 +1,2 @@
+#repo{ data: { url: content_url, project_name: project.name, refs_url: refs_project_path(project, format: :json), project_url: project_path(project), project_id: project.id, can_commit: (!!can_push_branch?(project, @ref)).to_s } }
+ %repo
diff --git a/app/workers/concerns/new_issuable.rb b/app/workers/concerns/new_issuable.rb
new file mode 100644
index 00000000000..3fd472bf0c1
--- /dev/null
+++ b/app/workers/concerns/new_issuable.rb
@@ -0,0 +1,23 @@
+module NewIssuable
+ attr_reader :issuable, :user
+
+ def ensure_objects_found(issuable_id, user_id)
+ @issuable = issuable_class.find_by(id: issuable_id)
+ unless @issuable
+ log_error(issuable_class, issuable_id)
+ return false
+ end
+
+ @user = User.find_by(id: user_id)
+ unless @user
+ log_error(User, user_id)
+ return false
+ end
+
+ true
+ end
+
+ def log_error(record_class, record_id)
+ Rails.logger.error("#{self.class}: couldn't find #{record_class} with ID=#{record_id}, skipping job")
+ end
+end
diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb
index 48e2da338f6..c3b58df92c1 100644
--- a/app/workers/merge_worker.rb
+++ b/app/workers/merge_worker.rb
@@ -7,6 +7,8 @@ class MergeWorker
current_user = User.find(current_user_id)
merge_request = MergeRequest.find(merge_request_id)
+ merge_request.update_column(:merge_jid, jid)
+
MergeRequests::MergeService.new(merge_request.target_project, current_user, params)
.execute(merge_request)
end
diff --git a/app/workers/new_issue_worker.rb b/app/workers/new_issue_worker.rb
new file mode 100644
index 00000000000..19a778ad522
--- /dev/null
+++ b/app/workers/new_issue_worker.rb
@@ -0,0 +1,17 @@
+class NewIssueWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+ include NewIssuable
+
+ def perform(issue_id, user_id)
+ return unless ensure_objects_found(issue_id, user_id)
+
+ EventCreateService.new.open_issue(issuable, user)
+ NotificationService.new.new_issue(issuable, user)
+ issuable.create_cross_references!(user)
+ end
+
+ def issuable_class
+ Issue
+ end
+end
diff --git a/app/workers/new_merge_request_worker.rb b/app/workers/new_merge_request_worker.rb
new file mode 100644
index 00000000000..3c8a68016ff
--- /dev/null
+++ b/app/workers/new_merge_request_worker.rb
@@ -0,0 +1,17 @@
+class NewMergeRequestWorker
+ include Sidekiq::Worker
+ include DedicatedSidekiqQueue
+ include NewIssuable
+
+ def perform(merge_request_id, user_id)
+ return unless ensure_objects_found(merge_request_id, user_id)
+
+ EventCreateService.new.open_mr(issuable, user)
+ NotificationService.new.new_merge_request(issuable, user)
+ issuable.create_cross_references!(user)
+ end
+
+ def issuable_class
+ MergeRequest
+ end
+end
diff --git a/app/workers/stuck_merge_jobs_worker.rb b/app/workers/stuck_merge_jobs_worker.rb
new file mode 100644
index 00000000000..7843179d77c
--- /dev/null
+++ b/app/workers/stuck_merge_jobs_worker.rb
@@ -0,0 +1,34 @@
+class StuckMergeJobsWorker
+ include Sidekiq::Worker
+ include CronjobQueue
+
+ def perform
+ stuck_merge_requests.find_in_batches(batch_size: 100) do |group|
+ jids = group.map(&:merge_jid)
+
+ # Find the jobs that aren't currently running or that exceeded the threshold.
+ completed_jids = Gitlab::SidekiqStatus.completed_jids(jids)
+
+ if completed_jids.any?
+ completed_ids = group.select { |merge_request| completed_jids.include?(merge_request.merge_jid) }.map(&:id)
+
+ apply_current_state!(completed_jids, completed_ids)
+ end
+ end
+ end
+
+ private
+
+ def apply_current_state!(completed_jids, completed_ids)
+ merge_requests = MergeRequest.where(id: completed_ids)
+
+ merge_requests.where.not(merge_commit_sha: nil).update_all(state: :merged)
+ merge_requests.where(merge_commit_sha: nil).update_all(state: :opened)
+
+ Rails.logger.info("Updated state of locked merge jobs. JIDs: #{completed_jids.join(', ')}")
+ end
+
+ def stuck_merge_requests
+ MergeRequest.select('id, merge_jid').with_state(:locked).where.not(merge_jid: nil).reorder(nil)
+ end
+end
diff --git a/changelogs/unreleased/13265-project_events_noteable_iid.yml b/changelogs/unreleased/13265-project_events_noteable_iid.yml
new file mode 100644
index 00000000000..54d538bb548
--- /dev/null
+++ b/changelogs/unreleased/13265-project_events_noteable_iid.yml
@@ -0,0 +1,4 @@
+---
+title: Expose noteable_iid in Note
+merge_request: 13265
+author: sue445
diff --git a/changelogs/unreleased/31207-clean-locked-merge-requests.yml b/changelogs/unreleased/31207-clean-locked-merge-requests.yml
new file mode 100644
index 00000000000..1f52987baef
--- /dev/null
+++ b/changelogs/unreleased/31207-clean-locked-merge-requests.yml
@@ -0,0 +1,4 @@
+---
+title: Unlock stuck merge request and set the proper state
+merge_request: 13207
+author:
diff --git a/changelogs/unreleased/32844-issuables-performance.yml b/changelogs/unreleased/32844-issuables-performance.yml
new file mode 100644
index 00000000000..e9b21c1aa45
--- /dev/null
+++ b/changelogs/unreleased/32844-issuables-performance.yml
@@ -0,0 +1,4 @@
+---
+title: Move some code from services to workers in order to improve performance
+merge_request: 13326
+author:
diff --git a/changelogs/unreleased/33095-mr-widget-ui.yml b/changelogs/unreleased/33095-mr-widget-ui.yml
new file mode 100644
index 00000000000..9ce3086df27
--- /dev/null
+++ b/changelogs/unreleased/33095-mr-widget-ui.yml
@@ -0,0 +1,4 @@
+---
+title: clean up merge request widget UI
+merge_request:
+author:
diff --git a/changelogs/unreleased/33874_confi.yml b/changelogs/unreleased/33874_confi.yml
new file mode 100644
index 00000000000..940753d9aaa
--- /dev/null
+++ b/changelogs/unreleased/33874_confi.yml
@@ -0,0 +1,5 @@
+---
+title: Update confidential issue UI - add confidential visibility and settings to
+ sidebar
+merge_request:
+author:
diff --git a/changelogs/unreleased/34028-collapse-sidebar.yml b/changelogs/unreleased/34028-collapse-sidebar.yml
new file mode 100644
index 00000000000..468212240ac
--- /dev/null
+++ b/changelogs/unreleased/34028-collapse-sidebar.yml
@@ -0,0 +1,4 @@
+---
+title: Make contextual sidebar collapsible
+merge_request:
+author:
diff --git a/changelogs/unreleased/34764-rename-to-overview.yml b/changelogs/unreleased/34764-rename-to-overview.yml
new file mode 100644
index 00000000000..5b9643285b7
--- /dev/null
+++ b/changelogs/unreleased/34764-rename-to-overview.yml
@@ -0,0 +1,4 @@
+---
+title: Rename about to overview for group and project page
+merge_request:
+author:
diff --git a/changelogs/unreleased/35052-please-select-a-file-when-attempting-to-upload-or-replace-from-the-ui.yml b/changelogs/unreleased/35052-please-select-a-file-when-attempting-to-upload-or-replace-from-the-ui.yml
new file mode 100644
index 00000000000..5925da14f89
--- /dev/null
+++ b/changelogs/unreleased/35052-please-select-a-file-when-attempting-to-upload-or-replace-from-the-ui.yml
@@ -0,0 +1,4 @@
+---
+title: improve file upload/replace experience
+merge_request:
+author:
diff --git a/changelogs/unreleased/35098-raise-encoding-confidence-threshold.yml b/changelogs/unreleased/35098-raise-encoding-confidence-threshold.yml
new file mode 100644
index 00000000000..3cdb3011f5b
--- /dev/null
+++ b/changelogs/unreleased/35098-raise-encoding-confidence-threshold.yml
@@ -0,0 +1,4 @@
+---
+title: Raise guessed encoding confidence threshold to 50
+merge_request: 12990
+author:
diff --git a/changelogs/unreleased/35136-barchart-not-display-label-at-0-hour.yml b/changelogs/unreleased/35136-barchart-not-display-label-at-0-hour.yml
new file mode 100644
index 00000000000..ea8f31cca9d
--- /dev/null
+++ b/changelogs/unreleased/35136-barchart-not-display-label-at-0-hour.yml
@@ -0,0 +1,4 @@
+---
+title: Fix bar chart does not display label at 0 hour
+merge_request: 35136
+author: Jason Dai
diff --git a/changelogs/unreleased/35483-improve-mobile-sidebar.yml b/changelogs/unreleased/35483-improve-mobile-sidebar.yml
new file mode 100644
index 00000000000..eb3dab1da9e
--- /dev/null
+++ b/changelogs/unreleased/35483-improve-mobile-sidebar.yml
@@ -0,0 +1,4 @@
+---
+title: Improve mobile sidebar
+merge_request:
+author:
diff --git a/changelogs/unreleased/35761-convdev-perc.yml b/changelogs/unreleased/35761-convdev-perc.yml
new file mode 100644
index 00000000000..319c4d18219
--- /dev/null
+++ b/changelogs/unreleased/35761-convdev-perc.yml
@@ -0,0 +1,4 @@
+---
+title: Store & use ConvDev percentages returned by the Version app
+merge_request:
+author:
diff --git a/changelogs/unreleased/add-star-for-action-scope.yml b/changelogs/unreleased/add-star-for-action-scope.yml
new file mode 100644
index 00000000000..a8119a01ec4
--- /dev/null
+++ b/changelogs/unreleased/add-star-for-action-scope.yml
@@ -0,0 +1,4 @@
+---
+title: Add star for action scope, in order to delete image from registry
+merge_request: 13248
+author: jean
diff --git a/changelogs/unreleased/bvl-nfs-circuitbreaker.yml b/changelogs/unreleased/bvl-nfs-circuitbreaker.yml
new file mode 100644
index 00000000000..151854ed31f
--- /dev/null
+++ b/changelogs/unreleased/bvl-nfs-circuitbreaker.yml
@@ -0,0 +1,4 @@
+---
+title: Block access to failing repository storage
+merge_request: 11449
+author:
diff --git a/changelogs/unreleased/dont-use-limit-offset-when-counting-projects.yml b/changelogs/unreleased/dont-use-limit-offset-when-counting-projects.yml
new file mode 100644
index 00000000000..8ecea635ce5
--- /dev/null
+++ b/changelogs/unreleased/dont-use-limit-offset-when-counting-projects.yml
@@ -0,0 +1,4 @@
+---
+title: "Improve performance of checking for projects on the projects dashboard"
+merge_request:
+author:
diff --git a/changelogs/unreleased/eager-load-project-creators-for-project-dashboards.yml b/changelogs/unreleased/eager-load-project-creators-for-project-dashboards.yml
new file mode 100644
index 00000000000..e550e0b2f44
--- /dev/null
+++ b/changelogs/unreleased/eager-load-project-creators-for-project-dashboards.yml
@@ -0,0 +1,4 @@
+---
+title: Eager load project creators for project dashboards
+merge_request:
+author:
diff --git a/changelogs/unreleased/github.yml b/changelogs/unreleased/github.yml
new file mode 100644
index 00000000000..585b9b13b65
--- /dev/null
+++ b/changelogs/unreleased/github.yml
@@ -0,0 +1,4 @@
+---
+title: Reduce memory usage of the GitHub importer
+merge_request: 12886
+author:
diff --git a/changelogs/unreleased/group-milestone-references-system-notes.yml b/changelogs/unreleased/group-milestone-references-system-notes.yml
new file mode 100644
index 00000000000..58215352305
--- /dev/null
+++ b/changelogs/unreleased/group-milestone-references-system-notes.yml
@@ -0,0 +1,4 @@
+---
+title: Support Markdown references, autocomplete, and quick actions for group milestones
+merge_request:
+author:
diff --git a/changelogs/unreleased/group-new-issue.yml b/changelogs/unreleased/group-new-issue.yml
new file mode 100644
index 00000000000..5480a44526b
--- /dev/null
+++ b/changelogs/unreleased/group-new-issue.yml
@@ -0,0 +1,4 @@
+---
+title: Cache recent projects for group-level new resource creation.
+merge_request: !13058
+author:
diff --git a/changelogs/unreleased/mattermost_fixes.yml b/changelogs/unreleased/mattermost_fixes.yml
new file mode 100644
index 00000000000..667109a0bb4
--- /dev/null
+++ b/changelogs/unreleased/mattermost_fixes.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Mattermost integration
+merge_request:
+author:
diff --git a/changelogs/unreleased/memoize-user-personal-projects-count.yml b/changelogs/unreleased/memoize-user-personal-projects-count.yml
new file mode 100644
index 00000000000..3839a97f185
--- /dev/null
+++ b/changelogs/unreleased/memoize-user-personal-projects-count.yml
@@ -0,0 +1,4 @@
+---
+title: Memoize the number of personal projects a user has to reduce COUNT queries
+merge_request:
+author:
diff --git a/changelogs/unreleased/pawel-add-sidekiq-metrics-endpoint-32145.yml b/changelogs/unreleased/pawel-add-sidekiq-metrics-endpoint-32145.yml
new file mode 100644
index 00000000000..71eabdc16d2
--- /dev/null
+++ b/changelogs/unreleased/pawel-add-sidekiq-metrics-endpoint-32145.yml
@@ -0,0 +1,4 @@
+---
+title: Add Prometheus metrics exporter to Sidekiq
+merge_request: 13082
+author:
diff --git a/changelogs/unreleased/rc-fix-branches-api-endpoint.yml b/changelogs/unreleased/rc-fix-branches-api-endpoint.yml
index a8f49298258..b36663bbe91 100644
--- a/changelogs/unreleased/rc-fix-branches-api-endpoint.yml
+++ b/changelogs/unreleased/rc-fix-branches-api-endpoint.yml
@@ -1,5 +1,5 @@
---
title: Fix the /projects/:id/repository/branches endpoint to handle dots in the branch
- name when the project full patch contains a `/`
+ name when the project full path contains a `/`
merge_request: 13115
author:
diff --git a/changelogs/unreleased/rc-fix-commits-api.yml b/changelogs/unreleased/rc-fix-commits-api.yml
new file mode 100644
index 00000000000..215429eaf6b
--- /dev/null
+++ b/changelogs/unreleased/rc-fix-commits-api.yml
@@ -0,0 +1,5 @@
+---
+title: Fix the /projects/:id/repository/commits endpoint to handle dots in the ref
+ name when the project full path contains a `/`
+merge_request: 13370
+author:
diff --git a/changelogs/unreleased/rc-fix-tags-api.yml b/changelogs/unreleased/rc-fix-tags-api.yml
new file mode 100644
index 00000000000..0a7dd5ca6ab
--- /dev/null
+++ b/changelogs/unreleased/rc-fix-tags-api.yml
@@ -0,0 +1,5 @@
+---
+title: Fix the /projects/:id/repository/tags endpoint to handle dots in the tag name
+ when the project full path contains a `/`
+merge_request: 13368
+author:
diff --git a/changelogs/unreleased/remove-redundant-query-when-retrieving-recent-pushes.yml b/changelogs/unreleased/remove-redundant-query-when-retrieving-recent-pushes.yml
new file mode 100644
index 00000000000..83934217e6a
--- /dev/null
+++ b/changelogs/unreleased/remove-redundant-query-when-retrieving-recent-pushes.yml
@@ -0,0 +1,4 @@
+---
+title: Remove redundant query when retrieving the most recent push of a user
+merge_request:
+author:
diff --git a/changelogs/unreleased/restrict-haml-javascript.yml b/changelogs/unreleased/restrict-haml-javascript.yml
new file mode 100644
index 00000000000..3d0a52f416d
--- /dev/null
+++ b/changelogs/unreleased/restrict-haml-javascript.yml
@@ -0,0 +1,4 @@
+---
+title: Add custom linter for inline JavaScript to haml_lint
+merge_request: 9742
+author: winniehell
diff --git a/changelogs/unreleased/wiki_title.yml b/changelogs/unreleased/wiki_title.yml
new file mode 100644
index 00000000000..3ef5fa2969b
--- /dev/null
+++ b/changelogs/unreleased/wiki_title.yml
@@ -0,0 +1,4 @@
+---
+title: Allow wiki pages to be renamed in the UI
+merge_request: 10069
+author: wendy0402
diff --git a/changelogs/unreleased/zj-project-templates.yml b/changelogs/unreleased/zj-project-templates.yml
new file mode 100644
index 00000000000..ab6e0f2d5f2
--- /dev/null
+++ b/changelogs/unreleased/zj-project-templates.yml
@@ -0,0 +1,4 @@
+---
+title: Projects can be created from templates
+merge_request: 13108
+author:
diff --git a/config/application.rb b/config/application.rb
index f7145566262..47887bf8596 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -181,7 +181,11 @@ module Gitlab
end
end
+ # We add the MilestonesRoutingHelper because we know that this does not
+ # conflict with the methods defined in `project_url_helpers`, and we want
+ # these methods available in the same places.
Gitlab::Routing.add_helpers(project_url_helpers)
+ Gitlab::Routing.add_helpers(MilestonesRoutingHelper)
end
end
end
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index 59c7050a14d..ca5b941aebf 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -398,3 +398,9 @@
:why: https://github.com/remy/undefsafe/blob/master/LICENSE
:versions: []
:when: 2017-04-10 06:30:00.002555000 Z
+- - :approve
+ - thunky
+ - :who: Mike Greiling
+ :why: https://github.com/mafintosh/thunky/blob/master/README.md#license
+ :versions: []
+ :when: 2017-08-07 05:56:09.907045000 Z
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 73a68c6da1b..e73db08fcac 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -506,6 +506,11 @@ production: &base
path: /home/git/repositories/
gitaly_address: unix:/home/git/gitlab/tmp/sockets/private/gitaly.socket # TCP connections are supported too (e.g. tcp://host:port)
# gitaly_token: 'special token' # Optional: override global gitaly.token for this storage.
+ failure_count_threshold: 10 # number of failures before stopping attempts
+ failure_wait_time: 30 # Seconds after an access failure before allowing access again
+ failure_reset_time: 1800 # Time in seconds to expire failures
+ storage_timeout: 5 # Time in seconds to wait before aborting a storage access attempt
+
## Backup settings
backup:
@@ -585,6 +590,12 @@ production: &base
ip_whitelist:
- 127.0.0.0/8
+ # Sidekiq exporter is webserver built in to Sidekiq to expose Prometheus metrics
+ sidekiq_exporter:
+ # enabled: true
+ # address: localhost
+ # port: 3807
+
#
# 5. Extra customization
# ==========================
@@ -638,6 +649,10 @@ test:
default:
path: tmp/tests/repositories/
gitaly_address: unix:tmp/tests/gitaly/gitaly.socket
+ broken:
+ path: tmp/tests/non-existent-repositories
+ gitaly_address: unix:tmp/tests/gitaly/gitaly.socket
+
gitaly:
enabled: true
token: secret
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 63f4c8c9e0a..2699173fc61 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -395,6 +395,10 @@ Settings.cron_jobs['remove_old_web_hook_logs_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['remove_old_web_hook_logs_worker']['cron'] ||= '40 0 * * *'
Settings.cron_jobs['remove_old_web_hook_logs_worker']['job_class'] = 'RemoveOldWebHookLogsWorker'
+Settings.cron_jobs['stuck_merge_jobs_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['stuck_merge_jobs_worker']['cron'] ||= '0 */2 * * *'
+Settings.cron_jobs['stuck_merge_jobs_worker']['job_class'] = 'StuckMergeJobsWorker'
+
#
# GitLab Shell
#
@@ -433,6 +437,17 @@ end
Settings.repositories.storages.values.each do |storage|
# Expand relative paths
storage['path'] = Settings.absolute(storage['path'])
+ # Set failure defaults
+ storage['failure_count_threshold'] ||= 10
+ storage['failure_wait_time'] ||= 30
+ storage['failure_reset_time'] ||= 1800
+ storage['storage_timeout'] ||= 5
+ # Set turn strings into numbers
+ storage['failure_count_threshold'] = storage['failure_count_threshold'].to_i
+ storage['failure_wait_time'] = storage['failure_wait_time'].to_i
+ storage['failure_reset_time'] = storage['failure_reset_time'].to_i
+ # We might want to have a timeout shorter than 1 second.
+ storage['storage_timeout'] = storage['storage_timeout'].to_f
end
#
@@ -513,6 +528,10 @@ Settings.webpack.dev_server['port'] ||= 3808
Settings['monitoring'] ||= Settingslogic.new({})
Settings.monitoring['ip_whitelist'] ||= ['127.0.0.1/8']
Settings.monitoring['unicorn_sampler_interval'] ||= 10
+Settings.monitoring['sidekiq_exporter'] ||= Settingslogic.new({})
+Settings.monitoring.sidekiq_exporter['enabled'] ||= false
+Settings.monitoring.sidekiq_exporter['address'] ||= 'localhost'
+Settings.monitoring.sidekiq_exporter['port'] ||= 3807
#
# Testing settings
diff --git a/config/initializers/6_validations.rb b/config/initializers/6_validations.rb
index 9e24f42d284..92ce4dd03cd 100644
--- a/config/initializers/6_validations.rb
+++ b/config/initializers/6_validations.rb
@@ -7,6 +7,13 @@ def find_parent_path(name, path)
Gitlab.config.repositories.storages.detect do |n, rs|
name != n && Pathname.new(rs['path']).realpath == parent
end
+rescue Errno::EIO, Errno::ENOENT => e
+ warning = "WARNING: couldn't verify #{path} (#{name}). "\
+ "If this is an external storage, it might be offline."
+ message = "#{warning}\n#{e.message}"
+ Rails.logger.error("#{message}\n\t" + e.backtrace.join("\n\t"))
+
+ nil
end
def storage_validation_error(message)
@@ -29,6 +36,15 @@ def validate_storages_config
if !repository_storage.is_a?(Hash) || repository_storage['path'].nil?
storage_validation_error("#{name} is not a valid storage, because it has no `path` key. Refer to gitlab.yml.example for an updated example")
end
+
+ %w(failure_count_threshold failure_wait_time failure_reset_time storage_timeout).each do |setting|
+ # Falling back to the defaults is fine!
+ next if repository_storage[setting].nil?
+
+ unless repository_storage[setting].to_f > 0
+ storage_validation_error("#{setting}, for storage `#{name}` needs to be greater than 0")
+ end
+ end
end
end
diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb
index a2f8421f5d7..54c797e0714 100644
--- a/config/initializers/7_prometheus_metrics.rb
+++ b/config/initializers/7_prometheus_metrics.rb
@@ -10,3 +10,9 @@ Prometheus::Client.configure do |config|
config.multiprocess_files_dir ||= Rails.root.join('tmp/prometheus_multiproc_dir')
end
end
+
+Sidekiq.configure_server do |config|
+ config.on(:startup) do
+ Gitlab::Metrics::SidekiqMetricsExporter.instance.start
+ end
+end
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 5427bab93ce..c0748231813 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -67,7 +67,9 @@ namespace :admin do
end
resource :logs, only: [:show]
- resource :health_check, controller: 'health_check', only: [:show]
+ resource :health_check, controller: 'health_check', only: [:show] do
+ post :reset_storage_health
+ end
resource :background_jobs, controller: 'background_jobs', only: [:show]
resource :system_info, controller: 'system_info', only: [:show]
resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ }
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 7496bfa4fbb..83abc83c9f0 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -23,6 +23,8 @@
- [update_merge_requests, 3]
- [process_commit, 3]
- [new_note, 2]
+ - [new_issue, 2]
+ - [new_merge_request, 2]
- [build, 2]
- [pipeline, 2]
- [gitlab_shell, 2]
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 30900db62e7..9625ffddb0a 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -3,7 +3,8 @@
var fs = require('fs');
var path = require('path');
var webpack = require('webpack');
-var StatsPlugin = require('stats-webpack-plugin');
+var StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
+var CopyWebpackPlugin = require('copy-webpack-plugin');
var CompressionPlugin = require('compression-webpack-plugin');
var NameAllModulesPlugin = require('name-all-modules-plugin');
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
@@ -61,10 +62,12 @@ var config = {
pipelines_details: './pipelines/pipeline_details_bundle.js',
pipelines_times: './pipelines/pipelines_times.js',
profile: './profile/profile_bundle.js',
+ project_import_gl: './projects/project_import_gitlab_project.js',
project_new: './projects/project_new.js',
prometheus_metrics: './prometheus_metrics',
protected_branches: './protected_branches',
protected_tags: './protected_tags',
+ repo: './repo/index.js',
sidebar: './sidebar/sidebar_bundle.js',
schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js',
schedules_index: './pipeline_schedules/pipeline_schedules_index_bundle.js',
@@ -122,18 +125,33 @@ var config = {
test: /locale\/\w+\/(.*)\.js$/,
loader: 'exports-loader?locales',
},
- ]
+ {
+ test: /monaco-editor\/\w+\/vs\/loader\.js$/,
+ use: [
+ { loader: 'exports-loader', options: 'l.global' },
+ { loader: 'imports-loader', options: 'l=>{},this=>l,AMDLoader=>this,module=>undefined' },
+ ],
+ }
+ ],
+
+ noParse: [/monaco-editor\/\w+\/vs\//],
},
plugins: [
// manifest filename must match config.webpack.manifest_filename
// webpack-rails only needs assetsByChunkName to function properly
- new StatsPlugin('manifest.json', {
- chunkModules: false,
- source: false,
- chunks: false,
- modules: false,
- assets: true
+ new StatsWriterPlugin({
+ filename: 'manifest.json',
+ transform: function(data, opts) {
+ var stats = opts.compiler.getStats().toJson({
+ chunkModules: false,
+ source: false,
+ chunks: false,
+ modules: false,
+ assets: true
+ });
+ return JSON.stringify(stats, null, 2);
+ }
}),
// prevent pikaday from including moment.js
@@ -182,6 +200,7 @@ var config = {
'pdf_viewer',
'pipelines',
'pipelines_details',
+ 'repo',
'schedule_form',
'schedules_index',
'sidebar',
@@ -205,6 +224,26 @@ var config = {
new webpack.optimize.CommonsChunkPlugin({
names: ['main', 'locale', 'common', 'webpack_runtime'],
}),
+
+ // copy pre-compiled vendor libraries verbatim
+ new CopyWebpackPlugin([
+ {
+ from: path.join(ROOT_PATH, `node_modules/monaco-editor/${IS_PRODUCTION ? 'min' : 'dev'}/vs`),
+ to: 'monaco-editor/vs',
+ transform: function(content, path) {
+ if (/\.js$/.test(path) && !/worker/i.test(path)) {
+ return (
+ '(function(){\n' +
+ 'var define = this.define, require = this.require;\n' +
+ 'window.define = define; window.require = require;\n' +
+ content +
+ '\n}.call(window.__monaco_context__ || (window.__monaco_context__ = {})));'
+ );
+ }
+ return content;
+ }
+ }
+ ]),
],
resolve: {
@@ -253,6 +292,7 @@ if (IS_DEV_SERVER) {
config.devServer = {
host: DEV_SERVER_HOST,
port: DEV_SERVER_PORT,
+ disableHostCheck: true,
headers: { 'Access-Control-Allow-Origin': '*' },
stats: 'errors-only',
hot: DEV_SERVER_LIVERELOAD,
diff --git a/db/migrate/20170731175128_add_percentages_to_conv_dev.rb b/db/migrate/20170731175128_add_percentages_to_conv_dev.rb
new file mode 100644
index 00000000000..1819bfc96bb
--- /dev/null
+++ b/db/migrate/20170731175128_add_percentages_to_conv_dev.rb
@@ -0,0 +1,32 @@
+class AddPercentagesToConvDev < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_column_with_default :conversational_development_index_metrics, :percentage_boards, :float, allow_null: false, default: 0
+ add_column_with_default :conversational_development_index_metrics, :percentage_ci_pipelines, :float, allow_null: false, default: 0
+ add_column_with_default :conversational_development_index_metrics, :percentage_deployments, :float, allow_null: false, default: 0
+ add_column_with_default :conversational_development_index_metrics, :percentage_environments, :float, allow_null: false, default: 0
+ add_column_with_default :conversational_development_index_metrics, :percentage_issues, :float, allow_null: false, default: 0
+ add_column_with_default :conversational_development_index_metrics, :percentage_merge_requests, :float, allow_null: false, default: 0
+ add_column_with_default :conversational_development_index_metrics, :percentage_milestones, :float, allow_null: false, default: 0
+ add_column_with_default :conversational_development_index_metrics, :percentage_notes, :float, allow_null: false, default: 0
+ add_column_with_default :conversational_development_index_metrics, :percentage_projects_prometheus_active, :float, allow_null: false, default: 0
+ add_column_with_default :conversational_development_index_metrics, :percentage_service_desk_issues, :float, allow_null: false, default: 0
+ end
+
+ def down
+ remove_column :conversational_development_index_metrics, :percentage_boards
+ remove_column :conversational_development_index_metrics, :percentage_ci_pipelines
+ remove_column :conversational_development_index_metrics, :percentage_deployments
+ remove_column :conversational_development_index_metrics, :percentage_environments
+ remove_column :conversational_development_index_metrics, :percentage_issues
+ remove_column :conversational_development_index_metrics, :percentage_merge_requests
+ remove_column :conversational_development_index_metrics, :percentage_milestones
+ remove_column :conversational_development_index_metrics, :percentage_notes
+ remove_column :conversational_development_index_metrics, :percentage_projects_prometheus_active
+ remove_column :conversational_development_index_metrics, :percentage_service_desk_issues
+ end
+end
diff --git a/db/migrate/20170731183033_add_merge_jid_to_merge_requests.rb b/db/migrate/20170731183033_add_merge_jid_to_merge_requests.rb
new file mode 100644
index 00000000000..a7d8f2f3604
--- /dev/null
+++ b/db/migrate/20170731183033_add_merge_jid_to_merge_requests.rb
@@ -0,0 +1,7 @@
+class AddMergeJidToMergeRequests < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ add_column :merge_requests, :merge_jid, :string
+ end
+end
diff --git a/db/post_migrate/20170803090603_calculate_conv_dev_index_percentages.rb b/db/post_migrate/20170803090603_calculate_conv_dev_index_percentages.rb
new file mode 100644
index 00000000000..9af76c94bf3
--- /dev/null
+++ b/db/post_migrate/20170803090603_calculate_conv_dev_index_percentages.rb
@@ -0,0 +1,30 @@
+class CalculateConvDevIndexPercentages < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ DOWNTIME = false
+
+ class ConversationalDevelopmentIndexMetric < ActiveRecord::Base
+ self.table_name = 'conversational_development_index_metrics'
+
+ METRICS = %w[boards ci_pipelines deployments environments issues merge_requests milestones notes
+ projects_prometheus_active service_desk_issues]
+ end
+
+ def up
+ ConversationalDevelopmentIndexMetric.find_each do |conv_dev_index|
+ update = []
+
+ ConversationalDevelopmentIndexMetric::METRICS.each do |metric|
+ instance_score = conv_dev_index["instance_#{metric}"].to_f
+ leader_score = conv_dev_index["leader_#{metric}"].to_f
+
+ percentage = leader_score.zero? ? 0.0 : (instance_score / leader_score) * 100
+ update << "percentage_#{metric} = '#{percentage}'"
+ end
+
+ execute("UPDATE conversational_development_index_metrics SET #{update.join(',')} WHERE id = #{conv_dev_index.id}")
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/post_migrate/20170807160457_remove_locked_at_column_from_merge_requests.rb b/db/post_migrate/20170807160457_remove_locked_at_column_from_merge_requests.rb
new file mode 100644
index 00000000000..ea3d1fb3e02
--- /dev/null
+++ b/db/post_migrate/20170807160457_remove_locked_at_column_from_merge_requests.rb
@@ -0,0 +1,11 @@
+class RemoveLockedAtColumnFromMergeRequests < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def up
+ remove_column :merge_requests, :locked_at
+ end
+
+ def down
+ add_column :merge_requests, :locked_at, :datetime_with_timezone
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f2f35acef95..ed3cf70bcdd 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: 20170803130232) do
+ActiveRecord::Schema.define(version: 20170807160457) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -451,6 +451,16 @@ ActiveRecord::Schema.define(version: 20170803130232) do
t.float "instance_service_desk_issues", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.float "percentage_boards", default: 0.0, null: false
+ t.float "percentage_ci_pipelines", default: 0.0, null: false
+ t.float "percentage_deployments", default: 0.0, null: false
+ t.float "percentage_environments", default: 0.0, null: false
+ t.float "percentage_issues", default: 0.0, null: false
+ t.float "percentage_merge_requests", default: 0.0, null: false
+ t.float "percentage_milestones", default: 0.0, null: false
+ t.float "percentage_notes", default: 0.0, null: false
+ t.float "percentage_projects_prometheus_active", default: 0.0, null: false
+ t.float "percentage_service_desk_issues", default: 0.0, null: false
end
create_table "deploy_keys_projects", force: :cascade do |t|
@@ -840,7 +850,6 @@ ActiveRecord::Schema.define(version: 20170803130232) do
t.integer "target_project_id", null: false
t.integer "iid"
t.text "description"
- t.datetime "locked_at"
t.integer "updated_by_id"
t.text "merge_error"
t.text "merge_params"
@@ -858,6 +867,7 @@ ActiveRecord::Schema.define(version: 20170803130232) do
t.integer "last_edited_by_id"
t.integer "head_pipeline_id"
t.boolean "ref_fetched"
+ t.string "merge_jid"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
diff --git a/doc/administration/img/failing_storage.png b/doc/administration/img/failing_storage.png
new file mode 100644
index 00000000000..82b393a58b2
--- /dev/null
+++ b/doc/administration/img/failing_storage.png
Binary files differ
diff --git a/doc/administration/repository_storage_paths.md b/doc/administration/repository_storage_paths.md
index 55a45119525..624a908b3a3 100644
--- a/doc/administration/repository_storage_paths.md
+++ b/doc/administration/repository_storage_paths.md
@@ -60,7 +60,7 @@ respectively.
path: /mnt/cephfs/repositories
```
-1. [Restart GitLab] for the changes to take effect.
+1. [Restart GitLab][restart-gitlab] for the changes to take effect.
>**Note:**
The [`gitlab_shell: repos_path` entry][repospath] in `gitlab.yml` will be
@@ -97,9 +97,80 @@ be stored via the **Application Settings** in the Admin area.
Beginning with GitLab 8.13.4, multiple paths can be chosen. New projects will be
randomly placed on one of the selected paths.
+## Handling failing repository storage
+
+> [Introduced][ce-11449] in GitLab 9.5.
+
+When GitLab detects access to the repositories storage fails repeatedly, it can
+gracefully prevent attempts to access the storage. This might be useful when
+the repositories are stored somewhere on the network.
+
+The configuration could look as follows:
+
+**For Omnibus installations**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ git_data_dirs({
+ "default" => {
+ "path" => "/mnt/nfs-01/git-data",
+ "failure_count_threshold" => 10,
+ "failure_wait_time" => 30,
+ "failure_reset_time" => 1800,
+ "storage_timeout" => 5
+ }
+ })
+ ```
+
+1. Save the file and [reconfigure GitLab][reconfigure-gitlab] for the changes to take effect.
+
+---
+
+**For installations from source**
+
+1. Edit `config/gitlab.yml`:
+
+ ```yaml
+ repositories:
+ storages: # You must have at least a `default` storage path.
+ default:
+ path: /home/git/repositories/
+ failure_count_threshold: 10 # number of failures before stopping attempts
+ failure_wait_time: 30 # Seconds after last access failure before trying again
+ failure_reset_time: 1800 # Time in seconds to expire failures
+ storage_timeout: 5 # Time in seconds to wait before aborting a storage access attempt
+ ```
+
+1. Save the file and [restart GitLab][restart-gitlab] for the changes to take effect.
+
+
+**`failure_count_threshold`:** The number of failures of after which GitLab will
+completely prevent access to the storage. The number of failures can be reset in
+the admin interface: `https://gitlab.example.com/admin/health_check` or using the
+[api](../api/repository_storage_health.md) to allow access to the storage again.
+
+**`failure_wait_time`:** When access to a storage fails. GitLab will prevent
+access to the storage for the time specified here. This allows the filesystem to
+recover without.
+
+**`failure_reset_time`:** The time in seconds GitLab will keep failure
+information. When no failures occur during this time, information about the
+mount is reset.
+
+**`storage_timeout`:** The time in seconds GitLab will try to access storage.
+After this time a timeout error will be raised.
+
+When storage failures occur, this will be visible in the admin interface like this:
+
+![failing storage](img/failing_storage.png)
+
+To allow access to all storages, click the `Reset git storage health information` button.
+
[ce-4578]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4578
-[restart gitlab]: restart_gitlab.md#installations-from-source
-[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart-gitlab]: restart_gitlab.md#installations-from-source
+[reconfigure-gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure
[backups]: ../raketasks/backup_restore.md
[raketask]: https://gitlab.com/gitlab-org/gitlab-ce/blob/033e5423a2594e08a7ebcd2379bd2331f4c39032/lib/backup/repository.rb#L54-56
[repospath]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-9-stable/config/gitlab.yml.example#L457
+[ce-11449]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11449
diff --git a/doc/api/commits.md b/doc/api/commits.md
index c91f9ecbdaf..2a78553782f 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -69,8 +69,9 @@ POST /projects/:id/repository/commits
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
-| `branch` | string | yes | The name of a branch |
+| `branch` | string | yes | Name of the branch to commit into. To create a new branch, also provide `start_branch`. |
| `commit_message` | string | yes | Commit message |
+| `start_branch` | string | no | Name of the branch to start the new commit from |
| `actions[]` | array | yes | An array of action hashes to commit as a batch. See the next table for what attributes it can take. |
| `author_email` | string | no | Specify the commit author's email address |
| `author_name` | string | no | Specify the commit author's name |
diff --git a/doc/api/events.md b/doc/api/events.md
index 6e530317f6c..3d5170f3f1e 100644
--- a/doc/api/events.md
+++ b/doc/api/events.md
@@ -338,6 +338,45 @@ Example response:
"web_url":"https://gitlab.example.com/ted"
},
"author_username":"ted"
+ },
+ {
+ "title": null,
+ "project_id": 1,
+ "action_name": "commented on",
+ "target_id": 1312,
+ "target_iid": 1312,
+ "target_type": "Note",
+ "author_id": 1,
+ "data": null,
+ "target_title": null,
+ "created_at": "2015-12-04T10:33:58.089Z",
+ "note": {
+ "id": 1312,
+ "body": "What an awesome day!",
+ "attachment": null,
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "created_at": "2015-12-04T10:33:56.698Z",
+ "system": false,
+ "noteable_id": 377,
+ "noteable_type": "Issue",
+ "noteable_iid": 377
+ },
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/root"
+ },
+ "author_username": "root"
}
]
```
diff --git a/doc/api/group_milestones.md b/doc/api/group_milestones.md
index 086fba7e91d..dbfc7529125 100644
--- a/doc/api/group_milestones.md
+++ b/doc/api/group_milestones.md
@@ -6,7 +6,7 @@ Returns a list of group milestones.
```
GET /groups/:id/milestones
-GET /groups/:id/milestones?iids=42
+GET /groups/:id/milestones?iids[]=42
GET /groups/:id/milestones?iids[]=42&iids[]=43
GET /groups/:id/milestones?state=active
GET /groups/:id/milestones?state=closed
@@ -18,7 +18,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `iids` | Array[integer] | optional | Return only the milestones having the given `iids` |
+| `iids[]` | Array[integer] | optional | Return only the milestones having the given `iid` |
| `state` | string | optional | Return only `active` or `closed` milestones` |
| `search` | string | optional | Return only milestones with a title or description matching the provided string |
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 6bac2927339..f30ed08d0fa 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -40,7 +40,7 @@ GET /issues?assignee_id=5
| `scope` | string | no | Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all`. Defaults to `created-by-me` _([Introduced][ce-13004] in GitLab 9.5)_ |
| `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned-to-me`. _([Introduced][ce-13004] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Return issues assigned to the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
-| `iids` | Array[integer] | no | Return only the issues having the given `iid` |
+| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Search issues against their `title` and `description` |
@@ -132,7 +132,7 @@ GET /groups/:id/issues?assignee_id=5
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
-| `iids` | Array[integer] | no | Return only the issues having the given `iid` |
+| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` |
| `milestone` | string | no | The milestone title |
| `scope` | string | no | Return issues for the given scope: `created-by-me`, `assigned-to-me` or `all` _([Introduced][ce-13004] in GitLab 9.5)_ |
| `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ |
@@ -227,7 +227,7 @@ GET /projects/:id/issues?assignee_id=5
| Attribute | Type | Required | Description |
|-------------|----------------|----------|-----------------------------------------------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `iids` | Array[integer] | no | Return only the milestone having the given `iid` |
+| `iids[]` | Array[integer] | no | Return only the milestone having the given `iid` |
| `state` | string | no | Return all issues or just those that are `opened` or `closed` |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `No+Label` lists all issues with no labels |
| `milestone` | string | no | The milestone title |
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index d0725b5e06e..802e5362d70 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -117,7 +117,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of a project |
-| `iids` | Array[integer] | no | Return the request having the given `iid` |
+| `iids[]` | Array[integer] | no | Return the request having the given `iid` |
| `state` | string | no | Return all merge requests or just those that are `opened`, `closed`, or `merged`|
| `order_by`| string | no | Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return requests sorted in `asc` or `desc` order. Default is `desc` |
diff --git a/doc/api/milestones.md b/doc/api/milestones.md
index a082d548499..84930f0bdc9 100644
--- a/doc/api/milestones.md
+++ b/doc/api/milestones.md
@@ -6,7 +6,7 @@ Returns a list of project milestones.
```
GET /projects/:id/milestones
-GET /projects/:id/milestones?iids=42
+GET /projects/:id/milestones?iids[]=42
GET /projects/:id/milestones?iids[]=42&iids[]=43
GET /projects/:id/milestones?state=active
GET /projects/:id/milestones?state=closed
@@ -18,7 +18,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `iids` | Array[integer] | optional | Return only the milestones having the given `iids` |
+| `iids[]` | Array[integer] | optional | Return only the milestones having the given `iid` |
| `state` | string | optional | Return only `active` or `closed` milestones` |
| `search` | string | optional | Return only milestones with a title or description matching the provided string |
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 388e6989df2..e627369e17b 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -35,7 +35,8 @@ Parameters:
"updated_at": "2013-10-02T10:22:45Z",
"system": true,
"noteable_id": 377,
- "noteable_type": "Issue"
+ "noteable_type": "Issue",
+ "noteable_iid": 377
},
{
"id": 305,
@@ -53,7 +54,8 @@ Parameters:
"updated_at": "2013-10-02T09:56:03Z",
"system": true,
"noteable_id": 121,
- "noteable_type": "Issue"
+ "noteable_type": "Issue",
+ "noteable_iid": 121
}
]
```
@@ -267,7 +269,8 @@ Parameters:
"updated_at": "2013-10-02T08:57:14Z",
"system": false,
"noteable_id": 2,
- "noteable_type": "MergeRequest"
+ "noteable_type": "MergeRequest",
+ "noteable_iid": 2
}
```
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index 1fc577561a0..c517a38a8ba 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -76,7 +76,8 @@ Example response:
Parameters:
- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
-- `branch` (required) - The name of branch
+- `branch` (required) - Name of the branch
+- `start_branch` (optional) - Name of the branch to start the new commit from
- `encoding` (optional) - Change encoding to 'base64'. Default is text.
- `author_email` (optional) - Specify the commit author's email address
- `author_name` (optional) - Specify the commit author's name
@@ -105,7 +106,8 @@ Example response:
Parameters:
- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
-- `branch` (required) - The name of branch
+- `branch` (required) - Name of the branch
+- `start_branch` (optional) - Name of the branch to start the new commit from
- `encoding` (optional) - Change encoding to 'base64'. Default is text.
- `author_email` (optional) - Specify the commit author's email address
- `author_name` (optional) - Specify the commit author's name
@@ -144,7 +146,8 @@ Example response:
Parameters:
- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
-- `branch` (required) - The name of branch
+- `branch` (required) - Name of the branch
+- `start_branch` (optional) - Name of the branch to start the new commit from
- `author_email` (optional) - Specify the commit author's email address
- `author_name` (optional) - Specify the commit author's name
- `commit_message` (required) - Commit message
diff --git a/doc/api/repository_storage_health.md b/doc/api/repository_storage_health.md
new file mode 100644
index 00000000000..e0c0315c2d7
--- /dev/null
+++ b/doc/api/repository_storage_health.md
@@ -0,0 +1,74 @@
+# Circuitbreaker API
+
+> [Introduced][ce-11449] in GitLab 9.5.
+
+The Circuitbreaker API is only accessible to administrators. All requests by
+guests will respond with `401 Unauthorized`, and all requests by normal users
+will respond with `403 Forbidden`.
+
+## Repository Storages
+
+### Get all storage information
+
+Returns of all currently configured storages and their health information.
+
+```
+GET /circuit_breakers/repository_storage
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/circuit_breakers/repository_storage
+```
+
+```json
+[
+ {
+ "storage_name": "default",
+ "failing_on_hosts": [],
+ "total_failures": 0
+ },
+ {
+ "storage_name": "broken",
+ "failing_on_hosts": [
+ "web01", "worker01"
+ ],
+ "total_failures": 1
+ }
+]
+```
+
+### Get failing storages
+
+This returns a list of all currently failing storages.
+
+```
+GET /circuit_breakers/repository_storage/failing
+```
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/circuit_breakers/repository_storage/failing
+```
+
+```json
+[
+ {
+ "storage_name":"broken",
+ "failing_on_hosts":["web01", "worker01"],
+ "total_failures":2
+ }
+]
+```
+
+## Reset failing storage information
+
+Use this remove all failing storage information and allow access to the storage again.
+
+```
+DELETE /circuit_breakers/repository_storage
+```
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/circuit_breakers/repository_storage
+```
+
+[ce-11449]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11449
diff --git a/doc/api/tags.md b/doc/api/tags.md
index 54f092d1d30..32fe5eea692 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -18,17 +18,20 @@ Parameters:
[
{
"commit": {
+ "id": "2695effb5807a22ff3d138d593fd856244e155e7",
+ "short_id": "2695effb",
+ "title": "Initial commit",
+ "created_at": "2017-07-26T11:08:53.000+02:00",
+ "parent_ids": [
+ "2a4b78934375d7f53875269ffd4f45fd83a84ebe"
+ ],
+ "message": "Initial commit",
"author_name": "John Smith",
"author_email": "john@example.com",
"authored_date": "2012-05-28T04:42:42-07:00",
- "committed_date": "2012-05-28T04:42:42-07:00",
"committer_name": "Jack Smith",
"committer_email": "jack@example.com",
- "id": "2695effb5807a22ff3d138d593fd856244e155e7",
- "message": "Initial commit",
- "parent_ids": [
- "2a4b78934375d7f53875269ffd4f45fd83a84ebe"
- ]
+ "committed_date": "2012-05-28T04:42:42-07:00"
},
"release": {
"tag_name": "1.0.0",
@@ -68,16 +71,19 @@ Example Response:
"message": null,
"commit": {
"id": "60a8ff033665e1207714d6670fcd7b65304ec02f",
- "message": "v5.0.0\n",
+ "short_id": "60a8ff03",
+ "title": "Initial commit",
+ "created_at": "2017-07-26T11:08:53.000+02:00",
"parent_ids": [
"f61c062ff8bcbdb00e0a1b3317a91aed6ceee06b"
],
- "authored_date": "2015-02-01T21:56:31.000+01:00",
+ "message": "v5.0.0\n",
"author_name": "Arthur Verschaeve",
"author_email": "contact@arthurverschaeve.be",
- "committed_date": "2015-02-01T21:56:31.000+01:00",
+ "authored_date": "2015-02-01T21:56:31.000+01:00",
"committer_name": "Arthur Verschaeve",
- "committer_email": "contact@arthurverschaeve.be"
+ "committer_email": "contact@arthurverschaeve.be",
+ "committed_date": "2015-02-01T21:56:31.000+01:00"
},
"release": null
}
@@ -102,17 +108,20 @@ Parameters:
```json
{
"commit": {
+ "id": "2695effb5807a22ff3d138d593fd856244e155e7",
+ "short_id": "2695effb",
+ "title": "Initial commit",
+ "created_at": "2017-07-26T11:08:53.000+02:00",
+ "parent_ids": [
+ "2a4b78934375d7f53875269ffd4f45fd83a84ebe"
+ ],
+ "message": "Initial commit",
"author_name": "John Smith",
"author_email": "john@example.com",
"authored_date": "2012-05-28T04:42:42-07:00",
- "committed_date": "2012-05-28T04:42:42-07:00",
"committer_name": "Jack Smith",
"committer_email": "jack@example.com",
- "id": "2695effb5807a22ff3d138d593fd856244e155e7",
- "message": "Initial commit",
- "parent_ids": [
- "2a4b78934375d7f53875269ffd4f45fd83a84ebe"
- ]
+ "committed_date": "2012-05-28T04:42:42-07:00"
},
"release": {
"tag_name": "1.0.0",
diff --git a/doc/development/background_migrations.md b/doc/development/background_migrations.md
index e67db9ff142..f83a60e49e8 100644
--- a/doc/development/background_migrations.md
+++ b/doc/development/background_migrations.md
@@ -7,6 +7,11 @@ storing data in a single JSON column the data is stored in a separate table.
## When To Use Background Migrations
+>**Note:**
+When adding background migrations _you must_ make sure they are announced in the
+monthly release post along with an estimate of how long it will take to complete
+the migrations.
+
In the vast majority of cases you will want to use a regular Rails migration
instead. Background migrations should _only_ be used when migrating _data_ in
tables that have so many rows this process would take hours when performed in a
@@ -91,6 +96,10 @@ BackgroundMigrationWorker.perform_bulk_in(5.minutes, jobs)
## Cleaning Up
+>**Note:**
+Cleaning up any remaining background migrations _must_ be done in either a major
+or minor release, you _must not_ do this in a patch release.
+
Because background migrations can take a long time you can't immediately clean
things up after scheduling them. For example, you can't drop a column that's
used in the migration process as this would cause jobs to fail. This means that
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 161d2544169..9b8ab5da74e 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -35,12 +35,11 @@ Please don't depend on GitLab-specific code since it can change in future
versions. If needed copy-paste GitLab code into the migration to make it forward
compatible.
-## Commit Guidelines
+## Schema Changes
-Each migration **must** be added in its own commit with a descriptive commit
-message. If a commit adds a migration it _should only_ include the migration and
-any corresponding changes to `db/schema.rb`. This makes it easy to revert a
-database migration without accidentally reverting other changes.
+Migrations that make changes to the database schema (e.g. adding a column) can
+only be added in the monthly release, patch releases may only contain data
+migrations _unless_ schema changes are absolutely required to solve a problem.
## Downtime Tagging
@@ -224,9 +223,9 @@ add_column(:projects, :foo, :integer, default: 10, limit: 8)
## Timestamp column type
-By default, Rails uses the `timestamp` data type that stores timestamp data without timezone information.
-The `timestamp` data type is used by calling either the `add_timestamps` or the `timestamps` method.
-Also Rails converts the `:datetime` data type to the `timestamp` one.
+By default, Rails uses the `timestamp` data type that stores timestamp data without timezone information.
+The `timestamp` data type is used by calling either the `add_timestamps` or the `timestamps` method.
+Also Rails converts the `:datetime` data type to the `timestamp` one.
Example:
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index 42bb5e8619c..bfd80aab6a4 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -146,3 +146,20 @@ If new emoji are added, the spritesheet may change size. To compensate for
such changes, first generate the `emoji.png` spritesheet with the above Rake
task, then check the dimensions of the new spritesheet and update the
`SPRITESHEET_WIDTH` and `SPRITESHEET_HEIGHT` constants accordingly.
+
+## Updating project templates
+
+Starting a project from a template needs this project to be exported. On a
+up to date master branch with run:
+
+```
+gdk run db
+# In a new terminal window
+bundle exec rake gitlab:update_project_templates
+git checkout -b update-project-templates
+git add vendor/project_templates
+git commit
+git push -u origin update-project-templates
+```
+
+Now create a merge request and merge that to master.
diff --git a/doc/update/9.4-to-9.5.md b/doc/update/9.4-to-9.5.md
new file mode 100644
index 00000000000..fc87b2d0f1e
--- /dev/null
+++ b/doc/update/9.4-to-9.5.md
@@ -0,0 +1,352 @@
+# From 9.4 to 9.5
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be
+sure to upgrade your interpreter if necessary.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz
+echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz
+cd ruby-2.3.3
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Update Node
+
+GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and
+it has a minimum requirement of node v4.3.0.
+
+You can check which version you are running with `node -v`. If you are running
+a version older than `v4.3.0` you will need to update to a newer version. You
+can find instructions to install from community maintained packages or compile
+from source at the nodejs.org website.
+
+<https://nodejs.org/en/download/>
+
+
+Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage
+JavaScript dependencies.
+
+```bash
+curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
+echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+sudo apt-get update
+sudo apt-get install yarn
+```
+
+More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install).
+
+### 5. Update Go
+
+NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go
+1.5.x through 1.7.x. Be sure to upgrade your installation if necessary.
+
+You can check which version you are running with `go version`.
+
+Download and install Go:
+
+```bash
+# Remove former Go installation folder
+sudo rm -rf /usr/local/go
+
+curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz
+echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
+ sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz
+sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
+rm go1.8.3.linux-amd64.tar.gz
+```
+
+### 6. Get latest code
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+sudo -u git -H git checkout -- locale
+```
+
+For GitLab Community Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 9-5-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H git checkout 9-5-stable-ee
+```
+
+### 7. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
+sudo -u git -H bin/compile
+```
+
+### 8. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. GitLab-Workhorse uses
+[GNU Make](https://www.gnu.org/software/make/).
+If you are not using Linux you may have to run `gmake` instead of
+`make` below.
+
+```bash
+cd /home/git/gitlab-workhorse
+
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
+sudo -u git -H make
+```
+
+### 9. Update Gitaly
+
+#### New Gitaly configuration options required
+
+In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell'.
+
+```shell
+echo '
+[gitaly-ruby]
+dir = "/home/git/gitaly/ruby"
+
+[gitlab-shell]
+dir = "/home/git/gitlab-shell"
+' | sudo -u git tee -a /home/git/gitaly/config.toml
+```
+
+#### Check Gitaly configuration
+
+Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly
+configuration file may contain syntax errors. The block name
+`[[storages]]`, which may occur more than once in your `config.toml`
+file, should be `[[storage]]` instead.
+
+```shell
+sudo -u git -H sed -i.pre-9.5 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml
+```
+
+#### Compile Gitaly
+
+```shell
+cd /home/git/gitaly
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
+sudo -u git -H make
+```
+
+### 10. Update MySQL permissions
+
+If you are using MySQL you need to grant the GitLab user the necessary
+permissions on the database:
+
+```bash
+mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';"
+```
+
+If you use MySQL with replication, or just have MySQL configured with binary logging,
+you will need to also run the following on all of your MySQL servers:
+
+```bash
+mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;"
+```
+
+You can make this setting permanent by adding it to your `my.cnf`:
+
+```
+log_bin_trust_function_creators=1
+```
+
+### 11. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/9-4-stable:config/gitlab.yml.example origin/9-5-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+cd /home/git/gitlab
+
+# For HTTPS configurations
+git diff origin/9-4-stable:lib/support/nginx/gitlab-ssl origin/9-5-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/9-4-stable:lib/support/nginx/gitlab origin/9-5-stable:lib/support/nginx/gitlab
+```
+
+If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx
+configuration as GitLab application no longer handles setting it.
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-5-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-4-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`:
+
+```sh
+cd /home/git/gitlab
+
+git diff origin/9-4-stable:lib/support/init.d/gitlab.default.example origin/9-5-stable:lib/support/init.d/gitlab.default.example
+```
+
+Ensure you're still up-to-date with the latest init script changes:
+
+```bash
+cd /home/git/gitlab
+
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+For Ubuntu 16.04.1 LTS:
+
+```bash
+sudo systemctl daemon-reload
+```
+
+### 12. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Update node dependencies and recompile assets
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+
+# Clean up cache
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
+```
+
+**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
+
+### 13. Start application
+
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
+
+### 14. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+To make sure you didn't miss anything run a more thorough check:
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (9.4)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 9.3 to 9.4](9.3-to-9.4.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-5-stable/config/gitlab.yml.example
+[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-5-stable/lib/support/init.d/gitlab.default.example
diff --git a/doc/update/README.md b/doc/update/README.md
index 22dbc7c750f..c98e20686e0 100644
--- a/doc/update/README.md
+++ b/doc/update/README.md
@@ -34,17 +34,67 @@ update them are in [a separate document][omnidocker].
## Upgrading without downtime
-Starting with GitLab 9.1.0 it's possible to upgrade to a newer major, minor, or patch version of GitLab
-without having to take your GitLab instance offline. However, for this to work
-there are the following requirements:
-
-1. You can only upgrade 1 minor release at a time. So from 9.1 to 9.2, not to 9.3.
-2. You have to be on the most recent patch release. For example, if 9.1.15 is the last
- release of 9.1 then you can safely upgrade from that version to any 9.2.x version.
- However, if you are running 9.1.14 you first need to upgrade to 9.1.15.
+Starting with GitLab 9.1.0 it's possible to upgrade to a newer major, minor, or
+patch version of GitLab without having to take your GitLab instance offline.
+However, for this to work there are the following requirements:
+
+1. You can only upgrade 1 minor release at a time. So from 9.1 to 9.2, not to
+ 9.3.
2. You have to use [post-deployment
migrations](../development/post_deployment_migrations.md).
-3. You are using PostgreSQL. If you are using MySQL please look at the release post to see if downtime is required.
+3. You are using PostgreSQL. If you are using MySQL please look at the release
+ post to see if downtime is required.
+
+Most of the time you can safely upgrade from a patch release to the next minor
+release if the patch release is not the latest. For example, upgrading from
+9.1.1 to 9.2.0 should be safe even if 9.1.2 has been released. We do recommend
+you check the release posts of any releases between your current and target
+version just in case they include any migrations that may require you to upgrade
+1 release at a time.
+
+Some releases may also include so called "background migrations". These
+migrations are performed in the background by Sidekiq and are often used for
+migrating data. Background migrations are only added in the monthly releases.
+
+Certain major/minor releases may require a set of background migrations to be
+finished. To guarantee this such a release will process any remaining jobs
+before continuing the upgrading procedure. While this won't require downtime
+(if the above conditions are met) we recommend users to keep at least 1 week
+between upgrading major/minor releases, allowing the background migrations to
+finish. The time necessary to complete these migrations can be reduced by
+increasing the number of Sidekiq workers that can process jobs in the
+`background_migration` queue.
+
+As a rule of thumb, any database smaller than 10 GB won't take too much time to
+upgrade; perhaps an hour at most per minor release. Larger databases however may
+require more time, but this is highly dependent on the size of the database and
+the migrations that are being performed.
+
+### Examples
+
+To help explain this, let's look at some examples.
+
+**Example 1:** You are running a large GitLab installation using version 9.4.2,
+which is the latest patch release of 9.4. When GitLab 9.5.0 is released this
+installation can be safely upgraded to 9.5.0 without requiring downtime if the
+requirements mentioned above are met. You can also skip 9.5.0 and upgrade to
+9.5.1 once it's released, but you **can not** upgrade straight to 9.6.0; you
+_have_ to first upgrade to a 9.5.x release.
+
+**Example 2:** You are running a large GitLab installation using version 9.4.2,
+which is the latest patch release of 9.4. GitLab 9.5 includes some background
+migrations, and 10.0 will require these to be completed (processing any
+remaining jobs for you). Skipping 9.5 is not possible without downtime, and due
+to the background migrations would require potentially hours of downtime
+depending on how long it takes for the background migrations to complete. To
+work around this you will have to upgrade to 9.5.x first, then wait at least a
+week before upgrading to 10.0.
+
+**Example 3:** You use MySQL as the database for GitLab. Any upgrade to a new
+major/minor release will require downtime. If a release includes any background
+migrations this could potentially lead to hours of downtime, depending on the
+size of your database. To work around this you will have to use PostgreSQL and
+meet the other online upgrade requirements mentioned above.
## Upgrading between editions
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 0d29b471d52..b42b8f0a525 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -248,7 +248,7 @@ GFM will recognize the following:
| `~123` | label by ID |
| `~bug` | one-word label by name |
| `~"feature request"` | multi-word label by name |
-| `%123` | milestone by ID |
+| `%123` | project milestone by ID |
| `%v1.23` | one-word milestone by name |
| `%"release candidate"` | multi-word milestone by name |
| `9ba12248` | specific commit |
@@ -262,7 +262,7 @@ GFM also recognizes certain cross-project references:
|:----------------------------------------|:------------------------|
| `namespace/project#123` | issue |
| `namespace/project!123` | merge request |
-| `namespace/project%123` | milestone |
+| `namespace/project%123` | project milestone |
| `namespace/project$123` | snippet |
| `namespace/project@9ba12248` | specific commit |
| `namespace/project@9ba12248...b19a04f5` | commit range comparison |
@@ -274,7 +274,7 @@ It also has a shorthand version to reference other projects from the same namesp
|:------------------------------|:------------------------|
| `project#123` | issue |
| `project!123` | merge request |
-| `project%123` | milestone |
+| `project%123` | project milestone |
| `project$123` | snippet |
| `project@9ba12248` | specific commit |
| `project@9ba12248...b19a04f5` | commit range comparison |
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index c03a2df9a72..47eb0b34f66 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -438,7 +438,6 @@ X-Gitlab-Event: Note Hook
"iid": 1,
"description": "Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.",
"position": 0,
- "locked_at": null,
"source":{
"name":"Gitlab Test",
"description":"Aut reprehenderit ut est.",
diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md
index 23ffde4e8bd..876b98a4dc5 100644
--- a/doc/user/project/milestones/index.md
+++ b/doc/user/project/milestones/index.md
@@ -56,4 +56,5 @@ total merge requests and issues.
## Quick actions
-[Quick actions](../quick_actions.md) are available for assigning and removing project milestones only. [In the future](https://gitlab.com/gitlab-org/gitlab-ce/issues/34778), this will also apply to group milestones.
+[Quick actions](../quick_actions.md) are available for assigning and removing
+project and group milestones.
diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb
index 2b8da2a6f19..855757e34b3 100644
--- a/features/steps/project/wiki.rb
+++ b/features/steps/project/wiki.rb
@@ -63,7 +63,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
end
step 'That page has two revisions' do
- @page.update("new content", message: "second commit")
+ @page.update(content: "new content", message: "second commit")
end
step 'I click the History button' do
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 982a2b88d62..94df543853b 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -95,6 +95,7 @@ module API
mount ::API::Boards
mount ::API::Branches
mount ::API::BroadcastMessages
+ mount ::API::CircuitBreakers
mount ::API::Commits
mount ::API::CommitStatuses
mount ::API::DeployKeys
diff --git a/lib/api/circuit_breakers.rb b/lib/api/circuit_breakers.rb
new file mode 100644
index 00000000000..118883f5ea5
--- /dev/null
+++ b/lib/api/circuit_breakers.rb
@@ -0,0 +1,50 @@
+module API
+ class CircuitBreakers < Grape::API
+ before { authenticated_as_admin! }
+
+ resource :circuit_breakers do
+ params do
+ requires :type,
+ type: String,
+ desc: "The type of circuitbreaker",
+ values: ['repository_storage']
+ end
+ resource ':type' do
+ namespace '', requirements: { type: 'repository_storage' } do
+ helpers do
+ def failing_storage_health
+ @failing_storage_health ||= Gitlab::Git::Storage::Health.for_failing_storages
+ end
+
+ def storage_health
+ @failing_storage_health ||= Gitlab::Git::Storage::Health.for_all_storages
+ end
+ end
+
+ desc 'Get all failing git storages' do
+ detail 'This feature was introduced in GitLab 9.5'
+ success Entities::RepositoryStorageHealth
+ end
+ get do
+ present storage_health, with: Entities::RepositoryStorageHealth
+ end
+
+ desc 'Get all failing git storages' do
+ detail 'This feature was introduced in GitLab 9.5'
+ success Entities::RepositoryStorageHealth
+ end
+ get 'failing' do
+ present failing_storage_health, with: Entities::RepositoryStorageHealth
+ end
+
+ desc 'Reset all storage failures and open circuitbreaker' do
+ detail 'This feature was introduced in GitLab 9.5'
+ end
+ delete do
+ Gitlab::Git::Storage::CircuitBreaker.reset_all!
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index bcb842b9211..ea78737288a 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -4,13 +4,14 @@ module API
class Commits < Grape::API
include PaginationParams
- before { authenticate! }
+ COMMIT_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(sha: API::NO_SLASH_URL_PART_REGEX)
+
before { authorize! :download_code, user_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a project repository commits' do
success Entities::RepoCommit
end
@@ -21,7 +22,7 @@ module API
optional :path, type: String, desc: 'The file path'
use :pagination
end
- get ":id/repository/commits" do
+ get ':id/repository/commits' do
path = params[:path]
before = params[:until]
after = params[:since]
@@ -53,16 +54,19 @@ module API
detail 'This feature was introduced in GitLab 8.13'
end
params do
- requires :branch, type: String, desc: 'The name of branch'
+ requires :branch, type: String, desc: 'Name of the branch to commit into. To create a new branch, also provide `start_branch`.'
requires :commit_message, type: String, desc: 'Commit message'
requires :actions, type: Array[Hash], desc: 'Actions to perform in commit'
+ optional :start_branch, type: String, desc: 'Name of the branch to start the new commit from'
optional :author_email, type: String, desc: 'Author email for commit'
optional :author_name, type: String, desc: 'Author name for commit'
end
- post ":id/repository/commits" do
+ post ':id/repository/commits' do
authorize! :push_code, user_project
- attrs = declared_params.merge(start_branch: declared_params[:branch], branch_name: declared_params[:branch])
+ attrs = declared_params
+ attrs[:branch_name] = attrs.delete(:branch)
+ attrs[:start_branch] ||= attrs[:branch_name]
result = ::Files::MultiService.new(user_project, current_user, attrs).execute
@@ -76,42 +80,42 @@ module API
desc 'Get a specific commit of a project' do
success Entities::RepoCommitDetail
- failure [[404, 'Not Found']]
+ failure [[404, 'Commit Not Found']]
end
params do
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
end
- get ":id/repository/commits/:sha" do
+ get ':id/repository/commits/:sha', requirements: COMMIT_ENDPOINT_REQUIREMENTS do
commit = user_project.commit(params[:sha])
- not_found! "Commit" unless commit
+ not_found! 'Commit' unless commit
present commit, with: Entities::RepoCommitDetail
end
desc 'Get the diff for a specific commit of a project' do
- failure [[404, 'Not Found']]
+ failure [[404, 'Commit Not Found']]
end
params do
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
end
- get ":id/repository/commits/:sha/diff" do
+ get ':id/repository/commits/:sha/diff', requirements: COMMIT_ENDPOINT_REQUIREMENTS do
commit = user_project.commit(params[:sha])
- not_found! "Commit" unless commit
+ not_found! 'Commit' unless commit
commit.raw_diffs.to_a
end
desc "Get a commit's comments" do
success Entities::CommitNote
- failure [[404, 'Not Found']]
+ failure [[404, 'Commit Not Found']]
end
params do
use :pagination
requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag'
end
- get ':id/repository/commits/:sha/comments' do
+ get ':id/repository/commits/:sha/comments', requirements: COMMIT_ENDPOINT_REQUIREMENTS do
commit = user_project.commit(params[:sha])
not_found! 'Commit' unless commit
@@ -125,10 +129,10 @@ module API
success Entities::RepoCommit
end
params do
- requires :sha, type: String, desc: 'A commit sha to be cherry picked'
+ requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag to be cherry picked'
requires :branch, type: String, desc: 'The name of the branch'
end
- post ':id/repository/commits/:sha/cherry_pick' do
+ post ':id/repository/commits/:sha/cherry_pick', requirements: COMMIT_ENDPOINT_REQUIREMENTS do
authorize! :push_code, user_project
commit = user_project.commit(params[:sha])
@@ -157,7 +161,7 @@ module API
success Entities::CommitNote
end
params do
- requires :sha, type: String, regexp: /\A\h{6,40}\z/, desc: "The commit's SHA"
+ requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag on which to post a comment'
requires :note, type: String, desc: 'The text of the comment'
optional :path, type: String, desc: 'The file path'
given :path do
@@ -165,7 +169,7 @@ module API
requires :line_type, type: String, values: %w(new old), default: 'new', desc: 'The type of the line'
end
end
- post ':id/repository/commits/:sha/comments' do
+ post ':id/repository/commits/:sha/comments', requirements: COMMIT_ENDPOINT_REQUIREMENTS do
commit = user_project.commit(params[:sha])
not_found! 'Commit' unless commit
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 29733481e2f..225879d94ac 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -66,13 +66,6 @@ module API
expose :job_events
end
- class BasicProjectDetails < Grape::Entity
- expose :id
- expose :http_url_to_repo, :web_url
- expose :name, :name_with_namespace
- expose :path, :path_with_namespace
- end
-
class SharedGroup < Grape::Entity
expose :group_id
expose :group_name do |group_link, options|
@@ -81,7 +74,16 @@ module API
expose :group_access, as: :group_access_level
end
- class Project < Grape::Entity
+ class BasicProjectDetails < Grape::Entity
+ expose :id, :description, :default_branch, :tag_list
+ expose :ssh_url_to_repo, :http_url_to_repo, :web_url
+ expose :name, :name_with_namespace
+ expose :path, :path_with_namespace
+ expose :star_count, :forks_count
+ expose :created_at, :last_activity_at
+ end
+
+ class Project < BasicProjectDetails
include ::API::Helpers::RelatedResourcesHelpers
expose :_links do
@@ -114,12 +116,9 @@ module API
end
end
- expose :id, :description, :default_branch, :tag_list
expose :archived?, as: :archived
- expose :visibility, :ssh_url_to_repo, :http_url_to_repo, :web_url
+ expose :visibility
expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group }
- expose :name, :name_with_namespace
- expose :path, :path_with_namespace
expose :container_registry_enabled
# Expose old field names with the new permissions methods to keep API compatible
@@ -129,7 +128,6 @@ module API
expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) }
- expose :created_at, :last_activity_at
expose :shared_runners_enabled
expose :lfs_enabled?, as: :lfs_enabled
expose :creator_id
@@ -140,7 +138,6 @@ module API
expose :avatar_url do |user, options|
user.avatar_url(only_path: false)
end
- expose :star_count, :forks_count
expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) }
expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
expose :public_builds, as: :public_jobs
@@ -457,6 +454,9 @@ module API
end
class Note < Grape::Entity
+ # Only Issue and MergeRequest have iid
+ NOTEABLE_TYPES_WITH_IID = %w(Issue MergeRequest).freeze
+
expose :id
expose :note, as: :body
expose :attachment_identifier, as: :attachment
@@ -464,6 +464,9 @@ module API
expose :created_at, :updated_at
expose :system?, as: :system
expose :noteable_id, :noteable_type
+
+ # Avoid N+1 queries as much as possible
+ expose(:noteable_iid) { |note| note.noteable.iid if NOTEABLE_TYPES_WITH_IID.include?(note.noteable_type) }
end
class AwardEmoji < Grape::Entity
@@ -702,7 +705,7 @@ module API
class RepoTag < Grape::Entity
expose :name, :message
- expose :commit do |repo_tag, options|
+ expose :commit, using: Entities::RepoCommit do |repo_tag, options|
options[:project].repository.commit(repo_tag.dereferenced_target)
end
@@ -954,5 +957,11 @@ module API
expose :ip_address
expose :submitted, as: :akismet_submitted
end
+
+ class RepositoryStorageHealth < Grape::Entity
+ expose :storage_name
+ expose :failing_on_hosts
+ expose :total_failures
+ end
end
end
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 521287ee2b4..450334fee84 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -4,7 +4,7 @@ module API
def commit_params(attrs)
{
file_path: attrs[:file_path],
- start_branch: attrs[:branch],
+ start_branch: attrs[:start_branch] || attrs[:branch],
branch_name: attrs[:branch],
commit_message: attrs[:commit_message],
file_content: attrs[:content],
@@ -37,8 +37,9 @@ module API
params :simple_file_params do
requires :file_path, type: String, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb'
- requires :branch, type: String, desc: 'The name of branch'
- requires :commit_message, type: String, desc: 'Commit Message'
+ requires :branch, type: String, desc: 'Name of the branch to commit into. To create a new branch, also provide `start_branch`.'
+ requires :commit_message, type: String, desc: 'Commit message'
+ optional :start_branch, type: String, desc: 'Name of the branch to start the new commit from'
optional :author_email, type: String, desc: 'The email of the author'
optional :author_name, type: String, desc: 'The name of the author'
end
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 633a858f8c7..1333747cced 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -2,19 +2,21 @@ module API
class Tags < Grape::API
include PaginationParams
+ TAG_ENDPOINT_REQUIREMENTS = API::PROJECT_ENDPOINT_REQUIREMENTS.merge(tag_name: API::NO_SLASH_URL_PART_REGEX)
+
before { authorize! :download_code, user_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
- resource :projects, requirements: { id: %r{[^/]+} } do
+ resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get a project repository tags' do
success Entities::RepoTag
end
params do
use :pagination
end
- get ":id/repository/tags" do
+ get ':id/repository/tags' do
tags = ::Kaminari.paginate_array(user_project.repository.tags.sort_by(&:name).reverse)
present paginate(tags), with: Entities::RepoTag, project: user_project
end
@@ -25,7 +27,7 @@ module API
params do
requires :tag_name, type: String, desc: 'The name of the tag'
end
- get ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do
+ get ':id/repository/tags/:tag_name', requirements: TAG_ENDPOINT_REQUIREMENTS do
tag = user_project.repository.find_tag(params[:tag_name])
not_found!('Tag') unless tag
@@ -60,7 +62,7 @@ module API
params do
requires :tag_name, type: String, desc: 'The name of the tag'
end
- delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do
+ delete ':id/repository/tags/:tag_name', requirements: TAG_ENDPOINT_REQUIREMENTS do
authorize_push_project
result = ::Tags::DestroyService.new(user_project, current_user)
@@ -78,7 +80,7 @@ module API
requires :tag_name, type: String, desc: 'The name of the tag'
requires :description, type: String, desc: 'Release notes with markdown support'
end
- post ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.+/ } do
+ post ':id/repository/tags/:tag_name/release', requirements: TAG_ENDPOINT_REQUIREMENTS do
authorize_push_project
result = CreateReleaseService.new(user_project, current_user)
@@ -98,7 +100,7 @@ module API
requires :tag_name, type: String, desc: 'The name of the tag'
requires :description, type: String, desc: 'Release notes with markdown support'
end
- put ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.+/ } do
+ put ':id/repository/tags/:tag_name/release', requirements: TAG_ENDPOINT_REQUIREMENTS do
authorize_push_project
result = UpdateReleaseService.new(user_project, current_user)
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index 685b43605ae..ef4578aabd6 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -54,42 +54,42 @@ module Banzai
self.class.references_in(*args, &block)
end
+ # Implement in child class
+ # Example: project.merge_requests.find
def find_object(project, id)
- # Implement in child class
- # Example: project.merge_requests.find
end
- def find_object_cached(project, id)
- if RequestStore.active?
- cache = find_objects_cache[object_class][project.id]
+ # Override if the link reference pattern produces a different ID (global
+ # ID vs internal ID, for instance) to the regular reference pattern.
+ def find_object_from_link(project, id)
+ find_object(project, id)
+ end
- get_or_set_cache(cache, id) { find_object(project, id) }
- else
+ # Implement in child class
+ # Example: project_merge_request_url
+ def url_for_object(object, project)
+ end
+
+ def find_object_cached(project, id)
+ cached_call(:banzai_find_object, id, path: [object_class, project.id]) do
find_object(project, id)
end
end
- def project_from_ref_cached(ref)
- if RequestStore.active?
- cache = project_refs_cache
-
- get_or_set_cache(cache, ref) { project_from_ref(ref) }
- else
- project_from_ref(ref)
+ def find_object_from_link_cached(project, id)
+ cached_call(:banzai_find_object_from_link, id, path: [object_class, project.id]) do
+ find_object_from_link(project, id)
end
end
- def url_for_object(object, project)
- # Implement in child class
- # Example: project_merge_request_url
+ def project_from_ref_cached(ref)
+ cached_call(:banzai_project_refs, ref) do
+ project_from_ref(ref)
+ end
end
def url_for_object_cached(object, project)
- if RequestStore.active?
- cache = url_for_object_cache[object_class][project.id]
-
- get_or_set_cache(cache, object) { url_for_object(object, project) }
- else
+ cached_call(:banzai_url_for_object, object, path: [object_class, project.id]) do
url_for_object(object, project)
end
end
@@ -120,7 +120,7 @@ module Banzai
if link == inner_html && inner_html =~ /\A#{link_pattern}/
replace_link_node_with_text(node, link) do
- object_link_filter(inner_html, link_pattern)
+ object_link_filter(inner_html, link_pattern, link_reference: true)
end
next
@@ -128,7 +128,7 @@ module Banzai
if link =~ /\A#{link_pattern}\z/
replace_link_node_with_href(node, link) do
- object_link_filter(link, link_pattern, link_content: inner_html)
+ object_link_filter(link, link_pattern, link_content: inner_html, link_reference: true)
end
next
@@ -146,15 +146,26 @@ module Banzai
# text - String text to replace references in.
# pattern - Reference pattern to match against.
# link_content - Original content of the link being replaced.
+ # link_reference - True if this was using the link reference pattern,
+ # false otherwise.
#
# Returns a String with references replaced with links. All links
# have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
- def object_link_filter(text, pattern, link_content: nil)
+ def object_link_filter(text, pattern, link_content: nil, link_reference: false)
references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches|
project_path = full_project_path(namespace_ref, project_ref)
project = project_from_ref_cached(project_path)
- if project && object = find_object_cached(project, id)
+ if project
+ object =
+ if link_reference
+ find_object_from_link_cached(project, id)
+ else
+ find_object_cached(project, id)
+ end
+ end
+
+ if object
title = object_link_title(object)
klass = reference_class(object_sym)
@@ -297,15 +308,17 @@ module Banzai
RequestStore[:banzai_project_refs] ||= {}
end
- def find_objects_cache
- RequestStore[:banzai_find_objects_cache] ||= Hash.new do |hash, key|
- hash[key] = Hash.new { |h, k| h[k] = {} }
- end
- end
+ def cached_call(request_store_key, cache_key, path: [])
+ if RequestStore.active?
+ cache = RequestStore[request_store_key] ||= Hash.new do |hash, key|
+ hash[key] = Hash.new { |h, k| h[k] = {} }
+ end
- def url_for_object_cache
- RequestStore[:banzai_url_for_object] ||= Hash.new do |hash, key|
- hash[key] = Hash.new { |h, k| h[k] = {} }
+ cache = cache.dig(*path) if path.any?
+
+ get_or_set_cache(cache, cache_key) { yield }
+ else
+ yield
end
end
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
index 45c033d32a8..4fc5f211e84 100644
--- a/lib/banzai/filter/milestone_reference_filter.rb
+++ b/lib/banzai/filter/milestone_reference_filter.rb
@@ -8,8 +8,15 @@ module Banzai
Milestone
end
+ # Links to project milestones contain the IID, but when we're handling
+ # 'regular' references, we need to use the global ID to disambiguate
+ # between group and project milestones.
def find_object(project, id)
- project.milestones.find_by(iid: id)
+ find_milestone_with_finder(project, id: id)
+ end
+
+ def find_object_from_link(project, iid)
+ find_milestone_with_finder(project, iid: iid)
end
def references_in(text, pattern = Milestone.reference_pattern)
@@ -22,7 +29,7 @@ module Banzai
milestone = find_milestone($~[:project], $~[:namespace], $~[:milestone_iid], $~[:milestone_name])
if milestone
- yield match, milestone.iid, $~[:project], $~[:namespace], $~
+ yield match, milestone.id, $~[:project], $~[:namespace], $~
else
match
end
@@ -36,7 +43,8 @@ module Banzai
return unless project
milestone_params = milestone_params(milestone_id, milestone_name)
- project.milestones.find_by(milestone_params)
+
+ find_milestone_with_finder(project, milestone_params)
end
def milestone_params(iid, name)
@@ -47,15 +55,27 @@ module Banzai
end
end
+ def find_milestone_with_finder(project, params)
+ finder_params = { project_ids: [project.id], order: nil }
+
+ # We don't support IID lookups for group milestones, because IIDs can
+ # clash between group and project milestones.
+ if project.group && !params[:iid]
+ finder_params[:group_ids] = [project.group.id]
+ end
+
+ MilestonesFinder.new(finder_params).execute.find_by(params)
+ end
+
def url_for_object(milestone, project)
- h = Gitlab::Routing.url_helpers
- h.project_milestone_url(project, milestone,
- only_path: context[:only_path])
+ Gitlab::Routing
+ .url_helpers
+ .milestone_url(milestone, only_path: context[:only_path])
end
def object_link_text(object, matches)
milestone_link = escape_once(super)
- reference = object.project.to_reference(project)
+ reference = object.project&.to_reference(project)
if reference.present?
"#{milestone_link} <i>in #{reference}</i>".html_safe
diff --git a/lib/declarative_policy/runner.rb b/lib/declarative_policy/runner.rb
index b5c615da4e3..56afd1f1392 100644
--- a/lib/declarative_policy/runner.rb
+++ b/lib/declarative_policy/runner.rb
@@ -76,6 +76,8 @@ module DeclarativePolicy
@state = State.new
steps_by_score do |step, score|
+ return if !debug && @state.prevented?
+
passed = nil
case step.action
when :enable then
@@ -93,10 +95,7 @@ module DeclarativePolicy
# been prevented.
unless @state.prevented?
passed = step.pass?
- if passed
- @state.prevent!
- return unless debug
- end
+ @state.prevent! if passed
end
debug << inspect_step(step, score, passed) if debug
@@ -141,13 +140,14 @@ module DeclarativePolicy
end
steps = Set.new(@steps)
+ remaining_enablers = steps.count { |s| s.enable? }
loop do
return if steps.empty?
# if the permission hasn't yet been enabled and we only have
# prevent steps left, we short-circuit the state here
- @state.prevent! if !@state.enabled? && steps.all?(&:prevent?)
+ @state.prevent! if !@state.enabled? && remaining_enablers == 0
lowest_score = Float::INFINITY
next_step = nil
@@ -162,6 +162,8 @@ module DeclarativePolicy
steps.delete(next_step)
+ remaining_enablers -= 1 if next_step.enable?
+
yield next_step, lowest_score
end
end
diff --git a/lib/github/client.rb b/lib/github/client.rb
index e65d908d232..9c476df7d46 100644
--- a/lib/github/client.rb
+++ b/lib/github/client.rb
@@ -1,13 +1,16 @@
module Github
class Client
+ TIMEOUT = 60
+
attr_reader :connection, :rate_limit
def initialize(options)
- @connection = Faraday.new(url: options.fetch(:url)) do |faraday|
- faraday.options.open_timeout = options.fetch(:timeout, 60)
- faraday.options.timeout = options.fetch(:timeout, 60)
+ @connection = Faraday.new(url: options.fetch(:url, root_endpoint)) do |faraday|
+ faraday.options.open_timeout = options.fetch(:timeout, TIMEOUT)
+ faraday.options.timeout = options.fetch(:timeout, TIMEOUT)
faraday.authorization 'token', options.fetch(:token)
faraday.adapter :net_http
+ faraday.ssl.verify = verify_ssl
end
@rate_limit = RateLimit.new(connection)
@@ -19,5 +22,32 @@ module Github
Github::Response.new(connection.get(url, query))
end
+
+ private
+
+ def root_endpoint
+ custom_endpoint || github_endpoint
+ end
+
+ def custom_endpoint
+ github_omniauth_provider.dig('args', 'client_options', 'site')
+ end
+
+ def verify_ssl
+ # If there is no config, we're connecting to github.com
+ # and we should verify ssl.
+ github_omniauth_provider.fetch('verify_ssl', true)
+ end
+
+ def github_endpoint
+ OmniAuth::Strategies::GitHub.default_options[:client_options][:site]
+ end
+
+ def github_omniauth_provider
+ @github_omniauth_provider ||=
+ Gitlab.config.omniauth.providers
+ .find { |provider| provider.name == 'github' }
+ .to_h
+ end
end
end
diff --git a/lib/github/import.rb b/lib/github/import.rb
index cea4be5460b..4cc01593ef4 100644
--- a/lib/github/import.rb
+++ b/lib/github/import.rb
@@ -41,13 +41,16 @@ module Github
self.reset_callbacks :validate
end
- attr_reader :project, :repository, :repo, :options, :errors, :cached, :verbose
+ attr_reader :project, :repository, :repo, :repo_url, :wiki_url,
+ :options, :errors, :cached, :verbose
- def initialize(project, options)
+ def initialize(project, options = {})
@project = project
@repository = project.repository
@repo = project.import_source
- @options = options
+ @repo_url = project.import_url
+ @wiki_url = project.import_url.sub(/\.git\z/, '.wiki.git')
+ @options = options.reverse_merge(token: project.import_data&.credentials&.fetch(:user))
@verbose = options.fetch(:verbose, false)
@cached = Hash.new { |hash, key| hash[key] = Hash.new }
@errors = []
@@ -65,6 +68,8 @@ module Github
fetch_pull_requests
puts 'Fetching issues...'.color(:aqua) if verbose
fetch_issues
+ puts 'Fetching releases...'.color(:aqua) if verbose
+ fetch_releases
puts 'Cloning wiki repository...'.color(:aqua) if verbose
fetch_wiki_repository
puts 'Expiring repository cache...'.color(:aqua) if verbose
@@ -72,6 +77,7 @@ module Github
true
rescue Github::RepositoryFetchError
+ expire_repository_cache
false
ensure
keep_track_of_errors
@@ -81,23 +87,21 @@ module Github
def fetch_repository
begin
- project.create_repository unless project.repository.exists?
- project.repository.add_remote('github', "https://#{options.fetch(:token)}@github.com/#{repo}.git")
+ project.ensure_repository
+ project.repository.add_remote('github', repo_url)
project.repository.set_remote_as_mirror('github')
project.repository.fetch_remote('github', forced: true)
- rescue Gitlab::Shell::Error => e
- error(:project, "https://github.com/#{repo}.git", e.message)
+ rescue Gitlab::Git::Repository::NoRepository, Gitlab::Shell::Error => e
+ error(:project, repo_url, e.message)
raise Github::RepositoryFetchError
end
end
def fetch_wiki_repository
- wiki_url = "https://#{options.fetch(:token)}@github.com/#{repo}.wiki.git"
- wiki_path = "#{project.full_path}.wiki"
+ return if project.wiki.repository_exists?
- unless project.wiki.repository_exists?
- gitlab_shell.import_repository(project.repository_storage_path, wiki_path, wiki_url)
- end
+ wiki_path = "#{project.disk_path}.wiki"
+ gitlab_shell.import_repository(project.repository_storage_path, wiki_path, wiki_url)
rescue Gitlab::Shell::Error => e
# GitHub error message when the wiki repo has not been created,
# this means that repo has wiki enabled, but have no pages. So,
@@ -309,7 +313,7 @@ module Github
next unless representation.valid?
release = ::Release.find_or_initialize_by(project_id: project.id, tag: representation.tag)
- next unless relese.new_record?
+ next unless release.new_record?
begin
release.description = representation.description
@@ -337,7 +341,7 @@ module Github
def user_id(user, fallback_id = nil)
return unless user.present?
- return cached[:user_ids][user.id] if cached[:user_ids].key?(user.id)
+ return cached[:user_ids][user.id] if cached[:user_ids][user.id].present?
gitlab_user_id = user_id_by_external_uid(user.id) || user_id_by_email(user.email)
@@ -367,7 +371,7 @@ module Github
end
def expire_repository_cache
- repository.expire_content_cache
+ repository.expire_content_cache if project.repository_exists?
end
def keep_track_of_errors
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 9bed81e7327..7d3aa532750 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -218,7 +218,8 @@ module Gitlab
def full_authentication_abilities
read_authentication_abilities + [
:push_code,
- :create_container_image
+ :create_container_image,
+ :admin_container_image
]
end
alias_method :api_scope_authentication_abilities, :full_authentication_abilities
diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb
index 1611eba31da..d671867e7c7 100644
--- a/lib/gitlab/conflict/file_collection.rb
+++ b/lib/gitlab/conflict/file_collection.rb
@@ -77,8 +77,8 @@ EOM
def initialize(merge_request, project)
@merge_request = merge_request
- @our_commit = merge_request.source_branch_head.raw.raw_commit
- @their_commit = merge_request.target_branch_head.raw.raw_commit
+ @our_commit = merge_request.source_branch_head.raw.rugged_commit
+ @their_commit = merge_request.target_branch_head.raw.rugged_commit
@project = project
end
end
diff --git a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
index b260822788d..2479b4a7706 100644
--- a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
@@ -54,7 +54,7 @@ module Gitlab
end
def serialize_commit(event, commit, query)
- commit = Commit.new(Gitlab::Git::Commit.new(commit.to_hash), @project)
+ commit = Commit.from_hash(commit.to_hash, @project)
AnalyticsCommitSerializer.new(project: @project, total_time: event['total_time']).represent(commit)
end
diff --git a/lib/gitlab/daemon.rb b/lib/gitlab/daemon.rb
new file mode 100644
index 00000000000..dfd17e35707
--- /dev/null
+++ b/lib/gitlab/daemon.rb
@@ -0,0 +1,62 @@
+module Gitlab
+ class Daemon
+ def self.initialize_instance(*args)
+ raise "#{name} singleton instance already initialized" if @instance
+ @instance = new(*args)
+ Kernel.at_exit(&@instance.method(:stop))
+ @instance
+ end
+
+ def self.instance
+ @instance ||= initialize_instance
+ end
+
+ attr_reader :thread
+
+ def thread?
+ !thread.nil?
+ end
+
+ def initialize
+ @mutex = Mutex.new
+ end
+
+ def enabled?
+ true
+ end
+
+ def start
+ return unless enabled?
+
+ @mutex.synchronize do
+ return thread if thread?
+
+ @thread = Thread.new { start_working }
+ end
+ end
+
+ def stop
+ @mutex.synchronize do
+ return unless thread?
+
+ stop_working
+
+ if thread
+ thread.wakeup if thread.alive?
+ thread.join
+ @thread = nil
+ end
+ end
+ end
+
+ private
+
+ def start_working
+ raise NotImplementedError
+ end
+
+ def stop_working
+ # no-ops
+ end
+ end
+end
diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb
index 781f9c56a42..8ddc91e341d 100644
--- a/lib/gitlab/encoding_helper.rb
+++ b/lib/gitlab/encoding_helper.rb
@@ -11,7 +11,7 @@ module Gitlab
# obscure encoding with low confidence.
# There is a lot more info with this merge request:
# https://gitlab.com/gitlab-org/gitlab_git/merge_requests/77#note_4754193
- ENCODING_CONFIDENCE_THRESHOLD = 40
+ ENCODING_CONFIDENCE_THRESHOLD = 50
def encode!(message)
return nil unless message.respond_to? :force_encoding
diff --git a/lib/gitlab/environment.rb b/lib/gitlab/environment.rb
new file mode 100644
index 00000000000..5e0dd6e7859
--- /dev/null
+++ b/lib/gitlab/environment.rb
@@ -0,0 +1,7 @@
+module Gitlab
+ module Environment
+ def self.hostname
+ @hostname ||= ENV['HOSTNAME'] || Socket.gethostname
+ end
+ end
+end
diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb
index 8dbe25e55f6..31effdba292 100644
--- a/lib/gitlab/git/blame.rb
+++ b/lib/gitlab/git/blame.rb
@@ -16,7 +16,7 @@ module Gitlab
def each
@blames.each do |blame|
yield(
- Gitlab::Git::Commit.new(blame.commit),
+ Gitlab::Git::Commit.new(@repo, blame.commit),
blame.line
)
end
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index db6cfc9671f..77b81d2d437 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -20,66 +20,7 @@ module Gitlab
if is_enabled
find_by_gitaly(repository, sha, path)
else
- find_by_rugged(repository, sha, path)
- end
- end
- end
-
- def find_by_gitaly(repository, sha, path)
- path = path.sub(/\A\/*/, '')
- path = '/' if path.empty?
- name = File.basename(path)
- entry = Gitlab::GitalyClient::CommitService.new(repository).tree_entry(sha, path, MAX_DATA_DISPLAY_SIZE)
- return unless entry
-
- case entry.type
- when :COMMIT
- new(
- id: entry.oid,
- name: name,
- size: 0,
- data: '',
- path: path,
- commit_id: sha
- )
- when :BLOB
- new(
- id: entry.oid,
- name: name,
- size: entry.size,
- data: entry.data.dup,
- mode: entry.mode.to_s(8),
- path: path,
- commit_id: sha,
- binary: binary?(entry.data)
- )
- end
- end
-
- def find_by_rugged(repository, sha, path)
- commit = repository.lookup(sha)
- root_tree = commit.tree
-
- blob_entry = find_entry_by_path(repository, root_tree.oid, path)
-
- return nil unless blob_entry
-
- if blob_entry[:type] == :commit
- submodule_blob(blob_entry, path, sha)
- else
- blob = repository.lookup(blob_entry[:oid])
-
- if blob
- new(
- id: blob.oid,
- name: blob_entry[:name],
- size: blob.size,
- data: blob.content(MAX_DATA_DISPLAY_SIZE),
- mode: blob_entry[:filemode].to_s(8),
- path: path,
- commit_id: sha,
- binary: blob.binary?
- )
+ find_by_rugged(repository, sha, path, limit: MAX_DATA_DISPLAY_SIZE)
end
end
end
@@ -109,6 +50,21 @@ module Gitlab
detect && detect[:type] == :binary
end
+ # Returns an array of Blob instances, specified in blob_references as
+ # [[commit_sha, path], [commit_sha, path], ...]. If blob_size_limit < 0 then the
+ # full blob contents are returned. If blob_size_limit >= 0 then each blob will
+ # contain no more than limit bytes in its data attribute.
+ #
+ # Keep in mind that this method may allocate a lot of memory. It is up
+ # to the caller to limit the number of blobs and blob_size_limit.
+ #
+ def batch(repository, blob_references, blob_size_limit: nil)
+ blob_size_limit ||= MAX_DATA_DISPLAY_SIZE
+ blob_references.map do |sha, path|
+ find_by_rugged(repository, sha, path, limit: blob_size_limit)
+ end
+ end
+
private
# Recursive search of blob id by path
@@ -153,6 +109,66 @@ module Gitlab
commit_id: sha
)
end
+
+ def find_by_gitaly(repository, sha, path)
+ path = path.sub(/\A\/*/, '')
+ path = '/' if path.empty?
+ name = File.basename(path)
+ entry = Gitlab::GitalyClient::CommitService.new(repository).tree_entry(sha, path, MAX_DATA_DISPLAY_SIZE)
+ return unless entry
+
+ case entry.type
+ when :COMMIT
+ new(
+ id: entry.oid,
+ name: name,
+ size: 0,
+ data: '',
+ path: path,
+ commit_id: sha
+ )
+ when :BLOB
+ new(
+ id: entry.oid,
+ name: name,
+ size: entry.size,
+ data: entry.data.dup,
+ mode: entry.mode.to_s(8),
+ path: path,
+ commit_id: sha,
+ binary: binary?(entry.data)
+ )
+ end
+ end
+
+ def find_by_rugged(repository, sha, path, limit:)
+ commit = repository.lookup(sha)
+ root_tree = commit.tree
+
+ blob_entry = find_entry_by_path(repository, root_tree.oid, path)
+
+ return nil unless blob_entry
+
+ if blob_entry[:type] == :commit
+ submodule_blob(blob_entry, path, sha)
+ else
+ blob = repository.lookup(blob_entry[:oid])
+
+ if blob
+ new(
+ id: blob.oid,
+ name: blob_entry[:name],
+ size: blob.size,
+ # Rugged::Blob#content is expensive; don't call it if we don't have to.
+ data: limit.zero? ? '' : blob.content(limit),
+ mode: blob_entry[:filemode].to_s(8),
+ path: path,
+ commit_id: sha,
+ binary: blob.binary?
+ )
+ end
+ end
+ end
end
def initialize(options)
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index ca7e3a7c4be..9256663f454 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -14,7 +14,7 @@ module Gitlab
attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator
- delegate :tree, to: :raw_commit
+ delegate :tree, to: :rugged_commit
def ==(other)
return false unless other.is_a?(Gitlab::Git::Commit)
@@ -50,19 +50,29 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/321
def find(repo, commit_id = "HEAD")
+ # Already a commit?
return commit_id if commit_id.is_a?(Gitlab::Git::Commit)
- return decorate(commit_id) if commit_id.is_a?(Rugged::Commit)
- obj = if commit_id.is_a?(String)
- repo.rev_parse_target(commit_id)
- else
- Gitlab::Git::Ref.dereference_object(commit_id)
- end
+ # A rugged reference?
+ commit_id = Gitlab::Git::Ref.dereference_object(commit_id)
+ return decorate(repo, commit_id) if commit_id.is_a?(Rugged::Commit)
- return nil unless obj.is_a?(Rugged::Commit)
+ # Some weird thing?
+ return nil unless commit_id.is_a?(String)
- decorate(obj)
- rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError, Gitlab::Git::Repository::NoRepository
+ commit = repo.gitaly_migrate(:find_commit) do |is_enabled|
+ if is_enabled
+ repo.gitaly_commit_client.find_commit(commit_id)
+ else
+ obj = repo.rev_parse_target(commit_id)
+
+ obj.is_a?(Rugged::Commit) ? obj : nil
+ end
+ end
+
+ decorate(repo, commit) if commit
+ rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError,
+ Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository
nil
end
@@ -102,7 +112,7 @@ module Gitlab
if is_enabled
repo.gitaly_commit_client.between(base, head)
else
- repo.commits_between(base, head).map { |c| decorate(c) }
+ repo.rugged_commits_between(base, head).map { |c| decorate(repo, c) }
end
end
rescue Rugged::ReferenceError
@@ -169,7 +179,7 @@ module Gitlab
offset = actual_options[:skip]
limit = actual_options[:max_count]
walker.each(offset: offset, limit: limit) do |commit|
- commits.push(decorate(commit))
+ commits.push(decorate(repo, commit))
end
walker.reset
@@ -183,27 +193,8 @@ module Gitlab
Gitlab::GitalyClient::CommitService.new(repo).find_all_commits(options)
end
- def decorate(commit, ref = nil)
- Gitlab::Git::Commit.new(commit, ref)
- end
-
- # Returns a diff object for the changes introduced by +rugged_commit+.
- # If +rugged_commit+ doesn't have a parent, then the diff is between
- # this commit and an empty repo. See Repository#diff for the keys
- # allowed in the +options+ hash.
- def diff_from_parent(rugged_commit, options = {})
- options ||= {}
- break_rewrites = options[:break_rewrites]
- actual_options = Gitlab::Git::Diff.filter_diff_options(options)
-
- diff = if rugged_commit.parents.empty?
- rugged_commit.diff(actual_options.merge(reverse: true))
- else
- rugged_commit.parents[0].diff(rugged_commit, actual_options)
- end
-
- diff.find_similar!(break_rewrites: break_rewrites)
- diff
+ def decorate(repository, commit, ref = nil)
+ Gitlab::Git::Commit.new(repository, commit, ref)
end
# Returns the `Rugged` sorting type constant for one or more given
@@ -221,7 +212,7 @@ module Gitlab
end
end
- def initialize(raw_commit, head = nil)
+ def initialize(repository, raw_commit, head = nil)
raise "Nil as raw commit passed" unless raw_commit
case raw_commit
@@ -229,12 +220,13 @@ module Gitlab
init_from_hash(raw_commit)
when Rugged::Commit
init_from_rugged(raw_commit)
- when Gitlab::GitalyClient::Commit
+ when Gitaly::GitCommit
init_from_gitaly(raw_commit)
else
raise "Invalid raw commit type: #{raw_commit.class}"
end
+ @repository = repository
@head = head
end
@@ -269,19 +261,50 @@ module Gitlab
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/324
def to_diff
- diff_from_parent.patch
+ rugged_diff_from_parent.patch
end
# Returns a diff object for the changes from this commit's first parent.
# If there is no parent, then the diff is between this commit and an
- # empty repo. See Repository#diff for keys allowed in the +options+
+ # empty repo. See Repository#diff for keys allowed in the +options+
# hash.
def diff_from_parent(options = {})
- Commit.diff_from_parent(raw_commit, options)
+ Gitlab::GitalyClient.migrate(:commit_raw_diffs) do |is_enabled|
+ if is_enabled
+ @repository.gitaly_commit_client.diff_from_parent(self, options)
+ else
+ rugged_diff_from_parent(options)
+ end
+ end
+ end
+
+ def rugged_diff_from_parent(options = {})
+ options ||= {}
+ break_rewrites = options[:break_rewrites]
+ actual_options = Gitlab::Git::Diff.filter_diff_options(options)
+
+ diff = if rugged_commit.parents.empty?
+ rugged_commit.diff(actual_options.merge(reverse: true))
+ else
+ rugged_commit.parents[0].diff(rugged_commit, actual_options)
+ end
+
+ diff.find_similar!(break_rewrites: break_rewrites)
+ diff
end
def deltas
- @deltas ||= diff_from_parent.each_delta.map { |d| Gitlab::Git::Diff.new(d) }
+ @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.map { |delta| Gitlab::Git::Diff.new(delta) }
+ end
end
def has_zero_stats?
@@ -309,14 +332,7 @@ module Gitlab
end
def parents
- case raw_commit
- when Rugged::Commit
- raw_commit.parents.map { |c| Gitlab::Git::Commit.new(c) }
- when Gitlab::GitalyClient::Commit
- parent_ids.map { |oid| self.class.find(raw_commit.repository, oid) }.compact
- else
- raise NotImplementedError, "commit source doesn't support #parents"
- end
+ parent_ids.map { |oid| self.class.find(@repository, oid) }.compact
end
# Get the gpg signature of this commit.
@@ -334,7 +350,7 @@ module Gitlab
def to_patch(options = {})
begin
- raw_commit.to_mbox(options)
+ rugged_commit.to_mbox(options)
rescue Rugged::InvalidError => ex
if ex.message =~ /commit \w+ is a merge commit/i
'Patch format is not currently supported for merge commits.'
@@ -382,6 +398,14 @@ module Gitlab
encode! @committer_email
end
+ def rugged_commit
+ @rugged_commit ||= if raw_commit.is_a?(Rugged::Commit)
+ raw_commit
+ else
+ @repository.rev_parse_target(id)
+ end
+ end
+
private
def init_from_hash(hash)
@@ -415,10 +439,10 @@ module Gitlab
# subject from the message to make it clearer when there's one
# available but not the other.
@message = (commit.body.presence || commit.subject).dup
- @authored_date = Time.at(commit.author.date.seconds)
+ @authored_date = Time.at(commit.author.date.seconds).utc
@author_name = commit.author.name.dup
@author_email = commit.author.email.dup
- @committed_date = Time.at(commit.committer.date.seconds)
+ @committed_date = Time.at(commit.committer.date.seconds).utc
@committer_name = commit.committer.name.dup
@committer_email = commit.committer.email.dup
@parent_ids = commit.parent_ids
diff --git a/lib/gitlab/git/commit_stats.rb b/lib/gitlab/git/commit_stats.rb
index 57c29ad112c..00acb4763e9 100644
--- a/lib/gitlab/git/commit_stats.rb
+++ b/lib/gitlab/git/commit_stats.rb
@@ -16,7 +16,7 @@ module Gitlab
@deletions = 0
@total = 0
- diff = commit.diff_from_parent
+ diff = commit.rugged_diff_from_parent
diff.each_patch do |p|
# TODO: Use the new Rugged convenience methods when they're released
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index 87ed9c3ea26..6a601561c2a 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -28,7 +28,6 @@ module Gitlab
@limits = self.class.collection_limits(options)
@enforce_limits = !!options.fetch(:limits, true)
@expanded = !!options.fetch(:expanded, true)
- @from_gitaly = options.fetch(:from_gitaly, false)
@line_count = 0
@byte_count = 0
@@ -44,7 +43,7 @@ module Gitlab
return if @iterator.nil?
Gitlab::GitalyClient.migrate(:commit_raw_diffs) do |is_enabled|
- if is_enabled && @from_gitaly
+ if is_enabled && @iterator.is_a?(Gitlab::GitalyClient::DiffStitcher)
each_gitaly_patch(&block)
else
each_rugged_patch(&block)
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 734aed8fbc1..371f8797ff2 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -58,17 +58,18 @@ module Gitlab
end
end
- # Alias to old method for compatibility
- def raw
- rugged
- end
-
def rugged
- @rugged ||= Rugged::Repository.new(path, alternates: alternate_object_directories)
+ @rugged ||= circuit_breaker.perform do
+ Rugged::Repository.new(path, alternates: alternate_object_directories)
+ end
rescue Rugged::RepositoryError, Rugged::OSError
raise NoRepository.new('no repository for such path')
end
+ def circuit_breaker
+ @circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(storage)
+ end
+
# Returns an Array of branch names
# sorted by name ASC
def branch_names
@@ -281,7 +282,14 @@ module Gitlab
# Return repo size in megabytes
def size
- size = popen(%w(du -sk), path).first.strip.to_i
+ size = gitaly_migrate(:repository_size) do |is_enabled|
+ if is_enabled
+ size_by_gitaly
+ else
+ size_by_shelling_out
+ end
+ end
+
(size.to_f / 1024).round(2)
end
@@ -296,8 +304,24 @@ module Gitlab
# after: Time.new(2016, 4, 21, 14, 32, 10)
# )
#
+ # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/446
def log(options)
- raw_log(options).map { |c| Commit.decorate(c) }
+ default_options = {
+ limit: 10,
+ offset: 0,
+ path: nil,
+ follow: false,
+ skip_merges: false,
+ disable_walk: false,
+ after: nil,
+ before: nil
+ }
+
+ options = default_options.merge(options)
+ options[:limit] ||= 0
+ options[:offset] ||= 0
+
+ raw_log(options).map { |c| Commit.decorate(self, c) }
end
def count_commits(options)
@@ -324,7 +348,9 @@ module Gitlab
# Return a collection of Rugged::Commits between the two revspec arguments.
# See http://git-scm.com/docs/git-rev-parse.html#_specifying_revisions for
# a detailed list of valid arguments.
- def commits_between(from, to)
+ #
+ # Gitaly note: JV: to be deprecated in favor of Commit.between
+ def rugged_commits_between(from, to)
walker = Rugged::Walker.new(rugged)
walker.sorting(Rugged::SORT_NONE | Rugged::SORT_REVERSE)
@@ -708,20 +734,6 @@ module Gitlab
end
def raw_log(options)
- default_options = {
- limit: 10,
- offset: 0,
- path: nil,
- follow: false,
- skip_merges: false,
- disable_walk: false,
- after: nil,
- before: nil
- }
-
- options = default_options.merge(options)
- options[:limit] ||= 0
- options[:offset] ||= 0
actual_ref = options[:ref] || root_ref
begin
sha = sha_from_ref(actual_ref)
@@ -857,46 +869,6 @@ module Gitlab
submodule_data.select { |path, data| data['id'] }
end
- # Returns true if +commit+ introduced changes to +path+, using commit
- # trees to make that determination. Uses the history simplification
- # rules that `git log` uses by default, where a commit is omitted if it
- # is TREESAME to any parent.
- #
- # If the +follow+ option is true and the file specified by +path+ was
- # renamed, then the path value is set to the old path.
- def commit_touches_path?(commit, path, follow, walker)
- entry = tree_entry(commit, path)
-
- if commit.parents.empty?
- # This is the root commit, return true if it has +path+ in its tree
- return !entry.nil?
- end
-
- num_treesame = 0
- commit.parents.each do |parent|
- parent_entry = tree_entry(parent, path)
-
- # Only follow the first TREESAME parent for merge commits
- if num_treesame > 0
- walker.hide(parent)
- next
- end
-
- if entry.nil? && parent_entry.nil?
- num_treesame += 1
- elsif entry && parent_entry && entry[:oid] == parent_entry[:oid]
- num_treesame += 1
- end
- end
-
- case num_treesame
- when 0
- detect_rename(commit, commit.parents.first, path) if follow
- true
- else false
- end
- end
-
# Find the entry for +path+ in the tree for +commit+
def tree_entry(commit, path)
pathname = Pathname.new(path)
@@ -924,43 +896,6 @@ module Gitlab
tmp_entry
end
- # Compare +commit+ and +parent+ for +path+. If +path+ is a file and was
- # renamed in +commit+, then set +path+ to the old filename.
- def detect_rename(commit, parent, path)
- diff = parent.diff(commit, paths: [path], disable_pathspec_match: true)
-
- # If +path+ is a filename, not a directory, then we should only have
- # one delta. We don't need to follow renames for directories.
- return nil if diff.each_delta.count > 1
-
- delta = diff.each_delta.first
- if delta.added?
- full_diff = parent.diff(commit)
- full_diff.find_similar!
-
- full_diff.each_delta do |full_delta|
- if full_delta.renamed? && path == full_delta.new_file[:path]
- # Look for the old path in ancestors
- path.replace(full_delta.old_file[:path])
- end
- end
- end
- end
-
- # Returns true if the index entry has the special file mode that denotes
- # a submodule.
- def submodule?(index_entry)
- index_entry[:mode] == 57344
- end
-
- # Return a Rugged::Index that has read from the tree at +ref_name+
- def populated_index(ref_name)
- commit = rev_parse_target(ref_name)
- index = rugged.index
- index.read_tree(commit.tree)
- index
- end
-
# Return the Rugged patches for the diff between +from+ and +to+.
def diff_patches(from, to, options = {}, *paths)
options ||= {}
@@ -1015,6 +950,14 @@ module Gitlab
gitaly_ref_client.tags
end
+ def size_by_shelling_out
+ popen(%w(du -sk), path).first.strip.to_i
+ end
+
+ def size_by_gitaly
+ gitaly_repository_client.repository_size
+ end
+
def count_commits_by_gitaly(options)
gitaly_commit_client.commit_count(options[:ref], options)
end
diff --git a/lib/gitlab/git/storage.rb b/lib/gitlab/git/storage.rb
new file mode 100644
index 00000000000..e28be4b8a38
--- /dev/null
+++ b/lib/gitlab/git/storage.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module Git
+ module Storage
+ class Inaccessible < StandardError
+ attr_reader :retry_after
+
+ def initialize(message = nil, retry_after = nil)
+ super(message)
+ @retry_after = retry_after
+ end
+ end
+
+ CircuitOpen = Class.new(Inaccessible)
+
+ REDIS_KEY_PREFIX = 'storage_accessible:'.freeze
+
+ def self.redis
+ Gitlab::Redis::SharedState
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/storage/circuit_breaker.rb b/lib/gitlab/git/storage/circuit_breaker.rb
new file mode 100644
index 00000000000..9ea9367d4b7
--- /dev/null
+++ b/lib/gitlab/git/storage/circuit_breaker.rb
@@ -0,0 +1,144 @@
+module Gitlab
+ module Git
+ module Storage
+ class CircuitBreaker
+ FailureInfo = Struct.new(:last_failure, :failure_count)
+
+ attr_reader :storage,
+ :hostname,
+ :storage_path,
+ :failure_count_threshold,
+ :failure_wait_time,
+ :failure_reset_time,
+ :storage_timeout
+
+ delegate :last_failure, :failure_count, to: :failure_info
+
+ def self.reset_all!
+ pattern = "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}*"
+
+ Gitlab::Git::Storage.redis.with do |redis|
+ all_storage_keys = redis.keys(pattern)
+ redis.del(*all_storage_keys) unless all_storage_keys.empty?
+ end
+
+ RequestStore.delete(:circuitbreaker_cache)
+ end
+
+ def self.for_storage(storage)
+ cached_circuitbreakers = RequestStore.fetch(:circuitbreaker_cache) do
+ Hash.new do |hash, storage_name|
+ hash[storage_name] = new(storage_name)
+ end
+ end
+
+ cached_circuitbreakers[storage]
+ end
+
+ def initialize(storage, hostname = Gitlab::Environment.hostname)
+ @storage = storage
+ @hostname = hostname
+
+ config = Gitlab.config.repositories.storages[@storage]
+ @storage_path = config['path']
+ @failure_count_threshold = config['failure_count_threshold']
+ @failure_wait_time = config['failure_wait_time']
+ @failure_reset_time = config['failure_reset_time']
+ @storage_timeout = config['storage_timeout']
+ end
+
+ def perform
+ return yield unless Feature.enabled?('git_storage_circuit_breaker')
+
+ check_storage_accessible!
+
+ yield
+ end
+
+ def circuit_broken?
+ return false if no_failures?
+
+ recent_failure = last_failure > failure_wait_time.seconds.ago
+ too_many_failures = failure_count > failure_count_threshold
+
+ recent_failure || too_many_failures
+ end
+
+ # Memoizing the `storage_available` call means we only do it once per
+ # request when the storage is available.
+ #
+ # When the storage appears not available, and the memoized value is `false`
+ # we might want to try again.
+ def storage_available?
+ return @storage_available if @storage_available
+
+ if @storage_available = Gitlab::Git::Storage::ForkedStorageCheck
+ .storage_available?(storage_path, storage_timeout)
+ track_storage_accessible
+ else
+ track_storage_inaccessible
+ end
+
+ @storage_available
+ end
+
+ def check_storage_accessible!
+ if circuit_broken?
+ raise Gitlab::Git::Storage::CircuitOpen.new("Circuit for #{storage} is broken", failure_wait_time)
+ end
+
+ unless storage_available?
+ raise Gitlab::Git::Storage::Inaccessible.new("#{storage} not accessible", failure_wait_time)
+ end
+ end
+
+ def no_failures?
+ last_failure.blank? && failure_count == 0
+ end
+
+ def track_storage_inaccessible
+ @failure_info = FailureInfo.new(Time.now, failure_count + 1)
+
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.pipelined do
+ redis.hset(cache_key, :last_failure, last_failure.to_i)
+ redis.hincrby(cache_key, :failure_count, 1)
+ redis.expire(cache_key, failure_reset_time)
+ end
+ end
+ end
+
+ def track_storage_accessible
+ return if no_failures?
+
+ @failure_info = FailureInfo.new(nil, 0)
+
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.pipelined do
+ redis.hset(cache_key, :last_failure, nil)
+ redis.hset(cache_key, :failure_count, 0)
+ end
+ end
+ end
+
+ def failure_info
+ @failure_info ||= get_failure_info
+ end
+
+ def get_failure_info
+ last_failure, failure_count = Gitlab::Git::Storage.redis.with do |redis|
+ redis.hmget(cache_key, :last_failure, :failure_count)
+ end
+
+ last_failure = Time.at(last_failure.to_i) if last_failure.present?
+
+ FailureInfo.new(last_failure, failure_count.to_i)
+ end
+
+ def cache_key
+ @cache_key ||= "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage}:#{hostname}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/storage/forked_storage_check.rb b/lib/gitlab/git/storage/forked_storage_check.rb
new file mode 100644
index 00000000000..91d8241f17b
--- /dev/null
+++ b/lib/gitlab/git/storage/forked_storage_check.rb
@@ -0,0 +1,55 @@
+module Gitlab
+ module Git
+ module Storage
+ module ForkedStorageCheck
+ extend self
+
+ def storage_available?(path, timeout_seconds = 5)
+ status = timeout_check(path, timeout_seconds)
+
+ status.success?
+ end
+
+ def timeout_check(path, timeout_seconds)
+ filesystem_check_pid = check_filesystem_in_process(path)
+
+ deadline = timeout_seconds.seconds.from_now.utc
+ wait_time = 0.01
+ status = nil
+
+ while status.nil?
+ if deadline > Time.now.utc
+ sleep(wait_time)
+ _pid, status = Process.wait2(filesystem_check_pid, Process::WNOHANG)
+ else
+ Process.kill('KILL', filesystem_check_pid)
+ # Blocking wait, so we are sure the process is gone before continuing
+ _pid, status = Process.wait2(filesystem_check_pid)
+ end
+ end
+
+ status
+ end
+
+ # This will spawn a new 2 processes to do the check:
+ # The outer child (waiter) will spawn another child process (stater).
+ #
+ # The stater is the process is performing the actual filesystem check
+ # the check might hang if the filesystem is acting up.
+ # In this case we will send a `KILL` to the waiter, which will still
+ # be responsive while the stater is hanging.
+ def check_filesystem_in_process(path)
+ spawn('ruby', '-e', ruby_check, path, [:out, :err] => '/dev/null')
+ end
+
+ def ruby_check
+ <<~RUBY_FILESYSTEM_CHECK
+ inner_pid = fork { File.stat(ARGV.first) }
+ Process.waitpid(inner_pid)
+ exit $?.exitstatus
+ RUBY_FILESYSTEM_CHECK
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/storage/health.rb b/lib/gitlab/git/storage/health.rb
new file mode 100644
index 00000000000..2d723147f4f
--- /dev/null
+++ b/lib/gitlab/git/storage/health.rb
@@ -0,0 +1,91 @@
+module Gitlab
+ module Git
+ module Storage
+ class Health
+ attr_reader :storage_name, :info
+
+ def self.pattern_for_storage(storage_name)
+ "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage_name}:*"
+ end
+
+ def self.for_all_storages
+ storage_names = Gitlab.config.repositories.storages.keys
+ results_per_storage = nil
+
+ Gitlab::Git::Storage.redis.with do |redis|
+ keys_per_storage = all_keys_for_storages(storage_names, redis)
+ results_per_storage = load_for_keys(keys_per_storage, redis)
+ end
+
+ results_per_storage.map do |name, info|
+ info.each { |i| i[:failure_count] = i[:failure_count].value.to_i }
+ new(name, info)
+ end
+ end
+
+ def self.all_keys_for_storages(storage_names, redis)
+ keys_per_storage = {}
+
+ redis.pipelined do
+ storage_names.each do |storage_name|
+ pattern = pattern_for_storage(storage_name)
+
+ keys_per_storage[storage_name] = redis.keys(pattern)
+ end
+ end
+
+ keys_per_storage
+ end
+
+ def self.load_for_keys(keys_per_storage, redis)
+ info_for_keys = {}
+
+ redis.pipelined do
+ keys_per_storage.each do |storage_name, keys_future|
+ info_for_storage = keys_future.value.map do |key|
+ { name: key, failure_count: redis.hget(key, :failure_count) }
+ end
+
+ info_for_keys[storage_name] = info_for_storage
+ end
+ end
+
+ info_for_keys
+ end
+
+ def self.for_failing_storages
+ for_all_storages.select(&:failing?)
+ end
+
+ def initialize(storage_name, info)
+ @storage_name = storage_name
+ @info = info
+ end
+
+ def failing_info
+ @failing_info ||= info.select { |info_for_host| info_for_host[:failure_count] > 0 }
+ end
+
+ def failing?
+ failing_info.any?
+ end
+
+ def failing_on_hosts
+ @failing_on_hosts ||= failing_info.map do |info_for_host|
+ info_for_host[:name].split(':').last
+ end
+ end
+
+ def failing_circuit_breakers
+ @failing_circuit_breakers ||= failing_on_hosts.map do |hostname|
+ CircuitBreaker.new(storage_name, hostname)
+ end
+ end
+
+ def total_failures
+ @total_failures ||= failing_info.sum { |info_for_host| info_for_host[:failure_count] }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index c90ef282fdd..70177cd0fec 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -100,5 +100,9 @@ module Gitlab
path = Rails.root.join(SERVER_VERSION_FILE)
path.read.chomp
end
+
+ def self.encode(s)
+ s.dup.force_encoding(Encoding::ASCII_8BIT)
+ end
end
end
diff --git a/lib/gitlab/gitaly_client/commit.rb b/lib/gitlab/gitaly_client/commit.rb
deleted file mode 100644
index 61fe462d762..00000000000
--- a/lib/gitlab/gitaly_client/commit.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-module Gitlab
- module GitalyClient
- class Commit
- attr_reader :repository, :gitaly_commit
-
- delegate :id, :subject, :body, :author, :committer, :parent_ids, to: :gitaly_commit
-
- def initialize(repository, gitaly_commit)
- @repository = repository
- @gitaly_commit = gitaly_commit
- end
- end
- end
-end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index ac6817e6d0e..692d7e02eef 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -29,22 +29,21 @@ module Gitlab
request = Gitaly::CommitDiffRequest.new(request_params)
response = GitalyClient.call(@repository.storage, :diff_service, :commit_diff, request)
- Gitlab::Git::DiffCollection.new(GitalyClient::DiffStitcher.new(response), options.merge(from_gitaly: true))
+ GitalyClient::DiffStitcher.new(response)
end
def commit_deltas(commit)
request = Gitaly::CommitDeltaRequest.new(commit_diff_request_params(commit))
response = GitalyClient.call(@repository.storage, :diff_service, :commit_delta, request)
- response.flat_map do |msg|
- msg.deltas.map { |d| Gitlab::Git::Diff.new(d) }
- end
+
+ response.flat_map { |msg| msg.deltas }
end
def tree_entry(ref, path, limit = nil)
request = Gitaly::TreeEntryRequest.new(
repository: @gitaly_repo,
revision: ref,
- path: path.dup.force_encoding(Encoding::ASCII_8BIT),
+ path: GitalyClient.encode(path),
limit: limit.to_i
)
@@ -100,15 +99,14 @@ module Gitlab
def last_commit_for_path(revision, path)
request = Gitaly::LastCommitForPathRequest.new(
repository: @gitaly_repo,
- revision: revision.force_encoding(Encoding::ASCII_8BIT),
- path: path.to_s.force_encoding(Encoding::ASCII_8BIT)
+ revision: GitalyClient.encode(revision),
+ path: GitalyClient.encode(path.to_s)
)
gitaly_commit = GitalyClient.call(@repository.storage, :commit_service, :last_commit_for_path, request).commit
return unless gitaly_commit
- commit = GitalyClient::Commit.new(@repository, gitaly_commit)
- Gitlab::Git::Commit.new(commit)
+ Gitlab::Git::Commit.new(@repository, gitaly_commit)
end
def between(from, to)
@@ -135,6 +133,20 @@ module Gitlab
consume_commits_response(response)
end
+ def commits_by_message(query, revision: '', path: '', limit: 1000, offset: 0)
+ request = Gitaly::CommitsByMessageRequest.new(
+ repository: @gitaly_repo,
+ query: query,
+ revision: revision.to_s.force_encoding(Encoding::ASCII_8BIT),
+ path: path.to_s.force_encoding(Encoding::ASCII_8BIT),
+ limit: limit.to_i,
+ offset: offset.to_i
+ )
+
+ response = GitalyClient.call(@repository.storage, :commit_service, :commits_by_message, request)
+ consume_commits_response(response)
+ end
+
def languages(ref = nil)
request = Gitaly::CommitLanguagesRequest.new(repository: @gitaly_repo, revision: ref || '')
response = GitalyClient.call(@repository.storage, :commit_service, :commit_languages, request)
@@ -153,10 +165,21 @@ module Gitlab
response.reduce("") { |memo, msg| memo << msg.data }
end
+ def find_commit(revision)
+ request = Gitaly::FindCommitRequest.new(
+ repository: @gitaly_repo,
+ revision: GitalyClient.encode(revision)
+ )
+
+ response = GitalyClient.call(@repository.storage, :commit_service, :find_commit, request)
+
+ response.commit
+ end
+
private
def commit_diff_request_params(commit, options = {})
- parent_id = commit.parents[0]&.id || EMPTY_TREE_ID
+ parent_id = commit.parent_ids.first || EMPTY_TREE_ID
{
repository: @gitaly_repo,
@@ -169,8 +192,7 @@ module Gitlab
def consume_commits_response(response)
response.flat_map do |message|
message.commits.map do |gitaly_commit|
- commit = GitalyClient::Commit.new(@repository, gitaly_commit)
- Gitlab::Git::Commit.new(commit)
+ Gitlab::Git::Commit.new(@repository, gitaly_commit)
end
end
end
diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb
index b0f7548b7dc..919fb68b8c7 100644
--- a/lib/gitlab/gitaly_client/ref_service.rb
+++ b/lib/gitlab/gitaly_client/ref_service.rb
@@ -16,8 +16,7 @@ module Gitlab
response.flat_map do |message|
message.branches.map do |branch|
- gitaly_commit = GitalyClient::Commit.new(@repository, branch.target)
- target_commit = Gitlab::Git::Commit.decorate(gitaly_commit)
+ target_commit = Gitlab::Git::Commit.decorate(@repository, branch.target)
Gitlab::Git::Branch.new(@repository, branch.name, branch.target.id, target_commit)
end
end
@@ -102,8 +101,7 @@ module Gitlab
response.flat_map do |message|
message.tags.map do |gitaly_tag|
if gitaly_tag.target_commit.present?
- commit = GitalyClient::Commit.new(@repository, gitaly_tag.target_commit)
- gitaly_commit = Gitlab::Git::Commit.new(commit)
+ gitaly_commit = Gitlab::Git::Commit.decorate(@repository, gitaly_tag.target_commit)
end
Gitlab::Git::Tag.new(
@@ -141,7 +139,7 @@ module Gitlab
committer_email: response.commit_committer.email.dup
}
- Gitlab::Git::Commit.decorate(hash)
+ Gitlab::Git::Commit.decorate(@repository, hash)
end
end
end
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index 13e75b256a7..79ce784f2f2 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -27,6 +27,11 @@ module Gitlab
request = Gitaly::RepackIncrementalRequest.new(repository: @gitaly_repo)
GitalyClient.call(@storage, :repository_service, :repack_incremental, request)
end
+
+ def repository_size
+ request = Gitaly::RepositorySizeRequest.new(repository: @gitaly_repo)
+ GitalyClient.call(@storage, :repository_service, :repository_size, request).size
+ end
end
end
end
diff --git a/lib/gitlab/health_checks/fs_shards_check.rb b/lib/gitlab/health_checks/fs_shards_check.rb
index 9e91c135956..eef97f54962 100644
--- a/lib/gitlab/health_checks/fs_shards_check.rb
+++ b/lib/gitlab/health_checks/fs_shards_check.rb
@@ -10,7 +10,9 @@ module Gitlab
def readiness
repository_storages.map do |storage_name|
begin
- if !storage_stat_test(storage_name)
+ if !storage_circuitbreaker_test(storage_name)
+ HealthChecks::Result.new(false, 'circuitbreaker tripped', shard: storage_name)
+ elsif !storage_stat_test(storage_name)
HealthChecks::Result.new(false, 'cannot stat storage', shard: storage_name)
else
with_temp_file(storage_name) do |tmp_file_path|
@@ -36,7 +38,8 @@ module Gitlab
[
storage_stat_metrics(storage_name),
storage_write_metrics(storage_name),
- storage_read_metrics(storage_name)
+ storage_read_metrics(storage_name),
+ storage_circuitbreaker_metrics(storage_name)
].flatten
end
end
@@ -121,6 +124,12 @@ module Gitlab
file_contents == RANDOM_STRING
end
+ def storage_circuitbreaker_test(storage_name)
+ Gitlab::Git::Storage::CircuitBreaker.new(storage_name).perform { "OK" }
+ rescue Gitlab::Git::Storage::Inaccessible
+ nil
+ end
+
def storage_stat_metrics(storage_name)
operation_metrics(:filesystem_accessible, :filesystem_access_latency_seconds, shard: storage_name) do
with_timing { storage_stat_test(storage_name) }
@@ -143,6 +152,14 @@ module Gitlab
end
end
end
+
+ def storage_circuitbreaker_metrics(storage_name)
+ operation_metrics(:filesystem_circuitbreaker,
+ :filesystem_circuitbreaker_latency_seconds,
+ shard: storage_name) do
+ with_timing { storage_circuitbreaker_test(storage_name) }
+ end
+ end
end
end
end
diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb
index cc282d1415b..5d106b5c075 100644
--- a/lib/gitlab/i18n.rb
+++ b/lib/gitlab/i18n.rb
@@ -16,7 +16,8 @@ module Gitlab
'eo' => 'Esperanto',
'it' => 'Italiano',
'uk' => 'Українська',
- 'ja' => '日本語'
+ 'ja' => '日本語',
+ 'ko' => '한국어'
}.freeze
def available_locales
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index c8ad3a7a5e0..c5c05bfe2fb 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -101,6 +101,7 @@ excluded_attributes:
merge_requests:
- :milestone_id
- :ref_fetched
+ - :merge_jid
award_emoji:
- :awardable_id
statuses:
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index 52276cbcd9a..5404dc11a87 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -8,7 +8,7 @@ module Gitlab
ImportSource = Struct.new(:name, :title, :importer)
ImportTable = [
- ImportSource.new('github', 'GitHub', Gitlab::GithubImport::Importer),
+ ImportSource.new('github', 'GitHub', Github::Import),
ImportSource.new('bitbucket', 'Bitbucket', Gitlab::BitbucketImport::Importer),
ImportSource.new('gitlab', 'GitLab.com', Gitlab::GitlabImport::Importer),
ImportSource.new('google_code', 'Google Code', Gitlab::GoogleCodeImport::Importer),
diff --git a/lib/gitlab/key_fingerprint.rb b/lib/gitlab/key_fingerprint.rb
index b75ae512d92..d9a79f7c291 100644
--- a/lib/gitlab/key_fingerprint.rb
+++ b/lib/gitlab/key_fingerprint.rb
@@ -1,55 +1,48 @@
module Gitlab
class KeyFingerprint
- include Gitlab::Popen
+ attr_reader :key, :ssh_key
- attr_accessor :key
+ # Unqualified MD5 fingerprint for compatibility
+ delegate :fingerprint, to: :ssh_key, allow_nil: true
def initialize(key)
@key = key
- end
-
- def fingerprint
- cmd_status = 0
- cmd_output = ''
-
- Tempfile.open('gitlab_key_file') do |file|
- file.puts key
- file.rewind
-
- cmd = []
- cmd.push('ssh-keygen')
- cmd.push('-E', 'md5') if explicit_fingerprint_algorithm?
- cmd.push('-lf', file.path)
-
- cmd_output, cmd_status = popen(cmd, '/tmp')
- end
-
- return nil unless cmd_status.zero?
- # 16 hex bytes separated by ':', optionally starting with "MD5:"
- fingerprint_matches = cmd_output.match(/(MD5:)?(?<fingerprint>(\h{2}:){15}\h{2})/)
- return nil unless fingerprint_matches
-
- fingerprint_matches[:fingerprint]
+ @ssh_key =
+ begin
+ Net::SSH::KeyFactory.load_data_public_key(key)
+ rescue Net::SSH::Exception, NotImplementedError
+ end
end
- private
-
- def explicit_fingerprint_algorithm?
- # OpenSSH 6.8 introduces a new default output format for fingerprints.
- # Check the version and decide which command to use.
-
- version_output, version_status = popen(%w(ssh -V))
- return false unless version_status.zero?
+ def valid?
+ ssh_key.present?
+ end
- version_matches = version_output.match(/OpenSSH_(?<major>\d+)\.(?<minor>\d+)/)
- return false unless version_matches
+ def type
+ return unless valid?
- version_info = Gitlab::VersionInfo.new(version_matches[:major].to_i, version_matches[:minor].to_i)
+ parts = ssh_key.ssh_type.split('-')
+ parts.shift if parts[0] == 'ssh'
- required_version_info = Gitlab::VersionInfo.new(6, 8)
+ parts[0].upcase
+ end
- version_info >= required_version_info
+ def bits
+ return unless valid?
+
+ case type
+ when 'RSA'
+ ssh_key.n.num_bits
+ when 'DSS', 'DSA'
+ ssh_key.p.num_bits
+ when 'ECDSA'
+ ssh_key.group.order.num_bits
+ when 'ED25519'
+ 256
+ else
+ raise "Unsupported key type: #{type}"
+ end
end
end
end
diff --git a/lib/gitlab/metrics/base_sampler.rb b/lib/gitlab/metrics/base_sampler.rb
index 219accfc029..716d20bb91a 100644
--- a/lib/gitlab/metrics/base_sampler.rb
+++ b/lib/gitlab/metrics/base_sampler.rb
@@ -1,20 +1,7 @@
require 'logger'
module Gitlab
module Metrics
- class BaseSampler
- def self.initialize_instance(*args)
- raise "#{name} singleton instance already initialized" if @instance
- @instance = new(*args)
- at_exit(&@instance.method(:stop))
- @instance
- end
-
- def self.instance
- @instance
- end
-
- attr_reader :running
-
+ class BaseSampler < Daemon
# interval - The sampling interval in seconds.
def initialize(interval)
interval_half = interval.to_f / 2
@@ -22,44 +9,7 @@ module Gitlab
@interval = interval
@interval_steps = (-interval_half..interval_half).step(0.1).to_a
- @mutex = Mutex.new
- end
-
- def enabled?
- true
- end
-
- def start
- return unless enabled?
-
- @mutex.synchronize do
- return if running
- @running = true
-
- @thread = Thread.new do
- sleep(sleep_interval)
-
- while running
- safe_sample
-
- sleep(sleep_interval)
- end
- end
- end
- end
-
- def stop
- @mutex.synchronize do
- return unless running
-
- @running = false
-
- if @thread
- @thread.wakeup if @thread.alive?
- @thread.join
- @thread = nil
- end
- end
+ super()
end
def safe_sample
@@ -81,7 +31,7 @@ module Gitlab
# potentially missing anything that happens in between samples).
# 2. Don't sample data at the same interval two times in a row.
def sleep_interval
- while step = @interval_steps.sample
+ while (step = @interval_steps.sample)
if step != @last_step
@last_step = step
@@ -89,6 +39,25 @@ module Gitlab
end
end
end
+
+ private
+
+ attr_reader :running
+
+ def start_working
+ @running = true
+ sleep(sleep_interval)
+
+ while running
+ safe_sample
+
+ sleep(sleep_interval)
+ end
+ end
+
+ def stop_working
+ @running = false
+ end
end
end
end
diff --git a/lib/gitlab/metrics/sidekiq_metrics_exporter.rb b/lib/gitlab/metrics/sidekiq_metrics_exporter.rb
new file mode 100644
index 00000000000..5980a4ded2b
--- /dev/null
+++ b/lib/gitlab/metrics/sidekiq_metrics_exporter.rb
@@ -0,0 +1,39 @@
+require 'webrick'
+require 'prometheus/client/rack/exporter'
+
+module Gitlab
+ module Metrics
+ class SidekiqMetricsExporter < Daemon
+ def enabled?
+ Gitlab::Metrics.metrics_folder_present? && settings.enabled
+ end
+
+ def settings
+ Settings.monitoring.sidekiq_exporter
+ end
+
+ private
+
+ attr_reader :server
+
+ def start_working
+ @server = ::WEBrick::HTTPServer.new(Port: settings.port, BindAddress: settings.address)
+ server.mount "/", Rack::Handler::WEBrick, rack_app
+ server.start
+ end
+
+ def stop_working
+ server.shutdown
+ @server = nil
+ end
+
+ def rack_app
+ Rack::Builder.app do
+ use Rack::Deflater
+ use ::Prometheus::Client::Rack::Exporter
+ run -> (env) { [404, {}, ['']] }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb
new file mode 100644
index 00000000000..cf461adf697
--- /dev/null
+++ b/lib/gitlab/project_template.rb
@@ -0,0 +1,45 @@
+module Gitlab
+ class ProjectTemplate
+ attr_reader :title, :name
+
+ def initialize(name, title)
+ @name, @title = name, title
+ end
+
+ alias_method :logo, :name
+
+ def file
+ archive_path.open
+ end
+
+ def archive_path
+ Rails.root.join("vendor/project_templates/#{name}.tar.gz")
+ end
+
+ def clone_url
+ "https://gitlab.com/gitlab-org/project-templates/#{name}.git"
+ end
+
+ def ==(other)
+ name == other.name && title == other.title
+ end
+
+ TEMPLATES_TABLE = [
+ ProjectTemplate.new('rails', 'Ruby on Rails')
+ ].freeze
+
+ class << self
+ def all
+ TEMPLATES_TABLE
+ end
+
+ def find(name)
+ all.find { |template| template.name == name.to_s }
+ end
+
+ def archive_directory
+ Rails.root.join("vendor_directory/project_templates")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb b/lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb
index db4708b22e4..32fe8201a8d 100644
--- a/lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb
+++ b/lib/gitlab/prometheus/queries/additional_metrics_environment_query.rb
@@ -5,7 +5,7 @@ module Gitlab
include QueryAdditionalMetrics
def query(environment_id)
- Environment.find_by(id: environment_id).try do |environment|
+ ::Environment.find_by(id: environment_id).try do |environment|
query_metrics(
common_query_context(environment, timeframe_start: 8.hours.ago.to_f, timeframe_end: Time.now.to_f)
)
diff --git a/lib/gitlab/prometheus/queries/environment_query.rb b/lib/gitlab/prometheus/queries/environment_query.rb
index 66f29d95177..1d17d3cfd56 100644
--- a/lib/gitlab/prometheus/queries/environment_query.rb
+++ b/lib/gitlab/prometheus/queries/environment_query.rb
@@ -3,7 +3,7 @@ module Gitlab
module Queries
class EnvironmentQuery < BaseQuery
def query(environment_id)
- Environment.find_by(id: environment_id).try do |environment|
+ ::Environment.find_by(id: environment_id).try do |environment|
environment_slug = environment.slug
timeframe_start = 8.hours.ago.to_f
timeframe_end = Time.now.to_f
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index 4366ff336ef..0cb28732402 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -105,12 +105,24 @@ module Gitlab
# fetch_remote("gitlab/gitlab-ci", "upstream")
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387
- def fetch_remote(storage, name, remote, forced: false, no_tags: false)
+ def fetch_remote(storage, name, remote, ssh_auth: nil, forced: false, no_tags: false)
args = [gitlab_shell_projects_path, 'fetch-remote', storage, "#{name}.git", remote, "#{Gitlab.config.gitlab_shell.git_timeout}"]
args << '--force' if forced
args << '--no-tags' if no_tags
- gitlab_shell_fast_execute_raise_error(args)
+ vars = {}
+
+ if ssh_auth&.ssh_import?
+ if ssh_auth.ssh_key_auth? && ssh_auth.ssh_private_key.present?
+ vars['GITLAB_SHELL_SSH_KEY'] = ssh_auth.ssh_private_key
+ end
+
+ if ssh_auth.ssh_known_hosts.present?
+ vars['GITLAB_SHELL_KNOWN_HOSTS'] = ssh_auth.ssh_known_hosts
+ end
+ end
+
+ gitlab_shell_fast_execute_raise_error(args, vars)
end
# Move repository
@@ -293,15 +305,15 @@ module Gitlab
false
end
- def gitlab_shell_fast_execute_raise_error(cmd)
- output, status = gitlab_shell_fast_execute_helper(cmd)
+ def gitlab_shell_fast_execute_raise_error(cmd, vars = {})
+ output, status = gitlab_shell_fast_execute_helper(cmd, vars)
raise Error, output unless status.zero?
true
end
- def gitlab_shell_fast_execute_helper(cmd)
- vars = ENV.to_h.slice(*GITLAB_SHELL_ENV_VARS)
+ def gitlab_shell_fast_execute_helper(cmd, vars = {})
+ vars.merge!(ENV.to_h.slice(*GITLAB_SHELL_ENV_VARS))
# Don't pass along the entire parent environment to prevent gitlab-shell
# from wasting I/O by searching through GEM_PATH
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index e0ac21305a5..748e0a29184 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -27,8 +27,8 @@ module Gitlab
ci_pipeline_schedules: ::Ci::PipelineSchedule.count,
deploy_keys: DeployKey.count,
deployments: Deployment.count,
- environments: Environment.count,
- in_review_folder: Environment.in_review_folder.count,
+ environments: ::Environment.count,
+ in_review_folder: ::Environment.in_review_folder.count,
groups: Group.count,
issues: Issue.count,
keys: Key.count,
diff --git a/lib/haml_lint/inline_javascript.rb b/lib/haml_lint/inline_javascript.rb
new file mode 100644
index 00000000000..05668c69006
--- /dev/null
+++ b/lib/haml_lint/inline_javascript.rb
@@ -0,0 +1,16 @@
+unless Rails.env.production?
+ require 'haml_lint/haml_visitor'
+ require 'haml_lint/linter'
+ require 'haml_lint/linter_registry'
+
+ module HamlLint
+ class Linter::InlineJavaScript < Linter
+ include LinterRegistry
+
+ def visit_filter(node)
+ return unless node.filter_type == 'javascript'
+ record_lint(node, 'Inline JavaScript is discouraged (https://docs.gitlab.com/ee/development/gotchas.html#do-not-use-inline-javascript-in-views)')
+ end
+ end
+ end
+end
diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb
index 688a79c0441..ef08bd46e17 100644
--- a/lib/mattermost/session.rb
+++ b/lib/mattermost/session.rb
@@ -36,11 +36,12 @@ module Mattermost
def with_session
with_lease do
- raise Mattermost::NoSessionError unless create
+ create
begin
yield self
- rescue Errno::ECONNREFUSED
+ rescue Errno::ECONNREFUSED => e
+ Rails.logger.error(e.message + "\n" + e.backtrace.join("\n"))
raise Mattermost::NoSessionError
ensure
destroy
@@ -85,10 +86,12 @@ module Mattermost
private
def create
- return unless oauth_uri
- return unless token_uri
+ raise Mattermost::NoSessionError unless oauth_uri
+ raise Mattermost::NoSessionError unless token_uri
@token = request_token
+ raise Mattermost::NoSessionError unless @token
+
@headers = {
Authorization: "Bearer #{@token}"
}
@@ -106,11 +109,16 @@ module Mattermost
@oauth_uri = nil
response = get("/api/v3/oauth/gitlab/login", follow_redirects: false)
- return unless 300 <= response.code && response.code < 400
+ return unless (300...400) === response.code
redirect_uri = response.headers['location']
return unless redirect_uri
+ oauth_cookie = parse_cookie(response)
+ @headers = {
+ Cookie: oauth_cookie.to_cookie_string
+ }
+
@oauth_uri = URI.parse(redirect_uri)
end
@@ -124,7 +132,7 @@ module Mattermost
def request_token
response = get(token_uri, follow_redirects: false)
- if 200 <= response.code && response.code < 400
+ if (200...400) === response.code
response.headers['token']
end
end
@@ -156,5 +164,11 @@ module Mattermost
rescue Errno::ECONNREFUSED => e
raise Mattermost::ConnectionError.new(e.message)
end
+
+ def parse_cookie(response)
+ cookie_hash = CookieHash.new
+ response.get_fields('Set-Cookie').each { |c| cookie_hash.add_cookies(c) }
+ cookie_hash
+ end
end
end
diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake
index 3703f9cfb5c..aaf00bd703a 100644
--- a/lib/tasks/gitlab/gitaly.rake
+++ b/lib/tasks/gitlab/gitaly.rake
@@ -21,7 +21,7 @@ namespace :gitlab do
create_gitaly_configuration
# In CI we run scripts/gitaly-test-build instead of this command
unless ENV['CI'].present?
- Bundler.with_original_env { run_command!([command]) }
+ Bundler.with_original_env { run_command!(%w[/usr/bin/env -u RUBYOPT] + [command]) }
end
end
end
@@ -66,6 +66,7 @@ namespace :gitlab do
config = { socket_path: address.sub(%r{\Aunix:}, ''), storage: storages }
config[:auth] = { token: 'secret' } if Rails.env.test?
config[:'gitaly-ruby'] = { dir: File.join(Dir.pwd, 'ruby') } if gitaly_ruby
+ config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path }
TOML.dump(config)
end
diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake
index 59c32bbe7a4..a7e30423c7a 100644
--- a/lib/tasks/gitlab/update_templates.rake
+++ b/lib/tasks/gitlab/update_templates.rake
@@ -4,6 +4,55 @@ namespace :gitlab do
TEMPLATE_DATA.each { |template| update(template) }
end
+ desc "GitLab | Update project templates"
+ task :update_project_templates do
+ if Rails.env.production?
+ puts "This rake task is not meant fo production instances".red
+ exit(1)
+ end
+ admin = User.find_by(admin: true)
+
+ unless admin
+ puts "No admin user could be found".red
+ exit(1)
+ end
+
+ Gitlab::ProjectTemplate.all.each do |template|
+ params = {
+ import_url: template.clone_url,
+ namespace_id: admin.namespace.id,
+ path: template.title,
+ skip_wiki: true
+ }
+
+ puts "Creating project for #{template.name}"
+ project = Projects::CreateService.new(admin, params).execute
+
+ loop do
+ if project.finished?
+ puts "Import finished for #{template.name}"
+ break
+ end
+
+ if project.failed?
+ puts "Failed to import from #{project_params[:import_url]}".red
+ exit(1)
+ end
+
+ puts "Waiting for the import to finish"
+
+ sleep(5)
+ project.reload
+ end
+
+ Projects::ImportExport::ExportService.new(project, admin).execute
+ FileUtils.cp(project.export_project_path, template.archive_path)
+ Projects::DestroyService.new(admin, project).execute
+ puts "Exported #{template.name}".green
+ end
+ puts "Done".green
+ end
+
def update(template)
sub_dir = template.repo_url.match(/([A-Za-z-]+)\.git\z/)[1]
dir = File.join(vendor_directory, sub_dir)
diff --git a/lib/tasks/haml-lint.rake b/lib/tasks/haml-lint.rake
index 609dfaa48e3..ad2d034b0b4 100644
--- a/lib/tasks/haml-lint.rake
+++ b/lib/tasks/haml-lint.rake
@@ -1,5 +1,6 @@
unless Rails.env.production?
require 'haml_lint/rake_task'
+ require 'haml_lint/inline_javascript'
HamlLint::RakeTask.new
end
diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake
index 50b8e331469..96b8f59242c 100644
--- a/lib/tasks/import.rake
+++ b/lib/tasks/import.rake
@@ -7,7 +7,7 @@ class GithubImport
end
def initialize(token, gitlab_username, project_path, extras)
- @options = { url: 'https://api.github.com', token: token, verbose: true }
+ @options = { token: token, verbose: true }
@project_path = project_path
@current_user = User.find_by_username(gitlab_username)
@github_repo = extras.empty? ? nil : extras.first
@@ -62,6 +62,7 @@ class GithubImport
visibility_level: visibility_level,
import_type: 'github',
import_source: @repo['full_name'],
+ import_url: @repo['clone_url'].sub('://', "://#{@options[:token]}@"),
skip_wiki: @repo['has_wiki']
).execute
end
diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po
index 1774c911d71..85d806e6f20 100644
--- a/locale/bg/gitlab.po
+++ b/locale/bg/gitlab.po
@@ -4,11 +4,11 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-07-05 08:50-0500\n"
+"POT-Creation-Date: 2017-07-13 12:07-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"PO-Revision-Date: 2017-07-13 08:13-0400\n"
+"PO-Revision-Date: 2017-08-03 04:43-0400\n"
"Last-Translator: Lyubomir Vasilev <lyubomirv@abv.bg>\n"
"Language-Team: Bulgarian (https://translate.zanata.org/project/view/GitLab)\n"
"Language: bg\n"
@@ -1151,6 +1151,9 @@ msgstr "Частен"
msgid "VisibilityLevel|Public"
msgstr "Публичен"
+msgid "VisibilityLevel|Unknown"
+msgstr "Неизвестно"
+
msgid "Want to see the data? Please ask an administrator for access."
msgstr "Искате ли да видите данните? Помолете администратор за достъп."
diff --git a/locale/eo/gitlab.po b/locale/eo/gitlab.po
index 62dbc2621f4..d688478972d 100644
--- a/locale/eo/gitlab.po
+++ b/locale/eo/gitlab.po
@@ -4,11 +4,11 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-07-05 08:50-0500\n"
+"POT-Creation-Date: 2017-07-13 12:07-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"PO-Revision-Date: 2017-07-13 08:46-0400\n"
+"PO-Revision-Date: 2017-08-03 04:44-0400\n"
"Last-Translator: Lyubomir Vasilev <lyubomirv@abv.bg>\n"
"Language-Team: Esperanto (https://translate.zanata.org/project/view/GitLab)\n"
"Language: eo\n"
@@ -1152,6 +1152,9 @@ msgstr "Privata"
msgid "VisibilityLevel|Public"
msgstr "Publika"
+msgid "VisibilityLevel|Unknown"
+msgstr "Nekonata"
+
msgid "Want to see the data? Please ask an administrator for access."
msgstr ""
"Ĉu vi volas vidi la datenojn? Bonvolu peti atingeblon de administranto."
diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po
index 959654c7849..c490933c6d4 100644
--- a/locale/fr/gitlab.po
+++ b/locale/fr/gitlab.po
@@ -5,13 +5,13 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-07-05 08:50-0500\n"
+"POT-Creation-Date: 2017-07-13 12:07-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"PO-Revision-Date: 2017-07-19 09:45-0400\n"
-"Last-Translator: Dremor <egeorget@opmbx.org>\n"
"Language-Team: French (https://translate.zanata.org/project/view/GitLab)\n"
+"PO-Revision-Date: 2017-08-03 03:35-0400\n"
+"Last-Translator: Rémy Coutable <remy@rymai.me>\n"
"Language: fr\n"
"X-Generator: Zanata 3.9.6\n"
"Plural-Forms: nplurals=2; plural=(n > 1)\n"
@@ -1161,6 +1161,9 @@ msgstr "Privé"
msgid "VisibilityLevel|Public"
msgstr "Public"
+msgid "VisibilityLevel|Unknown"
+msgstr "Inconnu"
+
msgid "Want to see the data? Please ask an administrator for access."
msgstr ""
"Vous voulez voir les données ? Merci de contacter un administrateur pour en "
diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po
index d4fac6ab34e..7ba23d84405 100644
--- a/locale/it/gitlab.po
+++ b/locale/it/gitlab.po
@@ -4,13 +4,13 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-06-28 13:32+0200\n"
+"POT-Creation-Date: 2017-07-13 12:07-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"PO-Revision-Date: 2017-07-12 05:45-0400\n"
-"Last-Translator: Paolo Falomo <info@paolofalomo.it>\n"
"Language-Team: Italian (https://translate.zanata.org/project/view/GitLab)\n"
+"PO-Revision-Date: 2017-08-07 10:15-0400\n"
+"Last-Translator: Paolo Falomo <info@paolofalomo.it>\n"
"Language: it\n"
"X-Generator: Zanata 3.9.6\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
@@ -647,6 +647,12 @@ msgstr "Tutto"
msgid "PipelineSchedules|Inactive"
msgstr "Inattiva"
+msgid "PipelineSchedules|Input variable key"
+msgstr "Chiave della variabile"
+
+msgid "PipelineSchedules|Input variable value"
+msgstr "Valore della variabile"
+
msgid "PipelineSchedules|Next Run"
msgstr "Prossima esecuzione"
@@ -656,12 +662,18 @@ msgstr "Nessuna"
msgid "PipelineSchedules|Provide a short description for this pipeline"
msgstr "Fornisci una breve descrizione per questa pipeline"
+msgid "PipelineSchedules|Remove variable row"
+msgstr "Rimuovi riga della variabile"
+
msgid "PipelineSchedules|Take ownership"
msgstr "Prendi possesso"
msgid "PipelineSchedules|Target"
msgstr "Target"
+msgid "PipelineSchedules|Variables"
+msgstr "Variabili"
+
msgid "PipelineSheduleIntervalPattern|Custom"
msgstr "Personalizzato"
@@ -1144,6 +1156,9 @@ msgstr "Privato"
msgid "VisibilityLevel|Public"
msgstr "Pubblico"
+msgid "VisibilityLevel|Unknown"
+msgstr "Sconosciuto"
+
msgid "Want to see the data? Please ask an administrator for access."
msgstr ""
"Vuoi visualizzare i dati? Richiedi l'accesso ad un amministratore, grazie."
@@ -1155,6 +1170,15 @@ msgid "Withdraw Access Request"
msgstr "Ritira richiesta d'accesso"
msgid ""
+"You are going to remove %{group_name}.\n"
+"Removed groups CANNOT be restored!\n"
+"Are you ABSOLUTELY sure?"
+msgstr ""
+"Stai per rimuovere il gruppo %{group_name}.\n"
+"I gruppi rimossi NON possono esser ripristinati!\n"
+"Sei ASSOLUTAMENTE sicuro?"
+
+msgid ""
"You are going to remove %{project_name_with_namespace}.\n"
"Removed project CANNOT be restored!\n"
"Are you ABSOLUTELY sure?"
diff --git a/locale/ja/gitlab.po b/locale/ja/gitlab.po
index 801674ce964..0b1db651c11 100644
--- a/locale/ja/gitlab.po
+++ b/locale/ja/gitlab.po
@@ -8,13 +8,13 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-07-05 08:50-0500\n"
+"POT-Creation-Date: 2017-07-13 12:07-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Last-Translator: YANO TETTER <tetuyano+zana@gmail.com>\n"
-"PO-Revision-Date: 2017-07-19 09:45-0400\n"
-"Last-Translator: YANO Tethurou <tetuyano+zana@gmail.com>\n"
+"PO-Revision-Date: 2017-08-06 11:23-0400\n"
+"Last-Translator: Taisuke Inoue <taisuke.inoue.jp@gmail.com>\n"
+"Language-Team: Japanese "Language-Team: Russian (https://translate.zanata.org/project/view/GitLab)\n"
"Language: ja\n"
"X-Generator: Zanata 3.9.6\n"
"Plural-Forms: nplurals=1; plural=0\n"
@@ -1107,6 +1107,9 @@ msgstr "プライベート"
msgid "VisibilityLevel|Public"
msgstr "パブリック"
+msgid "VisibilityLevel|Unknown"
+msgstr "不明"
+
msgid "Want to see the data? Please ask an administrator for access."
msgstr "このデータを参照したいですか?アクセスするには管理者に問い合わせてください。"
diff --git a/locale/ko/gitlab.po b/locale/ko/gitlab.po
new file mode 100644
index 00000000000..97a844ada7f
--- /dev/null
+++ b/locale/ko/gitlab.po
@@ -0,0 +1,1207 @@
+# chang-ho,cha <changho.cha@gmail.com>, 2017. #zanata
+# Huang Tao <htve@outlook.com>, 2017. #zanata
+msgid ""
+msgstr ""
+"Project-Id-Version: gitlab 1.0.0\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2017-07-13 12:07-0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"PO-Revision-Date: 2017-08-06 09:40-0400\n"
+"Last-Translator: chang-ho,cha <changho.cha@gmail.com>\n"
+"Language-Team: Korean (https://translate.zanata.org/project/view/GitLab)\n"
+"Language: ko\n"
+"X-Generator: Zanata 3.9.6\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+
+msgid "%d commit"
+msgid_plural "%d commits"
+msgstr[0] "%d 커밋"
+
+msgid "%s additional commit has been omitted to prevent performance issues."
+msgid_plural ""
+"%s additional commits have been omitted to prevent performance issues."
+msgstr[0] "%s 추가 커밋은 성능 이슈를 방지하기 위해 생략되었습니다."
+
+msgid "%{commit_author_link} committed %{commit_timeago}"
+msgstr "%{commit_timeago} 에 %{commit_author_link} 이(가) 커밋하였습니다. "
+
+msgid "1 pipeline"
+msgid_plural "%d pipelines"
+msgstr[0] "1 파이프라인"
+msgstr[1] "%d 파이프라인"
+
+msgid "A collection of graphs regarding Continuous Integration"
+msgstr "지속적인 통합에 관한 그래프 모음"
+
+msgid "About auto deploy"
+msgstr "자동 배포 정보"
+
+msgid "Active"
+msgstr "활성"
+
+msgid "Activity"
+msgstr "활동"
+
+msgid "Add Changelog"
+msgstr "변경 로그 추가"
+
+msgid "Add Contribution guide"
+msgstr "기여 가이드 추가"
+
+msgid "Add License"
+msgstr "라이선스 추가"
+
+msgid "Add an SSH key to your profile to pull or push via SSH."
+msgstr "프로필에 SSH 키를 추가하여 SSH를 통해 Pull 하거나 Push합니다."
+
+msgid "Add new directory"
+msgstr "새 디렉토리 추가"
+
+msgid "Archived project! Repository is read-only"
+msgstr "프로젝트가 보관되었습니다! 저장소는 읽기만 가능합니다."
+
+msgid "Are you sure you want to delete this pipeline schedule?"
+msgstr "이 파이프라인 스케쥴을 삭제 하시겠습니까?"
+
+msgid "Attach a file by drag &amp; drop or %{upload_link}"
+msgstr "드래그 &amp; 드롭 또는 %{upload_link}"
+
+msgid "Branch"
+msgid_plural "Branches"
+msgstr[0] "브랜치"
+
+msgid ""
+"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, "
+"choose a GitLab CI Yaml template and commit your changes. "
+"%{link_to_autodeploy_doc}"
+msgstr ""
+"<strong>%{branch_name}</strong> 브랜치가 생성되었습니다. 자동 배포를 설정하려면 GitLab CI Yaml "
+"템플릿을 선택하고 변경 사항을 적용하십시오. %{link_to_autodeploy_doc}"
+
+msgid "BranchSwitcherPlaceholder|Search branches"
+msgstr "브랜치 검색"
+
+msgid "BranchSwitcherTitle|Switch branch"
+msgstr "브랜치 변경"
+
+msgid "Branches"
+msgstr "브랜치"
+
+msgid "Browse Directory"
+msgstr "디렉토리 찾아보기"
+
+msgid "Browse File"
+msgstr "파일 찾아보기"
+
+msgid "Browse Files"
+msgstr "파일 찾아보기"
+
+msgid "Browse files"
+msgstr "파일 찾아보기"
+
+msgid "ByAuthor|by"
+msgstr "작성자"
+
+msgid "CI configuration"
+msgstr "CI 설정"
+
+msgid "Cancel"
+msgstr "취소"
+
+msgid "ChangeTypeActionLabel|Pick into branch"
+msgstr "브랜치에서 Pick"
+
+msgid "ChangeTypeActionLabel|Revert in branch"
+msgstr "브랜치에서 Revert"
+
+msgid "ChangeTypeAction|Cherry-pick"
+msgstr "Cherry-pick"
+
+msgid "ChangeTypeAction|Revert"
+msgstr "Revert"
+
+msgid "Changelog"
+msgstr "변경사항"
+
+msgid "Charts"
+msgstr "차트"
+
+msgid "Cherry-pick this commit"
+msgstr "이 커밋을 Cherry-pick"
+
+msgid "Cherry-pick this merge request"
+msgstr "이 머지 리퀘스트를 Cherry-pick"
+
+msgid "CiStatusLabel|canceled"
+msgstr "취소됨"
+
+msgid "CiStatusLabel|created"
+msgstr "생성되었습니다."
+
+msgid "CiStatusLabel|failed"
+msgstr "실패"
+
+msgid "CiStatusLabel|manual action"
+msgstr "수동 실행"
+
+msgid "CiStatusLabel|passed"
+msgstr "성공"
+
+msgid "CiStatusLabel|passed with warnings"
+msgstr "경고와 함께 성공"
+
+msgid "CiStatusLabel|pending"
+msgstr "대기"
+
+msgid "CiStatusLabel|skipped"
+msgstr "건너 뜀"
+
+msgid "CiStatusLabel|waiting for manual action"
+msgstr "수동 실행 대기 중"
+
+msgid "CiStatusText|blocked"
+msgstr "차단됨"
+
+msgid "CiStatusText|canceled"
+msgstr "취소됨"
+
+msgid "CiStatusText|created"
+msgstr "생성됨"
+
+msgid "CiStatusText|failed"
+msgstr "실패"
+
+msgid "CiStatusText|manual"
+msgstr "매뉴얼"
+
+msgid "CiStatusText|passed"
+msgstr "성공"
+
+msgid "CiStatusText|pending"
+msgstr "보류 중"
+
+msgid "CiStatusText|skipped"
+msgstr "건너 뜀"
+
+msgid "CiStatus|running"
+msgstr "실행 중"
+
+msgid "Commit"
+msgid_plural "Commits"
+msgstr[0] "커밋"
+
+msgid "Commit duration in minutes for last 30 commits"
+msgstr "최근 30 건의 커밋 소요시간 (분)"
+
+msgid "Commit message"
+msgstr "커밋 메시지"
+
+msgid "CommitBoxTitle|Commit"
+msgstr "커밋"
+
+msgid "CommitMessage|Add %{file_name}"
+msgstr "%{file_name} 추가"
+
+msgid "Commits"
+msgstr "커밋"
+
+msgid "Commits feed"
+msgstr "커밋 피드"
+
+msgid "Commits|History"
+msgstr "이력"
+
+msgid "Committed by"
+msgstr "커밋한 사용자"
+
+msgid "Compare"
+msgstr "비교"
+
+msgid "Contribution guide"
+msgstr "기여에 대한 안내"
+
+msgid "Contributors"
+msgstr "기여해 주신 분들"
+
+msgid "Copy URL to clipboard"
+msgstr "URL을 클립보드에 복사"
+
+msgid "Copy commit SHA to clipboard"
+msgstr "커밋의 SHA를 클립보드로 복사합니다"
+
+msgid "Create New Directory"
+msgstr "새 디렉토리 만들기"
+
+msgid ""
+"Create a personal access token on your account to pull or push via "
+"%{protocol}."
+msgstr "%{protocol}을 (를) 통해 Pull 하거나 Push 할 개인 액세스 토큰을 만드십시오."
+
+msgid "Create directory"
+msgstr "디렉토리 만들기"
+
+msgid "Create empty bare repository"
+msgstr "빈 bare 저장소 만들기"
+
+msgid "Create merge request"
+msgstr "머지 리퀘스트 만들기"
+
+msgid "Create new..."
+msgstr "새로 만들기 ..."
+
+msgid "CreateNewFork|Fork"
+msgstr "포크"
+
+msgid "CreateTag|Tag"
+msgstr "태그"
+
+msgid "CreateTokenToCloneLink|create a personal access token"
+msgstr "개인 액세스 토큰 만들기"
+
+msgid "Cron Timezone"
+msgstr "Cron 시간대"
+
+msgid "Cron syntax"
+msgstr "크론 구문"
+
+msgid "Custom notification events"
+msgstr "사용자 정의 알림 이벤트"
+
+msgid ""
+"Custom notification levels are the same as participating levels. With custom "
+"notification levels you will also receive notifications for select events. "
+"To find out more, check out %{notification_link}."
+msgstr ""
+"사용자 정의 알림 수준은 참여 수준과 동일합니다. 맞춤 알림 수준을 사용하면 일부 이벤트에 대한 알림도 받게됩니다. 자세한 내용은 "
+"%{notification_link}을 확인하십시오."
+
+msgid "Cycle Analytics"
+msgstr "Cycle Analytics"
+
+msgid ""
+"Cycle Analytics gives an overview of how much time it takes to go from idea "
+"to production in your project."
+msgstr "Cycle Analytics는 프로젝트에서 아이디어를 프로덕션으로 옮기는 데 걸리는 시간을 대략적으로 보여줍니다."
+
+msgid "CycleAnalyticsStage|Code"
+msgstr "코드"
+
+msgid "CycleAnalyticsStage|Issue"
+msgstr "이슈"
+
+msgid "CycleAnalyticsStage|Plan"
+msgstr "계획"
+
+msgid "CycleAnalyticsStage|Production"
+msgstr "프로덕션"
+
+msgid "CycleAnalyticsStage|Review"
+msgstr "리뷰"
+
+msgid "CycleAnalyticsStage|Staging"
+msgstr "스테이징"
+
+msgid "CycleAnalyticsStage|Test"
+msgstr "테스트"
+
+msgid "Define a custom pattern with cron syntax"
+msgstr "cron 구문을 사용하여 사용자 정의 패턴 정의"
+
+msgid "Delete"
+msgstr "삭제 "
+
+msgid "Deploy"
+msgid_plural "Deploys"
+msgstr[0] "배포"
+
+msgid "Description"
+msgstr "설명"
+
+msgid "Directory name"
+msgstr "디렉토리 이름"
+
+msgid "Don't show again"
+msgstr "다시 표시하지 않음"
+
+msgid "Download"
+msgstr "다운로드"
+
+msgid "Download tar"
+msgstr "tar 다운로드"
+
+msgid "Download tar.bz2"
+msgstr "tar.bz2 다운로드"
+
+msgid "Download tar.gz"
+msgstr "tar.gz 다운로드"
+
+msgid "Download zip"
+msgstr "zip 다운로드"
+
+msgid "DownloadArtifacts|Download"
+msgstr "다운로드"
+
+msgid "DownloadCommit|Email Patches"
+msgstr "이메일 패치"
+
+msgid "DownloadCommit|Plain Diff"
+msgstr "Plain Diff"
+
+msgid "DownloadSource|Download"
+msgstr "다운로드"
+
+msgid "Edit"
+msgstr "편집"
+
+msgid "Edit Pipeline Schedule %{id}"
+msgstr "파이프라인 스케줄 편집 %{id}"
+
+msgid "Every day (at 4:00am)"
+msgstr "매일 (오전 4시에)"
+
+msgid "Every month (on the 1st at 4:00am)"
+msgstr "매월 (1일 오전 4시)"
+
+msgid "Every week (Sundays at 4:00am)"
+msgstr "매주 (일요일 오전 4시에)"
+
+msgid "Failed to change the owner"
+msgstr "소유자를 변경하지 못했습니다"
+
+msgid "Failed to remove the pipeline schedule"
+msgstr "파이프라인 스케줄을 제거하지 못했습니다."
+
+msgid "Files"
+msgstr "파일"
+
+msgid "Filter by commit message"
+msgstr "커밋 메시지로 필터"
+
+msgid "Find by path"
+msgstr "경로로 찾기"
+
+msgid "Find file"
+msgstr "파일 찾기"
+
+msgid "FirstPushedBy|First"
+msgstr "처음"
+
+msgid "FirstPushedBy|pushed by"
+msgstr "푸시한 사용자"
+
+msgid "Fork"
+msgid_plural "Forks"
+msgstr[0] "포크"
+
+msgid "ForkedFromProjectPath|Forked from"
+msgstr "포크한 사용자"
+
+msgid "From issue creation until deploy to production"
+msgstr "이슈 생성에서 프로덕션 배포까지"
+
+msgid "From merge request merge until deploy to production"
+msgstr "머지 리퀘스트 머지에서 프로덕션 환경에 배포까지"
+
+msgid "Go to your fork"
+msgstr "당신의 포크로 이동하세요"
+
+msgid "GoToYourFork|Fork"
+msgstr "포크"
+
+msgid "Home"
+msgstr "홈"
+
+msgid "Housekeeping successfully started"
+msgstr "Housekeeping이 성공적으로 시작되었습니다"
+
+msgid "Import repository"
+msgstr "저장소 가져 오기"
+
+msgid "Interval Pattern"
+msgstr "주기 패턴"
+
+msgid "Introducing Cycle Analytics"
+msgstr "Cycle Analytics 소개"
+
+msgid "Jobs for last month"
+msgstr "지난달 Jobs"
+
+msgid "Jobs for last week"
+msgstr "지난주 Jobs"
+
+msgid "Jobs for last year"
+msgstr "지난해 Jobs"
+
+msgid "LFSStatus|Disabled"
+msgstr "Disabled"
+
+msgid "LFSStatus|Enabled"
+msgstr "Enabled"
+
+msgid "Last %d day"
+msgid_plural "Last %d days"
+msgstr[0] "최근 %d 일"
+
+msgid "Last Pipeline"
+msgstr "최근 파이프라인"
+
+msgid "Last Update"
+msgstr "최근 업데이트:"
+
+msgid "Last commit"
+msgstr "최근 커밋"
+
+msgid "Learn more in the"
+msgstr "더 자세히 알아보기"
+
+msgid "Learn more in the|pipeline schedules documentation"
+msgstr "파이프라인 스케쥴 문서로부터 더 알아보기"
+
+msgid "Leave group"
+msgstr "그룹 떠나기"
+
+msgid "Leave project"
+msgstr "프로젝트에서 나가기"
+
+msgid "Limited to showing %d event at most"
+msgid_plural "Limited to showing %d events at most"
+msgstr[0] "최대 %d 이벤트 만 표시하는 것으로 제한됩니다."
+
+msgid "Median"
+msgstr "중앙값"
+
+msgid "MissingSSHKeyWarningLink|add an SSH key"
+msgstr "SSH 키 추가"
+
+msgid "New Issue"
+msgid_plural "New Issues"
+msgstr[0] "새 이슈"
+
+msgid "New Pipeline Schedule"
+msgstr "새로운 파이프라인 일정"
+
+msgid "New branch"
+msgstr "새 브랜치"
+
+msgid "New directory"
+msgstr "새 디렉토리"
+
+msgid "New file"
+msgstr "새 파일"
+
+msgid "New issue"
+msgstr "새 이슈"
+
+msgid "New merge request"
+msgstr "새 머지 리퀘스트"
+
+msgid "New schedule"
+msgstr "새 일정"
+
+msgid "New snippet"
+msgstr "새 스니펫"
+
+msgid "New tag"
+msgstr "새 태그 "
+
+msgid "No repository"
+msgstr "저장소 없음"
+
+msgid "No schedules"
+msgstr "일정 없음"
+
+msgid "Not available"
+msgstr "사용할 수 없음"
+
+msgid "Not enough data"
+msgstr "데이터가 충분하지 않습니다."
+
+msgid "Notification events"
+msgstr "알림 이벤트"
+
+msgid "NotificationEvent|Close issue"
+msgstr "이슈 닫기"
+
+msgid "NotificationEvent|Close merge request"
+msgstr "머지 리퀘스트 닫기"
+
+msgid "NotificationEvent|Failed pipeline"
+msgstr "실패한 파이프라인"
+
+msgid "NotificationEvent|Merge merge request"
+msgstr "머지 리퀘스트 머지하기"
+
+msgid "NotificationEvent|New issue"
+msgstr "새 이슈"
+
+msgid "NotificationEvent|New merge request"
+msgstr "새 머지 리퀘스트"
+
+msgid "NotificationEvent|New note"
+msgstr "새 노트"
+
+msgid "NotificationEvent|Reassign issue"
+msgstr "이슈 재지정"
+
+msgid "NotificationEvent|Reassign merge request"
+msgstr "머지 리퀘스트 재 할당"
+
+msgid "NotificationEvent|Reopen issue"
+msgstr "이슈 다시 열기"
+
+msgid "NotificationEvent|Successful pipeline"
+msgstr "성공적인 파이프라인"
+
+msgid "NotificationLevel|Custom"
+msgstr "커스텀"
+
+msgid "NotificationLevel|Disabled"
+msgstr "사용 안 함"
+
+msgid "NotificationLevel|Global"
+msgstr "글로벌"
+
+msgid "NotificationLevel|On mention"
+msgstr "언급"
+
+msgid "NotificationLevel|Participate"
+msgstr "참여"
+
+msgid "NotificationLevel|Watch"
+msgstr "Watch"
+
+msgid "OfSearchInADropdown|Filter"
+msgstr "필터"
+
+msgid "OpenedNDaysAgo|Opened"
+msgstr "열린"
+
+msgid "Options"
+msgstr "옵션 "
+
+msgid "Owner"
+msgstr "소유자"
+
+msgid "Pipeline"
+msgstr "파이프라인"
+
+msgid "Pipeline Health"
+msgstr "파이프라인 상태"
+
+msgid "Pipeline Schedule"
+msgstr "파이프라인 스케쥴"
+
+msgid "Pipeline Schedules"
+msgstr "파이프라인 스케쥴"
+
+msgid "PipelineCharts|Failed:"
+msgstr "실패 :"
+
+msgid "PipelineCharts|Overall statistics"
+msgstr "전체 통계"
+
+msgid "PipelineCharts|Success ratio:"
+msgstr "성공 비율 :"
+
+msgid "PipelineCharts|Successful:"
+msgstr "성공 :"
+
+msgid "PipelineCharts|Total:"
+msgstr "합계 :"
+
+msgid "PipelineSchedules|Activated"
+msgstr "활성화 됨"
+
+msgid "PipelineSchedules|Active"
+msgstr "활성"
+
+msgid "PipelineSchedules|All"
+msgstr "모두"
+
+msgid "PipelineSchedules|Inactive"
+msgstr "비활성"
+
+msgid "PipelineSchedules|Input variable key"
+msgstr "입력 변수 키"
+
+msgid "PipelineSchedules|Input variable value"
+msgstr "입력 변수 값"
+
+msgid "PipelineSchedules|Next Run"
+msgstr "다음 실행"
+
+msgid "PipelineSchedules|None"
+msgstr "없음"
+
+msgid "PipelineSchedules|Provide a short description for this pipeline"
+msgstr "이 파이프라인에 대한 간단한 설명 제공"
+
+msgid "PipelineSchedules|Remove variable row"
+msgstr "변수 행 제거"
+
+msgid "PipelineSchedules|Take ownership"
+msgstr "소유권 가져 오기"
+
+msgid "PipelineSchedules|Target"
+msgstr "대상"
+
+msgid "PipelineSchedules|Variables"
+msgstr "변수"
+
+msgid "PipelineSheduleIntervalPattern|Custom"
+msgstr "사용자 정의"
+
+msgid "Pipelines"
+msgstr "파이프라인"
+
+msgid "Pipelines charts"
+msgstr "파이프라인 차트"
+
+msgid "Pipeline|all"
+msgstr "모두"
+
+msgid "Pipeline|success"
+msgstr "성공"
+
+msgid "Pipeline|with stage"
+msgstr "스테이징"
+
+msgid "Pipeline|with stages"
+msgstr "스테이징"
+
+msgid "Project '%{project_name}' queued for deletion."
+msgstr "'%{project_name}'프로젝트가 삭제 처리 중입니다."
+
+msgid "Project '%{project_name}' was successfully created."
+msgstr "'%{project_name}'프로젝트가 성공적으로 생성되었습니다."
+
+msgid "Project '%{project_name}' was successfully updated."
+msgstr "'%{project_name}'프로젝트가 성공적으로 업데이트되었습니다."
+
+msgid "Project '%{project_name}' will be deleted."
+msgstr "'%{project_name}'프로젝트가 삭제됩니다."
+
+msgid "Project access must be granted explicitly to each user."
+msgstr "프로젝트 액세스는 각 사용자에게 명시적으로 부여되어야합니다."
+
+msgid "Project export could not be deleted."
+msgstr "프로젝트 내보내기를 삭제할 수 없습니다."
+
+msgid "Project export has been deleted."
+msgstr "프로젝트 내보내기가 삭제되었습니다."
+
+msgid ""
+"Project export link has expired. Please generate a new export from your "
+"project settings."
+msgstr "프로젝트 내보내기 링크가 만료되었습니다. 프로젝트 설정에서 새 내보내기를 생성하십시오."
+
+msgid "Project export started. A download link will be sent by email."
+msgstr "프로젝트 내보내기가 시작되었습니다. 다운로드 링크는 이메일로 전송됩니다."
+
+msgid "Project home"
+msgstr "프로젝트 홈"
+
+msgid "ProjectFeature|Disabled"
+msgstr "사용 안 함"
+
+msgid "ProjectFeature|Everyone with access"
+msgstr "접근 권한을 가진 모든 이들"
+
+msgid "ProjectFeature|Only team members"
+msgstr "팀원 만"
+
+msgid "ProjectFileTree|Name"
+msgstr "이름"
+
+msgid "ProjectLastActivity|Never"
+msgstr "Never"
+
+msgid "ProjectLifecycle|Stage"
+msgstr "스테이징"
+
+msgid "ProjectNetworkGraph|Graph"
+msgstr "그래프"
+
+msgid "Read more"
+msgstr "더 읽기"
+
+msgid "Readme"
+msgstr "Readme"
+
+msgid "RefSwitcher|Branches"
+msgstr "브랜치"
+
+msgid "RefSwitcher|Tags"
+msgstr "태그"
+
+msgid "Related Commits"
+msgstr "관련 커밋"
+
+msgid "Related Deployed Jobs"
+msgstr "관련 배포 된 작업"
+
+msgid "Related Issues"
+msgstr "관련 이슈"
+
+msgid "Related Jobs"
+msgstr "관련 Jobs"
+
+msgid "Related Merge Requests"
+msgstr "관련 머지 리퀘스트"
+
+msgid "Related Merged Requests"
+msgstr "관련 머지 리퀘스트"
+
+msgid "Remind later"
+msgstr "나중에 다시 알림"
+
+msgid "Remove project"
+msgstr "프로젝트 삭제"
+
+msgid "Request Access"
+msgstr "액세스 요청"
+
+msgid "Revert this commit"
+msgstr "이 커밋 되돌리기"
+
+msgid "Revert this merge request"
+msgstr "이 머지 리퀘스트 되돌리기"
+
+msgid "Save pipeline schedule"
+msgstr "파이프라인 스케줄 저장"
+
+msgid "Schedule a new pipeline"
+msgstr "새로운 파이프라인 스케줄 잡기"
+
+msgid "Scheduling Pipelines"
+msgstr "파이프라인 스케줄링"
+
+msgid "Search branches and tags"
+msgstr "브랜치 및 태그 검색"
+
+msgid "Select Archive Format"
+msgstr "아카이브 포맷 선택"
+
+msgid "Select a timezone"
+msgstr "시간대 선택"
+
+msgid "Select target branch"
+msgstr "대상 브랜치 선택"
+
+msgid "Set a password on your account to pull or push via %{protocol}."
+msgstr "%{protocol}을(를) 통해 Pull 하거나 Push하려면 계정에 패스워드를 설정하십시오."
+
+msgid "Set up CI"
+msgstr "CI 설정"
+
+msgid "Set up Koding"
+msgstr "Koding 설정"
+
+msgid "Set up auto deploy"
+msgstr "자동 배포 설정"
+
+msgid "SetPasswordToCloneLink|set a password"
+msgstr "패스워드 설정"
+
+msgid "Showing %d event"
+msgid_plural "Showing %d events"
+msgstr[0] "%d 개의 이벤트 표시 중"
+
+msgid "Source code"
+msgstr "소스 코드"
+
+msgid "StarProject|Star"
+msgstr "별표"
+
+msgid "Start a %{new_merge_request} with these changes"
+msgstr "이 변경 사항으로 %{new_merge_request} 을 시작하십시오."
+
+msgid "Switch branch/tag"
+msgstr "스위치 브랜치/태그"
+
+msgid "Tag"
+msgid_plural "Tags"
+msgstr[0] "태그"
+
+msgid "Tags"
+msgstr "태그 "
+
+msgid "Target Branch"
+msgstr "대상 브랜치"
+
+msgid ""
+"The coding stage shows the time from the first commit to creating the merge "
+"request. The data will automatically be added here once you create your "
+"first merge request."
+msgstr ""
+"Coding Stage는 첫 번째 커밋에서부터 머지 리퀘스트 생성까지의 시간을 보여줍니다. 첫 번째 머지 리퀘스트을 생성하면 데이터가 "
+"자동으로 여기에 추가됩니다."
+
+msgid "The collection of events added to the data gathered for that stage."
+msgstr "해당 단계에서 수집 된 데이터가 이벤트 모음에 추가되었습니다."
+
+msgid "The fork relationship has been removed."
+msgstr "포크 관계가 제거되었습니다."
+
+msgid ""
+"The issue stage shows the time it takes from creating an issue to assigning "
+"the issue to a milestone, or add the issue to a list on your Issue Board. "
+"Begin creating issues to see data for this stage."
+msgstr ""
+"이슈 단계에는 이슈를 작성하여 마일스톤으로 지정하는 데 걸리는 시간 또는 이슈 보드의 목록에 이슈를 추가하는 시간이 표시됩니다. 이 "
+"단계의 데이터를 보기 위해서는 이슈를 먼저 작성해야 합니다."
+
+msgid "The phase of the development lifecycle."
+msgstr "개발 수명주기의 단계."
+
+msgid ""
+"The pipelines schedule runs pipelines in the future, repeatedly, for "
+"specific branches or tags. Those scheduled pipelines will inherit limited "
+"project access based on their associated user."
+msgstr ""
+"파이프라인 일정은 미래에 특정 브랜치 또는 태그에 대해 반복적으로 파이프라인을 실행합니다. 예정된 파이프라인은 관련 사용자를 기반으로 "
+"제한된 프로젝트 액세스 권한을 상속받습니다."
+
+msgid ""
+"The planning stage shows the time from the previous step to pushing your "
+"first commit. This time will be added automatically once you push your first "
+"commit."
+msgstr "계획 단계에서는 이전 단계에서 첫 번째 커밋 시간이 표시됩니다. 이 시간은 첫 번째 커밋을 누르면 자동으로 추가됩니다."
+
+msgid ""
+"The production stage shows the total time it takes between creating an issue "
+"and deploying the code to production. The data will be automatically added "
+"once you have completed the full idea to production cycle."
+msgstr ""
+"프로덕션 단계에서는 문제를 만들고 코드를 프로덕션 환경으로 배포하는 데 걸리는 총 시간을 보여줍니다. 생산주기에 대한 완전한 아이디어를 "
+"얻은 후에는 데이터가 자동으로 추가됩니다."
+
+msgid "The project can be accessed by any logged in user."
+msgstr "이 프로젝트는 로그인 한 사용자가만 액세스 할 수 있습니다."
+
+msgid "The project can be accessed without any authentication."
+msgstr "이 프로젝트는 인증없이 액세스 할 수 있습니다."
+
+msgid "The repository for this project does not exist."
+msgstr "이 프로젝트의 저장소가 존재하지 않습니다."
+
+msgid ""
+"The review stage shows the time from creating the merge request to merging "
+"it. The data will automatically be added after you merge your first merge "
+"request."
+msgstr ""
+"Review 단계에서는 머지 리퀘스트를 작성한 후 머지하기까지의 시간을 보여줍니다. 데이터는 첫 번째 머지 리퀘스트을 머지 한 후에 "
+"자동으로 추가됩니다."
+
+msgid ""
+"The staging stage shows the time between merging the MR and deploying code "
+"to the production environment. The data will be automatically added once you "
+"deploy to production for the first time."
+msgstr ""
+"Staging 단계에서는 MR 머지과 프로덕션 환경에 코드 배포 사이의 시간을 보여줍니다. 데이터를 Production 환경에 처음 "
+"배포하면 데이터가 자동으로 추가됩니다."
+
+msgid ""
+"The testing stage shows the time GitLab CI takes to run every pipeline for "
+"the related merge request. The data will automatically be added after your "
+"first pipeline finishes running."
+msgstr ""
+"테스트 단계에서는 GitLab CI가 관련 머지 리퀘스트을 위해 모든 파이프라인을 실행하는 데 걸리는 시간을 보여줍니다. 첫 번째 "
+"파이프라인 실행이 완료되면 데이터가 자동으로 추가됩니다."
+
+msgid "The time taken by each data entry gathered by that stage."
+msgstr "해당 단계에서 수집 한 각 데이터 입력에 소요 된 시간"
+
+msgid ""
+"The value lying at the midpoint of a series of observed values. E.g., "
+"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 ="
+" 6."
+msgstr ""
+"값은 일련의 관측 값 중점에 있습니다. 예를 들어, 3, 5, 9 사이의 중간 값은 5입니다. 3, 5, 7, 8 사이의 중간 값은 (5 "
+"+ 7) / 2 = 6입니다."
+
+msgid ""
+"This means you can not push code until you create an empty repository or "
+"import existing one."
+msgstr "즉, 빈 저장소를 만들거나 기존 저장소를 가져올 때까지 코드를 Push 할 수 없습니다."
+
+msgid "Time before an issue gets scheduled"
+msgstr "이슈가 스케줄되기 전의 시간"
+
+msgid "Time before an issue starts implementation"
+msgstr "이슈가 구현되기 전의 시간"
+
+msgid "Time between merge request creation and merge/close"
+msgstr "머지 리퀘스트 생성과 머지 / 닫기 사이의 시간"
+
+msgid "Time until first merge request"
+msgstr "첫 번째 머지 리퀘스트까지의 시간"
+
+msgid "Timeago|%s days ago"
+msgstr "%s 일 전"
+
+msgid "Timeago|%s days remaining"
+msgstr "%s 일 남음"
+
+msgid "Timeago|%s hours remaining"
+msgstr "%s 시간 남음"
+
+msgid "Timeago|%s minutes ago"
+msgstr "%s 분 전"
+
+msgid "Timeago|%s minutes remaining"
+msgstr "%s 분 남음"
+
+msgid "Timeago|%s months ago"
+msgstr "%s 개월 전"
+
+msgid "Timeago|%s months remaining"
+msgstr "%s 개월 남음"
+
+msgid "Timeago|%s seconds remaining"
+msgstr "%s 초 남음"
+
+msgid "Timeago|%s weeks ago"
+msgstr "%s 주 전"
+
+msgid "Timeago|%s weeks remaining"
+msgstr "%s 주 남음"
+
+msgid "Timeago|%s years ago"
+msgstr "%s 년 전"
+
+msgid "Timeago|%s years remaining"
+msgstr "%s 년 남음"
+
+msgid "Timeago|1 day remaining"
+msgstr "1 일 남음"
+
+msgid "Timeago|1 hour remaining"
+msgstr "1 시간 남음"
+
+msgid "Timeago|1 minute remaining"
+msgstr "1 분 남음"
+
+msgid "Timeago|1 month remaining"
+msgstr "1 개월 남음"
+
+msgid "Timeago|1 week remaining"
+msgstr "1 주 남음"
+
+msgid "Timeago|1 year remaining"
+msgstr "1 년 남음"
+
+msgid "Timeago|Past due"
+msgstr "기한 초과"
+
+msgid "Timeago|a day ago"
+msgstr "1 일 전"
+
+msgid "Timeago|a month ago"
+msgstr "1 달 전"
+
+msgid "Timeago|a week ago"
+msgstr "1 주일 전"
+
+msgid "Timeago|a while"
+msgstr "잠시 전"
+
+msgid "Timeago|a year ago"
+msgstr "1 년 전"
+
+msgid "Timeago|about %s hours ago"
+msgstr "약 %s 시간 전"
+
+msgid "Timeago|about a minute ago"
+msgstr "약 1 분 전"
+
+msgid "Timeago|about an hour ago"
+msgstr "약 1 시간 전"
+
+msgid "Timeago|in %s days"
+msgstr "%s 일 이내"
+
+msgid "Timeago|in %s hours"
+msgstr "%s 시간 이내"
+
+msgid "Timeago|in %s minutes"
+msgstr "%s 분 이내"
+
+msgid "Timeago|in %s months"
+msgstr "%s 개월 이내"
+
+msgid "Timeago|in %s seconds"
+msgstr "%s 초 이내"
+
+msgid "Timeago|in %s weeks"
+msgstr "%s 주 이내"
+
+msgid "Timeago|in %s years"
+msgstr "%s 년 이내"
+
+msgid "Timeago|in 1 day"
+msgstr "1 일 이내"
+
+msgid "Timeago|in 1 hour"
+msgstr "1 시간 이내"
+
+msgid "Timeago|in 1 minute"
+msgstr "1 분 이내"
+
+msgid "Timeago|in 1 month"
+msgstr "1 개월 이내"
+
+msgid "Timeago|in 1 week"
+msgstr "1 주일 이내"
+
+msgid "Timeago|in 1 year"
+msgstr "1 년 이내"
+
+msgid "Timeago|less than a minute ago"
+msgstr "1 분미만"
+
+msgid "Time|hr"
+msgid_plural "Time|hrs"
+msgstr[0] "시간"
+
+msgid "Time|min"
+msgid_plural "Time|mins"
+msgstr[0] "분"
+
+msgid "Time|s"
+msgstr "초"
+
+msgid "Total Time"
+msgstr "시간 합계:"
+
+msgid "Total test time for all commits/merges"
+msgstr "모든 커밋 / 머지의 총 테스트 시간"
+
+msgid "Unstar"
+msgstr "별표 제거"
+
+msgid "Upload New File"
+msgstr "새 파일 업로드"
+
+msgid "Upload file"
+msgstr "파일 업로드"
+
+msgid "UploadLink|click to upload"
+msgstr "업로드하려면 클릭하십시오."
+
+msgid "Use your global notification setting"
+msgstr "전체 알림 설정 사용"
+
+msgid "View open merge request"
+msgstr "열린 머지 리퀘스트보기"
+
+msgid "VisibilityLevel|Internal"
+msgstr "내부"
+
+msgid "VisibilityLevel|Private"
+msgstr "Private"
+
+msgid "VisibilityLevel|Public"
+msgstr "Public"
+
+msgid "VisibilityLevel|Unknown"
+msgstr "알 수 없음"
+
+msgid "Want to see the data? Please ask an administrator for access."
+msgstr "이 데이터를 보고 싶은가요? 관리자에게 액세스 권한을 요청하세요."
+
+msgid "We don't have enough data to show this stage."
+msgstr "이 단계를 보여주기에 충분한 데이터가 없습니다."
+
+msgid "Withdraw Access Request"
+msgstr "액세스 요청 철회"
+
+msgid ""
+"You are going to remove %{group_name}.\n"
+"Removed groups CANNOT be restored!\n"
+"Are you ABSOLUTELY sure?"
+msgstr "%{group_name}을(를) 제거하려고합니다.\n"
+"\"정말로\" 확실합니까?"
+
+msgid ""
+"You are going to remove %{project_name_with_namespace}.\n"
+"Removed project CANNOT be restored!\n"
+"Are you ABSOLUTELY sure?"
+msgstr ""
+"%{project_name_with_namespace}을(를) 삭제하려고합니다.\n"
+"삭제된 프로젝트를 복원 할 수 없습니다!\n"
+"\"정말로\" 확실합니까?"
+
+msgid ""
+"You are going to remove the fork relationship to source project "
+"%{forked_from_project}. Are you ABSOLUTELY sure?"
+msgstr "포크 관계를 소스 프로젝트 %{forked_from_project}에 대해 제거하려고합니다. \"정말로\" 확실합니까?"
+
+msgid ""
+"You are going to transfer %{project_name_with_namespace} to another owner. "
+"Are you ABSOLUTELY sure?"
+msgstr "%{project_name_with_namespace}을 다른 소유자에게 이전하려고합니다. \"정말로\" 확실합니까?"
+
+msgid "You can only add files when you are on a branch"
+msgstr "브랜치에 있을 때에만 파일을 추가 할 수 있습니다."
+
+msgid "You have reached your project limit"
+msgstr "프로젝트 숫자 한도에 도달했습니다."
+
+msgid "You must sign in to star a project"
+msgstr "프로젝트에 별표를 표시하려면 로그인 해야 합니다."
+
+msgid "You need permission."
+msgstr "권한이 필요합니다."
+
+msgid "You will not get any notifications via email"
+msgstr "이메일로 알림을 받지 않습니다."
+
+msgid "You will only receive notifications for the events you choose"
+msgstr "선택한 이벤트에 대한 알림만 받습니다."
+
+msgid ""
+"You will only receive notifications for threads you have participated in"
+msgstr "참여한 스레드에 대한 알림만 받습니다."
+
+msgid "You will receive notifications for any activity"
+msgstr "모든 활동에 대한 알림을 받게됩니다."
+
+msgid ""
+"You will receive notifications only for comments in which you were "
+"@mentioned"
+msgstr "당신은 당신이 @mentioned 한 코멘트에 대해서만 통지를 받게됩니다."
+
+msgid ""
+"You won't be able to pull or push project code via %{protocol} until you "
+"%{set_password_link} on your account"
+msgstr ""
+"당신의 계정에 %{set_password_link} 을 하기 전에는 %{protocol} 프로토콜을 통해 프로젝트 코드를 Pull 하거나 "
+"Push 할 수 없습니다"
+
+msgid ""
+"You won't be able to pull or push project code via SSH until you "
+"%{add_ssh_key_link} to your profile"
+msgstr ""
+"당신의 프로필에 %{add_ssh_key_link} 을(를) 하기 전에는 SSH를 통해 프로젝트 코드를 Pull 하거나 Push 할 수 "
+"없습니다"
+
+msgid "Your name"
+msgstr "귀하의 이름"
+
+msgid "day"
+msgid_plural "days"
+msgstr[0] "일"
+
+msgid "new merge request"
+msgstr "새 머지 리퀘스트"
+
+msgid "notification emails"
+msgstr "알림 이메일"
+
+msgid "parent"
+msgid_plural "parents"
+msgstr[0] "부모"
+
diff --git a/locale/ko/gitlab.po.time_stamp b/locale/ko/gitlab.po.time_stamp
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/locale/ko/gitlab.po.time_stamp
diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po
index 9e3c78b6148..2eaadb64124 100644
--- a/locale/pt_BR/gitlab.po
+++ b/locale/pt_BR/gitlab.po
@@ -6,13 +6,13 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-07-05 08:50-0500\n"
+"POT-Creation-Date: 2017-07-13 12:07-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language-Team: Portuguese (Brazil) (https://translate.zanata.org/project/view/GitLab)\n"
-"PO-Revision-Date: 2017-08-01 09:47-0400\n"
-"Last-Translator: Huang Tao <htve@outlook.com>\n"
+"PO-Revision-Date: 2017-08-03 11:29-0400\n"
+"Last-Translator: Alexandre Alencar <alexandre.alencar@gmail.com>\n"
"Language: pt-BR\n"
"X-Generator: Zanata 3.9.6\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
@@ -92,7 +92,7 @@ msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "Procurar por branches"
msgid "BranchSwitcherTitle|Switch branch"
-msgstr "BranchSwitcherTitle|Mudar de branch"
+msgstr "Mudar de branch"
msgid "Branches"
msgstr "Branches"
@@ -1152,6 +1152,9 @@ msgstr "Privado"
msgid "VisibilityLevel|Public"
msgstr "Público"
+msgid "VisibilityLevel|Unknown"
+msgstr "Desconhecido"
+
msgid "Want to see the data? Please ask an administrator for access."
msgstr "Precisa visualizar os dados? Solicite acesso ao administrador."
diff --git a/locale/ru/gitlab.po b/locale/ru/gitlab.po
index f9e8dcd05e7..78f7b059077 100644
--- a/locale/ru/gitlab.po
+++ b/locale/ru/gitlab.po
@@ -4,12 +4,12 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-07-05 08:50-0500\n"
+"POT-Creation-Date: 2017-07-13 12:07-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language-Team: Russian (https://translate.zanata.org/project/view/GitLab)\n"
-"PO-Revision-Date: 2017-08-01 09:15-0400\n"
+"PO-Revision-Date: 2017-08-06 11:23-0400\n"
"Last-Translator: Андрей П. <fenixnow33@gmail.com>\n"
"Language: ru\n"
"X-Generator: Zanata 3.9.6\n"
@@ -221,7 +221,7 @@ msgid "CommitBoxTitle|Commit"
msgstr "Коммит"
msgid "CommitMessage|Add %{file_name}"
-msgstr "CommitMessage|Добавить %{file_name}"
+msgstr "Добавлен %{file_name}"
msgid "Commits"
msgstr "Коммиты"
@@ -301,14 +301,14 @@ msgstr ""
"посмотрите %{notification_link}."
msgid "Cycle Analytics"
-msgstr "Аналитический цикл"
+msgstr "Цикл Аналитик"
msgid ""
"Cycle Analytics gives an overview of how much time it takes to go from idea "
"to production in your project."
msgstr ""
-"Аналитический цикл дает представление о том, сколько времени требуется, "
-"чтобы перейти от идеи к производству в проекте."
+"Цикл Аналитик дает представление о том, сколько времени требуется, чтобы "
+"перейти от идеи к производству в проекте."
msgid "CycleAnalyticsStage|Code"
msgstr "Написание кода"
@@ -452,7 +452,7 @@ msgid "Interval Pattern"
msgstr "Шаблон интервала"
msgid "Introducing Cycle Analytics"
-msgstr "Внедрение Цикла Аналитики"
+msgstr "Внедрение Цикла Аналитик"
msgid "Jobs for last month"
msgstr "Работы за прошлый месяц"
@@ -599,10 +599,10 @@ msgid "NotificationLevel|Global"
msgstr "Глобальный"
msgid "NotificationLevel|On mention"
-msgstr "С упоминанием"
+msgstr "Упоминание"
msgid "NotificationLevel|Participate"
-msgstr "По участию"
+msgstr "Участие"
msgid "NotificationLevel|Watch"
msgstr "Отслеживать"
@@ -740,7 +740,7 @@ msgstr ""
"почте."
msgid "Project home"
-msgstr "Домашняя страница проекта"
+msgstr "Домашняя страница"
msgid "ProjectFeature|Disabled"
msgstr "Отключено"
@@ -874,7 +874,7 @@ msgid "Tags"
msgstr "Теги"
msgid "Target Branch"
-msgstr "Целевая ветка"
+msgstr "Ветка"
msgid ""
"The coding stage shows the time from the first commit to creating the merge "
@@ -1167,6 +1167,9 @@ msgstr "Приватный"
msgid "VisibilityLevel|Public"
msgstr "Публичный"
+msgid "VisibilityLevel|Unknown"
+msgstr "Не определен"
+
msgid "Want to see the data? Please ask an administrator for access."
msgstr "Хотите увидеть данные? Обратитесь к администратору за доступом."
diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po
index c1b99be3433..78144d3755d 100644
--- a/locale/uk/gitlab.po
+++ b/locale/uk/gitlab.po
@@ -4,12 +4,12 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-07-05 08:50-0500\n"
+"POT-Creation-Date: 2017-07-13 12:07-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language-Team: Ukrainian (https://translate.zanata.org/project/view/GitLab)\n"
-"PO-Revision-Date: 2017-08-01 09:15-0400\n"
+"PO-Revision-Date: 2017-08-06 11:23-0400\n"
"Last-Translator: Андрей Витюк <andruwa13@gmail.com>\n"
"Language: uk\n"
"X-Generator: Zanata 3.9.6\n"
@@ -209,9 +209,7 @@ msgstr[1] "Комміта"
msgstr[2] "Коммітів"
msgid "Commit duration in minutes for last 30 commits"
-msgstr ""
-"Тривалість коммітів протягом декількох хвилин на протязі 30 останніх "
-"коммітів"
+msgstr "Тривалість останніх 30 коммітів у хвилинах"
msgid "Commit message"
msgstr "Комміт повідомлення"
@@ -399,7 +397,7 @@ msgid "Failed to remove the pipeline schedule"
msgstr "Не вдалося видалити розклад Конвеєра"
msgid "Files"
-msgstr "Файлів"
+msgstr "Файли"
msgid "Filter by commit message"
msgstr "Фільтрувати повідомлення коммітів"
@@ -496,9 +494,9 @@ msgstr "Залишити проект"
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "Обмеження %d події"
+msgstr[1] "Обмеження %d подій"
+msgstr[2] "Обмеження %d подій"
msgid "Median"
msgstr "Медіана"
@@ -846,9 +844,9 @@ msgstr "встановити пароль"
msgid "Showing %d event"
msgid_plural "Showing %d events"
-msgstr[0] ""
-msgstr[1] ""
-msgstr[2] ""
+msgstr[0] "Показано %d подію"
+msgstr[1] "Показано %d події"
+msgstr[2] "Показано %d подій"
msgid "Source code"
msgstr "Код"
@@ -879,9 +877,12 @@ msgid ""
"request. The data will automatically be added here once you create your "
"first merge request."
msgstr ""
+"На стадії написання коду, показує час першого комміту до створення запиту на "
+"об'єднання. Дані будуть автоматично додані після створення вашого першого "
+"запиту на об'єднання."
msgid "The collection of events added to the data gathered for that stage."
-msgstr ""
+msgstr "Колекція подій додана до даних, зібраних для цього етапу."
msgid "The fork relationship has been removed."
msgstr "Зв'язок форка видалена."
@@ -912,12 +913,17 @@ msgid ""
"first commit. This time will be added automatically once you push your first "
"commit."
msgstr ""
+"На етапі планування відображається час від попереднього кроку до першого "
+"комміту. Додається автоматично, як тільки відправится перший комміт."
msgid ""
"The production stage shows the total time it takes between creating an issue "
"and deploying the code to production. The data will be automatically added "
"once you have completed the full idea to production cycle."
msgstr ""
+"Стадія ПРОДакшин показує загальний час між створенням проблеми та "
+"розгортанням коду у ПРОДакшині. Дані будуть автоматично додані після "
+"завершення повної ідеї до ПРОДакшин циклу."
msgid "The project can be accessed by any logged in user."
msgstr "Доступ до проекту можливий будь-яким зареєстрованим користувачем."
@@ -933,27 +939,37 @@ msgid ""
"it. The data will automatically be added after you merge your first merge "
"request."
msgstr ""
+"Стадія огляду показує час від створення запиту про об'єднання до його "
+"виконання. Дані будуть автоматично додані після завершення першого запиту на "
+"злиття."
msgid ""
"The staging stage shows the time between merging the MR and deploying code "
"to the production environment. The data will be automatically added once you "
"deploy to production for the first time."
msgstr ""
+"Стадія ДЕВ показує час між злиттям \"MR\" та розгортанням коду у ПРОДакшин. "
+"Дані автоматично додаються після розгортання у ПРОДакшин вперше."
msgid ""
"The testing stage shows the time GitLab CI takes to run every pipeline for "
"the related merge request. The data will automatically be added after your "
"first pipeline finishes running."
msgstr ""
+"Стадія тестування показує час, який GitLab CI виконує для запуску кожного "
+"конвеєра для відповідного запиту злиття. Дані будуть автоматично додані "
+"після завершення першого конвеєра."
msgid "The time taken by each data entry gathered by that stage."
-msgstr ""
+msgstr "Час, витрачений на кожен елемент, зібраний на цьому етапі."
msgid ""
"The value lying at the midpoint of a series of observed values. E.g., "
"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 ="
" 6."
msgstr ""
+"Середнє значення в рядку. Приклад: між 3, 5, 9, середніми 5, між 3, 5, 7, 8, "
+"середніми (5 + 7) / 2 = 6."
msgid ""
"This means you can not push code until you create an empty repository or "
@@ -963,10 +979,10 @@ msgstr ""
"репозиторій або НЕ імпортуєте існуючий."
msgid "Time before an issue gets scheduled"
-msgstr ""
+msgstr "Час до початку потрапляння проблеми в планувальник"
msgid "Time before an issue starts implementation"
-msgstr ""
+msgstr "Час до початку роботи над проблемою"
msgid "Time between merge request creation and merge/close"
msgstr "Час між створенням запиту злиття і злиттям або закриттям"
@@ -1047,7 +1063,7 @@ msgid "Timeago|a year ago"
msgstr "рік тому"
msgid "Timeago|about %s hours ago"
-msgstr "Близько %s годин тому"
+msgstr "Близько %s годин(и) тому"
msgid "Timeago|about a minute ago"
msgstr "Близько хвилини тому"
@@ -1145,6 +1161,9 @@ msgstr "Приватний"
msgid "VisibilityLevel|Public"
msgstr "Публічний"
+msgid "VisibilityLevel|Unknown"
+msgstr "Невідомий"
+
msgid "Want to see the data? Please ask an administrator for access."
msgstr "Хочете побачити дані? Будь ласка, попросить у адміністратора доступ."
diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po
index f471e7def25..4a550db55d2 100644
--- a/locale/zh_CN/gitlab.po
+++ b/locale/zh_CN/gitlab.po
@@ -1089,6 +1089,9 @@ msgstr "私有"
msgid "VisibilityLevel|Public"
msgstr "公开"
+msgid "VisibilityLevel|Unknown"
+msgstr "未知"
+
msgid "Want to see the data? Please ask an administrator for access."
msgstr "权限不足。如需查看相关数据,请向管理员申请权限。"
diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po
index 1b7c39f8f62..69b2bf80dbf 100644
--- a/locale/zh_HK/gitlab.po
+++ b/locale/zh_HK/gitlab.po
@@ -1088,6 +1088,9 @@ msgstr "私有"
msgid "VisibilityLevel|Public"
msgstr "公開"
+msgid "VisibilityLevel|Unknown"
+msgstr "未知"
+
msgid "Want to see the data? Please ask an administrator for access."
msgstr "權限不足。如需查看相關數據,請向管理員申請權限。"
diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po
index af663275602..4fd728659c6 100644
--- a/locale/zh_TW/gitlab.po
+++ b/locale/zh_TW/gitlab.po
@@ -12,8 +12,8 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language-Team: Chinese (Taiwan) (https://translate.zanata.org/project/view/GitLab)\n"
-"PO-Revision-Date: 2017-07-20 09:50-0400\n"
-"Last-Translator: Lin Jen-Shin <godfat@godfat.org>\n"
+"PO-Revision-Date: 2017-08-07 03:30-0400\n"
+"Last-Translator: Huang Tao <htve@outlook.com>\n"
"Language: zh-TW\n"
"X-Generator: Zanata 3.9.6\n"
"Plural-Forms: nplurals=1; plural=0\n"
@@ -1099,6 +1099,9 @@ msgstr "私有"
msgid "VisibilityLevel|Public"
msgstr "公開"
+msgid "VisibilityLevel|Unknown"
+msgstr "不明"
+
msgid "Want to see the data? Please ask an administrator for access."
msgstr "權限不足。如需查看相關資料,請向管理員申請權限。"
diff --git a/package.json b/package.json
index 3211afbd0e7..9bc8db66e06 100644
--- a/package.json
+++ b/package.json
@@ -12,14 +12,16 @@
"webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
},
"dependencies": {
+ "axios": "^0.16.2",
"babel-core": "^6.22.1",
"babel-eslint": "^7.2.1",
- "babel-loader": "^6.2.10",
+ "babel-loader": "^7.1.1",
"babel-plugin-transform-define": "^1.2.0",
"babel-preset-latest": "^6.24.0",
"babel-preset-stage-2": "^6.22.0",
"bootstrap-sass": "^3.3.6",
"compression-webpack-plugin": "^0.3.2",
+ "copy-webpack-plugin": "^4.0.1",
"core-js": "^2.4.1",
"cropper": "^2.3.0",
"css-loader": "^0.28.0",
@@ -31,6 +33,7 @@
"eslint-plugin-html": "^2.0.1",
"exports-loader": "^0.6.4",
"file-loader": "^0.11.1",
+ "imports-loader": "^0.7.1",
"jed": "^1.1.1",
"jquery": "^2.2.1",
"jquery-ujs": "^1.2.1",
@@ -38,6 +41,7 @@
"jszip": "^3.1.3",
"jszip-utils": "^0.0.2",
"marked": "^0.3.6",
+ "monaco-editor": "0.8.3",
"mousetrap": "^1.4.6",
"name-all-modules-plugin": "^1.0.1",
"pikaday": "^1.5.1",
@@ -48,7 +52,6 @@
"react-dev-utils": "^0.5.2",
"select2": "3.5.2-browserify",
"sql.js": "^0.4.0",
- "stats-webpack-plugin": "^0.4.3",
"three": "^0.84.0",
"three-orbit-controls": "^82.1.0",
"three-stl-loader": "^1.0.4",
@@ -61,14 +64,15 @@
"vue-resource": "^1.3.4",
"vue-template-compiler": "^2.2.6",
"vuex": "^2.3.1",
- "webpack": "^2.6.1",
- "webpack-bundle-analyzer": "^2.8.2"
+ "webpack": "^3.4.0",
+ "webpack-bundle-analyzer": "^2.8.2",
+ "webpack-stats-plugin": "^0.1.5"
},
"devDependencies": {
"babel-plugin-istanbul": "^4.0.0",
"eslint": "^3.10.1",
"eslint-config-airbnb-base": "^10.0.1",
- "eslint-import-resolver-webpack": "^0.8.1",
+ "eslint-import-resolver-webpack": "^0.8.3",
"eslint-plugin-filenames": "^1.1.0",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-jasmine": "^2.1.0",
@@ -82,8 +86,8 @@
"karma-jasmine": "^1.1.0",
"karma-mocha-reporter": "^2.2.2",
"karma-sourcemap-loader": "^0.3.7",
- "karma-webpack": "^2.0.2",
+ "karma-webpack": "^2.0.4",
"nodemon": "^1.11.0",
- "webpack-dev-server": "^2.4.2"
+ "webpack-dev-server": "^2.6.1"
}
}
diff --git a/spec/controllers/admin/health_check_controller_spec.rb b/spec/controllers/admin/health_check_controller_spec.rb
new file mode 100644
index 00000000000..0b8e0c8a065
--- /dev/null
+++ b/spec/controllers/admin/health_check_controller_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Admin::HealthCheckController, broken_storage: true do
+ let(:admin) { create(:admin) }
+
+ before do
+ sign_in(admin)
+ end
+
+ describe 'GET show' do
+ it 'loads the git storage health information' do
+ get :show
+
+ expect(assigns[:failing_storage_statuses]).not_to be_nil
+ end
+ end
+
+ describe 'POST reset_storage_health' do
+ it 'resets all storage health information' do
+ expect(Gitlab::Git::Storage::CircuitBreaker).to receive(:reset_all!)
+
+ post :reset_storage_health
+ end
+ end
+end
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index 1641bddea11..331903a5543 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -108,6 +108,30 @@ describe ApplicationController do
end
end
+ describe 'rescue from Gitlab::Git::Storage::Inaccessible' do
+ controller(described_class) do
+ def index
+ raise Gitlab::Git::Storage::Inaccessible.new('broken', 100)
+ end
+ end
+
+ it 'renders a 503 when storage is not available' do
+ sign_in(create(:user))
+
+ get :index
+
+ expect(response.status).to eq(503)
+ end
+
+ it 'renders includes a Retry-After header' do
+ sign_in(create(:user))
+
+ get :index
+
+ expect(response.headers['Retry-After']).to eq(100)
+ end
+ end
+
describe 'response format' do
controller(described_class) do
def index
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 59f33197e8f..64b9af7b845 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -35,6 +35,26 @@ describe Projects::BlobController do
end
end
+ context 'with file path and JSON format' do
+ context "valid branch, valid file" do
+ let(:id) { 'master/README.md' }
+
+ before do
+ get(:show,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: id,
+ format: :json)
+ end
+
+ it do
+ expect(response).to be_ok
+ expect(json_response).to have_key 'html'
+ expect(json_response).to have_key 'raw_path'
+ end
+ end
+ end
+
context 'with tree path' do
before do
get(:show,
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 34095ef6250..8ecd8b6ca71 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -107,6 +107,20 @@ describe ProjectsController do
end
end
+ context 'when the storage is not available', broken_storage: true do
+ let(:project) { create(:project, :broken_storage) }
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ end
+
+ it 'renders a 503' do
+ get :show, namespace_id: project.namespace, id: project
+
+ expect(response).to have_http_status(503)
+ end
+ end
+
context "project with empty repo" do
let(:empty_project) { create(:project_empty_repo, :public) }
diff --git a/spec/factories/conversational_development_index_metrics.rb b/spec/factories/conversational_development_index_metrics.rb
index a5412629195..3806c43ba15 100644
--- a/spec/factories/conversational_development_index_metrics.rb
+++ b/spec/factories/conversational_development_index_metrics.rb
@@ -2,32 +2,42 @@ FactoryGirl.define do
factory :conversational_development_index_metric, class: ConversationalDevelopmentIndex::Metric do
leader_issues 9.256
instance_issues 1.234
+ percentage_issues 13.331
leader_notes 30.33333
instance_notes 28.123
+ percentage_notes 92.713
leader_milestones 16.2456
instance_milestones 1.234
+ percentage_milestones 7.595
leader_boards 5.2123
instance_boards 3.254
+ percentage_boards 62.429
leader_merge_requests 1.2
instance_merge_requests 0.6
+ percentage_merge_requests 50.0
leader_ci_pipelines 12.1234
instance_ci_pipelines 2.344
+ percentage_ci_pipelines 19.334
leader_environments 3.3333
instance_environments 2.2222
+ percentage_environments 66.672
leader_deployments 1.200
instance_deployments 0.771
+ percentage_deployments 64.25
leader_projects_prometheus_active 0.111
instance_projects_prometheus_active 0.109
+ percentage_projects_prometheus_active 98.198
leader_service_desk_issues 15.891
instance_service_desk_issues 13.345
+ percentage_service_desk_issues 83.978
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index be3f219e8bf..3f8e7030b1c 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -54,6 +54,12 @@ FactoryGirl.define do
avatar { File.open(Rails.root.join('spec/fixtures/dk.png')) }
end
+ trait :broken_storage do
+ after(:create) do |project|
+ project.update_column(:repository_storage, 'broken')
+ end
+ end
+
# Test repository - https://gitlab.com/gitlab-org/gitlab-test
trait :repository do
path { 'gitlabhq' }
diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb
index 106e7370a98..37fd3e171eb 100644
--- a/spec/features/admin/admin_health_check_spec.rb
+++ b/spec/features/admin/admin_health_check_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature "Admin Health Check" do
+feature "Admin Health Check", feature: true, broken_storage: true do
include StubENV
before do
@@ -55,4 +55,26 @@ feature "Admin Health Check" do
expect(page).to have_content('The server is on fire')
end
end
+
+ context 'with repository storage failures' do
+ before do
+ # Track a failure
+ Gitlab::Git::Storage::CircuitBreaker.for_storage('broken').perform { nil } rescue nil
+ visit admin_health_check_path
+ end
+
+ it 'shows storage failure information' do
+ hostname = Gitlab::Environment.hostname
+
+ expect(page).to have_content('broken: failed storage access attempt on host:')
+ expect(page).to have_content("#{hostname}: 1 of 10 failures.")
+ end
+
+ it 'allows resetting storage failures' do
+ click_button 'Reset git storage health information'
+
+ expect(page).to have_content('Git storage health information has been reset')
+ expect(page).not_to have_content('failed storage access attempt')
+ end
+ end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index c51b81c1cff..ce458431c55 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -233,7 +233,7 @@ describe 'Issue Boards', js: true do
wait_for_board_cards(4, 1)
expect(find('.board:nth-child(3)')).to have_content(issue6.title)
- expect(find('.board:nth-child(3)').all('.card').last).not_to have_content(development.title)
+ expect(find('.board:nth-child(3)').all('.card').last).to have_content(development.title)
end
it 'issue moves between lists' do
@@ -244,7 +244,7 @@ describe 'Issue Boards', js: true do
wait_for_board_cards(4, 1)
expect(find('.board:nth-child(2)')).to have_content(issue7.title)
- expect(find('.board:nth-child(2)').all('.card').first).not_to have_content(planning.title)
+ expect(find('.board:nth-child(2)').all('.card').first).to have_content(planning.title)
end
it 'issue moves from closed' do
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 373cd92793e..8d3d4ff8773 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -257,7 +257,7 @@ describe 'Issue Boards', js: true do
end
end
- expect(card).to have_selector('.label', count: 2)
+ expect(card).to have_selector('.label', count: 3)
expect(card).to have_content(bug.title)
end
@@ -283,7 +283,7 @@ describe 'Issue Boards', js: true do
end
end
- expect(card).to have_selector('.label', count: 3)
+ expect(card).to have_selector('.label', count: 4)
expect(card).to have_content(bug.title)
expect(card).to have_content(regression.title)
end
@@ -308,7 +308,7 @@ describe 'Issue Boards', js: true do
end
end
- expect(card).not_to have_selector('.label')
+ expect(card).to have_selector('.label', count: 1)
expect(card).not_to have_content(stretch.title)
end
end
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index be6f78ee607..795335aa106 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -79,12 +79,21 @@ RSpec.describe 'Dashboard Issues' do
end
end
- it 'shows the new issue page', :js do
+ it 'shows the new issue page', js: true do
find('.new-project-item-select-button').trigger('click')
+
wait_for_requests
- find('.select2-results li').click
- expect(page).to have_current_path("/#{project.path_with_namespace}/issues/new")
+ project_path = "/#{project.path_with_namespace}"
+ project_json = { name: project.name_with_namespace, url: project_path }.to_json
+
+ # similate selection, and prevent overlap by dropdown menu
+ execute_script("$('.project-item-select').val('#{project_json}').trigger('change');")
+ execute_script("$('#select2-drop-mask').remove();")
+
+ find('.new-project-item-link').trigger('click')
+
+ expect(page).to have_current_path("#{project_path}/issues/new")
page.within('#content-body') do
expect(page).to have_selector('.issue-form')
diff --git a/spec/features/groups/empty_states_spec.rb b/spec/features/groups/empty_states_spec.rb
index 7f28553c44e..243e8536168 100644
--- a/spec/features/groups/empty_states_spec.rb
+++ b/spec/features/groups/empty_states_spec.rb
@@ -38,7 +38,7 @@ feature 'Groups Merge Requests Empty States' do
it 'should show a new merge request button' do
within '.empty-state' do
- expect(page).to have_content('New merge request')
+ expect(page).to have_content('create merge request')
end
end
@@ -63,7 +63,7 @@ feature 'Groups Merge Requests Empty States' do
it 'should not show a new merge request button' do
within '.empty-state' do
- expect(page).not_to have_link('New merge request')
+ expect(page).not_to have_link('create merge request')
end
end
end
diff --git a/spec/features/issues/create_branch_merge_request_spec.rb b/spec/features/issues/create_branch_merge_request_spec.rb
index f59f687cf51..546dc7e8a49 100644
--- a/spec/features/issues/create_branch_merge_request_spec.rb
+++ b/spec/features/issues/create_branch_merge_request_spec.rb
@@ -1,6 +1,6 @@
require 'rails_helper'
-feature 'Create Branch/Merge Request Dropdown on issue page', js: true do
+feature 'Create Branch/Merge Request Dropdown on issue page', :feature, :js do
let(:user) { create(:user) }
let!(:project) { create(:project, :repository) }
let(:issue) { create(:issue, project: project, title: 'Cherry-Coloured Funk') }
@@ -14,10 +14,14 @@ feature 'Create Branch/Merge Request Dropdown on issue page', js: true do
it 'allows creating a merge request from the issue page' do
visit project_issue_path(project, issue)
- select_dropdown_option('create-mr')
-
- expect(page).to have_content('WIP: Resolve "Cherry-Coloured Funk"')
- expect(current_path).to eq(project_merge_request_path(project, MergeRequest.first))
+ perform_enqueued_jobs do
+ select_dropdown_option('create-mr')
+
+ expect(page).to have_content('WIP: Resolve "Cherry-Coloured Funk"')
+ expect(current_path).to eq(project_merge_request_path(project, MergeRequest.first))
+
+ wait_for_requests
+ end
visit project_issue_path(project, issue)
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 8e22441e0e8..af11b474842 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -130,8 +130,8 @@ feature 'Issue Sidebar' do
it 'adds new label' do
page.within('.block.labels') do
fill_in 'new_label_name', with: 'wontfix'
- page.find(".suggest-colors a", match: :first).click
- click_button 'Create'
+ page.find('.suggest-colors a', match: :first).trigger('click')
+ page.find('button', text: 'Create').trigger('click')
page.within('.dropdown-page-one') do
expect(page).to have_content 'wontfix'
@@ -142,8 +142,8 @@ feature 'Issue Sidebar' do
it 'shows error message if label title is taken' do
page.within('.block.labels') do
fill_in 'new_label_name', with: label.title
- page.find('.suggest-colors a', match: :first).click
- click_button 'Create'
+ page.find('.suggest-colors a', match: :first).trigger('click')
+ page.find('button', text: 'Create').trigger('click')
page.within('.dropdown-page-two') do
expect(page).to have_content 'Title has already been taken'
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 489baa4291f..a5bb642221c 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -706,4 +706,30 @@ describe 'Issues' do
expect(page).to have_text("updated title")
end
end
+
+ describe 'confidential issue#show', js: true do
+ it 'shows confidential sibebar information as confidential and can be turned off' do
+ issue = create(:issue, :confidential, project: project)
+
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_css('.confidential-issue-warning')
+ expect(page).to have_css('.is-confidential')
+ expect(page).not_to have_css('.is-not-confidential')
+
+ find('.confidential-edit').click
+ expect(page).to have_css('.confidential-warning-message')
+
+ within('.confidential-warning-message') do
+ find('.btn-close').click
+ end
+
+ wait_for_requests
+
+ visit project_issue_path(project, issue)
+
+ expect(page).not_to have_css('.is-confidential')
+ expect(page).to have_css('.is-not-confidential')
+ end
+ end
end
diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb
index 0e97254eada..299b4f5708a 100644
--- a/spec/features/merge_requests/closes_issues_spec.rb
+++ b/spec/features/merge_requests/closes_issues_spec.rb
@@ -26,17 +26,11 @@ feature 'Merge Request closing issues message', js: true do
wait_for_requests
end
- context 'not closing or mentioning any issue' do
- it 'does not display closing issue message' do
- expect(page).not_to have_css('.mr-widget-footer')
- end
- end
-
context 'closing issues but not mentioning any other issue' do
let(:merge_request_description) { "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" }
it 'does not display closing issue message' do
- expect(page).to have_content("Closes issues #{issue_1.to_reference} and #{issue_2.to_reference}")
+ expect(page).to have_content("Closes #{issue_1.to_reference} and #{issue_2.to_reference}")
end
end
@@ -44,7 +38,7 @@ feature 'Merge Request closing issues message', js: true do
let(:merge_request_description) { "Description\n\nRefers to #{issue_1.to_reference} and #{issue_2.to_reference}" }
it 'does not display closing issue message' do
- expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not be closed.")
+ expect(page).to have_content("Mentions #{issue_1.to_reference} and #{issue_2.to_reference}")
end
end
@@ -52,8 +46,8 @@ feature 'Merge Request closing issues message', js: true do
let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
it 'does not display closing issue message' do
- expect(page).to have_content("Closes issue #{issue_1.to_reference}.")
- expect(page).to have_content("Issue #{issue_2.to_reference} is mentioned but will not be closed.")
+ expect(page).to have_content("Closes #{issue_1.to_reference}")
+ expect(page).to have_content("Mentions #{issue_2.to_reference}")
end
end
@@ -61,7 +55,7 @@ feature 'Merge Request closing issues message', js: true do
let(:merge_request_title) { "closing #{issue_1.to_reference}, #{issue_2.to_reference}" }
it 'does not display closing issue message' do
- expect(page).to have_content("Closes issues #{issue_1.to_reference} and #{issue_2.to_reference}")
+ expect(page).to have_content("Closes #{issue_1.to_reference} and #{issue_2.to_reference}")
end
end
@@ -69,7 +63,7 @@ feature 'Merge Request closing issues message', js: true do
let(:merge_request_title) { "Refers to #{issue_1.to_reference} and #{issue_2.to_reference}" }
it 'does not display closing issue message' do
- expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not be closed.")
+ expect(page).to have_content("Mentions #{issue_1.to_reference} and #{issue_2.to_reference}")
end
end
@@ -77,8 +71,8 @@ feature 'Merge Request closing issues message', js: true do
let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
it 'does not display closing issue message' do
- expect(page).to have_content("Closes issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not be closed.")
- expect(page).to have_content("Issue #{issue_2.to_reference} is mentioned but will not be closed.")
+ expect(page).to have_content("Closes #{issue_1.to_reference}")
+ expect(page).to have_content("Mentions #{issue_2.to_reference}")
end
end
end
diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb
index 6ffb05c5030..89410b0e90f 100644
--- a/spec/features/merge_requests/form_spec.rb
+++ b/spec/features/merge_requests/form_spec.rb
@@ -41,7 +41,7 @@ describe 'New/edit merge request', :js do
expect(page).to have_content user2.name
end
- click_link 'Assign to me'
+ find('a', text: 'Assign to me').trigger('click')
expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user.name
diff --git a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
index 574f5fe353e..ac46cc1f0e4 100644
--- a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
+++ b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb
@@ -41,8 +41,8 @@ feature 'Merge When Pipeline Succeeds', :js do
it 'activates the Merge when pipeline succeeds feature' do
click_button "Merge when pipeline succeeds"
- expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds."
- expect(page).to have_content "The source branch will not be removed."
+ expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds"
+ expect(page).to have_content "The source branch will not be removed"
expect(page).to have_selector ".js-cancel-auto-merge"
visit_merge_request(merge_request) # Needed to refresh the page
expect(page).to have_content /enabled an automatic merge when the pipeline for \h{8} succeeds/i
@@ -97,11 +97,11 @@ feature 'Merge When Pipeline Succeeds', :js do
describe 'enabling Merge when pipeline succeeds via dropdown' do
it 'activates the Merge when pipeline succeeds feature' do
- click_button 'Select merge moment'
+ find('.js-merge-moment').click
click_link 'Merge when pipeline succeeds'
- expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds."
- expect(page).to have_content "The source branch will not be removed."
+ expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds"
+ expect(page).to have_content "The source branch will not be removed"
expect(page).to have_link "Cancel automatic merge"
end
end
diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
index 5c6eec44ff7..59e67420333 100644
--- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
+++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb
@@ -43,7 +43,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', js: t
wait_for_requests
expect(page).to have_button 'Merge when pipeline succeeds'
- expect(page).not_to have_button 'Select merge moment'
+ expect(page).not_to have_button '.js-merge-moment'
end
end
@@ -56,7 +56,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', js: t
wait_for_requests
expect(page).to have_css('button[disabled="disabled"]', text: 'Merge')
- expect(page).to have_content('Please retry the job or push a new commit to fix the failure.')
+ expect(page).to have_content('Please retry the job or push a new commit to fix the failure')
end
end
@@ -69,7 +69,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', js: t
wait_for_requests
expect(page).not_to have_button 'Merge'
- expect(page).to have_content('Please retry the job or push a new commit to fix the failure.')
+ expect(page).to have_content('Please retry the job or push a new commit to fix the failure')
end
end
@@ -113,7 +113,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', js: t
expect(page).to have_button 'Merge when pipeline succeeds'
- click_button 'Select merge moment'
+ page.find('.js-merge-moment').click
expect(page).to have_content 'Merge immediately'
end
end
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
index f0d36489672..9b5c21d752c 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -3,17 +3,17 @@ require 'rails_helper'
feature 'Merge Requests > User uses quick actions', js: true do
include QuickActionsHelpers
- let(:user) { create(:user) }
- let(:project) { create(:project, :public, :repository) }
- let(:merge_request) { create(:merge_request, source_project: project) }
- let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
-
it_behaves_like 'issuable record that supports quick actions in its description and notes', :merge_request do
let(:issuable) { create(:merge_request, source_project: project) }
let(:new_url_opts) { { merge_request: { source_branch: 'feature', target_branch: 'master' } } }
end
describe 'merge-request-only commands' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
+
before do
project.team << [user, :master]
sign_in(user)
diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb
index 69e31c7481f..fd991293ee9 100644
--- a/spec/features/merge_requests/widget_spec.rb
+++ b/spec/features/merge_requests/widget_spec.rb
@@ -219,4 +219,17 @@ describe 'Merge request', :js do
expect(page).to have_field('remove-source-branch-input', disabled: true)
end
end
+
+ context 'ongoing merge process' do
+ it 'shows Merging state' do
+ allow_any_instance_of(MergeRequest).to receive(:merge_ongoing?).and_return(true)
+
+ visit project_merge_request_path(project, merge_request)
+
+ wait_for_requests
+
+ expect(page).not_to have_button('Merge')
+ expect(page).to have_content('This merge request is in the process of being merged')
+ end
+ end
end
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index c0cfb9eafe2..24e7843db63 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -29,8 +29,9 @@ feature 'Import/Export - project import integration test', js: true do
fill_in :project_path, with: 'test-project-path', visible: true
click_link 'GitLab export'
- expect(page).to have_content('GitLab project export')
+ expect(page).to have_content('Import an exported GitLab project')
expect(URI.parse(current_url).query).to eq("namespace_id=#{namespace.id}&path=test-project-path")
+ expect(Gitlab::ImportExport).to receive(:import_upload_path).with(filename: /\A\h{32}_test-project-path\z/).and_call_original
attach_file('file', file)
@@ -60,17 +61,6 @@ feature 'Import/Export - project import integration test', js: true do
expect(page).to have_content('Project could not be imported')
end
end
-
- scenario 'project with no name' do
- create(:project, namespace: namespace)
-
- visit new_project_path
-
- select2(namespace.id, from: '#project_namespace_id')
-
- # Check for tooltip disabled import button
- expect(find('.import_gitlab_project')['title']).to eq('Please enter a valid project name.')
- end
end
context 'when limited to the default user namespace' do
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index e3739a705bf..64a80aec205 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -55,7 +55,7 @@ feature 'Projects > Wiki > User updates wiki page' do
scenario 'page has been updated since the user opened the edit page' do
click_link 'Edit'
- wiki_page.update('Update')
+ wiki_page.update(content: 'Update')
click_button 'Save changes'
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index dbcdac902d5..d3d7915bebf 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -1,6 +1,27 @@
require 'spec_helper'
feature 'Project' do
+ describe 'creating from template' do
+ let(:user) { create(:user) }
+ let(:template) { Gitlab::ProjectTemplate.find(:rails) }
+
+ before do
+ sign_in user
+ visit new_project_path
+ end
+
+ it "allows creation from templates" do
+ page.choose(template.name)
+ fill_in("project_path", with: template.name)
+
+ page.within '#content-body' do
+ click_button "Create project"
+ end
+
+ expect(page).to have_content 'This project Loading..'
+ end
+ end
+
describe 'description' do
let(:project) { create(:project, :repository) }
let(:path) { project_path(project) }
@@ -146,6 +167,21 @@ feature 'Project' do
end
end
+ describe 'activity view' do
+ let(:user) { create(:user, project_view: 'activity') }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ project.team << [user, :master]
+ sign_in user
+ visit project_path(project)
+ end
+
+ it 'loads activity', :js do
+ expect(page).to have_selector('.event-item')
+ end
+ end
+
def remove_with_confirm(button_text, confirm_with)
click_button button_text
fill_in 'confirm_name_input', with: confirm_with
diff --git a/spec/fixtures/api/schemas/entities/merge_request.json b/spec/fixtures/api/schemas/entities/merge_request.json
index 7ffa82fc4bd..2f12b671dec 100644
--- a/spec/fixtures/api/schemas/entities/merge_request.json
+++ b/spec/fixtures/api/schemas/entities/merge_request.json
@@ -19,7 +19,6 @@
"human_time_estimate": { "type": ["integer", "null"] },
"human_total_time_spent": { "type": ["integer", "null"] },
"in_progress_merge_commit_sha": { "type": ["string", "null"] },
- "locked_at": { "type": ["string", "null"] },
"merge_error": { "type": ["string", "null"] },
"merge_commit_sha": { "type": ["string", "null"] },
"merge_params": { "type": ["object", "null"] },
@@ -94,7 +93,8 @@
"commit_change_content_path": { "type": "string" },
"remove_wip_path": { "type": "string" },
"commits_count": { "type": "integer" },
- "remove_source_branch": { "type": ["boolean", "null"] }
+ "remove_source_branch": { "type": ["boolean", "null"] },
+ "merge_ongoing": { "type": "boolean" }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/comment.json b/spec/fixtures/api/schemas/public_api/v4/comment.json
new file mode 100644
index 00000000000..52cfe86aeeb
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/comment.json
@@ -0,0 +1,21 @@
+{
+ "type": "object",
+ "required" : [
+ "name",
+ "message",
+ "commit",
+ "release"
+ ],
+ "properties" : {
+ "name": { "type": "string" },
+ "message": { "type": ["string", "null"] },
+ "commit": { "$ref": "commit/basic.json" },
+ "release": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "release.json" }
+ ]
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/commit/detail.json b/spec/fixtures/api/schemas/public_api/v4/commit/detail.json
new file mode 100644
index 00000000000..b7b2535c204
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/commit/detail.json
@@ -0,0 +1,16 @@
+{
+ "type": "object",
+ "allOf": [
+ { "$ref": "basic.json" },
+ {
+ "required" : [
+ "stats",
+ "status"
+ ],
+ "properties": {
+ "stats": { "$ref": "../commit_stats.json" },
+ "status": { "type": ["string", "null"] }
+ }
+ }
+ ]
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/commit_note.json b/spec/fixtures/api/schemas/public_api/v4/commit_note.json
new file mode 100644
index 00000000000..02081989271
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/commit_note.json
@@ -0,0 +1,19 @@
+{
+ "type": "object",
+ "required" : [
+ "note",
+ "path",
+ "line",
+ "line_type",
+ "author",
+ "created_at"
+ ],
+ "properties" : {
+ "note": { "type": ["string", "null"] },
+ "path": { "type": ["string", "null"] },
+ "line": { "type": ["integer", "null"] },
+ "line_type": { "type": ["string", "null"] },
+ "author": { "$ref": "user/basic.json" },
+ "created_at": { "type": "date" }
+ }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/commit_notes.json b/spec/fixtures/api/schemas/public_api/v4/commit_notes.json
new file mode 100644
index 00000000000..d65a7d677ea
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/commit_notes.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "commit_note.json" }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/commit_stats.json b/spec/fixtures/api/schemas/public_api/v4/commit_stats.json
new file mode 100644
index 00000000000..779384c62e6
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/commit_stats.json
@@ -0,0 +1,14 @@
+{
+ "type": "object",
+ "required" : [
+ "additions",
+ "deletions",
+ "total"
+ ],
+ "properties" : {
+ "additions": { "type": "integer" },
+ "deletions": { "type": "integer" },
+ "total": { "type": "integer" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/commits.json b/spec/fixtures/api/schemas/public_api/v4/commits.json
new file mode 100644
index 00000000000..98b17a96071
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/commits.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "commit/basic.json" }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/release.json b/spec/fixtures/api/schemas/public_api/v4/release.json
new file mode 100644
index 00000000000..6612c2a9911
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/release.json
@@ -0,0 +1,12 @@
+{
+ "type": "object",
+ "required" : [
+ "tag_name",
+ "description"
+ ],
+ "properties" : {
+ "tag_name": { "type": ["string", "null"] },
+ "description": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/tag.json b/spec/fixtures/api/schemas/public_api/v4/tag.json
new file mode 100644
index 00000000000..52cfe86aeeb
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/tag.json
@@ -0,0 +1,21 @@
+{
+ "type": "object",
+ "required" : [
+ "name",
+ "message",
+ "commit",
+ "release"
+ ],
+ "properties" : {
+ "name": { "type": "string" },
+ "message": { "type": ["string", "null"] },
+ "commit": { "$ref": "commit/basic.json" },
+ "release": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "release.json" }
+ ]
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/tags.json b/spec/fixtures/api/schemas/public_api/v4/tags.json
new file mode 100644
index 00000000000..eae352e7f87
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/tags.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "tag.json" }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/basic.json b/spec/fixtures/api/schemas/public_api/v4/user/basic.json
new file mode 100644
index 00000000000..9f69d31971c
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/user/basic.json
@@ -0,0 +1,15 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "state",
+ "avatar_url",
+ "web_url"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "string" },
+ "web_url": { "type": "string" }
+ }
+}
diff --git a/spec/fixtures/encoding/Japanese.md b/spec/fixtures/encoding/Japanese.md
new file mode 100644
index 00000000000..dd469c9f232
--- /dev/null
+++ b/spec/fixtures/encoding/Japanese.md
@@ -0,0 +1,42 @@
++++
+date = "2017-05-21T13:05:07+09:00"
+title = "レイヤ"
+weight = 10
+
++++
+
+## このチュートリアルで扱う内容
+1. Redactedにおける2D開発でのレイヤの基本的な概要
+2. スクリーン上のスプライトの順序付け方法
+
+### Redactedにおける2D開発でのレイヤの基本的な概要
+2Dにおいてはz軸が存在しないため、シーン内要素の描画順を制御するためには代替となる仕組みが必要です。
+Redactedでは**レイヤ**における**zIndex**属性を制御可能にすることで、この課題を解決しています。
+**デフォルトでは、zIndexは0となりオブジェクトはレイヤに追加された順番に描画されます。**
+
+レイヤにはいくつかの重要な特性があります。
+
+* レイヤにはレイヤ化されたオブジェクトのみを含めることができます。(**3Dモデルは絶対に追加しないでください**)
+* レイヤはレイヤ化されたオブジェクトです。(したがって、レイヤには他のレイヤを含めることができます)
+* レイヤ化されたオブジェクトは、最大で1つのレイヤに属すことができます。
+
+レイヤを直接初期化することはできませんが、その派生クラスは初期化することが可能です。**Scene2D**と**コンテナ**は、**レイヤ**から派生する2つの主なオブジェクトです。すべての初期化(createContainer、instantiate、...)はレイヤ上で行われます。つまり、2Dで初期化されるすべてのオブジェクトは、zIndexプロパティを持つレイヤ化されたオブジェクトです。
+
+**zIndexはグローバルではありません!**
+
+CSSとは異なり、zIndexはすべてのオブジェクトに対してグローバルではありません。zIndexプロパティは親レイヤに対してローカルです。詳細につきましては、コンテナチュートリアルで説明しています。 [TODO: Link]。
+
+### スクリーン上のスプライトの順序付け方法
+これまで学んだことを生かして、画面にスプライトを表示して、zIndexの設定をしてみましょう!
+
+* まず、最初に (A,B,C) スプライトを生成します。
+* スプライトAをシーンに追加します(zIndex = 0、標準色)
+* スプライトBをシーン2に追加すると、**スプライトAの上に**表示されます(zIndex = 0、赤色)
+* 最後にスプライトCをシーンに追加します(青色)が、スプライトのzIndexを-1に設定すると、スプライトはAとBの後側に表示されます。
+
+{{< code "static/tutorials/layers.html" >}}
+
+### ソースコード全体
+```js
+{{< snippet "static/tutorials/layers.html" >}}
+```
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 58b43805705..4f46e40ce7a 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -227,8 +227,11 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Milestone in another project: <%= xmilestone.to_reference(project) %>
- Ignored in code: `<%= simple_milestone.to_reference %>`
- Ignored in links: [Link to <%= simple_milestone.to_reference %>](#milestone-link)
-- Milestone by URL: <%= urls.project_milestone_url(milestone.project, milestone) %>
+- Milestone by URL: <%= urls.milestone_url(milestone) %>
- Link to milestone by URL: [Milestone](<%= milestone.to_reference %>)
+- Group milestone by name: <%= Milestone.reference_prefix %><%= group_milestone.name %>
+- Group milestone by name in quotes: <%= group_milestone.to_reference(format: :name) %>
+- Group milestone by URL is ignore: <%= urls.milestone_url(group_milestone) %>
### Task Lists
diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb
index 537e457513f..a44b200c5da 100644
--- a/spec/helpers/gitlab_routing_helper_spec.rb
+++ b/spec/helpers/gitlab_routing_helper_spec.rb
@@ -63,44 +63,4 @@ describe GitlabRoutingHelper do
it { expect(resend_invite_group_member_path(group_member)).to eq resend_invite_group_group_member_path(group_member.source, group_member) }
end
end
-
- describe '#milestone_path' do
- context 'for a group milestone' do
- let(:milestone) { build_stubbed(:milestone, group: group, iid: 1) }
-
- it 'links to the group milestone page' do
- expect(milestone_path(milestone))
- .to eq(group_milestone_path(group, milestone))
- end
- end
-
- context 'for a project milestone' do
- let(:milestone) { build_stubbed(:milestone, project: project, iid: 1) }
-
- it 'links to the project milestone page' do
- expect(milestone_path(milestone))
- .to eq(project_milestone_path(project, milestone))
- end
- end
- end
-
- describe '#milestone_url' do
- context 'for a group milestone' do
- let(:milestone) { build_stubbed(:milestone, group: group, iid: 1) }
-
- it 'links to the group milestone page' do
- expect(milestone_url(milestone))
- .to eq(group_milestone_url(group, milestone))
- end
- end
-
- context 'for a project milestone' do
- let(:milestone) { build_stubbed(:milestone, project: project, iid: 1) }
-
- it 'links to the project milestone page' do
- expect(milestone_url(milestone))
- .to eq(project_milestone_url(project, milestone))
- end
- end
- end
end
diff --git a/spec/helpers/milestones_routing_helper_spec.rb b/spec/helpers/milestones_routing_helper_spec.rb
new file mode 100644
index 00000000000..dc13a43c2ab
--- /dev/null
+++ b/spec/helpers/milestones_routing_helper_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe MilestonesRoutingHelper do
+ let(:project) { build_stubbed(:project) }
+ let(:group) { build_stubbed(:group) }
+
+ describe '#milestone_path' do
+ context 'for a group milestone' do
+ let(:milestone) { build_stubbed(:milestone, group: group, iid: 1) }
+
+ it 'links to the group milestone page' do
+ expect(milestone_path(milestone))
+ .to eq(group_milestone_path(group, milestone))
+ end
+ end
+
+ context 'for a project milestone' do
+ let(:milestone) { build_stubbed(:milestone, project: project, iid: 1) }
+
+ it 'links to the project milestone page' do
+ expect(milestone_path(milestone))
+ .to eq(project_milestone_path(project, milestone))
+ end
+ end
+ end
+
+ describe '#milestone_url' do
+ context 'for a group milestone' do
+ let(:milestone) { build_stubbed(:milestone, group: group, iid: 1) }
+
+ it 'links to the group milestone page' do
+ expect(milestone_url(milestone))
+ .to eq(group_milestone_url(group, milestone))
+ end
+ end
+
+ context 'for a project milestone' do
+ let(:milestone) { build_stubbed(:milestone, project: project, iid: 1) }
+
+ it 'links to the project milestone page' do
+ expect(milestone_url(milestone))
+ .to eq(project_milestone_url(project, milestone))
+ end
+ end
+ end
+end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 236a7c29634..37a5e6b474e 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -411,4 +411,48 @@ describe ProjectsHelper do
end
end
end
+
+ describe '#has_projects_or_name?' do
+ let(:projects) do
+ create(:project)
+ Project.all
+ end
+
+ it 'returns true when there are projects' do
+ expect(helper.has_projects_or_name?(projects, {})).to eq(true)
+ end
+
+ it 'returns true when there are no projects but a name is given' do
+ expect(helper.has_projects_or_name?(Project.none, name: 'foo')).to eq(true)
+ end
+
+ it 'returns false when there are no projects and there is no name' do
+ expect(helper.has_projects_or_name?(Project.none, {})).to eq(false)
+ end
+ end
+
+ describe '#any_projects?' do
+ before do
+ create(:project)
+ end
+
+ it 'returns true when projects will be returned' do
+ expect(helper.any_projects?(Project.all)).to eq(true)
+ end
+
+ it 'returns false when no projects will be returned' do
+ expect(helper.any_projects?(Project.none)).to eq(false)
+ end
+
+ it 'only executes a single query when a LIMIT is applied' do
+ relation = Project.limit(1)
+ recorder = ActiveRecord::QueryRecorder.new do
+ 2.times do
+ helper.any_projects?(relation)
+ end
+ end
+
+ expect(recorder.count).to eq(1)
+ end
+ end
end
diff --git a/spec/helpers/storage_health_helper_spec.rb b/spec/helpers/storage_health_helper_spec.rb
new file mode 100644
index 00000000000..874498e6338
--- /dev/null
+++ b/spec/helpers/storage_health_helper_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe StorageHealthHelper do
+ describe '#failing_storage_health_message' do
+ let(:health) do
+ Gitlab::Git::Storage::Health.new(
+ "<script>alert('storage name');)</script>",
+ []
+ )
+ end
+
+ it 'escapes storage names' do
+ escaped_storage_name = '&lt;script&gt;alert(&#39;storage name&#39;);)&lt;/script&gt;'
+
+ result = helper.failing_storage_health_message(health)
+
+ expect(result).to include(escaped_storage_name)
+ end
+ end
+end
diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb
index 0877770c167..83283f03940 100644
--- a/spec/initializers/6_validations_spec.rb
+++ b/spec/initializers/6_validations_spec.rb
@@ -23,6 +23,16 @@ describe '6_validations' do
end
end
+ context 'when one of the settings is incorrect' do
+ before do
+ mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c', 'failure_count_threshold' => 'not a number' })
+ end
+
+ it 'throws an error' do
+ expect { validate_storages_config }.to raise_error(/failure_count_threshold/)
+ end
+ end
+
context 'with invalid storage names' do
before do
mock_storages('name with spaces' => { 'path' => 'tmp/tests/paths/a/b/c' })
@@ -84,6 +94,17 @@ describe '6_validations' do
expect { validate_storages_paths }.not_to raise_error
end
end
+
+ describe 'inaccessible storage' do
+ before do
+ mock_storages('foo' => { 'path' => 'tmp/tests/a/path/that/does/not/exist' })
+ end
+
+ it 'passes through with a warning' do
+ expect(Rails.logger).to receive(:error)
+ expect { validate_storages_paths }.not_to raise_error
+ end
+ end
end
def mock_storages(storages)
diff --git a/spec/initializers/settings_spec.rb b/spec/initializers/settings_spec.rb
index ebdabcf93f1..e5ec90cb8f9 100644
--- a/spec/initializers/settings_spec.rb
+++ b/spec/initializers/settings_spec.rb
@@ -2,6 +2,17 @@ require 'spec_helper'
require_relative '../../config/initializers/1_settings'
describe Settings do
+ describe '#repositories' do
+ it 'assigns the default failure attributes' do
+ repository_settings = Gitlab.config.repositories.storages['broken']
+
+ expect(repository_settings['failure_count_threshold']).to eq(10)
+ expect(repository_settings['failure_wait_time']).to eq(30)
+ expect(repository_settings['failure_reset_time']).to eq(1800)
+ expect(repository_settings['storage_timeout']).to eq(5)
+ end
+ end
+
describe '#host_without_www' do
context 'URL with protocol' do
it 'returns the host' do
diff --git a/spec/javascripts/blob/blob_file_dropzone_spec.js b/spec/javascripts/blob/blob_file_dropzone_spec.js
new file mode 100644
index 00000000000..2c8183ff77b
--- /dev/null
+++ b/spec/javascripts/blob/blob_file_dropzone_spec.js
@@ -0,0 +1,42 @@
+import 'dropzone';
+import BlobFileDropzone from '~/blob/blob_file_dropzone';
+
+describe('BlobFileDropzone', () => {
+ preloadFixtures('blob/show.html.raw');
+
+ beforeEach(() => {
+ loadFixtures('blob/show.html.raw');
+ const form = $('.js-upload-blob-form');
+ this.blobFileDropzone = new BlobFileDropzone(form, 'POST');
+ this.dropzone = $('.js-upload-blob-form .dropzone').get(0).dropzone;
+ this.replaceFileButton = $('#submit-all');
+ });
+
+ describe('submit button', () => {
+ it('requires file', () => {
+ spyOn(window, 'alert');
+
+ this.replaceFileButton.click();
+
+ expect(window.alert).toHaveBeenCalled();
+ });
+
+ it('is disabled while uploading', () => {
+ spyOn(window, 'alert');
+
+ const file = {
+ name: 'some-file.jpg',
+ type: 'jpg',
+ };
+ const fakeEvent = jQuery.Event('drop', {
+ dataTransfer: { files: [file] },
+ });
+
+ this.dropzone.listeners[0].events.drop(fakeEvent);
+ this.replaceFileButton.click();
+
+ expect(window.alert).not.toHaveBeenCalled();
+ expect(this.replaceFileButton.is(':disabled')).toEqual(true);
+ });
+ });
+});
diff --git a/spec/javascripts/blob/viewer/index_spec.js b/spec/javascripts/blob/viewer/index_spec.js
index af04e7c1e72..cfa6650d85f 100644
--- a/spec/javascripts/blob/viewer/index_spec.js
+++ b/spec/javascripts/blob/viewer/index_spec.js
@@ -3,10 +3,10 @@ import BlobViewer from '~/blob/viewer/index';
describe('Blob viewer', () => {
let blob;
- preloadFixtures('blob/show.html.raw');
+ preloadFixtures('snippets/show.html.raw');
beforeEach(() => {
- loadFixtures('blob/show.html.raw');
+ loadFixtures('snippets/show.html.raw');
$('#modal-upload-blob').remove();
blob = new BlobViewer();
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index bd9b4fbfdd3..69cfcbbce5a 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -238,12 +238,6 @@ describe('Issue card component', () => {
});
describe('labels', () => {
- it('does not render any', () => {
- expect(
- component.$el.querySelector('.label'),
- ).toBeNull();
- });
-
describe('exists', () => {
beforeEach((done) => {
component.issue.addLabel(label1);
@@ -251,16 +245,21 @@ describe('Issue card component', () => {
Vue.nextTick(() => done());
});
- it('does not render list label', () => {
+ it('renders list label', () => {
expect(
component.$el.querySelectorAll('.label').length,
- ).toBe(1);
+ ).toBe(2);
});
it('renders label', () => {
+ const nodes = [];
+ component.$el.querySelectorAll('.label').forEach((label) => {
+ nodes.push(label.title);
+ });
+
expect(
- component.$el.querySelector('.label').textContent,
- ).toContain(label1.title);
+ nodes.includes(label1.description),
+ ).toBe(true);
});
it('sets label description as title', () => {
@@ -270,9 +269,14 @@ describe('Issue card component', () => {
});
it('sets background color of button', () => {
+ const nodes = [];
+ component.$el.querySelectorAll('.label').forEach((label) => {
+ nodes.push(label.style.backgroundColor);
+ });
+
expect(
- component.$el.querySelector('.label').style.backgroundColor,
- ).toContain(label1.color);
+ nodes.includes(label1.color),
+ ).toBe(true);
});
});
});
diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js
index be90dbdd88a..35149611095 100644
--- a/spec/javascripts/build_spec.js
+++ b/spec/javascripts/build_spec.js
@@ -5,7 +5,6 @@ import '~/lib/utils/datetime_utility';
import '~/lib/utils/url_utility';
import '~/build';
import '~/breakpoints';
-import 'vendor/jquery.nicescroll';
describe('Build', () => {
const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`;
diff --git a/spec/javascripts/fixtures/project_select_combo_button.html.haml b/spec/javascripts/fixtures/project_select_combo_button.html.haml
new file mode 100644
index 00000000000..54bc1a59279
--- /dev/null
+++ b/spec/javascripts/fixtures/project_select_combo_button.html.haml
@@ -0,0 +1,6 @@
+.project-item-select-holder
+ %input.project-item-select{ data: { group_id: '12345' , relative_path: 'issues/new' } }
+ %a.new-project-item-link{ data: { label: 'New issue' }, href: ''}
+ %i.fa.fa-spinner.spin
+ %a.new-project-item-select-button
+ %i.fa.fa-caret-down
diff --git a/spec/javascripts/fixtures/blob.rb b/spec/javascripts/fixtures/snippet.rb
index 16490ad5039..cc825c82190 100644
--- a/spec/javascripts/fixtures/blob.rb
+++ b/spec/javascripts/fixtures/snippet.rb
@@ -1,27 +1,25 @@
require 'spec_helper'
-describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do
+describe SnippetsController, '(JavaScript fixtures)', type: :controller do
include JavaScriptFixturesHelpers
let(:admin) { create(:admin) }
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
+ let(:snippet) { create(:personal_snippet, title: 'snippet.md', content: '# snippet', file_name: 'snippet.md', author: admin) }
render_views
before(:all) do
- clean_frontend_fixtures('blob/')
+ clean_frontend_fixtures('snippets/')
end
before(:each) do
sign_in(admin)
end
- it 'blob/show.html.raw' do |example|
- get(:show,
- namespace_id: project.namespace,
- project_id: project,
- id: 'add-ipython-files/files/ipython/basic.ipynb')
+ it 'snippets/show.html.raw' do |example|
+ get(:show, id: snippet.to_param)
expect(response).to be_success
store_frontend_fixture(response, example.description)
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js b/spec/javascripts/labels_issue_sidebar_spec.js
index c99f379b871..e47adc49224 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js
+++ b/spec/javascripts/labels_issue_sidebar_spec.js
@@ -4,7 +4,6 @@
import '~/gl_dropdown';
import 'select2';
-import 'vendor/jquery.nicescroll';
import '~/api';
import '~/create_label';
import '~/issuable_context';
diff --git a/spec/javascripts/project_select_combo_button_spec.js b/spec/javascripts/project_select_combo_button_spec.js
new file mode 100644
index 00000000000..e10a5a3bef6
--- /dev/null
+++ b/spec/javascripts/project_select_combo_button_spec.js
@@ -0,0 +1,105 @@
+import ProjectSelectComboButton from '~/project_select_combo_button';
+
+const fixturePath = 'static/project_select_combo_button.html.raw';
+
+describe('Project Select Combo Button', function () {
+ preloadFixtures(fixturePath);
+
+ beforeEach(function () {
+ this.defaults = {
+ label: 'Select project to create issue',
+ groupId: 12345,
+ projectMeta: {
+ name: 'My Cool Project',
+ url: 'http://mycoolproject.com',
+ },
+ newProjectMeta: {
+ name: 'My Other Cool Project',
+ url: 'http://myothercoolproject.com',
+ },
+ localStorageKey: 'group-12345-new-issue-recent-project',
+ relativePath: 'issues/new',
+ };
+
+ loadFixtures(fixturePath);
+
+ this.newItemBtn = document.querySelector('.new-project-item-link');
+ this.projectSelectInput = document.querySelector('.project-item-select');
+ });
+
+ describe('on page load when localStorage is empty', function () {
+ beforeEach(function () {
+ this.comboButton = new ProjectSelectComboButton(this.projectSelectInput);
+ });
+
+ it('newItemBtn is disabled', function () {
+ expect(this.newItemBtn.hasAttribute('disabled')).toBe(true);
+ expect(this.newItemBtn.classList.contains('disabled')).toBe(true);
+ });
+
+ it('newItemBtn href is null', function () {
+ expect(this.newItemBtn.getAttribute('href')).toBe('');
+ });
+
+ it('newItemBtn text is the plain default label', function () {
+ expect(this.newItemBtn.textContent).toBe(this.defaults.label);
+ });
+ });
+
+ describe('on page load when localStorage is filled', function () {
+ beforeEach(function () {
+ window.localStorage
+ .setItem(this.defaults.localStorageKey, JSON.stringify(this.defaults.projectMeta));
+ this.comboButton = new ProjectSelectComboButton(this.projectSelectInput);
+ });
+
+ it('newItemBtn is not disabled', function () {
+ expect(this.newItemBtn.hasAttribute('disabled')).toBe(false);
+ expect(this.newItemBtn.classList.contains('disabled')).toBe(false);
+ });
+
+ it('newItemBtn href is correctly set', function () {
+ expect(this.newItemBtn.getAttribute('href')).toBe(this.defaults.projectMeta.url);
+ });
+
+ it('newItemBtn text is the cached label', function () {
+ expect(this.newItemBtn.textContent)
+ .toBe(`New issue in ${this.defaults.projectMeta.name}`);
+ });
+
+ afterEach(function () {
+ window.localStorage.clear();
+ });
+ });
+
+ describe('after selecting a new project', function () {
+ beforeEach(function () {
+ this.comboButton = new ProjectSelectComboButton(this.projectSelectInput);
+
+ // mock the effect of selecting an item from the projects dropdown (select2)
+ $('.project-item-select')
+ .val(JSON.stringify(this.defaults.newProjectMeta))
+ .trigger('change');
+ });
+
+ it('newItemBtn is not disabled', function () {
+ expect(this.newItemBtn.hasAttribute('disabled')).toBe(false);
+ expect(this.newItemBtn.classList.contains('disabled')).toBe(false);
+ });
+
+ it('newItemBtn href is correctly set', function () {
+ expect(this.newItemBtn.getAttribute('href'))
+ .toBe('http://myothercoolproject.com/issues/new');
+ });
+
+ it('newItemBtn text is the selected project label', function () {
+ expect(this.newItemBtn.textContent)
+ .toBe(`New issue in ${this.defaults.newProjectMeta.name}`);
+ });
+
+ afterEach(function () {
+ window.localStorage.clear();
+ });
+ });
+});
+
diff --git a/spec/javascripts/projects/project_import_gitlab_project_spec.js b/spec/javascripts/projects/project_import_gitlab_project_spec.js
new file mode 100644
index 00000000000..2f1aae109e3
--- /dev/null
+++ b/spec/javascripts/projects/project_import_gitlab_project_spec.js
@@ -0,0 +1,25 @@
+import projectImportGitlab from '~/projects/project_import_gitlab_project';
+
+describe('Import Gitlab project', () => {
+ let projectName;
+ beforeEach(() => {
+ projectName = 'project';
+ window.history.pushState({}, null, `?path=${projectName}`);
+
+ setFixtures(`
+ <input class="js-path-name" />
+ `);
+
+ projectImportGitlab.bindEvents();
+ });
+
+ afterEach(() => {
+ window.history.pushState({}, null, '');
+ });
+
+ describe('path name', () => {
+ it('should fill in the project name derived from the previously filled project name', () => {
+ expect(document.querySelector('.js-path-name').value).toEqual(projectName);
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js
new file mode 100644
index 00000000000..db2b7d51626
--- /dev/null
+++ b/spec/javascripts/repo/components/repo_commit_section_spec.js
@@ -0,0 +1,158 @@
+import Vue from 'vue';
+import repoCommitSection from '~/repo/components/repo_commit_section.vue';
+import RepoStore from '~/repo/stores/repo_store';
+import RepoHelper from '~/repo/helpers/repo_helper';
+import Api from '~/api';
+
+describe('RepoCommitSection', () => {
+ const branch = 'master';
+ const projectUrl = 'projectUrl';
+ const openedFiles = [{
+ id: 0,
+ changed: true,
+ url: `/namespace/${projectUrl}/blob/${branch}/dir/file0.ext`,
+ newContent: 'a',
+ }, {
+ id: 1,
+ changed: true,
+ url: `/namespace/${projectUrl}/blob/${branch}/dir/file1.ext`,
+ newContent: 'b',
+ }, {
+ id: 2,
+ url: `/namespace/${projectUrl}/blob/${branch}/dir/file2.ext`,
+ changed: false,
+ }];
+
+ RepoStore.projectUrl = projectUrl;
+
+ function createComponent() {
+ const RepoCommitSection = Vue.extend(repoCommitSection);
+
+ return new RepoCommitSection().$mount();
+ }
+
+ it('renders a commit section', () => {
+ RepoStore.isCommitable = true;
+ RepoStore.targetBranch = branch;
+ RepoStore.openedFiles = openedFiles;
+
+ spyOn(RepoHelper, 'getBranch').and.returnValue(branch);
+
+ const vm = createComponent();
+ const changedFiles = [...vm.$el.querySelectorAll('.changed-files > li')];
+ const commitMessage = vm.$el.querySelector('#commit-message');
+ const submitCommit = vm.$el.querySelector('.submit-commit');
+ const targetBranch = vm.$el.querySelector('.target-branch');
+
+ expect(vm.$el.querySelector(':scope > form')).toBeTruthy();
+ expect(vm.$el.querySelector('.staged-files').textContent).toEqual('Staged files (2)');
+ expect(changedFiles.length).toEqual(2);
+
+ changedFiles.forEach((changedFile, i) => {
+ const filePath = RepoHelper.getFilePathFromFullPath(openedFiles[i].url, branch);
+
+ expect(changedFile.textContent).toEqual(filePath);
+ });
+
+ expect(commitMessage.tagName).toEqual('TEXTAREA');
+ expect(commitMessage.name).toEqual('commit-message');
+ expect(submitCommit.type).toEqual('submit');
+ expect(submitCommit.disabled).toBeTruthy();
+ expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeFalsy();
+ expect(vm.$el.querySelector('.commit-summary').textContent).toEqual('Commit 2 files');
+ expect(targetBranch.querySelector(':scope > label').textContent).toEqual('Target branch');
+ expect(targetBranch.querySelector('.help-block').textContent).toEqual(branch);
+ });
+
+ it('does not render if not isCommitable', () => {
+ RepoStore.isCommitable = false;
+ RepoStore.openedFiles = [{
+ id: 0,
+ changed: true,
+ }];
+
+ const vm = createComponent();
+
+ expect(vm.$el.innerHTML).toBeFalsy();
+ });
+
+ it('does not render if no changedFiles', () => {
+ RepoStore.isCommitable = true;
+ RepoStore.openedFiles = [];
+
+ const vm = createComponent();
+
+ expect(vm.$el.innerHTML).toBeFalsy();
+ });
+
+ it('shows commit submit and summary if commitMessage and spinner if submitCommitsLoading', (done) => {
+ const projectId = 'projectId';
+ const commitMessage = 'commitMessage';
+ RepoStore.isCommitable = true;
+ RepoStore.openedFiles = openedFiles;
+ RepoStore.projectId = projectId;
+
+ spyOn(RepoHelper, 'getBranch').and.returnValue(branch);
+
+ const vm = createComponent();
+ const commitMessageEl = vm.$el.querySelector('#commit-message');
+ const submitCommit = vm.$el.querySelector('.submit-commit');
+
+ vm.commitMessage = commitMessage;
+
+ Vue.nextTick(() => {
+ expect(commitMessageEl.value).toBe(commitMessage);
+ expect(submitCommit.disabled).toBeFalsy();
+
+ spyOn(vm, 'makeCommit').and.callThrough();
+ spyOn(Api, 'commitMultiple');
+
+ submitCommit.click();
+
+ Vue.nextTick(() => {
+ expect(vm.makeCommit).toHaveBeenCalled();
+ expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeTruthy();
+
+ const args = Api.commitMultiple.calls.allArgs()[0];
+ const { commit_message, actions, branch: payloadBranch } = args[1];
+
+ expect(args[0]).toBe(projectId);
+ expect(commit_message).toBe(commitMessage);
+ expect(actions.length).toEqual(2);
+ expect(payloadBranch).toEqual(branch);
+ expect(actions[0].action).toEqual('update');
+ expect(actions[1].action).toEqual('update');
+ expect(actions[0].content).toEqual(openedFiles[0].newContent);
+ expect(actions[1].content).toEqual(openedFiles[1].newContent);
+ expect(actions[0].file_path)
+ .toEqual(RepoHelper.getFilePathFromFullPath(openedFiles[0].url, branch));
+ expect(actions[1].file_path)
+ .toEqual(RepoHelper.getFilePathFromFullPath(openedFiles[1].url, branch));
+
+ done();
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('resetCommitState', () => {
+ it('should reset store vars and scroll to top', () => {
+ const vm = {
+ submitCommitsLoading: true,
+ changedFiles: new Array(10),
+ openedFiles: new Array(10),
+ commitMessage: 'commitMessage',
+ editMode: true,
+ };
+
+ repoCommitSection.methods.resetCommitState.call(vm);
+
+ expect(vm.submitCommitsLoading).toEqual(false);
+ expect(vm.changedFiles).toEqual([]);
+ expect(vm.openedFiles).toEqual([]);
+ expect(vm.commitMessage).toEqual('');
+ expect(vm.editMode).toEqual(false);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_edit_button_spec.js b/spec/javascripts/repo/components/repo_edit_button_spec.js
new file mode 100644
index 00000000000..df2f9697acc
--- /dev/null
+++ b/spec/javascripts/repo/components/repo_edit_button_spec.js
@@ -0,0 +1,51 @@
+import Vue from 'vue';
+import repoEditButton from '~/repo/components/repo_edit_button.vue';
+import RepoStore from '~/repo/stores/repo_store';
+
+describe('RepoEditButton', () => {
+ function createComponent() {
+ const RepoEditButton = Vue.extend(repoEditButton);
+
+ return new RepoEditButton().$mount();
+ }
+
+ it('renders an edit button that toggles the view state', (done) => {
+ RepoStore.isCommitable = true;
+ RepoStore.changedFiles = [];
+
+ const vm = createComponent();
+
+ expect(vm.$el.tagName).toEqual('BUTTON');
+ expect(vm.$el.textContent).toMatch('Edit');
+
+ spyOn(vm, 'editClicked').and.callThrough();
+
+ vm.$el.click();
+
+ Vue.nextTick(() => {
+ expect(vm.editClicked).toHaveBeenCalled();
+ expect(vm.$el.textContent).toMatch('Cancel edit');
+ done();
+ });
+ });
+
+ it('does not render if not isCommitable', () => {
+ RepoStore.isCommitable = false;
+
+ const vm = createComponent();
+
+ expect(vm.$el.innerHTML).toBeUndefined();
+ });
+
+ describe('methods', () => {
+ describe('editClicked', () => {
+ it('sets dialog to open when there are changedFiles', () => {
+
+ });
+
+ it('toggles editMode and calls toggleBlobView', () => {
+
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_editor_spec.js b/spec/javascripts/repo/components/repo_editor_spec.js
new file mode 100644
index 00000000000..35e0c995163
--- /dev/null
+++ b/spec/javascripts/repo/components/repo_editor_spec.js
@@ -0,0 +1,26 @@
+import Vue from 'vue';
+import repoEditor from '~/repo/components/repo_editor.vue';
+import RepoStore from '~/repo/stores/repo_store';
+
+describe('RepoEditor', () => {
+ function createComponent() {
+ const RepoEditor = Vue.extend(repoEditor);
+
+ return new RepoEditor().$mount();
+ }
+
+ it('renders an ide container', () => {
+ const monacoInstance = jasmine.createSpyObj('monacoInstance', ['onMouseUp', 'onKeyUp', 'setModel', 'updateOptions']);
+ const monaco = {
+ editor: jasmine.createSpyObj('editor', ['create']),
+ };
+ RepoStore.monaco = monaco;
+
+ monaco.editor.create.and.returnValue(monacoInstance);
+ spyOn(repoEditor.watch, 'blobRaw');
+
+ const vm = createComponent();
+
+ expect(vm.$el.id).toEqual('ide');
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_file_buttons_spec.js b/spec/javascripts/repo/components/repo_file_buttons_spec.js
new file mode 100644
index 00000000000..e1f25e4485f
--- /dev/null
+++ b/spec/javascripts/repo/components/repo_file_buttons_spec.js
@@ -0,0 +1,82 @@
+import Vue from 'vue';
+import repoFileButtons from '~/repo/components/repo_file_buttons.vue';
+import RepoStore from '~/repo/stores/repo_store';
+
+describe('RepoFileButtons', () => {
+ function createComponent() {
+ const RepoFileButtons = Vue.extend(repoFileButtons);
+
+ return new RepoFileButtons().$mount();
+ }
+
+ it('renders Raw, Blame, History, Permalink and Preview toggle', () => {
+ const activeFile = {
+ extension: 'md',
+ url: 'url',
+ raw_path: 'raw_path',
+ blame_path: 'blame_path',
+ commits_path: 'commits_path',
+ permalink: 'permalink',
+ };
+ const activeFileLabel = 'activeFileLabel';
+ RepoStore.openedFiles = new Array(1);
+ RepoStore.activeFile = activeFile;
+ RepoStore.activeFileLabel = activeFileLabel;
+ RepoStore.editMode = true;
+
+ const vm = createComponent();
+ const raw = vm.$el.querySelector('.raw');
+ const blame = vm.$el.querySelector('.blame');
+ const history = vm.$el.querySelector('.history');
+
+ expect(vm.$el.id).toEqual('repo-file-buttons');
+ expect(raw.href).toMatch(`/${activeFile.raw_path}`);
+ expect(raw.textContent).toEqual('Raw');
+ expect(blame.href).toMatch(`/${activeFile.blame_path}`);
+ expect(blame.textContent).toEqual('Blame');
+ expect(history.href).toMatch(`/${activeFile.commits_path}`);
+ expect(history.textContent).toEqual('History');
+ expect(vm.$el.querySelector('.permalink').textContent).toEqual('Permalink');
+ expect(vm.$el.querySelector('.preview').textContent).toEqual(activeFileLabel);
+ });
+
+ it('triggers rawPreviewToggle on preview click', () => {
+ const activeFile = {
+ extension: 'md',
+ url: 'url',
+ };
+ RepoStore.openedFiles = new Array(1);
+ RepoStore.activeFile = activeFile;
+ RepoStore.editMode = true;
+
+ const vm = createComponent();
+ const preview = vm.$el.querySelector('.preview');
+
+ spyOn(vm, 'rawPreviewToggle');
+
+ preview.click();
+
+ expect(vm.rawPreviewToggle).toHaveBeenCalled();
+ });
+
+ it('does not render preview toggle if not canPreview', () => {
+ const activeFile = {
+ extension: 'abcd',
+ url: 'url',
+ };
+ RepoStore.openedFiles = new Array(1);
+ RepoStore.activeFile = activeFile;
+
+ const vm = createComponent();
+
+ expect(vm.$el.querySelector('.preview')).toBeFalsy();
+ });
+
+ it('does not render if not isMini', () => {
+ RepoStore.openedFiles = [];
+
+ const vm = createComponent();
+
+ expect(vm.$el.innerHTML).toBeFalsy();
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_file_options_spec.js b/spec/javascripts/repo/components/repo_file_options_spec.js
new file mode 100644
index 00000000000..9759b4bf12d
--- /dev/null
+++ b/spec/javascripts/repo/components/repo_file_options_spec.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import repoFileOptions from '~/repo/components/repo_file_options.vue';
+
+describe('RepoFileOptions', () => {
+ const projectName = 'projectName';
+
+ function createComponent(propsData) {
+ const RepoFileOptions = Vue.extend(repoFileOptions);
+
+ return new RepoFileOptions({
+ propsData,
+ }).$mount();
+ }
+
+ it('renders the title and new file/folder buttons if isMini is true', () => {
+ const vm = createComponent({
+ isMini: true,
+ projectName,
+ });
+
+ expect(vm.$el.classList.contains('repo-file-options')).toBeTruthy();
+ expect(vm.$el.querySelector('.title').textContent).toEqual(projectName);
+ });
+
+ it('does not render if isMini is false', () => {
+ const vm = createComponent({
+ isMini: false,
+ projectName,
+ });
+
+ expect(vm.$el.innerHTML).toBeFalsy();
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js
new file mode 100644
index 00000000000..90616ae13ca
--- /dev/null
+++ b/spec/javascripts/repo/components/repo_file_spec.js
@@ -0,0 +1,136 @@
+import Vue from 'vue';
+import repoFile from '~/repo/components/repo_file.vue';
+
+describe('RepoFile', () => {
+ const updated = 'updated';
+ const file = {
+ icon: 'icon',
+ url: 'url',
+ name: 'name',
+ lastCommitMessage: 'message',
+ lastCommitUpdate: Date.now(),
+ level: 10,
+ };
+ const activeFile = {
+ url: 'url',
+ };
+
+ function createComponent(propsData) {
+ const RepoFile = Vue.extend(repoFile);
+
+ return new RepoFile({
+ propsData,
+ }).$mount();
+ }
+
+ beforeEach(() => {
+ spyOn(repoFile.mixins[0].methods, 'timeFormated').and.returnValue(updated);
+ });
+
+ it('renders link, icon, name and last commit details', () => {
+ const vm = createComponent({
+ file,
+ activeFile,
+ });
+ const name = vm.$el.querySelector('.repo-file-name');
+ const fileIcon = vm.$el.querySelector('.file-icon');
+
+ expect(vm.$el.classList.contains('active')).toBeTruthy();
+ expect(vm.$el.querySelector(`.${file.icon}`).style.marginLeft).toEqual('100px');
+ expect(name.title).toEqual(file.url);
+ expect(name.href).toMatch(`/${file.url}`);
+ expect(name.textContent).toEqual(file.name);
+ expect(vm.$el.querySelector('.commit-message').textContent).toBe(file.lastCommitMessage);
+ expect(vm.$el.querySelector('.commit-update').textContent).toBe(updated);
+ expect(fileIcon.classList.contains(file.icon)).toBeTruthy();
+ expect(fileIcon.style.marginLeft).toEqual(`${file.level * 10}px`);
+ });
+
+ it('does render if hasFiles is true and is loading tree', () => {
+ const vm = createComponent({
+ file,
+ activeFile,
+ loading: {
+ tree: true,
+ },
+ hasFiles: true,
+ });
+
+ expect(vm.$el.innerHTML).toBeTruthy();
+ expect(vm.$el.querySelector('.fa-spin.fa-spinner')).toBeFalsy();
+ });
+
+ it('renders a spinner if the file is loading', () => {
+ file.loading = true;
+ const vm = createComponent({
+ file,
+ activeFile,
+ loading: {
+ tree: true,
+ },
+ hasFiles: true,
+ });
+
+ expect(vm.$el.innerHTML).toBeTruthy();
+ expect(vm.$el.querySelector('.fa-spin.fa-spinner').style.marginLeft).toEqual(`${file.level * 10}px`);
+ });
+
+ it('does not render if loading tree', () => {
+ const vm = createComponent({
+ file,
+ activeFile,
+ loading: {
+ tree: true,
+ },
+ });
+
+ expect(vm.$el.innerHTML).toBeFalsy();
+ });
+
+ it('does not render commit message and datetime if mini', () => {
+ const vm = createComponent({
+ file,
+ activeFile,
+ isMini: true,
+ });
+
+ expect(vm.$el.querySelector('.commit-message')).toBeFalsy();
+ expect(vm.$el.querySelector('.commit-update')).toBeFalsy();
+ });
+
+ it('does not set active class if file is active file', () => {
+ const vm = createComponent({
+ file,
+ activeFile: {},
+ });
+
+ expect(vm.$el.classList.contains('active')).toBeFalsy();
+ });
+
+ it('fires linkClicked when the link is clicked', () => {
+ const vm = createComponent({
+ file,
+ activeFile,
+ });
+
+ spyOn(vm, 'linkClicked');
+
+ vm.$el.querySelector('.repo-file-name').click();
+
+ expect(vm.linkClicked).toHaveBeenCalledWith(file);
+ });
+
+ describe('methods', () => {
+ describe('linkClicked', () => {
+ const vm = jasmine.createSpyObj('vm', ['$emit']);
+
+ it('$emits linkclicked with file obj', () => {
+ const theFile = {};
+
+ repoFile.methods.linkClicked.call(vm, theFile);
+
+ expect(vm.$emit).toHaveBeenCalledWith('linkclicked', theFile);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_loading_file_spec.js b/spec/javascripts/repo/components/repo_loading_file_spec.js
new file mode 100644
index 00000000000..d84f4c5609e
--- /dev/null
+++ b/spec/javascripts/repo/components/repo_loading_file_spec.js
@@ -0,0 +1,79 @@
+import Vue from 'vue';
+import repoLoadingFile from '~/repo/components/repo_loading_file.vue';
+
+describe('RepoLoadingFile', () => {
+ function createComponent(propsData) {
+ const RepoLoadingFile = Vue.extend(repoLoadingFile);
+
+ return new RepoLoadingFile({
+ propsData,
+ }).$mount();
+ }
+
+ function assertLines(lines) {
+ lines.forEach((line, n) => {
+ const index = n + 1;
+ expect(line.classList.contains(`line-of-code-${index}`)).toBeTruthy();
+ });
+ }
+
+ function assertColumns(columns) {
+ columns.forEach((column) => {
+ const container = column.querySelector('.animation-container');
+ const lines = [...container.querySelectorAll(':scope > div')];
+
+ expect(container).toBeTruthy();
+ expect(lines.length).toEqual(6);
+ assertLines(lines);
+ });
+ }
+
+ it('renders 3 columns of animated LoC', () => {
+ const vm = createComponent({
+ loading: {
+ tree: true,
+ },
+ hasFiles: false,
+ });
+ const columns = [...vm.$el.querySelectorAll('td')];
+
+ expect(columns.length).toEqual(3);
+ assertColumns(columns);
+ });
+
+ it('renders 1 column of animated LoC if isMini', () => {
+ const vm = createComponent({
+ loading: {
+ tree: true,
+ },
+ hasFiles: false,
+ isMini: true,
+ });
+ const columns = [...vm.$el.querySelectorAll('td')];
+
+ expect(columns.length).toEqual(1);
+ assertColumns(columns);
+ });
+
+ it('does not render if tree is not loading', () => {
+ const vm = createComponent({
+ loading: {
+ tree: false,
+ },
+ hasFiles: false,
+ });
+
+ expect(vm.$el.innerHTML).toBeFalsy();
+ });
+
+ it('does not render if hasFiles is true', () => {
+ const vm = createComponent({
+ loading: {
+ tree: true,
+ },
+ hasFiles: true,
+ });
+
+ expect(vm.$el.innerHTML).toBeFalsy();
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_prev_directory_spec.js b/spec/javascripts/repo/components/repo_prev_directory_spec.js
new file mode 100644
index 00000000000..34dde545e6a
--- /dev/null
+++ b/spec/javascripts/repo/components/repo_prev_directory_spec.js
@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import repoPrevDirectory from '~/repo/components/repo_prev_directory.vue';
+
+describe('RepoPrevDirectory', () => {
+ function createComponent(propsData) {
+ const RepoPrevDirectory = Vue.extend(repoPrevDirectory);
+
+ return new RepoPrevDirectory({
+ propsData,
+ }).$mount();
+ }
+
+ it('renders a prev dir link', () => {
+ const prevUrl = 'prevUrl';
+ const vm = createComponent({
+ prevUrl,
+ });
+ const link = vm.$el.querySelector('a');
+
+ spyOn(vm, 'linkClicked');
+
+ expect(link.href).toMatch(`/${prevUrl}`);
+ expect(link.textContent).toEqual('..');
+
+ link.click();
+
+ expect(vm.linkClicked).toHaveBeenCalledWith(prevUrl);
+ });
+
+ describe('methods', () => {
+ describe('linkClicked', () => {
+ const vm = jasmine.createSpyObj('vm', ['$emit']);
+
+ it('$emits linkclicked with file obj', () => {
+ const file = {};
+
+ repoPrevDirectory.methods.linkClicked.call(vm, file);
+
+ expect(vm.$emit).toHaveBeenCalledWith('linkclicked', file);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_preview_spec.js b/spec/javascripts/repo/components/repo_preview_spec.js
new file mode 100644
index 00000000000..4920cf02083
--- /dev/null
+++ b/spec/javascripts/repo/components/repo_preview_spec.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import repoPreview from '~/repo/components/repo_preview.vue';
+import RepoStore from '~/repo/stores/repo_store';
+
+describe('RepoPreview', () => {
+ function createComponent() {
+ const RepoPreview = Vue.extend(repoPreview);
+
+ return new RepoPreview().$mount();
+ }
+
+ it('renders a div with the activeFile html', () => {
+ const activeFile = {
+ html: '<p class="file-content">html</p>',
+ };
+ RepoStore.activeFile = activeFile;
+
+ const vm = createComponent();
+
+ expect(vm.$el.tagName).toEqual('DIV');
+ expect(vm.$el.innerHTML).toContain(activeFile.html);
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/repo_sidebar_spec.js
new file mode 100644
index 00000000000..0d216c9c026
--- /dev/null
+++ b/spec/javascripts/repo/components/repo_sidebar_spec.js
@@ -0,0 +1,61 @@
+import Vue from 'vue';
+import RepoStore from '~/repo/stores/repo_store';
+import repoSidebar from '~/repo/components/repo_sidebar.vue';
+
+describe('RepoSidebar', () => {
+ function createComponent() {
+ const RepoSidebar = Vue.extend(repoSidebar);
+
+ return new RepoSidebar().$mount();
+ }
+
+ it('renders a sidebar', () => {
+ RepoStore.files = [{
+ id: 0,
+ }];
+ const vm = createComponent();
+ const thead = vm.$el.querySelector('thead');
+ const tbody = vm.$el.querySelector('tbody');
+
+ expect(vm.$el.id).toEqual('sidebar');
+ expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy();
+ expect(thead.querySelector('.name').textContent).toEqual('Name');
+ expect(thead.querySelector('.last-commit').textContent).toEqual('Last Commit');
+ expect(thead.querySelector('.last-update').textContent).toEqual('Last Update');
+ expect(tbody.querySelector('.repo-file-options')).toBeFalsy();
+ expect(tbody.querySelector('.prev-directory')).toBeFalsy();
+ expect(tbody.querySelector('.loading-file')).toBeFalsy();
+ expect(tbody.querySelector('.file')).toBeTruthy();
+ });
+
+ it('does not render a thead, renders repo-file-options and sets sidebar-mini class if isMini', () => {
+ RepoStore.openedFiles = [{
+ id: 0,
+ }];
+ const vm = createComponent();
+
+ expect(vm.$el.classList.contains('sidebar-mini')).toBeTruthy();
+ expect(vm.$el.querySelector('thead')).toBeFalsy();
+ expect(vm.$el.querySelector('tbody .repo-file-options')).toBeTruthy();
+ });
+
+ it('renders 5 loading files if tree is loading and not hasFiles', () => {
+ RepoStore.loading = {
+ tree: true,
+ };
+ RepoStore.files = [];
+ const vm = createComponent();
+
+ expect(vm.$el.querySelectorAll('tbody .loading-file').length).toEqual(5);
+ });
+
+ it('renders a prev directory if isRoot', () => {
+ RepoStore.files = [{
+ id: 0,
+ }];
+ RepoStore.isRoot = true;
+ const vm = createComponent();
+
+ expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy();
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_tab_spec.js b/spec/javascripts/repo/components/repo_tab_spec.js
new file mode 100644
index 00000000000..f3572804b4a
--- /dev/null
+++ b/spec/javascripts/repo/components/repo_tab_spec.js
@@ -0,0 +1,88 @@
+import Vue from 'vue';
+import repoTab from '~/repo/components/repo_tab.vue';
+
+describe('RepoTab', () => {
+ function createComponent(propsData) {
+ const RepoTab = Vue.extend(repoTab);
+
+ return new RepoTab({
+ propsData,
+ }).$mount();
+ }
+
+ it('renders a close link and a name link', () => {
+ const tab = {
+ loading: false,
+ url: 'url',
+ name: 'name',
+ };
+ const vm = createComponent({
+ tab,
+ });
+ const close = vm.$el.querySelector('.close');
+ const name = vm.$el.querySelector(`a[title="${tab.url}"]`);
+
+ spyOn(vm, 'xClicked');
+ spyOn(vm, 'tabClicked');
+
+ expect(close.querySelector('.fa-times')).toBeTruthy();
+ expect(name.textContent).toEqual(tab.name);
+
+ close.click();
+ name.click();
+
+ expect(vm.xClicked).toHaveBeenCalledWith(tab);
+ expect(vm.tabClicked).toHaveBeenCalledWith(tab);
+ });
+
+ it('renders a spinner if tab is loading', () => {
+ const tab = {
+ loading: true,
+ url: 'url',
+ };
+ const vm = createComponent({
+ tab,
+ });
+ const close = vm.$el.querySelector('.close');
+ const name = vm.$el.querySelector(`a[title="${tab.url}"]`);
+
+ expect(close).toBeFalsy();
+ expect(name).toBeFalsy();
+ expect(vm.$el.querySelector('.fa.fa-spinner.fa-spin')).toBeTruthy();
+ });
+
+ it('renders an fa-circle icon if tab is changed', () => {
+ const tab = {
+ loading: false,
+ url: 'url',
+ name: 'name',
+ changed: true,
+ };
+ const vm = createComponent({
+ tab,
+ });
+
+ expect(vm.$el.querySelector('.close .fa-circle')).toBeTruthy();
+ });
+
+ describe('methods', () => {
+ describe('xClicked', () => {
+ const vm = jasmine.createSpyObj('vm', ['$emit']);
+
+ it('returns undefined and does not $emit if file is changed', () => {
+ const file = { changed: true };
+ const returnVal = repoTab.methods.xClicked.call(vm, file);
+
+ expect(returnVal).toBeUndefined();
+ expect(vm.$emit).not.toHaveBeenCalled();
+ });
+
+ it('$emits xclicked event with file obj', () => {
+ const file = { changed: false };
+ repoTab.methods.xClicked.call(vm, file);
+
+ expect(vm.$emit).toHaveBeenCalledWith('xclicked', file);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_tabs_spec.js b/spec/javascripts/repo/components/repo_tabs_spec.js
new file mode 100644
index 00000000000..fdb12cfc00f
--- /dev/null
+++ b/spec/javascripts/repo/components/repo_tabs_spec.js
@@ -0,0 +1,64 @@
+import Vue from 'vue';
+import RepoStore from '~/repo/stores/repo_store';
+import repoTabs from '~/repo/components/repo_tabs.vue';
+
+describe('RepoTabs', () => {
+ const openedFiles = [{
+ id: 0,
+ active: true,
+ }, {
+ id: 1,
+ }];
+
+ function createComponent() {
+ const RepoTabs = Vue.extend(repoTabs);
+
+ return new RepoTabs().$mount();
+ }
+
+ it('renders a list of tabs', () => {
+ RepoStore.openedFiles = openedFiles;
+ RepoStore.tabsOverflow = true;
+
+ const vm = createComponent();
+ const tabs = [...vm.$el.querySelectorAll(':scope > li')];
+
+ expect(vm.$el.id).toEqual('tabs');
+ expect(vm.$el.classList.contains('overflown')).toBeTruthy();
+ expect(tabs.length).toEqual(3);
+ expect(tabs[0].classList.contains('active')).toBeTruthy();
+ expect(tabs[1].classList.contains('active')).toBeFalsy();
+ expect(tabs[2].classList.contains('tabs-divider')).toBeTruthy();
+ });
+
+ it('does not render a tabs list if not isMini', () => {
+ RepoStore.openedFiles = [];
+
+ const vm = createComponent();
+
+ expect(vm.$el.innerHTML).toBeFalsy();
+ });
+
+ it('does not apply overflown class if not tabsOverflow', () => {
+ RepoStore.openedFiles = openedFiles;
+ RepoStore.tabsOverflow = false;
+
+ const vm = createComponent();
+
+ expect(vm.$el.classList.contains('overflown')).toBeFalsy();
+ });
+
+ describe('methods', () => {
+ describe('xClicked', () => {
+ it('calls removeFromOpenedFiles with file obj', () => {
+ const file = {};
+
+ spyOn(RepoStore, 'removeFromOpenedFiles');
+
+ repoTabs.methods.xClicked(file);
+
+ expect(RepoStore.removeFromOpenedFiles).toHaveBeenCalledWith(file);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/repo/monaco_loader_spec.js b/spec/javascripts/repo/monaco_loader_spec.js
new file mode 100644
index 00000000000..be6e779c50f
--- /dev/null
+++ b/spec/javascripts/repo/monaco_loader_spec.js
@@ -0,0 +1,17 @@
+/* global __webpack_public_path__ */
+import monacoContext from 'monaco-editor/dev/vs/loader';
+
+describe('MonacoLoader', () => {
+ it('calls require.config and exports require', () => {
+ spyOn(monacoContext.require, 'config');
+
+ const monacoLoader = require('~/repo/monaco_loader'); // eslint-disable-line global-require
+
+ expect(monacoContext.require.config).toHaveBeenCalledWith({
+ paths: {
+ vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase
+ },
+ });
+ expect(monacoLoader.default).toBe(monacoContext.require);
+ });
+});
diff --git a/spec/javascripts/repo/services/repo_service_spec.js b/spec/javascripts/repo/services/repo_service_spec.js
new file mode 100644
index 00000000000..d74e6a67b1e
--- /dev/null
+++ b/spec/javascripts/repo/services/repo_service_spec.js
@@ -0,0 +1,121 @@
+import axios from 'axios';
+import RepoService from '~/repo/services/repo_service';
+
+describe('RepoService', () => {
+ it('has default json format param', () => {
+ expect(RepoService.options.params.format).toBe('json');
+ });
+
+ describe('buildParams', () => {
+ let newParams;
+ const url = 'url';
+
+ beforeEach(() => {
+ newParams = {};
+
+ spyOn(Object, 'assign').and.returnValue(newParams);
+ });
+
+ it('clones params', () => {
+ const params = RepoService.buildParams(url);
+
+ expect(Object.assign).toHaveBeenCalledWith({}, RepoService.options.params);
+
+ expect(params).toBe(newParams);
+ });
+
+ it('sets and returns viewer params to richif urlIsRichBlob is true', () => {
+ spyOn(RepoService, 'urlIsRichBlob').and.returnValue(true);
+
+ const params = RepoService.buildParams(url);
+
+ expect(params.viewer).toEqual('rich');
+ });
+
+ it('returns params urlIsRichBlob is false', () => {
+ spyOn(RepoService, 'urlIsRichBlob').and.returnValue(false);
+
+ const params = RepoService.buildParams(url);
+
+ expect(params.viewer).toBeUndefined();
+ });
+
+ it('calls urlIsRichBlob with the objects url prop if no url arg is provided', () => {
+ spyOn(RepoService, 'urlIsRichBlob');
+ RepoService.url = url;
+
+ RepoService.buildParams();
+
+ expect(RepoService.urlIsRichBlob).toHaveBeenCalledWith(url);
+ });
+ });
+
+ describe('urlIsRichBlob', () => {
+ it('returns true for md extension', () => {
+ const isRichBlob = RepoService.urlIsRichBlob('url.md');
+
+ expect(isRichBlob).toBeTruthy();
+ });
+
+ it('returns false for js extension', () => {
+ const isRichBlob = RepoService.urlIsRichBlob('url.js');
+
+ expect(isRichBlob).toBeFalsy();
+ });
+ });
+
+ describe('getContent', () => {
+ const params = {};
+ const url = 'url';
+ const requestPromise = Promise.resolve();
+
+ beforeEach(() => {
+ spyOn(RepoService, 'buildParams').and.returnValue(params);
+ spyOn(axios, 'get').and.returnValue(requestPromise);
+ });
+
+ it('calls buildParams and axios.get', () => {
+ const request = RepoService.getContent(url);
+
+ expect(RepoService.buildParams).toHaveBeenCalledWith(url);
+ expect(axios.get).toHaveBeenCalledWith(url, {
+ params,
+ });
+ expect(request).toBe(requestPromise);
+ });
+
+ it('uses object url prop if no url arg is provided', () => {
+ RepoService.url = url;
+
+ RepoService.getContent();
+
+ expect(axios.get).toHaveBeenCalledWith(url, {
+ params,
+ });
+ });
+ });
+
+ describe('getBase64Content', () => {
+ const url = 'url';
+ const response = { data: 'data' };
+
+ beforeEach(() => {
+ spyOn(RepoService, 'bufferToBase64');
+ spyOn(axios, 'get').and.returnValue(Promise.resolve(response));
+ });
+
+ it('calls axios.get and bufferToBase64 on completion', (done) => {
+ const request = RepoService.getBase64Content(url);
+
+ expect(axios.get).toHaveBeenCalledWith(url, {
+ responseType: 'arraybuffer',
+ });
+ expect(request).toEqual(jasmine.any(Promise));
+
+ request.then(() => {
+ expect(RepoService.bufferToBase64).toHaveBeenCalledWith(response.data);
+ done();
+ }).catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/sidebar/confidential_edit_buttons_spec.js b/spec/javascripts/sidebar/confidential_edit_buttons_spec.js
new file mode 100644
index 00000000000..482be466aad
--- /dev/null
+++ b/spec/javascripts/sidebar/confidential_edit_buttons_spec.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import editFormButtons from '~/sidebar/components/confidential/edit_form_buttons.vue';
+
+describe('Edit Form Buttons', () => {
+ let vm1;
+ let vm2;
+
+ beforeEach(() => {
+ const Component = Vue.extend(editFormButtons);
+ const toggleForm = () => { };
+ const updateConfidentialAttribute = () => { };
+
+ vm1 = new Component({
+ propsData: {
+ isConfidential: true,
+ toggleForm,
+ updateConfidentialAttribute,
+ },
+ }).$mount();
+
+ vm2 = new Component({
+ propsData: {
+ isConfidential: false,
+ toggleForm,
+ updateConfidentialAttribute,
+ },
+ }).$mount();
+ });
+
+ it('renders on or off text based on confidentiality', () => {
+ expect(
+ vm1.$el.innerHTML.includes('Turn Off'),
+ ).toBe(true);
+
+ expect(
+ vm2.$el.innerHTML.includes('Turn On'),
+ ).toBe(true);
+ });
+});
diff --git a/spec/javascripts/sidebar/confidential_edit_form_buttons_spec.js b/spec/javascripts/sidebar/confidential_edit_form_buttons_spec.js
new file mode 100644
index 00000000000..724f5126945
--- /dev/null
+++ b/spec/javascripts/sidebar/confidential_edit_form_buttons_spec.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import editForm from '~/sidebar/components/confidential/edit_form.vue';
+
+describe('Edit Form Dropdown', () => {
+ let vm1;
+ let vm2;
+
+ beforeEach(() => {
+ const Component = Vue.extend(editForm);
+ const toggleForm = () => { };
+ const updateConfidentialAttribute = () => { };
+
+ vm1 = new Component({
+ propsData: {
+ isConfidential: true,
+ toggleForm,
+ updateConfidentialAttribute,
+ },
+ }).$mount();
+
+ vm2 = new Component({
+ propsData: {
+ isConfidential: false,
+ toggleForm,
+ updateConfidentialAttribute,
+ },
+ }).$mount();
+ });
+
+ it('renders on the appropriate warning text', () => {
+ expect(
+ vm1.$el.innerHTML.includes('You are going to turn off the confidentiality.'),
+ ).toBe(true);
+
+ expect(
+ vm2.$el.innerHTML.includes('You are going to turn on the confidentiality.'),
+ ).toBe(true);
+ });
+});
diff --git a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
new file mode 100644
index 00000000000..90eac1ed1ab
--- /dev/null
+++ b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
@@ -0,0 +1,65 @@
+import Vue from 'vue';
+import confidentialIssueSidebar from '~/sidebar/components/confidential/confidential_issue_sidebar.vue';
+
+describe('Confidential Issue Sidebar Block', () => {
+ let vm1;
+ let vm2;
+
+ beforeEach(() => {
+ const Component = Vue.extend(confidentialIssueSidebar);
+ const service = {
+ update: () => new Promise((resolve, reject) => {
+ resolve(true);
+ reject('failed!');
+ }),
+ };
+
+ vm1 = new Component({
+ propsData: {
+ isConfidential: true,
+ isEditable: true,
+ service,
+ },
+ }).$mount();
+
+ vm2 = new Component({
+ propsData: {
+ isConfidential: false,
+ isEditable: false,
+ service,
+ },
+ }).$mount();
+ });
+
+ it('shows if confidential and/or editable', () => {
+ expect(
+ vm1.$el.innerHTML.includes('Edit'),
+ ).toBe(true);
+
+ expect(
+ vm1.$el.innerHTML.includes('This issue is confidential'),
+ ).toBe(true);
+
+ expect(
+ vm2.$el.innerHTML.includes('None'),
+ ).toBe(true);
+ });
+
+ it('displays the edit form when editable', (done) => {
+ expect(vm1.edit).toBe(false);
+
+ vm1.$el.querySelector('.confidential-edit').click();
+
+ expect(vm1.edit).toBe(true);
+
+ setTimeout(() => {
+ expect(
+ vm1.$el
+ .innerHTML
+ .includes('You are going to turn off the confidentiality.'),
+ ).toBe(true);
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
index ab8a3f6c64c..7ee998c8fce 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
@@ -1,7 +1,6 @@
import Vue from 'vue';
import deploymentComponent from '~/vue_merge_request_widget/components/mr_widget_deployment';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
-import { statusIconEntityMap } from '~/vue_shared/ci_status_icons';
const deploymentMockData = [
{
@@ -43,15 +42,6 @@ describe('MRWidgetDeployment', () => {
});
});
- describe('computed', () => {
- describe('svg', () => {
- it('should have the proper SVG icon', () => {
- const vm = createComponent(deploymentMockData);
- expect(vm.svg).toEqual(statusIconEntityMap.icon_status_success);
- });
- });
- });
-
describe('methods', () => {
let vm = createComponent();
const deployment = deploymentMockData[0];
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
index 6adcbc73ed7..2ae3adc1f93 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
@@ -52,10 +52,10 @@ const createComponent = () => {
};
const messages = {
- loadingMetrics: 'Loading deployment statistics.',
+ loadingMetrics: 'Loading deployment statistics',
hasMetrics: 'Memory usage unchanged from 0MB to 0MB',
- loadFailed: 'Failed to load deployment statistics.',
- metricsUnavailable: 'Deployment statistics are not available currently.',
+ loadFailed: 'Failed to load deployment statistics',
+ metricsUnavailable: 'Deployment statistics are not available currently',
};
describe('MemoryUsage', () => {
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
index 647b59520f8..c763487d12f 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -81,13 +81,12 @@ describe('MRWidgetPipeline', () => {
expect(el.querySelectorAll('.ci-status-icon.ci-status-icon-success').length).toEqual(1);
expect(el.querySelector('.pipeline-id').textContent).toContain(`#${pipeline.id}`);
expect(el.innerText).toContain('passed');
- expect(el.innerText).toContain('with stages');
expect(el.querySelector('.pipeline-id').getAttribute('href')).toEqual(pipeline.path);
expect(el.querySelectorAll('.stage-container').length).toEqual(2);
expect(el.querySelector('.js-ci-error')).toEqual(null);
expect(el.querySelector('.js-commit-link').getAttribute('href')).toEqual(pipeline.commit.commit_path);
expect(el.querySelector('.js-commit-link').textContent).toContain(pipeline.commit.short_id);
- expect(el.querySelector('.js-mr-coverage').textContent).toContain(`Coverage ${pipeline.coverage}%.`);
+ expect(el.querySelector('.js-mr-coverage').textContent).toContain(`Coverage ${pipeline.coverage}%`);
});
it('should list single stage', (done) => {
@@ -95,7 +94,6 @@ describe('MRWidgetPipeline', () => {
Vue.nextTick(() => {
expect(el.querySelectorAll('.stage-container button').length).toEqual(1);
- expect(el.innerText).toContain('with stage');
done();
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js
index f6e0c3dfb74..f86fb6a0b4b 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js
@@ -22,15 +22,16 @@ describe('MRWidgetRelatedLinks', () => {
});
describe('computed', () => {
+ const data = {
+ relatedLinks: {
+ closing: '/foo',
+ mentioned: '/foo',
+ assignToMe: '/foo',
+ },
+ };
+
describe('hasLinks', () => {
it('should return correct value when we have links reference', () => {
- const data = {
- relatedLinks: {
- closing: '/foo',
- mentioned: '/foo',
- assignToMe: '/foo',
- },
- };
const vm = createComponent(data);
expect(vm.hasLinks).toBeTruthy();
@@ -44,44 +45,24 @@ describe('MRWidgetRelatedLinks', () => {
expect(vm.hasLinks).toBeFalsy();
});
});
- });
-
- describe('methods', () => {
- const data = {
- relatedLinks: {
- closing: '<a href="#">#23</a> and <a>#42</a>',
- mentioned: '<a href="#">#7</a>',
- },
- };
- const vm = createComponent(data);
-
- describe('hasMultipleIssues', () => {
- it('should return true if the given text has multiple issues', () => {
- expect(vm.hasMultipleIssues(data.relatedLinks.closing)).toBeTruthy();
- });
-
- it('should return false if the given text has one issue', () => {
- expect(vm.hasMultipleIssues(data.relatedLinks.mentioned)).toBeFalsy();
- });
- });
- describe('issueLabel', () => {
- it('should return true if the given text has multiple issues', () => {
- expect(vm.issueLabel('closing')).toEqual('issues');
- });
-
- it('should return false if the given text has one issue', () => {
- expect(vm.issueLabel('mentioned')).toEqual('issue');
+ describe('closesText', () => {
+ it('returns correct text for open merge request', () => {
+ data.state = 'open';
+ const vm = createComponent(data);
+ expect(vm.closesText).toEqual('Closes');
});
- });
- describe('verbLabel', () => {
- it('should return true if the given text has multiple issues', () => {
- expect(vm.verbLabel('closing')).toEqual('are');
+ it('returns correct text for closed merge request', () => {
+ data.state = 'closed';
+ const vm = createComponent(data);
+ expect(vm.closesText).toEqual('Did not close');
});
- it('should return false if the given text has one issue', () => {
- expect(vm.verbLabel('mentioned')).toEqual('is');
+ it('returns correct tense for merged request', () => {
+ data.state = 'merged';
+ const vm = createComponent(data);
+ expect(vm.closesText).toEqual('Closed');
});
});
});
@@ -95,8 +76,8 @@ describe('MRWidgetRelatedLinks', () => {
});
const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
- expect(content).toContain('Closes issues #23 and #42');
- expect(content).not.toContain('mentioned');
+ expect(content).toContain('Closes #23 and #42');
+ expect(content).not.toContain('Mentions');
});
it('should have only have mentioned issues text', () => {
@@ -106,8 +87,7 @@ describe('MRWidgetRelatedLinks', () => {
},
});
- expect(vm.$el.innerText).toContain('issue #7');
- expect(vm.$el.innerText).toContain('is mentioned but will not be closed.');
+ expect(vm.$el.innerText).toContain('Mentions #7');
expect(vm.$el.innerText).not.toContain('Closes');
});
@@ -120,9 +100,8 @@ describe('MRWidgetRelatedLinks', () => {
});
const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
- expect(content).toContain('Closes issue #7.');
- expect(content).toContain('issues #23 and #42');
- expect(content).toContain('are mentioned but will not be closed.');
+ expect(content).toContain('Closes #7');
+ expect(content).toContain('Mentions #23 and #42');
});
it('should have assing issues link', () => {
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js
index cac2f561a0b..4869fb17d96 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js
@@ -12,7 +12,7 @@ describe('MRWidgetArchived', () => {
expect(el.classList.contains('mr-widget-body')).toBeTruthy();
expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy();
expect(el.querySelector('button').disabled).toBeTruthy();
- expect(el.innerText).toContain('This project is archived, write access has been disabled.');
+ expect(el.innerText).toContain('This project is archived, write access has been disabled');
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js
index 47b4ba893e0..6042d7384d5 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js
@@ -24,8 +24,8 @@ describe('MRWidgetAutoMergeFailed', () => {
it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
- expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
- expect(vm.$el.innerText).toContain('This merge request failed to be merged automatically.');
+ expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeFalsy();
+ expect(vm.$el.innerText).toContain('This merge request failed to be merged automatically');
expect(vm.$el.innerText).toContain(mergeError);
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js
index 3be11d47227..6b7aa935ad3 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js
@@ -12,7 +12,7 @@ describe('MRWidgetChecking', () => {
expect(el.classList.contains('mr-widget-body')).toBeTruthy();
expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy();
expect(el.querySelector('button').disabled).toBeTruthy();
- expect(el.innerText).toContain('Checking ability to merge automatically.');
+ expect(el.innerText).toContain('Checking ability to merge automatically');
expect(el.querySelector('i')).toBeDefined();
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
index e7ae85caec4..3b7b7d93662 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -29,15 +29,16 @@ describe('MRWidgetConflicts', () => {
describe('template', () => {
it('should have correct elements', () => {
const el = createComponent().$el;
- const resolveButton = el.querySelectorAll('.btn-group .btn')[0];
- const mergeLocallyButton = el.querySelectorAll('.btn-group .btn')[1];
+ const resolveButton = el.querySelector('.js-resolve-conflicts-button');
+ const mergeButton = el.querySelector('.mr-widget-body .btn');
+ const mergeLocallyButton = el.querySelector('.js-merge-locally-button');
- expect(el.textContent).toContain('There are merge conflicts.');
+ expect(el.textContent).toContain('There are merge conflicts');
expect(el.textContent).not.toContain('ask someone with write access');
expect(el.querySelector('.btn-success').disabled).toBeTruthy();
- expect(el.querySelectorAll('.btn-group .btn').length).toBe(2);
expect(resolveButton.textContent).toContain('Resolve conflicts');
expect(resolveButton.getAttribute('href')).toEqual(path);
+ expect(mergeButton.textContent).toContain('Merge');
expect(mergeLocallyButton.textContent).toContain('Merge locally');
});
@@ -59,8 +60,8 @@ describe('MRWidgetConflicts', () => {
it('should not have action buttons', (done) => {
Vue.nextTick(() => {
expect(vm.$el.querySelectorAll('.btn').length).toBe(1);
- expect(vm.$el.querySelector('a.js-resolve-conflicts-button')).toEqual(null);
- expect(vm.$el.querySelector('a.js-merge-locally-button')).toEqual(null);
+ expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toEqual(null);
+ expect(vm.$el.querySelector('.js-merge-locally-button')).toEqual(null);
done();
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
index 587b83430d9..cef365eec8a 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
@@ -94,7 +94,7 @@ describe('MRWidgetFailedToMerge', () => {
expect(el.querySelector('button').innerText).toContain('Merge');
expect(el.querySelector('.js-refresh-button').innerText).toContain('Refresh now');
expect(el.querySelector('.js-refresh-label')).toEqual(null);
- expect(el.innerText).not.toContain('Refreshing now...');
+ expect(el.innerText).not.toContain('Refreshing now');
setTimeout(() => {
expect(el.innerText).toContain('Refreshing in 9 seconds');
done();
@@ -115,7 +115,7 @@ describe('MRWidgetFailedToMerge', () => {
vm.refresh();
Vue.nextTick(() => {
expect(el.innerText).not.toContain('Merge failed. Refreshing');
- expect(el.innerText).toContain('Refreshing now...');
+ expect(el.innerText).toContain('Refreshing now');
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js
index fb2ef606604..237035648cf 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js
@@ -1,10 +1,10 @@
import Vue from 'vue';
-import lockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_locked';
+import mergingComponent from '~/vue_merge_request_widget/components/states/mr_widget_merging';
-describe('MRWidgetLocked', () => {
+describe('MRWidgetMerging', () => {
describe('props', () => {
it('should have props', () => {
- const { mr } = lockedComponent.props;
+ const { mr } = mergingComponent.props;
expect(mr.type instanceof Object).toBeTruthy();
expect(mr.required).toBeTruthy();
@@ -13,7 +13,7 @@ describe('MRWidgetLocked', () => {
describe('template', () => {
it('should have correct elements', () => {
- const Component = Vue.extend(lockedComponent);
+ const Component = Vue.extend(mergingComponent);
const mr = {
targetBranchPath: '/branch-path',
targetBranch: 'branch',
@@ -24,7 +24,7 @@ describe('MRWidgetLocked', () => {
}).$el;
expect(el.classList.contains('mr-widget-body')).toBeTruthy();
- expect(el.innerText).toContain('it is locked');
+ expect(el.innerText).toContain('This merge request is in the process of being merged');
expect(el.innerText).toContain('changes will be merged into');
expect(el.querySelector('.label-branch a').getAttribute('href')).toEqual(mr.targetBranchPath);
expect(el.querySelector('.label-branch a').textContent).toContain(mr.targetBranch);
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js
index 8d8b90cea16..9a71d0b47d7 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js
@@ -162,10 +162,10 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
it('should have correct elements', () => {
expect(el.classList.contains('mr-widget-body')).toBeTruthy();
- expect(el.innerText).toContain('to be merged automatically when the pipeline succeeds.');
+ expect(el.innerText).toContain('to be merged automatically when the pipeline succeeds');
expect(el.innerText).toContain('The changes will be merged into');
expect(el.innerText).toContain(targetBranch);
- expect(el.innerText).toContain('The source branch will not be removed.');
+ expect(el.innerText).toContain('The source branch will not be removed');
expect(el.querySelector('.js-cancel-auto-merge').innerText).toContain('Cancel automatic merge');
expect(el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeFalsy();
expect(el.querySelector('.js-remove-source-branch').innerText).toContain('Remove source branch');
@@ -186,8 +186,8 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
Vue.nextTick(() => {
const normalizedText = el.innerText.replace(/\s+/g, ' ');
- expect(normalizedText).toContain('The source branch will be removed.');
- expect(normalizedText).not.toContain('The source branch will not be removed.');
+ expect(normalizedText).toContain('The source branch will be removed');
+ expect(normalizedText).not.toContain('The source branch will not be removed');
done();
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
index 6628010112d..afaa750199a 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -142,19 +142,19 @@ describe('MRWidgetMerged', () => {
expect(el.querySelector('.js-mr-widget-author')).toBeDefined();
expect(el.innerText).toContain('The changes were merged into');
expect(el.innerText).toContain(targetBranch);
- expect(el.innerText).toContain('The source branch has been removed.');
+ expect(el.innerText).toContain('The source branch has been removed');
expect(el.innerText).toContain('Revert');
expect(el.innerText).toContain('Cherry-pick');
- expect(el.innerText).not.toContain('You can remove source branch now.');
- expect(el.innerText).not.toContain('The source branch is being removed.');
+ expect(el.innerText).not.toContain('You can remove source branch now');
+ expect(el.innerText).not.toContain('The source branch is being removed');
});
it('should not show source branch removed text', (done) => {
vm.mr.sourceBranchRemoved = false;
Vue.nextTick(() => {
- expect(el.innerText).toContain('You can remove source branch now.');
- expect(el.innerText).not.toContain('The source branch has been removed.');
+ expect(el.innerText).toContain('You can remove source branch now');
+ expect(el.innerText).not.toContain('The source branch has been removed');
done();
});
});
@@ -164,9 +164,9 @@ describe('MRWidgetMerged', () => {
vm.mr.sourceBranchRemoved = false;
Vue.nextTick(() => {
- expect(el.innerText).toContain('The source branch is being removed.');
- expect(el.innerText).not.toContain('You can remove source branch now.');
- expect(el.innerText).not.toContain('The source branch has been removed.');
+ expect(el.innerText).toContain('The source branch is being removed');
+ expect(el.innerText).not.toContain('You can remove source branch now');
+ expect(el.innerText).not.toContain('The source branch has been removed');
done();
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
index 98674d12afb..720effb5c1c 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
@@ -49,7 +49,7 @@ describe('MRWidgetMissingBranch', () => {
expect(el.classList.contains('mr-widget-body')).toBeTruthy();
expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(content).toContain('source branch does not exist.');
- expect(content).toContain('Please restore the source branch or use a different source branch.');
+ expect(content).toContain('Please restore it or use a different source branch');
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js
index 61e00f4cf79..33f20ab132d 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js
@@ -11,7 +11,7 @@ describe('MRWidgetNotAllowed', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(vm.$el.innerText).toContain('Ready to be merged automatically.');
- expect(vm.$el.innerText).toContain('Ask someone with write access to this repository to merge this request.');
+ expect(vm.$el.innerText).toContain('Ask someone with write access to this repository to merge this request');
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
index b293d118571..d0702f9f503 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
@@ -10,7 +10,7 @@ describe('MRWidgetPipelineBlocked', () => {
it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
- expect(vm.$el.innerText).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed.');
+ expect(vm.$el.innerText).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed');
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
index 807fba705d4..78bac1c61a5 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
@@ -10,7 +10,7 @@ describe('MRWidgetPipelineFailed', () => {
it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
- expect(vm.$el.innerText).toContain('The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure.');
+ expect(vm.$el.innerText).toContain('The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure');
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index 732b516badd..c607c9746a4 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -72,7 +72,7 @@ describe('MRWidgetReadyToMerge', () => {
const withDesc = 'Include description in commit message';
const withoutDesc = "Don't include description in commit message";
- it('should return message wit description', () => {
+ it('should return message with description', () => {
expect(vm.commitMessageLinkTitle).toEqual(withDesc);
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
index 5fb1d69a8b3..4c67504b642 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
@@ -10,7 +10,7 @@ describe('MRWidgetSHAMismatch', () => {
it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
- expect(vm.$el.innerText).toContain('The source branch HEAD has recently changed. Please reload the page and review the changes before merging.');
+ expect(vm.$el.innerText).toContain('The source branch HEAD has recently changed. Please reload the page and review the changes before merging');
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js
index 45bd1a69964..2cb3aaa6951 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js
@@ -78,7 +78,7 @@ describe('MRWidgetWIP', () => {
it('should have correct elements', () => {
expect(el.classList.contains('mr-widget-body')).toBeTruthy();
- expect(el.innerText).toContain('This merge request is currently Work In Progress and therefore unable to merge');
+ expect(el.innerText).toContain('This is a Work in Progress');
expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(el.querySelector('button').innerText).toContain('Merge');
expect(el.querySelector('.js-remove-wip').innerText).toContain('Resolve WIP status');
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index e6f96d5588b..0795d0aaa82 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -20,7 +20,6 @@ export default {
"human_time_estimate": null,
"human_total_time_spent": null,
"in_progress_merge_commit_sha": null,
- "locked_at": null,
"merge_commit_sha": "53027d060246c8f47e4a9310fb332aa52f221775",
"merge_error": null,
"merge_params": {
@@ -30,6 +29,7 @@ export default {
"merge_user_id": null,
"merge_when_pipeline_succeeds": false,
"source_branch": "daaaa",
+ "source_branch_link": "daaaa",
"source_project_id": 19,
"target_branch": "master",
"target_project_id": 19,
diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
index 3a0c50b750f..669ee248bf1 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -342,7 +342,7 @@ describe('mrWidgetOptions', () => {
expect(comps['mr-widget-related-links']).toBeDefined();
expect(comps['mr-widget-merged']).toBeDefined();
expect(comps['mr-widget-closed']).toBeDefined();
- expect(comps['mr-widget-locked']).toBeDefined();
+ expect(comps['mr-widget-merging']).toBeDefined();
expect(comps['mr-widget-failed-to-merge']).toBeDefined();
expect(comps['mr-widget-wip']).toBeDefined();
expect(comps['mr-widget-archived']).toBeDefined();
diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
index 5db77566513..ebd6c79077e 100644
--- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
@@ -3,57 +3,57 @@ require 'spec_helper'
describe Banzai::Filter::MilestoneReferenceFilter do
include FilterSpecHelper
- let(:project) { create(:project, :public) }
- let(:milestone) { create(:milestone, project: project) }
- let(:reference) { milestone.to_reference }
+ let(:group) { create(:group, :public) }
+ let(:project) { create(:project, :public, group: group) }
it 'requires project context' do
expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
end
- %w(pre code a style).each do |elem|
- it "ignores valid references contained inside '#{elem}' element" do
- exp = act = "<#{elem}>milestone #{milestone.to_reference}</#{elem}>"
- expect(reference_filter(act).to_html).to eq exp
+ shared_examples 'reference parsing' do
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>milestone #{reference}</#{elem}>"
+ expect(reference_filter(act).to_html).to eq exp
+ end
end
- end
- it 'includes default classes' do
- doc = reference_filter("Milestone #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone has-tooltip'
- end
+ it 'includes default classes' do
+ doc = reference_filter("Milestone #{reference}")
- it 'includes a data-project attribute' do
- doc = reference_filter("Milestone #{reference}")
- link = doc.css('a').first
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone has-tooltip'
+ end
- expect(link).to have_attribute('data-project')
- expect(link.attr('data-project')).to eq project.id.to_s
- end
+ it 'includes a data-project attribute' do
+ doc = reference_filter("Milestone #{reference}")
+ link = doc.css('a').first
- it 'includes a data-milestone attribute' do
- doc = reference_filter("See #{reference}")
- link = doc.css('a').first
+ expect(link).to have_attribute('data-project')
+ expect(link.attr('data-project')).to eq project.id.to_s
+ end
- expect(link).to have_attribute('data-milestone')
- expect(link.attr('data-milestone')).to eq milestone.id.to_s
- end
+ it 'includes a data-milestone attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-milestone')
+ expect(link.attr('data-milestone')).to eq milestone.id.to_s
+ end
- it 'supports an :only_path context' do
- doc = reference_filter("Milestone #{reference}", only_path: true)
- link = doc.css('a').first.attr('href')
+ it 'supports an :only_path context' do
+ doc = reference_filter("Milestone #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
- expect(link).not_to match %r(https?://)
- expect(link).to eq urls
- .project_milestone_path(project, milestone)
+ expect(link).not_to match %r(https?://)
+ expect(link).to eq urls.milestone_path(milestone)
+ end
end
- context 'Integer-based references' do
+ shared_examples 'Integer-based references' do
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
- expect(doc.css('a').first.attr('href')).to eq urls
- .project_milestone_url(project, milestone)
+ expect(doc.css('a').first.attr('href')).to eq urls.milestone_url(milestone)
end
it 'links with adjacent text' do
@@ -68,15 +68,17 @@ describe Banzai::Filter::MilestoneReferenceFilter do
end
end
- context 'String-based single-word references' do
- let(:milestone) { create(:milestone, name: 'gfm', project: project) }
+ shared_examples 'String-based single-word references' do
let(:reference) { "#{Milestone.reference_prefix}#{milestone.name}" }
+ before do
+ milestone.update!(name: 'gfm')
+ end
+
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
- expect(doc.css('a').first.attr('href')).to eq urls
- .project_milestone_url(project, milestone)
+ expect(doc.css('a').first.attr('href')).to eq urls.milestone_url(milestone)
expect(doc.text).to eq 'See gfm'
end
@@ -92,15 +94,17 @@ describe Banzai::Filter::MilestoneReferenceFilter do
end
end
- context 'String-based multi-word references in quotes' do
- let(:milestone) { create(:milestone, name: 'gfm references', project: project) }
+ shared_examples 'String-based multi-word references in quotes' do
let(:reference) { milestone.to_reference(format: :name) }
+ before do
+ milestone.update!(name: 'gfm references')
+ end
+
it 'links to a valid reference' do
doc = reference_filter("See #{reference}")
- expect(doc.css('a').first.attr('href')).to eq urls
- .project_milestone_url(project, milestone)
+ expect(doc.css('a').first.attr('href')).to eq urls.milestone_url(milestone)
expect(doc.text).to eq 'See gfm references'
end
@@ -116,23 +120,27 @@ describe Banzai::Filter::MilestoneReferenceFilter do
end
end
- describe 'referencing a milestone in a link href' do
- let(:reference) { %Q{<a href="#{milestone.to_reference}">Milestone</a>} }
+ shared_examples 'referencing a milestone in a link href' do
+ let(:unquoted_reference) { "#{Milestone.reference_prefix}#{milestone.name}" }
+ let(:link_reference) { %Q{<a href="#{unquoted_reference}">Milestone</a>} }
+
+ before do
+ milestone.update!(name: 'gfm')
+ end
it 'links to a valid reference' do
- doc = reference_filter("See #{reference}")
+ doc = reference_filter("See #{link_reference}")
- expect(doc.css('a').first.attr('href')).to eq urls
- .project_milestone_url(project, milestone)
+ expect(doc.css('a').first.attr('href')).to eq urls.milestone_url(milestone)
end
it 'links with adjacent text' do
- doc = reference_filter("Milestone (#{reference}.)")
+ doc = reference_filter("Milestone (#{link_reference}.)")
expect(doc.to_html).to match(%r(\(<a.+>Milestone</a>\.\)))
end
it 'includes a data-project attribute' do
- doc = reference_filter("Milestone #{reference}")
+ doc = reference_filter("Milestone #{link_reference}")
link = doc.css('a').first
expect(link).to have_attribute('data-project')
@@ -140,7 +148,7 @@ describe Banzai::Filter::MilestoneReferenceFilter do
end
it 'includes a data-milestone attribute' do
- doc = reference_filter("See #{reference}")
+ doc = reference_filter("See #{link_reference}")
link = doc.css('a').first
expect(link).to have_attribute('data-milestone')
@@ -148,7 +156,35 @@ describe Banzai::Filter::MilestoneReferenceFilter do
end
end
- describe 'cross-project / cross-namespace complete reference' do
+ shared_examples 'linking to a milestone as the entire link' do
+ let(:unquoted_reference) { "#{Milestone.reference_prefix}#{milestone.name}" }
+ let(:link) { urls.milestone_url(milestone) }
+ let(:link_reference) { %Q{<a href="#{link}">#{link}</a>} }
+
+ it 'replaces the link text with the milestone reference' do
+ doc = reference_filter("See #{link}")
+
+ expect(doc.css('a').first.text).to eq(unquoted_reference)
+ end
+
+ it 'includes a data-project attribute' do
+ doc = reference_filter("Milestone #{link_reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-project')
+ expect(link.attr('data-project')).to eq project.id.to_s
+ end
+
+ it 'includes a data-milestone attribute' do
+ doc = reference_filter("See #{link_reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-milestone')
+ expect(link.attr('data-milestone')).to eq milestone.id.to_s
+ end
+ end
+
+ shared_examples 'cross-project / cross-namespace complete reference' do
let(:namespace) { create(:namespace) }
let(:another_project) { create(:project, :public, namespace: namespace) }
let(:milestone) { create(:milestone, project: another_project) }
@@ -184,7 +220,7 @@ describe Banzai::Filter::MilestoneReferenceFilter do
end
end
- describe 'cross-project / same-namespace complete reference' do
+ shared_examples 'cross-project / same-namespace complete reference' do
let(:namespace) { create(:namespace) }
let(:project) { create(:project, :public, namespace: namespace) }
let(:another_project) { create(:project, :public, namespace: namespace) }
@@ -221,7 +257,7 @@ describe Banzai::Filter::MilestoneReferenceFilter do
end
end
- describe 'cross project shorthand reference' do
+ shared_examples 'cross project shorthand reference' do
let(:namespace) { create(:namespace) }
let(:project) { create(:project, :public, namespace: namespace) }
let(:another_project) { create(:project, :public, namespace: namespace) }
@@ -258,27 +294,53 @@ describe Banzai::Filter::MilestoneReferenceFilter do
end
end
- describe 'cross project milestone references' do
- let(:another_project) { create(:project, :public) }
- let(:project_path) { another_project.full_path }
- let(:milestone) { create(:milestone, project: another_project) }
- let(:reference) { milestone.to_reference(project) }
+ context 'project milestones' do
+ let(:milestone) { create(:milestone, project: project) }
+ let(:reference) { milestone.to_reference }
- let!(:result) { reference_filter("See #{reference}") }
+ include_examples 'reference parsing'
- it 'points to referenced project milestone page' do
- expect(result.css('a').first.attr('href')).to eq urls
- .project_milestone_url(another_project, milestone)
+ it_behaves_like 'Integer-based references'
+ it_behaves_like 'String-based single-word references'
+ it_behaves_like 'String-based multi-word references in quotes'
+ it_behaves_like 'referencing a milestone in a link href'
+ it_behaves_like 'cross-project / cross-namespace complete reference'
+ it_behaves_like 'cross-project / same-namespace complete reference'
+ it_behaves_like 'cross project shorthand reference'
+ end
+
+ context 'group milestones' do
+ let(:milestone) { create(:milestone, group: group) }
+ let(:reference) { milestone.to_reference(format: :name) }
+
+ include_examples 'reference parsing'
+
+ it_behaves_like 'String-based single-word references'
+ it_behaves_like 'String-based multi-word references in quotes'
+ it_behaves_like 'referencing a milestone in a link href'
+
+ it 'does not support references by IID' do
+ doc = reference_filter("See #{Milestone.reference_prefix}#{milestone.iid}")
+
+ expect(doc.css('a')).to be_empty
end
- it 'contains cross project content' do
- expect(result.css('a').first.text).to eq "#{milestone.name} in #{project_path}"
+ it 'does not support references by link' do
+ doc = reference_filter("See #{urls.milestone_url(milestone)}")
+
+ expect(doc.css('a').first.text).to eq(urls.milestone_url(milestone))
end
- it 'escapes the name attribute' do
- allow_any_instance_of(Milestone).to receive(:title).and_return(%{"></a>whatever<a title="})
- doc = reference_filter("See #{reference}")
- expect(doc.css('a').first.text).to eq "#{milestone.name} in #{project_path}"
+ it 'does not support cross-project references' do
+ another_group = create(:group)
+ another_project = create(:project, :public, group: group)
+ project_reference = another_project.to_reference(project)
+
+ milestone.update!(group: another_group)
+
+ doc = reference_filter("See #{project_reference}#{reference}")
+
+ expect(doc.css('a')).to be_empty
end
end
end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 8f57e73e40d..4a498e79c87 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -313,7 +313,8 @@ describe Gitlab::Auth do
def full_authentication_abilities
read_authentication_abilities + [
:push_code,
- :create_container_image
+ :create_container_image,
+ :admin_container_image
]
end
end
diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
index 18843cbe992..f4dfa53f050 100644
--- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
+++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
@@ -170,7 +170,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do
context 'when the merge request diffs are Rugged::Patch instances' do
let(:commits) { merge_request_diff.commits.map(&:to_hash) }
let(:first_commit) { merge_request.project.repository.commit(merge_request_diff.head_commit_sha) }
- let(:diffs) { first_commit.diff_from_parent.patches }
+ let(:diffs) { first_commit.rugged_diff_from_parent.patches }
let(:expected_diffs) { [] }
include_examples 'updated MR diff'
@@ -179,7 +179,7 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits do
context 'when the merge request diffs are Rugged::Diff::Delta instances' do
let(:commits) { merge_request_diff.commits.map(&:to_hash) }
let(:first_commit) { merge_request.project.repository.commit(merge_request_diff.head_commit_sha) }
- let(:diffs) { first_commit.diff_from_parent.deltas }
+ let(:diffs) { first_commit.rugged_diff_from_parent.deltas }
let(:expected_diffs) { [] }
include_examples 'updated MR diff'
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index d7d6a37f7cf..a66347ead76 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -54,7 +54,7 @@ describe Gitlab::BitbucketImport::Importer do
create(
:project,
import_source: project_identifier,
- import_data: ProjectImportData.new(credentials: data)
+ import_data_attributes: { credentials: data }
)
end
diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
index f43d89d7ccd..16704ff5e77 100644
--- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
+++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
@@ -48,8 +48,9 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do
described_class.load_in_batch_for_projects([project_without_status])
end
- it 'only connects to redis_cache twice' do
- # Once to load, once to store in the cache
+ it 'only connects to redis twice' do
+ # Stub circuitbreaker so it doesn't count the redis connections in there
+ stub_circuit_breaker(project_without_status)
expect(Gitlab::Redis::Cache).to receive(:with).exactly(2).and_call_original
described_class.load_in_batch_for_projects([project_without_status])
@@ -301,4 +302,13 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do
end
end
end
+
+ def stub_circuit_breaker(project)
+ fake_circuitbreaker = double
+ allow(fake_circuitbreaker).to receive(:perform).and_yield
+ allow(project.repository.raw_repository)
+ .to receive(:circuit_breaker).and_return(fake_circuitbreaker)
+ allow(project.repository)
+ .to receive(:circuit_breaker).and_return(fake_circuitbreaker)
+ end
end
diff --git a/spec/lib/gitlab/daemon_spec.rb b/spec/lib/gitlab/daemon_spec.rb
new file mode 100644
index 00000000000..c519984a267
--- /dev/null
+++ b/spec/lib/gitlab/daemon_spec.rb
@@ -0,0 +1,103 @@
+require 'spec_helper'
+
+describe Gitlab::Daemon do
+ subject { described_class.new }
+
+ before do
+ allow(subject).to receive(:start_working)
+ allow(subject).to receive(:stop_working)
+ end
+
+ describe '.instance' do
+ before do
+ allow(Kernel).to receive(:at_exit)
+ end
+
+ after(:each) do
+ described_class.instance_variable_set(:@instance, nil)
+ end
+
+ it 'provides instance of Daemon' do
+ expect(described_class.instance).to be_instance_of(described_class)
+ end
+
+ it 'subsequent invocations provide the same instance' do
+ expect(described_class.instance).to eq(described_class.instance)
+ end
+
+ it 'creates at_exit hook when instance is created' do
+ expect(described_class.instance).not_to be_nil
+
+ expect(Kernel).to have_received(:at_exit)
+ end
+ end
+
+ describe 'when Daemon is enabled' do
+ before do
+ allow(subject).to receive(:enabled?).and_return(true)
+ end
+
+ describe 'when Daemon is stopped' do
+ describe '#start' do
+ it 'starts the Daemon' do
+ expect { subject.start.join }.to change { subject.thread? }.from(false).to(true)
+
+ expect(subject).to have_received(:start_working)
+ end
+ end
+
+ describe '#stop' do
+ it "doesn't shutdown stopped Daemon" do
+ expect { subject.stop }.not_to change { subject.thread? }
+
+ expect(subject).not_to have_received(:start_working)
+ end
+ end
+ end
+
+ describe 'when Daemon is running' do
+ before do
+ subject.start.join
+ end
+
+ describe '#start' do
+ it "doesn't start running Daemon" do
+ expect { subject.start.join }.not_to change { subject.thread? }
+
+ expect(subject).to have_received(:start_working).once
+ end
+ end
+
+ describe '#stop' do
+ it 'shutdowns Daemon' do
+ expect { subject.stop }.to change { subject.thread? }.from(true).to(false)
+
+ expect(subject).to have_received(:stop_working)
+ end
+ end
+ end
+ end
+
+ describe 'when Daemon is disabled' do
+ before do
+ allow(subject).to receive(:enabled?).and_return(false)
+ end
+
+ describe '#start' do
+ it "doesn't start working" do
+ expect(subject.start).to be_nil
+ expect { subject.start }.not_to change { subject.thread? }
+
+ expect(subject).not_to have_received(:start_working)
+ end
+ end
+
+ describe '#stop' do
+ it "doesn't stop working" do
+ expect { subject.stop }.not_to change { subject.thread? }
+
+ expect(subject).not_to have_received(:stop_working)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb
index 1482ef7132d..8b14b227e65 100644
--- a/spec/lib/gitlab/encoding_helper_spec.rb
+++ b/spec/lib/gitlab/encoding_helper_spec.rb
@@ -30,6 +30,53 @@ describe Gitlab::EncodingHelper do
it 'leaves binary string as is' do
expect(ext_class.encode!(binary_string)).to eq(binary_string)
end
+
+ context 'with corrupted diff' do
+ let(:corrupted_diff) do
+ with_empty_bare_repository do |repo|
+ content = File.read(Rails.root.join(
+ 'spec/fixtures/encoding/Japanese.md').to_s)
+ commit_a = commit(repo, 'Japanese.md', content)
+ commit_b = commit(repo, 'Japanese.md',
+ content.sub('[TODO: Link]', '[現在作業中です: Link]'))
+
+ repo.diff(commit_a, commit_b).each_line.map(&:content).join
+ end
+ end
+
+ let(:cleaned_diff) do
+ corrupted_diff.dup.force_encoding('UTF-8')
+ .encode!('UTF-8', invalid: :replace, replace: '')
+ end
+
+ let(:encoded_diff) do
+ described_class.encode!(corrupted_diff.dup)
+ end
+
+ it 'does not corrupt data but remove invalid characters' do
+ expect(encoded_diff).to eq(cleaned_diff)
+ end
+
+ def commit(repo, path, content)
+ oid = repo.write(content, :blob)
+ index = repo.index
+
+ index.read_tree(repo.head.target.tree) unless repo.empty?
+
+ index.add(path: path, oid: oid, mode: 0100644)
+ user = { name: 'Test', email: 'test@example.com' }
+
+ Rugged::Commit.create(
+ repo,
+ tree: index.write_tree(repo),
+ author: user,
+ committer: user,
+ message: "Update #{path}",
+ parents: repo.empty? ? [] : [repo.head.target].compact,
+ update_ref: 'HEAD'
+ )
+ end
+ end
end
describe '#encode_utf8' do
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 18320bb23b9..dfab0c2fe85 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -152,6 +152,77 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
+ describe '.batch' do
+ let(:blob_references) do
+ [
+ [SeedRepo::Commit::ID, "files/ruby/popen.rb"],
+ [SeedRepo::Commit::ID, 'six']
+ ]
+ end
+
+ subject { described_class.batch(repository, blob_references) }
+
+ it { expect(subject.size).to eq(blob_references.size) }
+
+ context 'first blob' do
+ let(:blob) { subject[0] }
+
+ it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) }
+ it { expect(blob.name).to eq(SeedRepo::RubyBlob::NAME) }
+ it { expect(blob.path).to eq("files/ruby/popen.rb") }
+ it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) }
+ it { expect(blob.data[0..10]).to eq(SeedRepo::RubyBlob::CONTENT[0..10]) }
+ it { expect(blob.size).to eq(669) }
+ it { expect(blob.mode).to eq("100644") }
+ end
+
+ context 'second blob' do
+ let(:blob) { subject[1] }
+
+ it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') }
+ it { expect(blob.data).to eq('') }
+ it 'does not mark the blob as binary' do
+ expect(blob).not_to be_binary
+ end
+ end
+
+ context 'limiting' do
+ subject { described_class.batch(repository, blob_references, blob_size_limit: blob_size_limit) }
+
+ context 'default' do
+ let(:blob_size_limit) { nil }
+
+ it 'limits to MAX_DATA_DISPLAY_SIZE' do
+ stub_const('Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE', 100)
+
+ expect(subject.first.data.size).to eq(100)
+ end
+ end
+
+ context 'positive' do
+ let(:blob_size_limit) { 10 }
+
+ it { expect(subject.first.data.size).to eq(10) }
+ end
+
+ context 'zero' do
+ let(:blob_size_limit) { 0 }
+
+ it { expect(subject.first.data).to eq('') }
+ end
+
+ context 'negative' do
+ let(:blob_size_limit) { -1 }
+
+ it 'ignores MAX_DATA_DISPLAY_SIZE' do
+ stub_const('Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE', 100)
+
+ expect(subject.first.data.size).to eq(669)
+ end
+ end
+ end
+ end
+
describe 'encoding' do
context 'file with russian text' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/russian.rb") }
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 730fdb112d9..c531d4b055f 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -2,7 +2,7 @@ require "spec_helper"
describe Gitlab::Git::Commit, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) }
- let(:commit) { Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID) }
+ let(:commit) { described_class.find(repository, SeedRepo::Commit::ID) }
let(:rugged_commit) do
repository.rugged.lookup(SeedRepo::Commit::ID)
end
@@ -24,7 +24,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
}
@parents = [repo.head.target]
- @gitlab_parents = @parents.map { |c| Gitlab::Git::Commit.decorate(c) }
+ @gitlab_parents = @parents.map { |c| described_class.decorate(repository, c) }
@tree = @parents.first.tree
sha = Rugged::Commit.create(
@@ -38,7 +38,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
)
@raw_commit = repo.lookup(sha)
- @commit = Gitlab::Git::Commit.new(@raw_commit)
+ @commit = described_class.new(repository, @raw_commit)
end
it { expect(@commit.short_id).to eq(@raw_commit.oid[0..10]) }
@@ -66,6 +66,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
describe "Commit info from gitaly commit" do
let(:id) { 'f00' }
+ let(:parent_ids) { %w(b45 b46) }
let(:subject) { "My commit".force_encoding('ASCII-8BIT') }
let(:body) { subject + "My body".force_encoding('ASCII-8BIT') }
let(:committer) do
@@ -88,10 +89,11 @@ describe Gitlab::Git::Commit, seed_helper: true do
subject: subject,
body: body,
author: author,
- committer: committer
+ committer: committer,
+ parent_ids: parent_ids
)
end
- let(:commit) { described_class.new(Gitlab::GitalyClient::Commit.new(repository, gitaly_commit)) }
+ let(:commit) { described_class.new(repository, gitaly_commit) }
it { expect(commit.short_id).to eq(id[0..10]) }
it { expect(commit.id).to eq(id) }
@@ -102,6 +104,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
it { expect(commit.author_name).to eq(author.name) }
it { expect(commit.committer_name).to eq(committer.name) }
it { expect(commit.committer_email).to eq(committer.email) }
+ it { expect(commit.parent_ids).to eq(parent_ids) }
context 'no body' do
let(:body) { "".force_encoding('ASCII-8BIT') }
@@ -113,45 +116,45 @@ describe Gitlab::Git::Commit, seed_helper: true do
context 'Class methods' do
describe '.find' do
it "should return first head commit if without params" do
- expect(Gitlab::Git::Commit.last(repository).id).to eq(
- repository.raw.head.target.oid
+ expect(described_class.last(repository).id).to eq(
+ repository.rugged.head.target.oid
)
end
it "should return valid commit" do
- expect(Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID)).to be_valid_commit
+ expect(described_class.find(repository, SeedRepo::Commit::ID)).to be_valid_commit
end
it "should return valid commit for tag" do
- expect(Gitlab::Git::Commit.find(repository, 'v1.0.0').id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ expect(described_class.find(repository, 'v1.0.0').id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
end
it "should return nil for non-commit ids" do
blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb")
- expect(Gitlab::Git::Commit.find(repository, blob.id)).to be_nil
+ expect(described_class.find(repository, blob.id)).to be_nil
end
it "should return nil for parent of non-commit object" do
blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb")
- expect(Gitlab::Git::Commit.find(repository, "#{blob.id}^")).to be_nil
+ expect(described_class.find(repository, "#{blob.id}^")).to be_nil
end
it "should return nil for nonexisting ids" do
- expect(Gitlab::Git::Commit.find(repository, "+123_4532530XYZ")).to be_nil
+ expect(described_class.find(repository, "+123_4532530XYZ")).to be_nil
end
context 'with broken repo' do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_BROKEN_REPO_PATH) }
it 'returns nil' do
- expect(Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID)).to be_nil
+ expect(described_class.find(repository, SeedRepo::Commit::ID)).to be_nil
end
end
end
describe '.last_for_path' do
context 'no path' do
- subject { Gitlab::Git::Commit.last_for_path(repository, 'master') }
+ subject { described_class.last_for_path(repository, 'master') }
describe '#id' do
subject { super().id }
@@ -160,7 +163,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
context 'path' do
- subject { Gitlab::Git::Commit.last_for_path(repository, 'master', 'files/ruby') }
+ subject { described_class.last_for_path(repository, 'master', 'files/ruby') }
describe '#id' do
subject { super().id }
@@ -169,7 +172,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
context 'ref + path' do
- subject { Gitlab::Git::Commit.last_for_path(repository, SeedRepo::Commit::ID, 'encoding') }
+ subject { described_class.last_for_path(repository, SeedRepo::Commit::ID, 'encoding') }
describe '#id' do
subject { super().id }
@@ -181,7 +184,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
describe '.where' do
context 'path is empty string' do
subject do
- commits = Gitlab::Git::Commit.where(
+ commits = described_class.where(
repo: repository,
ref: 'master',
path: '',
@@ -199,7 +202,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
context 'path is nil' do
subject do
- commits = Gitlab::Git::Commit.where(
+ commits = described_class.where(
repo: repository,
ref: 'master',
path: nil,
@@ -217,7 +220,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
context 'ref is branch name' do
subject do
- commits = Gitlab::Git::Commit.where(
+ commits = described_class.where(
repo: repository,
ref: 'master',
path: 'files',
@@ -237,7 +240,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
context 'ref is commit id' do
subject do
- commits = Gitlab::Git::Commit.where(
+ commits = described_class.where(
repo: repository,
ref: "874797c3a73b60d2187ed6e2fcabd289ff75171e",
path: 'files',
@@ -257,7 +260,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
context 'ref is tag' do
subject do
- commits = Gitlab::Git::Commit.where(
+ commits = described_class.where(
repo: repository,
ref: 'v1.0.0',
path: 'files',
@@ -278,7 +281,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
describe '.between' do
subject do
- commits = Gitlab::Git::Commit.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID)
+ commits = described_class.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID)
commits.map { |c| c.id }
end
@@ -294,12 +297,12 @@ describe Gitlab::Git::Commit, seed_helper: true do
it 'should return a return a collection of commits' do
commits = described_class.find_all(repository)
- expect(commits).to all( be_a_kind_of(Gitlab::Git::Commit) )
+ expect(commits).to all( be_a_kind_of(described_class) )
end
context 'max_count' do
subject do
- commits = Gitlab::Git::Commit.find_all(
+ commits = described_class.find_all(
repository,
max_count: 50
)
@@ -322,7 +325,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
context 'ref + max_count + skip' do
subject do
- commits = Gitlab::Git::Commit.find_all(
+ commits = described_class.find_all(
repository,
ref: 'master',
max_count: 50,
@@ -374,7 +377,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
describe '#init_from_rugged' do
- let(:gitlab_commit) { Gitlab::Git::Commit.new(rugged_commit) }
+ let(:gitlab_commit) { described_class.new(repository, rugged_commit) }
subject { gitlab_commit }
describe '#id' do
@@ -384,7 +387,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
describe '#init_from_hash' do
- let(:commit) { Gitlab::Git::Commit.new(sample_commit_hash) }
+ let(:commit) { described_class.new(repository, sample_commit_hash) }
subject { commit }
describe '#id' do
@@ -451,7 +454,7 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
describe '#ref_names' do
- let(:commit) { Gitlab::Git::Commit.find(repository, 'master') }
+ let(:commit) { described_class.find(repository, 'master') }
subject { commit.ref_names(repository) }
it 'has 1 element' do
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 9bfad0c9bdf..858616117d5 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -22,7 +22,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe "Respond to" do
subject { repository }
- it { is_expected.to respond_to(:raw) }
it { is_expected.to respond_to(:rugged) }
it { is_expected.to respond_to(:root_ref) }
it { is_expected.to respond_to(:tags) }
@@ -55,6 +54,20 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe "#rugged" do
+ describe 'when storage is broken', broken_storage: true do
+ it 'raises a storage exception when storage is not available' do
+ broken_repo = described_class.new('broken', 'a/path.git')
+
+ expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Storage::Inaccessible)
+ end
+ end
+
+ it 'raises a no repository exception when there is no repo' do
+ broken_repo = described_class.new('default', 'a/path.git')
+
+ expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Repository::NoRepository)
+ end
+
context 'with no Git env stored' do
before do
expect(Gitlab::Git::Env).to receive(:all).and_return({})
@@ -492,17 +505,22 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe "#log" do
- commit_with_old_name = nil
- commit_with_new_name = nil
- rename_commit = nil
+ let(:commit_with_old_name) do
+ Gitlab::Git::Commit.decorate(repository, @commit_with_old_name_id)
+ end
+ let(:commit_with_new_name) do
+ Gitlab::Git::Commit.decorate(repository, @commit_with_new_name_id)
+ end
+ let(:rename_commit) do
+ Gitlab::Git::Commit.decorate(repository, @rename_commit_id)
+ end
before(:context) do
# Add new commits so that there's a renamed file in the commit history
repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH).rugged
-
- commit_with_old_name = Gitlab::Git::Commit.decorate(new_commit_edit_old_file(repo))
- rename_commit = Gitlab::Git::Commit.decorate(new_commit_move_file(repo))
- commit_with_new_name = Gitlab::Git::Commit.decorate(new_commit_edit_new_file(repo))
+ @commit_with_old_name_id = new_commit_edit_old_file(repo)
+ @rename_commit_id = new_commit_move_file(repo)
+ @commit_with_new_name_id = new_commit_edit_new_file(repo)
end
after(:context) do
@@ -741,7 +759,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:options) { { ref: 'master', path: ['PROCESS.md', 'README.md'] } }
def commit_files(commit)
- commit.diff_from_parent.deltas.flat_map do |delta|
+ commit.rugged_diff_from_parent.deltas.flat_map do |delta|
[delta.old_file[:path], delta.new_file[:path]].uniq.compact
end
end
@@ -757,13 +775,13 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
- describe "#commits_between" do
+ describe "#rugged_commits_between" do
context 'two SHAs' do
let(:first_sha) { 'b0e52af38d7ea43cf41d8a6f2471351ac036d6c9' }
let(:second_sha) { '0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326' }
it 'returns the number of commits between' do
- expect(repository.commits_between(first_sha, second_sha).count).to eq(3)
+ expect(repository.rugged_commits_between(first_sha, second_sha).count).to eq(3)
end
end
@@ -772,11 +790,11 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:branch) { 'master' }
it 'returns the number of commits between a sha and a branch' do
- expect(repository.commits_between(sha, branch).count).to eq(5)
+ expect(repository.rugged_commits_between(sha, branch).count).to eq(5)
end
it 'returns the number of commits between a branch and a sha' do
- expect(repository.commits_between(branch, sha).count).to eq(0) # sha is before branch
+ expect(repository.rugged_commits_between(branch, sha).count).to eq(0) # sha is before branch
end
end
@@ -785,7 +803,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:second_branch) { 'master' }
it 'returns the number of commits between' do
- expect(repository.commits_between(first_branch, second_branch).count).to eq(17)
+ expect(repository.rugged_commits_between(first_branch, second_branch).count).to eq(17)
end
end
end
diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb
new file mode 100644
index 00000000000..b2886628601
--- /dev/null
+++ b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb
@@ -0,0 +1,294 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: true, broken_storage: true do
+ let(:circuit_breaker) { described_class.new('default') }
+ let(:hostname) { Gitlab::Environment.hostname }
+ let(:cache_key) { "storage_accessible:default:#{hostname}" }
+
+ def value_from_redis(name)
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.hmget(cache_key, name)
+ end.first
+ end
+
+ def set_in_redis(name, value)
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.hmset(cache_key, name, value)
+ end.first
+ end
+
+ describe '.reset_all!' do
+ it 'clears all entries form redis' do
+ set_in_redis(:failure_count, 10)
+
+ described_class.reset_all!
+
+ key_exists = Gitlab::Git::Storage.redis.with { |redis| redis.exists(cache_key) }
+
+ expect(key_exists).to be_falsey
+ end
+ end
+
+ describe '.for_storage' do
+ it 'only builds a single circuitbreaker per storage' do
+ expect(described_class).to receive(:new).once.and_call_original
+
+ breaker = described_class.for_storage('default')
+
+ expect(breaker).to be_a(described_class)
+ expect(described_class.for_storage('default')).to eq(breaker)
+ end
+ end
+
+ describe '#initialize' do
+ it 'assigns the settings' do
+ expect(circuit_breaker.hostname).to eq(hostname)
+ expect(circuit_breaker.storage).to eq('default')
+ expect(circuit_breaker.storage_path).to eq(TestEnv.repos_path)
+ expect(circuit_breaker.failure_count_threshold).to eq(10)
+ expect(circuit_breaker.failure_wait_time).to eq(30)
+ expect(circuit_breaker.failure_reset_time).to eq(1800)
+ expect(circuit_breaker.storage_timeout).to eq(5)
+ end
+ end
+
+ describe '#perform' do
+ it 'raises an exception with retry time when the circuit is open' do
+ allow(circuit_breaker).to receive(:circuit_broken?).and_return(true)
+
+ expect { |b| circuit_breaker.perform(&b) }
+ .to raise_error(Gitlab::Git::Storage::CircuitOpen)
+ end
+
+ it 'yields the block' do
+ expect { |b| circuit_breaker.perform(&b) }
+ .to yield_control
+ end
+
+ it 'checks if the storage is available' do
+ expect(circuit_breaker).to receive(:check_storage_accessible!)
+
+ circuit_breaker.perform { 'hello world' }
+ end
+
+ it 'returns the value of the block' do
+ result = circuit_breaker.perform { 'return value' }
+
+ expect(result).to eq('return value')
+ end
+
+ it 'raises possible errors' do
+ expect { circuit_breaker.perform { raise Rugged::OSError.new('Broken') } }
+ .to raise_error(Rugged::OSError)
+ end
+
+ context 'with the feature disabled' do
+ it 'returns the block without checking accessibility' do
+ stub_feature_flags(git_storage_circuit_breaker: false)
+
+ expect(circuit_breaker).not_to receive(:circuit_broken?)
+
+ result = circuit_breaker.perform { 'hello' }
+
+ expect(result).to eq('hello')
+ end
+ end
+ end
+
+ describe '#circuit_broken?' do
+ it 'is closed when there is no last failure' do
+ set_in_redis(:last_failure, nil)
+ set_in_redis(:failure_count, 0)
+
+ expect(circuit_breaker.circuit_broken?).to be_falsey
+ end
+
+ it 'is open when there was a recent failure' do
+ Timecop.freeze do
+ set_in_redis(:last_failure, 1.second.ago.to_f)
+ set_in_redis(:failure_count, 1)
+
+ expect(circuit_breaker.circuit_broken?).to be_truthy
+ end
+ end
+
+ it 'is open when there are to many failures' do
+ set_in_redis(:last_failure, 1.day.ago.to_f)
+ set_in_redis(:failure_count, 200)
+
+ expect(circuit_breaker.circuit_broken?).to be_truthy
+ end
+ end
+
+ describe "storage_available?" do
+ context 'when the storage is available' do
+ it 'tracks that the storage was accessible an raises the error' do
+ expect(circuit_breaker).to receive(:track_storage_accessible)
+
+ circuit_breaker.storage_available?
+ end
+
+ it 'only performs the check once' do
+ expect(Gitlab::Git::Storage::ForkedStorageCheck)
+ .to receive(:storage_available?).once.and_call_original
+
+ 2.times { circuit_breaker.storage_available? }
+ end
+ end
+
+ context 'when storage is not available' do
+ let(:circuit_breaker) { described_class.new('broken') }
+
+ it 'tracks that the storage was inaccessible' do
+ expect(circuit_breaker).to receive(:track_storage_inaccessible)
+
+ circuit_breaker.storage_available?
+ end
+ end
+ end
+
+ describe '#check_storage_accessible!' do
+ it 'raises an exception with retry time when the circuit is open' do
+ allow(circuit_breaker).to receive(:circuit_broken?).and_return(true)
+
+ expect { circuit_breaker.check_storage_accessible! }
+ .to raise_error do |exception|
+ expect(exception).to be_kind_of(Gitlab::Git::Storage::CircuitOpen)
+ expect(exception.retry_after).to eq(30)
+ end
+ end
+
+ context 'when the storage is not available' do
+ let(:circuit_breaker) { described_class.new('broken') }
+
+ it 'raises an error' do
+ expect(circuit_breaker).to receive(:track_storage_inaccessible)
+
+ expect { circuit_breaker.check_storage_accessible! }
+ .to raise_error do |exception|
+ expect(exception).to be_kind_of(Gitlab::Git::Storage::Inaccessible)
+ expect(exception.retry_after).to eq(30)
+ end
+ end
+ end
+ end
+
+ describe '#track_storage_inaccessible' do
+ around(:each) do |example|
+ Timecop.freeze
+
+ example.run
+
+ Timecop.return
+ end
+
+ it 'records the failure time in redis' do
+ circuit_breaker.track_storage_inaccessible
+
+ failure_time = value_from_redis(:last_failure)
+
+ expect(Time.at(failure_time.to_i)).to be_within(1.second).of(Time.now)
+ end
+
+ it 'sets the failure time on the breaker without reloading' do
+ circuit_breaker.track_storage_inaccessible
+
+ expect(circuit_breaker).not_to receive(:get_failure_info)
+ expect(circuit_breaker.last_failure).to eq(Time.now)
+ end
+
+ it 'increments the failure count in redis' do
+ set_in_redis(:failure_count, 10)
+
+ circuit_breaker.track_storage_inaccessible
+
+ expect(value_from_redis(:failure_count).to_i).to be(11)
+ end
+
+ it 'increments the failure count on the breaker without reloading' do
+ set_in_redis(:failure_count, 10)
+
+ circuit_breaker.track_storage_inaccessible
+
+ expect(circuit_breaker).not_to receive(:get_failure_info)
+ expect(circuit_breaker.failure_count).to eq(11)
+ end
+ end
+
+ describe '#track_storage_accessible' do
+ it 'sets the failure count to zero in redis' do
+ set_in_redis(:failure_count, 10)
+
+ circuit_breaker.track_storage_accessible
+
+ expect(value_from_redis(:failure_count).to_i).to be(0)
+ end
+
+ it 'sets the failure count to zero on the breaker without reloading' do
+ set_in_redis(:failure_count, 10)
+
+ circuit_breaker.track_storage_accessible
+
+ expect(circuit_breaker).not_to receive(:get_failure_info)
+ expect(circuit_breaker.failure_count).to eq(0)
+ end
+
+ it 'removes the last failure time from redis' do
+ set_in_redis(:last_failure, Time.now.to_i)
+
+ circuit_breaker.track_storage_accessible
+
+ expect(circuit_breaker).not_to receive(:get_failure_info)
+ expect(circuit_breaker.last_failure).to be_nil
+ end
+
+ it 'removes the last failure time from the breaker without reloading' do
+ set_in_redis(:last_failure, Time.now.to_i)
+
+ circuit_breaker.track_storage_accessible
+
+ expect(value_from_redis(:last_failure)).to be_empty
+ end
+
+ it 'wont connect to redis when there are no failures' do
+ expect(Gitlab::Git::Storage.redis).to receive(:with).once
+ .and_call_original
+ expect(circuit_breaker).to receive(:track_storage_accessible)
+ .and_call_original
+
+ circuit_breaker.track_storage_accessible
+ end
+ end
+
+ describe '#no_failures?' do
+ it 'is false when a failure was tracked' do
+ set_in_redis(:last_failure, Time.now.to_i)
+ set_in_redis(:failure_count, 1)
+
+ expect(circuit_breaker.no_failures?).to be_falsey
+ end
+ end
+
+ describe '#last_failure' do
+ it 'returns the last failure time' do
+ time = Time.parse("2017-05-26 17:52:30")
+ set_in_redis(:last_failure, time.to_i)
+
+ expect(circuit_breaker.last_failure).to eq(time)
+ end
+ end
+
+ describe '#failure_count' do
+ it 'returns the failure count' do
+ set_in_redis(:failure_count, 7)
+
+ expect(circuit_breaker.failure_count).to eq(7)
+ end
+ end
+
+ describe '#cache_key' do
+ it 'includes storage and host' do
+ expect(circuit_breaker.cache_key).to eq(cache_key)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb b/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb
new file mode 100644
index 00000000000..12366151f44
--- /dev/null
+++ b/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Storage::ForkedStorageCheck, skip_database_cleaner: true do
+ let(:existing_path) do
+ existing_path = TestEnv.repos_path
+ FileUtils.mkdir_p(existing_path)
+ existing_path
+ end
+
+ describe '.storage_accessible?' do
+ it 'detects when a storage is not available' do
+ expect(described_class.storage_available?('/non/existant/path')).to be_falsey
+ end
+
+ it 'detects when a storage is available' do
+ expect(described_class.storage_available?(existing_path)).to be_truthy
+ end
+
+ it 'returns false when the check takes to long' do
+ # We're forking a process here that takes too long
+ # It will be killed it's parent process will be killed by it's parent
+ # and waited for inside `Gitlab::Git::Storage::ForkedStorageCheck.timeout_check`
+ allow(described_class).to receive(:check_filesystem_in_process) do
+ Process.spawn("sleep 10")
+ end
+ result = true
+
+ runtime = Benchmark.realtime do
+ result = described_class.storage_available?(existing_path, 0.5)
+ end
+
+ expect(result).to be_falsey
+ expect(runtime).to be < 1.0
+ end
+
+ describe 'when using paths with spaces' do
+ let(:test_dir) { Rails.root.join('tmp', 'tests', 'storage_check') }
+ let(:path_with_spaces) { File.join(test_dir, 'path with spaces') }
+
+ around do |example|
+ FileUtils.mkdir_p(path_with_spaces)
+ example.run
+ FileUtils.rm_r(test_dir)
+ end
+
+ it 'works for paths with spaces' do
+ expect(described_class.storage_available?(path_with_spaces)).to be_truthy
+ end
+
+ it 'works for a realpath with spaces' do
+ symlink_location = File.join(test_dir, 'a symlink')
+ FileUtils.ln_s(path_with_spaces, symlink_location)
+
+ expect(described_class.storage_available?(symlink_location)).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/storage/health_spec.rb b/spec/lib/gitlab/git/storage/health_spec.rb
new file mode 100644
index 00000000000..2d3af387971
--- /dev/null
+++ b/spec/lib/gitlab/git/storage/health_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Storage::Health, clean_gitlab_redis_shared_state: true, broken_storage: true do
+ let(:host1_key) { 'storage_accessible:broken:web01' }
+ let(:host2_key) { 'storage_accessible:default:kiq01' }
+
+ def set_in_redis(cache_key, value)
+ Gitlab::Git::Storage.redis.with do |redis|
+ redis.hmset(cache_key, :failure_count, value)
+ end.first
+ end
+
+ describe '.for_failing_storages' do
+ it 'only includes health status for failures' do
+ set_in_redis(host1_key, 10)
+ set_in_redis(host2_key, 0)
+
+ expect(described_class.for_failing_storages.map(&:storage_name))
+ .to contain_exactly('broken')
+ end
+ end
+
+ describe '.load_for_keys' do
+ let(:subject) do
+ results = Gitlab::Git::Storage.redis.with do |redis|
+ fake_future = double
+ allow(fake_future).to receive(:value).and_return([host1_key])
+ described_class.load_for_keys({ 'broken' => fake_future }, redis)
+ end
+
+ # Make sure the `Redis#future is loaded
+ results.inject({}) do |result, (name, info)|
+ info.each { |i| i[:failure_count] = i[:failure_count].value.to_i }
+
+ result[name] = info
+
+ result
+ end
+ end
+
+ it 'loads when there is no info in redis' do
+ expect(subject).to eq('broken' => [{ name: host1_key, failure_count: 0 }])
+ end
+
+ it 'reads the correct values for a storage from redis' do
+ set_in_redis(host1_key, 5)
+ set_in_redis(host2_key, 7)
+
+ expect(subject).to eq('broken' => [{ name: host1_key, failure_count: 5 }])
+ end
+ end
+
+ describe '.for_all_storages' do
+ it 'loads health status for all configured storages' do
+ healths = described_class.for_all_storages
+
+ expect(healths.map(&:storage_name)).to contain_exactly('default', 'broken')
+ end
+ end
+
+ describe '#failing_info' do
+ it 'only contains storages that have failures' do
+ health = described_class.new('broken', [{ name: host1_key, failure_count: 0 },
+ { name: host2_key, failure_count: 3 }])
+
+ expect(health.failing_info).to contain_exactly({ name: host2_key, failure_count: 3 })
+ end
+ end
+
+ describe '#total_failures' do
+ it 'sums up all the failures' do
+ health = described_class.new('broken', [{ name: host1_key, failure_count: 2 },
+ { name: host2_key, failure_count: 3 }])
+
+ expect(health.total_failures).to eq(5)
+ end
+ end
+
+ describe '#failing_on_hosts' do
+ it 'collects only the failing hostnames' do
+ health = described_class.new('broken', [{ name: host1_key, failure_count: 2 },
+ { name: host2_key, failure_count: 0 }])
+
+ expect(health.failing_on_hosts).to contain_exactly('web01')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index d71e0f84c65..7fe698fcb18 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::GitalyClient::CommitService do
context 'when a commit does not have a parent' do
it 'sends an RPC request with empty tree ref as left commit' do
- initial_commit = project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863')
+ initial_commit = project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863').raw
request = Gitaly::CommitDiffRequest.new(
repository: repository_message,
left_commit_id: '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
@@ -46,18 +46,10 @@ describe Gitlab::GitalyClient::CommitService do
end
end
- it 'returns a Gitlab::Git::DiffCollection' do
+ it 'returns a Gitlab::GitalyClient::DiffStitcher' do
ret = client.diff_from_parent(commit)
- expect(ret).to be_kind_of(Gitlab::Git::DiffCollection)
- end
-
- it 'passes options to Gitlab::Git::DiffCollection' do
- options = { max_files: 31, max_lines: 13, from_gitaly: true }
-
- expect(Gitlab::Git::DiffCollection).to receive(:new).with(kind_of(Enumerable), options)
-
- client.diff_from_parent(commit, options)
+ expect(ret).to be_kind_of(Gitlab::GitalyClient::DiffStitcher)
end
end
@@ -120,4 +112,18 @@ describe Gitlab::GitalyClient::CommitService do
client.tree_entries(repository, revision, path)
end
end
+
+ describe '#find_commit' do
+ let(:revision) { '4b825dc642cb6eb9a060e54bf8d69288fbee4904' }
+ it 'sends an RPC request' do
+ request = Gitaly::FindCommitRequest.new(
+ repository: repository_message, revision: revision
+ )
+
+ expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:find_commit)
+ .with(request, kind_of(Hash)).and_return(double(commit: nil))
+
+ described_class.new(repository).find_commit(revision)
+ end
+ end
end
diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
index 8abc4320c59..26574df8bb5 100644
--- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb
@@ -44,6 +44,15 @@ describe Gitlab::HealthChecks::FsShardsCheck do
describe '#readiness' do
subject { described_class.readiness }
+ context 'storage has a tripped circuitbreaker', broken_storage: true do
+ let(:repository_storages) { ['broken'] }
+ let(:storages_paths) do
+ Gitlab.config.repositories.storages
+ end
+
+ it { is_expected.to include(result_class.new(false, 'circuitbreaker tripped', shard: 'broken')) }
+ end
+
context 'storage points to not existing folder' do
let(:storages_paths) do
{
@@ -51,6 +60,10 @@ describe Gitlab::HealthChecks::FsShardsCheck do
}.with_indifferent_access
end
+ before do
+ allow(described_class).to receive(:storage_circuitbreaker_test) { true }
+ end
+
it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: :default)) }
end
@@ -109,6 +122,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
expect(metrics).to include(an_object_having_attributes(name: :filesystem_access_latency_seconds, value: be >= 0))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_read_latency_seconds, value: be >= 0))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_write_latency_seconds, value: be >= 0))
+ expect(metrics).to include(an_object_having_attributes(name: :filesystem_circuitbreaker_latency_seconds, value: be >= 0))
end
end
@@ -127,6 +141,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do
expect(metrics).to include(an_object_having_attributes(name: :filesystem_access_latency_seconds, value: be >= 0))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_read_latency_seconds, value: be >= 0))
expect(metrics).to include(an_object_having_attributes(name: :filesystem_write_latency_seconds, value: be >= 0))
+ expect(metrics).to include(an_object_having_attributes(name: :filesystem_circuitbreaker_latency_seconds, value: be >= 0))
end
it 'cleans up files used for metrics' do
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 469a014e4d2..4e631e13410 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -2534,7 +2534,6 @@
"iid": 9,
"description": null,
"position": 0,
- "locked_at": null,
"updated_by_id": null,
"merge_error": null,
"merge_params": {
@@ -2983,7 +2982,6 @@
"iid": 8,
"description": null,
"position": 0,
- "locked_at": null,
"updated_by_id": null,
"merge_error": null,
"merge_params": {
@@ -3267,7 +3265,6 @@
"iid": 7,
"description": "Et commodi deserunt aspernatur vero rerum. Ut non dolorum alias in odit est libero. Voluptatibus eos in et vitae repudiandae facilis ex mollitia.",
"position": 0,
- "locked_at": null,
"updated_by_id": null,
"merge_error": null,
"merge_params": {
@@ -3551,7 +3548,6 @@
"iid": 6,
"description": "Dicta magnam non voluptates nam dignissimos nostrum deserunt. Dolorum et suscipit iure quae doloremque. Necessitatibus saepe aut labore sed.",
"position": 0,
- "locked_at": null,
"updated_by_id": null,
"merge_error": null,
"merge_params": {
@@ -4241,7 +4237,6 @@
"iid": 5,
"description": "Est eaque quasi qui qui. Similique voluptatem impedit iusto ratione reprehenderit. Itaque est illum ut nulla aut.",
"position": 0,
- "locked_at": null,
"updated_by_id": null,
"merge_error": null,
"merge_params": {
@@ -4789,7 +4784,6 @@
"iid": 4,
"description": "Nam magnam odit velit rerum. Sapiente dolore sunt saepe debitis. Culpa maiores ut ad dolores dolorem et.",
"position": 0,
- "locked_at": null,
"updated_by_id": null,
"merge_error": null,
"merge_params": {
@@ -5288,7 +5282,6 @@
"iid": 3,
"description": "Libero nesciunt mollitia quis odit eos vero quasi. Iure voluptatem ut sint pariatur voluptates ut aut. Laborum possimus unde illum ipsum eum.",
"position": 0,
- "locked_at": null,
"updated_by_id": null,
"merge_error": null,
"merge_params": {
@@ -5548,7 +5541,6 @@
"iid": 2,
"description": "Ut dolor quia aliquid dolore et nisi. Est minus suscipit enim quaerat sapiente consequatur rerum. Eveniet provident consequatur dolor accusantium reiciendis.",
"position": 0,
- "locked_at": null,
"updated_by_id": null,
"merge_error": null,
"merge_params": {
@@ -6238,7 +6230,6 @@
"iid": 1,
"description": "Eveniet nihil ratione veniam similique qui aut sapiente tempora. Sed praesentium iusto dignissimos possimus id repudiandae quo nihil. Qui doloremque autem et iure fugit.",
"position": 0,
- "locked_at": null,
"updated_by_id": null,
"merge_error": null,
"merge_params": {
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 11f4c16ff96..4dce48f8079 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -145,7 +145,6 @@ MergeRequest:
- iid
- description
- position
-- locked_at
- updated_by_id
- merge_error
- merge_params
diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb
index b3b5e5e7e33..c5725f47453 100644
--- a/spec/lib/gitlab/import_sources_spec.rb
+++ b/spec/lib/gitlab/import_sources_spec.rb
@@ -56,7 +56,7 @@ describe Gitlab::ImportSources do
describe '.importer' do
import_sources = {
- 'github' => Gitlab::GithubImport::Importer,
+ 'github' => Github::Import,
'bitbucket' => Gitlab::BitbucketImport::Importer,
'gitlab' => Gitlab::GitlabImport::Importer,
'google_code' => Gitlab::GoogleCodeImport::Importer,
diff --git a/spec/lib/gitlab/key_fingerprint_spec.rb b/spec/lib/gitlab/key_fingerprint_spec.rb
index d7bebaca675..f5fd5a96bc9 100644
--- a/spec/lib/gitlab/key_fingerprint_spec.rb
+++ b/spec/lib/gitlab/key_fingerprint_spec.rb
@@ -1,12 +1,82 @@
-require "spec_helper"
+require 'spec_helper'
-describe Gitlab::KeyFingerprint do
- let(:key) { "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" }
- let(:fingerprint) { "3f:a2:ee:de:b5:de:53:c3:aa:2f:9c:45:24:4c:47:7b" }
+describe Gitlab::KeyFingerprint, lib: true do
+ KEYS = {
+ rsa:
+ 'example.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC5z65PwQ1GE6foJgwk' \
+ '9rmQi/glaXbUeVa5uvQpnZ3Z5+forcI7aTngh3aZ/H2UDP2L70TGy7kKNyp0J3a8/OdG' \
+ 'Z08y5yi3JlbjFARO1NyoFEjw2H1SJxeJ43L6zmvTlu+hlK1jSAlidl7enS0ufTlzEEj4' \
+ 'iJcuTPKdVzKRgZuTRVm9woWNVKqIrdRC0rJiTinERnfSAp/vNYERMuaoN4oJt8p/NEek' \
+ 'rmFoDsQOsyDW5RAnCnjWUU+jFBKDpfkJQ1U2n6BjJewC9dl6ODK639l3yN4WOLZEk4tN' \
+ 'UysfbGeF3rmMeflaD6O1Jplpv3YhwVGFNKa7fMq6k3Z0tszTJPYh',
+ ecdsa:
+ 'example.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAI' \
+ 'bmlzdHAyNTYAAABBBKTJy43NZzJSfNxpv/e2E6Zy3qoHoTQbmOsU5FEfpWfWa1MdTeXQ' \
+ 'YvKOi+qz/1AaNx6BK421jGu74JCDJtiZWT8=',
+ ed25519:
+ '@revoked example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjq' \
+ 'uxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf',
+ dss:
+ 'example.com ssh-dss AAAAB3NzaC1kc3MAAACBAP1/U4EddRIpUt9KnC7s5Of2EbdS' \
+ 'PO9EAMMeP4C2USZpRV1AIlH7WT2NWPq/xfW6MPbLm1Vs14E7gB00b/JmYLdrmVClpJ+f' \
+ '6AR7ECLCT7up1/63xhv4O1fnxqimFQ8E+4P208UewwI1VBNaFpEy9nXzrith1yrv8iID' \
+ 'GZ3RSAHHAAAAFQCXYFCPFSMLzLKSuYKi64QL8Fgc9QAAAIEA9+GghdabPd7LvKtcNrhX' \
+ 'uXmUr7v6OuqC+VdMCz0HgmdRWVeOutRZT+ZxBxCBgLRJFnEj6EwoFhO3zwkyjMim4TwW' \
+ 'eotUfI0o4KOuHiuzpnWRbqN/C/ohNWLx+2J6ASQ7zKTxvqhRkImog9/hWuWfBpKLZl6A' \
+ 'e1UlZAFMO/7PSSoAAACBAJcQ4JODqhuGbXIEpqxetm7PWbdbCcr3y/GzIZ066pRovpL6' \
+ 'qm3qCVIym4cyChxWwb8qlyCIi+YRUUWm1z/wiBYT2Vf3S4FXBnyymCkKEaV/EY7+jd4X' \
+ '1bXI58OD2u+bLCB/sInM4fGB8CZUIWT9nJH0Ve9jJUge2ms348/QOJ1+'
+ }.freeze
- describe "#fingerprint" do
- it "generates the key's fingerprint" do
- expect(described_class.new(key).fingerprint).to eq(fingerprint)
+ MD5_FINGERPRINTS = {
+ rsa: '06:b2:8a:92:df:0e:11:2c:ca:7b:8f:a4:ba:6e:4b:fd',
+ ecdsa: '45:ff:5b:98:9a:b6:8a:41:13:c1:30:8b:09:5e:7b:4e',
+ ed25519: '2e:65:6a:c8:cf:bf:b2:8b:9a:bd:6d:9f:11:5c:12:16',
+ dss: '57:98:86:02:5f:9c:f4:9b:ad:5a:1e:51:92:0e:fd:2b'
+ }.freeze
+
+ BIT_COUNTS = {
+ rsa: 2048,
+ ecdsa: 256,
+ ed25519: 256,
+ dss: 1024
+ }.freeze
+
+ describe '#type' do
+ KEYS.each do |type, key|
+ it "calculates the type of #{type} keys" do
+ calculated_type = described_class.new(key).type
+
+ expect(calculated_type).to eq(type.to_s.upcase)
+ end
+ end
+ end
+
+ describe '#fingerprint' do
+ KEYS.each do |type, key|
+ it "calculates the MD5 fingerprint for #{type} keys" do
+ fp = described_class.new(key).fingerprint
+
+ expect(fp).to eq(MD5_FINGERPRINTS[type])
+ end
+ end
+ end
+
+ describe '#bits' do
+ KEYS.each do |type, key|
+ it "calculates the number of bits in #{type} keys" do
+ bits = described_class.new(key).bits
+
+ expect(bits).to eq(BIT_COUNTS[type])
+ end
+ end
+ end
+
+ describe '#key' do
+ it 'carries the unmodified key data' do
+ key = described_class.new(KEYS[:rsa]).key
+
+ expect(key).to eq(KEYS[:rsa])
end
end
end
diff --git a/spec/lib/gitlab/metrics/influx_sampler_spec.rb b/spec/lib/gitlab/metrics/influx_sampler_spec.rb
index 0bc68d64276..999a9536d82 100644
--- a/spec/lib/gitlab/metrics/influx_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/influx_sampler_spec.rb
@@ -11,7 +11,7 @@ describe Gitlab::Metrics::InfluxSampler do
it 'runs once and gathers a sample at a given interval' do
expect(sampler).to receive(:sleep).with(a_kind_of(Numeric)).twice
expect(sampler).to receive(:sample).once
- expect(sampler).to receive(:running).and_return(false, true, false)
+ expect(sampler).to receive(:running).and_return(true, false)
sampler.start.join
end
diff --git a/spec/lib/gitlab/metrics/sidekiq_metrics_exporter_spec.rb b/spec/lib/gitlab/metrics/sidekiq_metrics_exporter_spec.rb
new file mode 100644
index 00000000000..6721e02fb85
--- /dev/null
+++ b/spec/lib/gitlab/metrics/sidekiq_metrics_exporter_spec.rb
@@ -0,0 +1,101 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::SidekiqMetricsExporter do
+ let(:exporter) { described_class.new }
+ let(:server) { double('server') }
+
+ before do
+ allow(::WEBrick::HTTPServer).to receive(:new).and_return(server)
+ allow(server).to receive(:mount)
+ allow(server).to receive(:start)
+ allow(server).to receive(:shutdown)
+ end
+
+ describe 'when exporter is enabled' do
+ before do
+ allow(Settings.monitoring.sidekiq_exporter).to receive(:enabled).and_return(true)
+ end
+
+ describe 'when exporter is stopped' do
+ describe '#start' do
+ it 'starts the exporter' do
+ expect { exporter.start.join }.to change { exporter.thread? }.from(false).to(true)
+
+ expect(server).to have_received(:start)
+ end
+
+ describe 'with custom settings' do
+ let(:port) { 99999 }
+ let(:address) { 'sidekiq_exporter_address' }
+
+ before do
+ allow(Settings.monitoring.sidekiq_exporter).to receive(:port).and_return(port)
+ allow(Settings.monitoring.sidekiq_exporter).to receive(:address).and_return(address)
+ end
+
+ it 'starts server with port and address from settings' do
+ exporter.start.join
+
+ expect(::WEBrick::HTTPServer).to have_received(:new).with(
+ Port: port,
+ BindAddress: address
+ )
+ end
+ end
+ end
+
+ describe '#stop' do
+ it "doesn't shutdown stopped server" do
+ expect { exporter.stop }.not_to change { exporter.thread? }
+
+ expect(server).not_to have_received(:shutdown)
+ end
+ end
+ end
+
+ describe 'when exporter is running' do
+ before do
+ exporter.start.join
+ end
+
+ describe '#start' do
+ it "doesn't start running server" do
+ expect { exporter.start.join }.not_to change { exporter.thread? }
+
+ expect(server).to have_received(:start).once
+ end
+ end
+
+ describe '#stop' do
+ it 'shutdowns server' do
+ expect { exporter.stop }.to change { exporter.thread? }.from(true).to(false)
+
+ expect(server).to have_received(:shutdown)
+ end
+ end
+ end
+ end
+
+ describe 'when exporter is disabled' do
+ before do
+ allow(Settings.monitoring.sidekiq_exporter).to receive(:enabled).and_return(false)
+ end
+
+ describe '#start' do
+ it "doesn't start" do
+ expect(exporter.start).to be_nil
+ expect { exporter.start }.not_to change { exporter.thread? }
+
+ expect(server).not_to have_received(:start)
+ end
+ end
+
+ describe '#stop' do
+ it "doesn't shutdown" do
+ expect { exporter.stop }.not_to change { exporter.thread? }
+
+ expect(server).not_to have_received(:shutdown)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb
new file mode 100644
index 00000000000..12e75cdd5d0
--- /dev/null
+++ b/spec/lib/gitlab/project_template_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+describe Gitlab::ProjectTemplate do
+ describe '.all' do
+ it 'returns a all templates' do
+ expected = [
+ described_class.new('rails', 'Ruby on Rails')
+ ]
+
+ expect(described_class.all).to be_an(Array)
+ expect(described_class.all).to eq(expected)
+ end
+ end
+
+ describe '.find' do
+ subject { described_class.find(query) }
+
+ context 'when there is a match' do
+ let(:query) { :rails }
+
+ it { is_expected.to be_a(described_class) }
+ end
+
+ context 'when there is no match' do
+ let(:query) { 'no-match' }
+
+ it { is_expected.to be(nil) }
+ end
+ end
+
+ describe 'instance methods' do
+ subject { described_class.new('phoenix', 'Phoenix Framework') }
+
+ it { is_expected.to respond_to(:logo, :file, :archive_path) }
+ end
+
+ describe 'validate all templates' do
+ set(:admin) { create(:admin) }
+
+ described_class.all.each do |template|
+ it "#{template.name} has a valid archive" do
+ archive = template.archive_path
+
+ expect(File.exist?(archive)).to be(true)
+ end
+
+ context 'with valid parameters' do
+ it 'can be imported' do
+ params = {
+ template_name: template.name,
+ namespace_id: admin.namespace.id,
+ path: template.name
+ }
+
+ project = Projects::CreateFromTemplateService.new(admin, params).execute
+
+ expect(project).to be_valid
+ expect(project).to be_persisted
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index b90d8dede0f..2345874cf10 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -174,20 +174,94 @@ describe Gitlab::Shell do
end
describe '#fetch_remote' do
+ def fetch_remote(ssh_auth = nil)
+ gitlab_shell.fetch_remote('current/storage', 'project/path', 'new/storage', ssh_auth: ssh_auth)
+ end
+
+ def expect_popen(vars = {})
+ popen_args = [
+ projects_path,
+ 'fetch-remote',
+ 'current/storage',
+ 'project/path.git',
+ 'new/storage',
+ Gitlab.config.gitlab_shell.git_timeout.to_s
+ ]
+
+ expect(Gitlab::Popen).to receive(:popen).with(popen_args, nil, popen_vars.merge(vars))
+ end
+
+ def build_ssh_auth(opts = {})
+ defaults = {
+ ssh_import?: true,
+ ssh_key_auth?: false,
+ ssh_known_hosts: nil,
+ ssh_private_key: nil
+ }
+
+ double(:ssh_auth, defaults.merge(opts))
+ end
+
it 'returns true when the command succeeds' do
- expect(Gitlab::Popen).to receive(:popen)
- .with([projects_path, 'fetch-remote', 'current/storage', 'project/path.git', 'new/storage', '800'],
- nil, popen_vars).and_return([nil, 0])
+ expect_popen.and_return([nil, 0])
- expect(gitlab_shell.fetch_remote('current/storage', 'project/path', 'new/storage')).to be true
+ expect(fetch_remote).to be_truthy
end
it 'raises an exception when the command fails' do
- expect(Gitlab::Popen).to receive(:popen)
- .with([projects_path, 'fetch-remote', 'current/storage', 'project/path.git', 'new/storage', '800'],
- nil, popen_vars).and_return(["error", 1])
+ expect_popen.and_return(["error", 1])
+
+ expect { fetch_remote }.to raise_error(Gitlab::Shell::Error, "error")
+ end
+
+ context 'SSH auth' do
+ it 'passes the SSH key if specified' do
+ expect_popen('GITLAB_SHELL_SSH_KEY' => 'foo').and_return([nil, 0])
+
+ ssh_auth = build_ssh_auth(ssh_key_auth?: true, ssh_private_key: 'foo')
+
+ expect(fetch_remote(ssh_auth)).to be_truthy
+ end
+
+ it 'does not pass an empty SSH key' do
+ expect_popen.and_return([nil, 0])
+
+ ssh_auth = build_ssh_auth(ssh_key_auth: true, ssh_private_key: '')
+
+ expect(fetch_remote(ssh_auth)).to be_truthy
+ end
+
+ it 'does not pass the key unless SSH key auth is to be used' do
+ expect_popen.and_return([nil, 0])
+
+ ssh_auth = build_ssh_auth(ssh_key_auth: false, ssh_private_key: 'foo')
+
+ expect(fetch_remote(ssh_auth)).to be_truthy
+ end
+
+ it 'passes the known_hosts data if specified' do
+ expect_popen('GITLAB_SHELL_KNOWN_HOSTS' => 'foo').and_return([nil, 0])
+
+ ssh_auth = build_ssh_auth(ssh_known_hosts: 'foo')
+
+ expect(fetch_remote(ssh_auth)).to be_truthy
+ end
+
+ it 'does not pass empty known_hosts data' do
+ expect_popen.and_return([nil, 0])
+
+ ssh_auth = build_ssh_auth(ssh_known_hosts: '')
+
+ expect(fetch_remote(ssh_auth)).to be_truthy
+ end
+
+ it 'does not pass known_hosts data unless SSH is to be used' do
+ expect_popen(popen_vars).and_return([nil, 0])
+
+ ssh_auth = build_ssh_auth(ssh_import?: false, ssh_known_hosts: 'foo')
- expect { gitlab_shell.fetch_remote('current/storage', 'project/path', 'new/storage') }.to raise_error(Gitlab::Shell::Error, "error")
+ expect(fetch_remote(ssh_auth)).to be_truthy
+ end
end
end
diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb
index be3908e8f6a..3db19d06305 100644
--- a/spec/lib/mattermost/session_spec.rb
+++ b/spec/lib/mattermost/session_spec.rb
@@ -20,9 +20,10 @@ describe Mattermost::Session, type: :request do
describe '#with session' do
let(:location) { 'http://location.tld' }
+ let(:cookie_header) {'MMOAUTH=taskik8az7rq8k6rkpuas7htia; Path=/;'}
let!(:stub) do
WebMock.stub_request(:get, "#{mattermost_url}/api/v3/oauth/gitlab/login")
- .to_return(headers: { 'location' => location }, status: 307)
+ .to_return(headers: { 'location' => location, 'Set-Cookie' => cookie_header }, status: 307)
end
context 'without oauth uri' do
@@ -34,9 +35,9 @@ describe Mattermost::Session, type: :request do
context 'with oauth_uri' do
let!(:doorkeeper) do
Doorkeeper::Application.create(
- name: "GitLab Mattermost",
+ name: 'GitLab Mattermost',
redirect_uri: "#{mattermost_url}/signup/gitlab/complete\n#{mattermost_url}/login/gitlab/complete",
- scopes: "")
+ scopes: '')
end
context 'without token_uri' do
diff --git a/spec/migrations/calculate_conv_dev_index_percentages_spec.rb b/spec/migrations/calculate_conv_dev_index_percentages_spec.rb
new file mode 100644
index 00000000000..597d8eab51c
--- /dev/null
+++ b/spec/migrations/calculate_conv_dev_index_percentages_spec.rb
@@ -0,0 +1,41 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170803090603_calculate_conv_dev_index_percentages.rb')
+
+describe CalculateConvDevIndexPercentages, truncate: true do
+ let(:migration) { described_class.new }
+ let!(:conv_dev_index) do
+ create(:conversational_development_index_metric,
+ leader_notes: 0,
+ instance_milestones: 0,
+ percentage_issues: 0,
+ percentage_notes: 0,
+ percentage_milestones: 0,
+ percentage_boards: 0,
+ percentage_merge_requests: 0,
+ percentage_ci_pipelines: 0,
+ percentage_environments: 0,
+ percentage_deployments: 0,
+ percentage_projects_prometheus_active: 0,
+ percentage_service_desk_issues: 0)
+ end
+
+ describe '#up' do
+ it 'calculates percentages correctly' do
+ migration.up
+ conv_dev_index.reload
+
+ expect(conv_dev_index.percentage_issues).to be_within(0.1).of(13.3)
+ expect(conv_dev_index.percentage_notes).to be_zero # leader 0
+ expect(conv_dev_index.percentage_milestones).to be_zero # instance 0
+ expect(conv_dev_index.percentage_boards).to be_within(0.1).of(62.4)
+ expect(conv_dev_index.percentage_merge_requests).to eq(50.0)
+ expect(conv_dev_index.percentage_ci_pipelines).to be_within(0.1).of(19.3)
+ expect(conv_dev_index.percentage_environments).to be_within(0.1).of(66.7)
+ expect(conv_dev_index.percentage_deployments).to be_within(0.1).of(64.2)
+ expect(conv_dev_index.percentage_projects_prometheus_active).to be_within(0.1).of(98.2)
+ expect(conv_dev_index.percentage_service_desk_issues).to be_within(0.1).of(84.0)
+ end
+ end
+end
diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
index cf2d5827306..e5793a3c0ee 100644
--- a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
+++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb
@@ -6,7 +6,7 @@ require Rails.root.join('db', 'migrate', '20161124141322_migrate_process_commit_
describe MigrateProcessCommitWorkerJobs do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
- let(:commit) { project.commit.raw.raw_commit }
+ let(:commit) { project.commit.raw.rugged_commit }
describe 'Project' do
describe 'find_including_path' do
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 08693b5da33..c18c635d811 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -33,7 +33,6 @@ describe Commit do
describe '#to_reference' do
let(:project) { create(:project, :repository, path: 'sample-project') }
- let(:commit) { project.commit }
it 'returns a String reference to the object' do
expect(commit.to_reference).to eq commit.id
@@ -47,7 +46,6 @@ describe Commit do
describe '#reference_link_text' do
let(:project) { create(:project, :repository, path: 'sample-project') }
- let(:commit) { project.commit }
it 'returns a String reference to the object' do
expect(commit.reference_link_text).to eq commit.short_id
@@ -191,7 +189,7 @@ eos
it { expect(data).to be_a(Hash) }
it { expect(data[:message]).to include('adds bar folder and branch-test text file to check Repository merged_to_root_ref method') }
- it { expect(data[:timestamp]).to eq('2016-09-27T14:37:46+00:00') }
+ it { expect(data[:timestamp]).to eq('2016-09-27T14:37:46Z') }
it { expect(data[:added]).to eq(["bar/branch-test.txt"]) }
it { expect(data[:modified]).to eq([]) }
it { expect(data[:removed]).to eq([]) }
diff --git a/spec/models/conversational_development_index/metric_spec.rb b/spec/models/conversational_development_index/metric_spec.rb
new file mode 100644
index 00000000000..b3193619503
--- /dev/null
+++ b/spec/models/conversational_development_index/metric_spec.rb
@@ -0,0 +1,11 @@
+require 'rails_helper'
+
+describe ConversationalDevelopmentIndex::Metric do
+ let(:conv_dev_index) { create(:conversational_development_index_metric) }
+
+ describe '#percentage_score' do
+ it 'returns stored percentage score' do
+ expect(conv_dev_index.percentage_score('issues')).to eq(13.331)
+ end
+ end
+end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index fa22eee3dea..c055863d298 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -191,14 +191,10 @@ describe Issue do
end
it 'returns the merge request to close this issue' do
- mr
-
expect(issue.closed_by_merge_requests(mr.author)).to eq([mr])
end
it "returns an empty array when the merge request is closed already" do
- closed_mr
-
expect(issue.closed_by_merge_requests(closed_mr.author)).to eq([])
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 0daeb337168..3508391c721 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -83,15 +83,6 @@ describe Key, :mailer do
expect(build(:key)).to be_valid
end
- it 'rejects an unfingerprintable key that contains a space' do
- key = build(:key)
-
- # Not always the middle, but close enough
- key.key = key.key[0..100] + ' ' + key.key[101..-1]
-
- expect(key).not_to be_valid
- end
-
it 'accepts a key with newline charecters after stripping them' do
key = build(:key)
key.key = key.key.insert(100, "\n")
@@ -102,7 +93,6 @@ describe Key, :mailer do
it 'rejects the unfingerprintable key (not a key)' do
expect(build(:key, key: 'ssh-rsa an-invalid-key==')).not_to be_valid
end
-
end
context 'callbacks' do
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 3402c260f27..a1a3e70a7d2 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1369,6 +1369,32 @@ describe MergeRequest do
end
end
+ describe '#merge_ongoing?' do
+ it 'returns true when merge process is ongoing for merge_jid' do
+ merge_request = create(:merge_request, merge_jid: 'foo')
+
+ allow(Gitlab::SidekiqStatus).to receive(:num_running).with(['foo']).and_return(1)
+
+ expect(merge_request.merge_ongoing?).to be(true)
+ end
+
+ it 'returns false when no merge process running for merge_jid' do
+ merge_request = build(:merge_request, merge_jid: 'foo')
+
+ allow(Gitlab::SidekiqStatus).to receive(:num_running).with(['foo']).and_return(0)
+
+ expect(merge_request.merge_ongoing?).to be(false)
+ end
+
+ it 'returns false when merge_jid is nil' do
+ merge_request = build(:merge_request, merge_jid: nil)
+
+ expect(Gitlab::SidekiqStatus).not_to receive(:num_running)
+
+ expect(merge_request.merge_ongoing?).to be(false)
+ end
+ end
+
describe "#closed_without_fork?" do
let(:project) { create(:project) }
let(:fork_project) { create(:project, forked_from_project: project) }
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index b48aa9558d5..d3da0107d5c 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -230,16 +230,40 @@ describe Milestone do
end
describe '#to_reference' do
- let(:project) { build(:project, name: 'sample-project') }
- let(:milestone) { build(:milestone, iid: 1, project: project) }
+ let(:group) { build_stubbed(:group) }
+ let(:project) { build_stubbed(:project, name: 'sample-project') }
+ let(:another_project) { build_stubbed(:project, name: 'another-project', namespace: project.namespace) }
+
+ context 'for a project milestone' do
+ let(:milestone) { build_stubbed(:milestone, iid: 1, project: project, name: 'milestone') }
+
+ it 'returns a String reference to the object' do
+ expect(milestone.to_reference).to eq '%1'
+ end
+
+ it 'returns a reference by name when the format is set to :name' do
+ expect(milestone.to_reference(format: :name)).to eq '%"milestone"'
+ end
- it 'returns a String reference to the object' do
- expect(milestone.to_reference).to eq "%1"
+ it 'supports a cross-project reference' do
+ expect(milestone.to_reference(another_project)).to eq 'sample-project%1'
+ end
end
- it 'supports a cross-project reference' do
- another_project = build(:project, name: 'another-project', namespace: project.namespace)
- expect(milestone.to_reference(another_project)).to eq "sample-project%1"
+ context 'for a group milestone' do
+ let(:milestone) { build_stubbed(:milestone, iid: 1, group: group, name: 'milestone') }
+
+ it 'returns nil with the default format' do
+ expect(milestone.to_reference).to be_nil
+ end
+
+ it 'returns a reference by name when the format is set to :name' do
+ expect(milestone.to_reference(format: :name)).to eq '%"milestone"'
+ end
+
+ it 'does not supports cross-project references' do
+ expect(milestone.to_reference(another_project, format: :name)).to eq '%"milestone"'
+ end
end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 6e33431bbe9..953df7746eb 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -223,7 +223,12 @@ describe ProjectWiki do
before do
create_page("update-page", "some content")
@gollum_page = subject.wiki.paged("update-page")
- subject.update_page(@gollum_page, "some other content", :markdown, "updated page")
+ subject.update_page(
+ @gollum_page,
+ content: "some other content",
+ format: :markdown,
+ message: "updated page"
+ )
@page = subject.pages.first.page
end
@@ -240,7 +245,12 @@ describe ProjectWiki do
end
it 'updates project activity' do
- subject.update_page(@gollum_page, 'Yet more content', :markdown, 'Updated page again')
+ subject.update_page(
+ @gollum_page,
+ content: 'Yet more content',
+ format: :markdown,
+ message: 'Updated page again'
+ )
project.reload
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index f876baaa805..cfa77648338 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1,11 +1,12 @@
require 'spec_helper'
-describe Repository do
+describe Repository, models: true do
include RepoHelpers
TestBlob = Struct.new(:path)
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
+ let(:broken_repository) { create(:project, :broken_storage).repository }
let(:user) { create(:user) }
let(:commit_options) do
@@ -27,12 +28,27 @@ describe Repository do
let(:author_email) { 'user@example.org' }
let(:author_name) { 'John Doe' }
+ def expect_to_raise_storage_error
+ expect { yield }.to raise_error do |exception|
+ storage_exceptions = [Gitlab::Git::Storage::Inaccessible, Gitlab::Git::CommandError, GRPC::Unavailable]
+ expect(exception.class).to be_in(storage_exceptions)
+ end
+ end
+
describe '#branch_names_contains' do
subject { repository.branch_names_contains(sample_commit.id) }
it { is_expected.to include('master') }
it { is_expected.not_to include('feature') }
it { is_expected.not_to include('fix') }
+
+ describe 'when storage is broken', broken_storage: true do
+ it 'should raise a storage error' do
+ expect_to_raise_storage_error do
+ broken_repository.branch_names_contains(sample_commit.id)
+ end
+ end
+ end
end
describe '#tag_names_contains' do
@@ -143,6 +159,14 @@ describe Repository do
subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id }
it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') }
+
+ describe 'when storage is broken', broken_storage: true do
+ it 'should raise a storage error' do
+ expect_to_raise_storage_error do
+ broken_repository.last_commit_id_for_path(sample_commit.id, '.gitignore')
+ end
+ end
+ end
end
context 'when Gitaly feature last_commit_for_path is enabled' do
@@ -169,6 +193,14 @@ describe Repository do
expect(cache).to receive(:fetch).with(key).and_return('c1acaa5')
is_expected.to eq('c1acaa5')
end
+
+ describe 'when storage is broken', broken_storage: true do
+ it 'should raise a storage error' do
+ expect_to_raise_storage_error do
+ broken_repository.last_commit_for_path(sample_commit.id, '.gitignore').id
+ end
+ end
+ end
end
context 'when Gitaly feature last_commit_for_path is enabled' do
@@ -202,19 +234,37 @@ describe Repository do
end
describe '#find_commits_by_message' do
- it 'returns commits with messages containing a given string' do
- commit_ids = repository.find_commits_by_message('submodule').map(&:id)
+ shared_examples 'finding commits by message' do
+ it 'returns commits with messages containing a given string' do
+ commit_ids = repository.find_commits_by_message('submodule').map(&:id)
+
+ expect(commit_ids).to include(
+ '5937ac0a7beb003549fc5fd26fc247adbce4a52e',
+ '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9',
+ 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660'
+ )
+ expect(commit_ids).not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e')
+ end
+
+ it 'is case insensitive' do
+ commit_ids = repository.find_commits_by_message('SUBMODULE').map(&:id)
+
+ expect(commit_ids).to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ end
+ end
- expect(commit_ids).to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
- expect(commit_ids).to include('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
- expect(commit_ids).to include('cfe32cf61b73a0d5e9f13e774abde7ff789b1660')
- expect(commit_ids).not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e')
+ context 'when Gitaly commits_by_message feature is enabled' do
+ it_behaves_like 'finding commits by message'
end
- it 'is case insensitive' do
- commit_ids = repository.find_commits_by_message('SUBMODULE').map(&:id)
+ context 'when Gitaly commits_by_message feature is disabled', skip_gitaly_mock: true do
+ it_behaves_like 'finding commits by message'
+ end
- expect(commit_ids).to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ describe 'when storage is broken', broken_storage: true do
+ it 'should raise a storage error' do
+ expect_to_raise_storage_error { broken_repository.find_commits_by_message('s') }
+ end
end
end
@@ -541,6 +591,14 @@ describe Repository do
expect(results).to match_array([])
end
+ describe 'when storage is broken', broken_storage: true do
+ it 'should raise a storage error' do
+ expect_to_raise_storage_error do
+ broken_repository.search_files_by_content('feature', 'master')
+ end
+ end
+ end
+
describe 'result' do
subject { results.first }
@@ -569,6 +627,22 @@ describe Repository do
expect(results).to match_array([])
end
+
+ describe 'when storage is broken', broken_storage: true do
+ it 'should raise a storage error' do
+ expect_to_raise_storage_error { broken_repository.search_files_by_name('files', 'master') }
+ end
+ end
+ end
+
+ describe '#fetch_ref' do
+ describe 'when storage is broken', broken_storage: true do
+ it 'should raise a storage error' do
+ path = broken_repository.path_to_repo
+
+ expect_to_raise_storage_error { broken_repository.fetch_ref(path, '1', '2') }
+ end
+ end
end
describe '#create_ref' do
@@ -986,6 +1060,12 @@ describe Repository do
expect(repository.exists?).to eq(false)
end
+
+ context 'with broken storage', broken_storage: true do
+ it 'should raise a storage error' do
+ expect_to_raise_storage_error { broken_repository.exists? }
+ end
+ end
end
describe '#exists?' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index a6bd6052006..0103fb6040e 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1976,4 +1976,28 @@ describe User do
expect(user.allow_password_authentication?).to be_falsey
end
end
+
+ describe '#personal_projects_count' do
+ it 'returns the number of personal projects using a single query' do
+ user = build(:user)
+ projects = double(:projects, count: 1)
+
+ expect(user).to receive(:personal_projects).once.and_return(projects)
+
+ 2.times do
+ expect(user.personal_projects_count).to eq(1)
+ end
+ end
+ end
+
+ describe '#projects_limit_left' do
+ it 'returns the number of projects that can be created by the user' do
+ user = build(:user)
+
+ allow(user).to receive(:projects_limit).and_return(10)
+ allow(user).to receive(:personal_projects_count).and_return(5)
+
+ expect(user.projects_limit_left).to eq(5)
+ end
+ end
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index b7eb412a8de..40a222be24d 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -178,12 +178,12 @@ describe WikiPage do
end
it "updates the content of the page" do
- @page.update("new content")
+ @page.update(content: "new content")
@page = wiki.find_page(title)
end
it "returns true" do
- expect(@page.update("more content")).to be_truthy
+ expect(@page.update(content: "more content")).to be_truthy
end
end
end
@@ -195,29 +195,42 @@ describe WikiPage do
end
after do
- destroy_page("Update")
+ destroy_page(@page.title)
end
context "with valid attributes" do
it "updates the content of the page" do
- @page.update("new content")
+ new_content = "new content"
+
+ @page.update(content: new_content)
@page = wiki.find_page("Update")
+
+ expect(@page.content).to eq("new content")
+ end
+
+ it "updates the title of the page" do
+ new_title = "Index v.1.2.4"
+
+ @page.update(title: new_title)
+ @page = wiki.find_page(new_title)
+
+ expect(@page.title).to eq(new_title)
end
it "returns true" do
- expect(@page.update("more content")).to be_truthy
+ expect(@page.update(content: "more content")).to be_truthy
end
end
context 'with same last commit sha' do
it 'returns true' do
- expect(@page.update('more content', last_commit_sha: @page.last_commit_sha)).to be_truthy
+ expect(@page.update(content: 'more content', last_commit_sha: @page.last_commit_sha)).to be_truthy
end
end
context 'with different last commit sha' do
it 'raises exception' do
- expect { @page.update('more content', last_commit_sha: 'xxx') }.to raise_error(WikiPage::PageChangedError)
+ expect { @page.update(content: 'more content', last_commit_sha: 'xxx') }.to raise_error(WikiPage::PageChangedError)
end
end
end
@@ -249,7 +262,7 @@ describe WikiPage do
end
it "returns an array of all commits for the page" do
- 3.times { |i| @page.update("content #{i}") }
+ 3.times { |i| @page.update(content: "content #{i}") }
expect(@page.versions.count).to eq(4)
end
end
@@ -294,7 +307,7 @@ describe WikiPage do
before do
create_page('Update', 'content')
@page = wiki.find_page('Update')
- 3.times { |i| @page.update("content #{i}") }
+ 3.times { |i| @page.update(content: "content #{i}") }
end
after do
@@ -338,7 +351,7 @@ describe WikiPage do
end
it 'returns false for updated wiki page' do
- updated_wiki_page = original_wiki_page.update("Updated content")
+ updated_wiki_page = original_wiki_page.update(content: "Updated content")
expect(original_wiki_page).not_to eq(updated_wiki_page)
end
end
@@ -360,7 +373,7 @@ describe WikiPage do
it 'is changed after page updated' do
last_commit_sha_before_update = @page.last_commit_sha
- @page.update("new content")
+ @page.update(content: "new content")
@page = wiki.find_page("Update")
expect(@page.last_commit_sha).not_to eq last_commit_sha_before_update
diff --git a/spec/presenters/conversational_development_index/metric_presenter_spec.rb b/spec/presenters/conversational_development_index/metric_presenter_spec.rb
index 1e015c71f5b..81eb05a9a6b 100644
--- a/spec/presenters/conversational_development_index/metric_presenter_spec.rb
+++ b/spec/presenters/conversational_development_index/metric_presenter_spec.rb
@@ -8,9 +8,9 @@ describe ConversationalDevelopmentIndex::MetricPresenter do
it 'includes instance score, leader score and percentage score' do
issues_card = subject.cards.first
- expect(issues_card.instance_score).to eq 1.234
- expect(issues_card.leader_score).to eq 9.256
- expect(issues_card.percentage_score).to be_within(0.1).of(13.3)
+ expect(issues_card.instance_score).to eq(1.234)
+ expect(issues_card.leader_score).to eq(9.256)
+ expect(issues_card.percentage_score).to eq(13.331)
end
end
diff --git a/spec/requests/api/circuit_breakers_spec.rb b/spec/requests/api/circuit_breakers_spec.rb
new file mode 100644
index 00000000000..76521e55994
--- /dev/null
+++ b/spec/requests/api/circuit_breakers_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe API::CircuitBreakers do
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+
+ describe 'GET circuit_breakers/repository_storage' do
+ it 'returns a 401 for anonymous users' do
+ get api('/circuit_breakers/repository_storage')
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns a 403 for users' do
+ get api('/circuit_breakers/repository_storage', user)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it 'returns an Array of storages' do
+ expect(Gitlab::Git::Storage::Health).to receive(:for_all_storages) do
+ [Gitlab::Git::Storage::Health.new('broken', [{ name: 'prefix:broken:web01', failure_count: 4 }])]
+ end
+
+ get api('/circuit_breakers/repository_storage', admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_kind_of(Array)
+ expect(json_response.first['storage_name']).to eq('broken')
+ expect(json_response.first['failing_on_hosts']).to eq(['web01'])
+ expect(json_response.first['total_failures']).to eq(4)
+ end
+
+ describe 'GET circuit_breakers/repository_storage/failing' do
+ it 'returns an array of failing storages' do
+ expect(Gitlab::Git::Storage::Health).to receive(:for_failing_storages) do
+ [Gitlab::Git::Storage::Health.new('broken', [{ name: 'prefix:broken:web01', failure_count: 4 }])]
+ end
+
+ get api('/circuit_breakers/repository_storage/failing', admin)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_kind_of(Array)
+ end
+ end
+ end
+
+ describe 'DELETE circuit_breakers/repository_storage' do
+ it 'clears all circuit_breakers' do
+ expect(Gitlab::Git::Storage::CircuitBreaker).to receive(:reset_all!)
+
+ delete api('/circuit_breakers/repository_storage', admin)
+
+ expect(response).to have_http_status(204)
+ end
+ end
+end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 0dad547735d..992a6e8d76a 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -3,29 +3,27 @@ require 'mime/types'
describe API::Commits do
let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let!(:project) { create(:project, :repository, creator: user, namespace: user.namespace) }
- let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
- let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') }
- let!(:another_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'another comment on a commit') }
+ let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+ let(:branch_with_dot) { project.repository.find_branch('ends-with.json') }
+ let(:branch_with_slash) { project.repository.find_branch('improve/awesome') }
+
+ let(:project_id) { project.id }
+ let(:current_user) { nil }
before do
- project.team << [user, :reporter]
+ project.add_master(user)
end
- describe "List repository commits" do
- context "authorized user" do
- before do
- project.team << [user2, :reporter]
- end
-
+ describe 'GET /projects/:id/repository/commits' do
+ context 'authorized user' do
it "returns project commits" do
commit = project.repository.commit
- get api("/projects/#{project.id}/repository/commits", user)
+ get api("/projects/#{project_id}/repository/commits", user)
expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
+ expect(response).to match_response_schema('public_api/v4/commits')
expect(json_response.first['id']).to eq(commit.id)
expect(json_response.first['committer_name']).to eq(commit.committer_name)
expect(json_response.first['committer_email']).to eq(commit.committer_email)
@@ -34,7 +32,7 @@ describe API::Commits do
it 'include correct pagination headers' do
commit_count = project.repository.count_commits(ref: 'master').to_s
- get api("/projects/#{project.id}/repository/commits", user)
+ get api("/projects/#{project_id}/repository/commits", user)
expect(response).to include_pagination_headers
expect(response.headers['X-Total']).to eq(commit_count)
@@ -44,8 +42,9 @@ describe API::Commits do
context "unauthorized user" do
it "does not return project commits" do
- get api("/projects/#{project.id}/repository/commits")
- expect(response).to have_http_status(401)
+ get api("/projects/#{project_id}/repository/commits")
+
+ expect(response).to have_http_status(404)
end
end
@@ -54,7 +53,7 @@ describe API::Commits do
commits = project.repository.commits("master")
after = commits.second.created_at
- get api("/projects/#{project.id}/repository/commits?since=#{after.utc.iso8601}", user)
+ get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user)
expect(json_response.size).to eq 2
expect(json_response.first["id"]).to eq(commits.first.id)
@@ -66,7 +65,7 @@ describe API::Commits do
after = commits.second.created_at
commit_count = project.repository.count_commits(ref: 'master', after: after).to_s
- get api("/projects/#{project.id}/repository/commits?since=#{after.utc.iso8601}", user)
+ get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user)
expect(response).to include_pagination_headers
expect(response.headers['X-Total']).to eq(commit_count)
@@ -79,7 +78,7 @@ describe API::Commits do
commits = project.repository.commits("master")
before = commits.second.created_at
- get api("/projects/#{project.id}/repository/commits?until=#{before.utc.iso8601}", user)
+ get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user)
if commits.size >= 20
expect(json_response.size).to eq(20)
@@ -96,7 +95,7 @@ describe API::Commits do
before = commits.second.created_at
commit_count = project.repository.count_commits(ref: 'master', before: before).to_s
- get api("/projects/#{project.id}/repository/commits?until=#{before.utc.iso8601}", user)
+ get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user)
expect(response).to include_pagination_headers
expect(response.headers['X-Total']).to eq(commit_count)
@@ -106,7 +105,7 @@ describe API::Commits do
context "invalid xmlschema date parameters" do
it "returns an invalid parameter error message" do
- get api("/projects/#{project.id}/repository/commits?since=invalid-date", user)
+ get api("/projects/#{project_id}/repository/commits?since=invalid-date", user)
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('since is invalid')
@@ -118,7 +117,7 @@ describe API::Commits do
path = 'files/ruby/popen.rb'
commit_count = project.repository.count_commits(ref: 'master', path: path).to_s
- get api("/projects/#{project.id}/repository/commits?path=#{path}", user)
+ get api("/projects/#{project_id}/repository/commits?path=#{path}", user)
expect(json_response.size).to eq(3)
expect(json_response.first["id"]).to eq("570e7b2abdd848b95f2f578043fc23bd6f6fd24d")
@@ -130,7 +129,7 @@ describe API::Commits do
path = 'files/ruby/popen.rb'
commit_count = project.repository.count_commits(ref: 'master', path: path).to_s
- get api("/projects/#{project.id}/repository/commits?path=#{path}", user)
+ get api("/projects/#{project_id}/repository/commits?path=#{path}", user)
expect(response).to include_pagination_headers
expect(response.headers['X-Total']).to eq(commit_count)
@@ -143,7 +142,7 @@ describe API::Commits do
let(:per_page) { 5 }
let(:ref_name) { 'master' }
let!(:request) do
- get api("/projects/#{project.id}/repository/commits?page=#{page}&per_page=#{per_page}&ref_name=#{ref_name}", user)
+ get api("/projects/#{project_id}/repository/commits?page=#{page}&per_page=#{per_page}&ref_name=#{ref_name}", user)
end
it 'returns correct headers' do
@@ -181,10 +180,10 @@ describe API::Commits do
end
describe "POST /projects/:id/repository/commits" do
- let!(:url) { "/projects/#{project.id}/repository/commits" }
+ let!(:url) { "/projects/#{project_id}/repository/commits" }
it 'returns a 403 unauthorized for user without permissions' do
- post api(url, user2)
+ post api(url, guest)
expect(response).to have_http_status(403)
end
@@ -227,7 +226,7 @@ describe API::Commits do
it 'a new file in project repo' do
post api(url, user), valid_c_params
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
expect(json_response['title']).to eq(message)
expect(json_response['committer_name']).to eq(user.name)
expect(json_response['committer_email']).to eq(user.email)
@@ -453,13 +452,17 @@ describe API::Commits do
end
end
- describe "Get a single commit" do
- context "authorized user" do
- it "returns a commit by sha" do
- get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+ describe 'GET /projects/:id/repository/commits/:sha' do
+ let(:commit) { project.repository.commit }
+ let(:commit_id) { commit.id }
+ let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}" }
- expect(response).to have_http_status(200)
- commit = project.repository.commit
+ shared_examples_for 'ref commit' do
+ it 'returns the ref last commit' do
+ get api(route, current_user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/commit/detail')
expect(json_response['id']).to eq(commit.id)
expect(json_response['short_id']).to eq(commit.short_id)
expect(json_response['title']).to eq(commit.title)
@@ -474,222 +477,539 @@ describe API::Commits do
expect(json_response['stats']['additions']).to eq(commit.stats.additions)
expect(json_response['stats']['deletions']).to eq(commit.stats.deletions)
expect(json_response['stats']['total']).to eq(commit.stats.total)
+ expect(json_response['status']).to be_nil
end
- it "returns a 404 error if not found" do
- get api("/projects/#{project.id}/repository/commits/invalid_sha", user)
- expect(response).to have_http_status(404)
+ context 'when ref does not exist' do
+ let(:commit_id) { 'unknown' }
+
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, current_user) }
+ let(:message) { '404 Commit Not Found' }
+ end
end
- it "returns nil for commit without CI" do
- get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
- expect(response).to have_http_status(200)
- expect(json_response['status']).to be_nil
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, current_user) }
+ end
end
+ end
- it "returns status for CI" do
- pipeline = project.pipelines.create(source: :push, ref: 'master', sha: project.repository.commit.sha)
- pipeline.update(status: 'success')
+ context 'when unauthenticated', 'and project is public' do
+ let(:project) { create(:project, :public, :repository) }
- get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+ it_behaves_like 'ref commit'
+ end
- expect(response).to have_http_status(200)
- expect(json_response['status']).to eq(pipeline.status)
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
end
+ end
- it "returns status for CI when pipeline is created" do
- project.pipelines.create(source: :push, ref: 'master', sha: project.repository.commit.sha)
+ context 'when authenticated', 'as a master' do
+ let(:current_user) { user }
- get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
+ it_behaves_like 'ref commit'
- expect(response).to have_http_status(200)
- expect(json_response['status']).to eq("created")
+ context 'when branch contains a dot' do
+ let(:commit) { project.repository.commit(branch_with_dot.name) }
+ let(:commit_id) { branch_with_dot.name }
+
+ it_behaves_like 'ref commit'
end
- end
- context "unauthorized user" do
- it "does not return the selected commit" do
- get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}")
- expect(response).to have_http_status(401)
+ context 'when branch contains a slash' do
+ let(:commit_id) { branch_with_slash.name }
+
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, current_user) }
+ end
+ end
+
+ context 'when branch contains an escaped slash' do
+ let(:commit) { project.repository.commit(branch_with_slash.name) }
+ let(:commit_id) { CGI.escape(branch_with_slash.name) }
+
+ it_behaves_like 'ref commit'
+ end
+
+ context 'requesting with the escaped project full path' do
+ let(:project_id) { CGI.escape(project.full_path) }
+
+ it_behaves_like 'ref commit'
+
+ context 'when branch contains a dot' do
+ let(:commit) { project.repository.commit(branch_with_dot.name) }
+ let(:commit_id) { branch_with_dot.name }
+
+ it_behaves_like 'ref commit'
+ end
+ end
+
+ context 'when the ref has a pipeline' do
+ let!(:pipeline) { project.pipelines.create(source: :push, ref: 'master', sha: commit.sha) }
+
+ it 'includes a "created" status' do
+ get api(route, current_user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/commit/detail')
+ expect(json_response['status']).to eq('created')
+ end
+
+ context 'when pipeline succeeds' do
+ before do
+ pipeline.update(status: 'success')
+ end
+
+ it 'includes a "success" status' do
+ get api(route, current_user)
+
+ expect(response).to have_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/commit/detail')
+ expect(json_response['status']).to eq('success')
+ end
+ end
end
end
end
- describe "Get the diff of a commit" do
- context "authorized user" do
- before do
- project.team << [user2, :reporter]
+ describe 'GET /projects/:id/repository/commits/:sha/diff' do
+ let(:commit) { project.repository.commit }
+ let(:commit_id) { commit.id }
+ let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/diff" }
+
+ shared_examples_for 'ref diff' do
+ it 'returns the diff of the selected commit' do
+ get api(route, current_user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.size).to be >= 1
+ expect(json_response.first.keys).to include 'diff'
end
- it "returns the diff of the selected commit" do
- get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff", user)
- expect(response).to have_http_status(200)
+ context 'when ref does not exist' do
+ let(:commit_id) { 'unknown' }
- expect(json_response).to be_an Array
- expect(json_response.length).to be >= 1
- expect(json_response.first.keys).to include "diff"
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, current_user) }
+ let(:message) { '404 Commit Not Found' }
+ end
end
- it "returns a 404 error if invalid commit" do
- get api("/projects/#{project.id}/repository/commits/invalid_sha/diff", user)
- expect(response).to have_http_status(404)
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, current_user) }
+ end
end
end
- context "unauthorized user" do
- it "does not return the diff of the selected commit" do
- get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/diff")
- expect(response).to have_http_status(401)
+ context 'when unauthenticated', 'and project is public' do
+ let(:project) { create(:project, :public, :repository) }
+
+ it_behaves_like 'ref diff'
+ end
+
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+
+ context 'when authenticated', 'as a master' do
+ let(:current_user) { user }
+
+ it_behaves_like 'ref diff'
+
+ context 'when branch contains a dot' do
+ let(:commit_id) { branch_with_dot.name }
+
+ it_behaves_like 'ref diff'
+ end
+
+ context 'when branch contains a slash' do
+ let(:commit_id) { branch_with_slash.name }
+
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, current_user) }
+ end
+ end
+
+ context 'when branch contains an escaped slash' do
+ let(:commit_id) { CGI.escape(branch_with_slash.name) }
+
+ it_behaves_like 'ref diff'
+ end
+
+ context 'requesting with the escaped project full path' do
+ let(:project_id) { CGI.escape(project.full_path) }
+
+ it_behaves_like 'ref diff'
+
+ context 'when branch contains a dot' do
+ let(:commit_id) { branch_with_dot.name }
+
+ it_behaves_like 'ref diff'
+ end
end
end
end
- describe 'Get the comments of a commit' do
- context 'authorized user' do
- it 'returns merge_request comments' do
- get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(2)
- expect(json_response.first['note']).to eq('a comment on a commit')
- expect(json_response.first['author']['id']).to eq(user.id)
+ describe 'GET /projects/:id/repository/commits/:sha/comments' do
+ let(:commit) { project.repository.commit }
+ let(:commit_id) { commit.id }
+ let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/comments" }
+
+ shared_examples_for 'ref comments' do
+ context 'when ref exists' do
+ before do
+ create(:note_on_commit, author: user, project: project, commit_id: commit.id, note: 'a comment on a commit')
+ create(:note_on_commit, author: user, project: project, commit_id: commit.id, note: 'another comment on a commit')
+ end
+
+ it 'returns the diff of the selected commit' do
+ get api(route, current_user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/commit_notes')
+ expect(json_response.size).to eq(2)
+ expect(json_response.first['note']).to eq('a comment on a commit')
+ expect(json_response.first['author']['id']).to eq(user.id)
+ end
end
- it 'returns a 404 error if merge_request_id not found' do
- get api("/projects/#{project.id}/repository/commits/1234ab/comments", user)
- expect(response).to have_http_status(404)
+ context 'when ref does not exist' do
+ let(:commit_id) { 'unknown' }
+
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, current_user) }
+ let(:message) { '404 Commit Not Found' }
+ end
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, current_user) }
+ end
+ end
+ end
+
+ context 'when unauthenticated', 'and project is public' do
+ let(:project) { create(:project, :public, :repository) }
+
+ it_behaves_like 'ref comments'
+ end
+
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
end
end
- context 'unauthorized user' do
- it 'does not return the diff of the selected commit' do
- get api("/projects/#{project.id}/repository/commits/1234ab/comments")
- expect(response).to have_http_status(401)
+ context 'when authenticated', 'as a master' do
+ let(:current_user) { user }
+
+ it_behaves_like 'ref comments'
+
+ context 'when branch contains a dot' do
+ let(:commit) { project.repository.commit(branch_with_dot.name) }
+ let(:commit_id) { branch_with_dot.name }
+
+ it_behaves_like 'ref comments'
+ end
+
+ context 'when branch contains a slash' do
+ let(:commit) { project.repository.commit(branch_with_slash.name) }
+ let(:commit_id) { branch_with_slash.name }
+
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, current_user) }
+ end
+ end
+
+ context 'when branch contains an escaped slash' do
+ let(:commit) { project.repository.commit(branch_with_slash.name) }
+ let(:commit_id) { CGI.escape(branch_with_slash.name) }
+
+ it_behaves_like 'ref comments'
+ end
+
+ context 'requesting with the escaped project full path' do
+ let(:project_id) { CGI.escape(project.full_path) }
+
+ it_behaves_like 'ref comments'
+
+ context 'when branch contains a dot' do
+ let(:commit) { project.repository.commit(branch_with_dot.name) }
+ let(:commit_id) { branch_with_dot.name }
+
+ it_behaves_like 'ref comments'
+ end
end
end
context 'when the commit is present on two projects' do
- let(:forked_project) { create(:project, :repository, creator: user2, namespace: user2.namespace) }
- let!(:forked_project_note) { create(:note_on_commit, author: user2, project: forked_project, commit_id: forked_project.repository.commit.id, note: 'a comment on a commit for fork') }
+ let(:forked_project) { create(:project, :repository, creator: guest, namespace: guest.namespace) }
+ let!(:forked_project_note) { create(:note_on_commit, author: guest, project: forked_project, commit_id: forked_project.repository.commit.id, note: 'a comment on a commit for fork') }
+ let(:project_id) { forked_project.id }
+ let(:commit_id) { forked_project.repository.commit.id }
it 'returns the comments for the target project' do
- get api("/projects/#{forked_project.id}/repository/commits/#{forked_project.repository.commit.id}/comments", user2)
+ get api(route, guest)
- expect(response).to have_http_status(200)
- expect(json_response.length).to eq(1)
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/commit_notes')
+ expect(json_response.size).to eq(1)
expect(json_response.first['note']).to eq('a comment on a commit for fork')
- expect(json_response.first['author']['id']).to eq(user2.id)
+ expect(json_response.first['author']['id']).to eq(guest.id)
end
end
end
describe 'POST :id/repository/commits/:sha/cherry_pick' do
- let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
+ let(:commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
+ let(:commit_id) { commit.id }
+ let(:branch) { 'master' }
+ let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/cherry_pick" }
+
+ shared_examples_for 'ref cherry-pick' do
+ context 'when ref exists' do
+ it 'cherry-picks the ref commit' do
+ post api(route, current_user), branch: branch
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(response).to match_response_schema('public_api/v4/commit/basic')
+ expect(json_response['title']).to eq(commit.title)
+ expect(json_response['message']).to eq(commit.message)
+ expect(json_response['author_name']).to eq(commit.author_name)
+ expect(json_response['committer_name']).to eq(user.name)
+ end
+ end
- context 'authorized user' do
- it 'cherry picks a commit' do
- post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'master'
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
- expect(response).to have_http_status(201)
- expect(json_response['title']).to eq(master_pickable_commit.title)
- expect(json_response['message']).to eq(master_pickable_commit.message)
- expect(json_response['author_name']).to eq(master_pickable_commit.author_name)
- expect(json_response['committer_name']).to eq(user.name)
+ it_behaves_like '403 response' do
+ let(:request) { post api(route, current_user), branch: 'master' }
+ end
end
+ end
- it 'returns 400 if commit is already included in the target branch' do
- post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'markdown'
+ context 'when unauthenticated', 'and project is public' do
+ let(:project) { create(:project, :public, :repository) }
- expect(response).to have_http_status(400)
- expect(json_response['message']).to include('Sorry, we cannot cherry-pick this commit automatically.')
+ it_behaves_like '403 response' do
+ let(:request) { post api(route), branch: 'master' }
+ end
+ end
+
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { post api(route), branch: 'master' }
+ let(:message) { '404 Project Not Found' }
end
+ end
- it 'returns 400 if you are not allowed to push to the target branch' do
- project.team << [user2, :developer]
- protected_branch = create(:protected_branch, project: project, name: 'feature')
+ context 'when authenticated', 'as an owner' do
+ let(:current_user) { user }
- post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user2), branch: protected_branch.name
+ it_behaves_like 'ref cherry-pick'
- expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('You are not allowed to push into this branch')
+ context 'when ref does not exist' do
+ let(:commit_id) { 'unknown' }
+
+ it_behaves_like '404 response' do
+ let(:request) { post api(route, current_user), branch: 'master' }
+ let(:message) { '404 Commit Not Found' }
+ end
+ end
+
+ context 'when branch is missing' do
+ it_behaves_like '400 response' do
+ let(:request) { post api(route, current_user) }
+ end
end
- it 'returns 400 for missing parameters' do
- post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user)
+ context 'when branch does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { post api(route, current_user), branch: 'foo' }
+ let(:message) { '404 Branch Not Found' }
+ end
+ end
- expect(response).to have_http_status(400)
- expect(json_response['error']).to eq('branch is missing')
+ context 'when commit is already included in the target branch' do
+ it_behaves_like '400 response' do
+ let(:request) { post api(route, current_user), branch: 'markdown' }
+ end
end
- it 'returns 404 if commit is not found' do
- post api("/projects/#{project.id}/repository/commits/abcd0123/cherry_pick", user), branch: 'master'
+ context 'when ref contains a dot' do
+ let(:commit) { project.repository.commit(branch_with_dot.name) }
+ let(:commit_id) { branch_with_dot.name }
- expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Commit Not Found')
+ it_behaves_like 'ref cherry-pick'
end
- it 'returns 404 if branch is not found' do
- post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user), branch: 'foo'
+ context 'when ref contains a slash' do
+ let(:commit_id) { branch_with_slash.name }
- expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Branch Not Found')
+ it_behaves_like '404 response' do
+ let(:request) { post api(route, current_user), branch: 'master' }
+ end
end
- it 'returns 400 for missing parameters' do
- post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick", user)
+ context 'requesting with the escaped project full path' do
+ let(:project_id) { CGI.escape(project.full_path) }
- expect(response).to have_http_status(400)
- expect(json_response['error']).to eq('branch is missing')
+ it_behaves_like 'ref cherry-pick'
+
+ context 'when ref contains a dot' do
+ let(:commit) { project.repository.commit(branch_with_dot.name) }
+ let(:commit_id) { branch_with_dot.name }
+
+ it_behaves_like 'ref cherry-pick'
+ end
end
end
- context 'unauthorized user' do
- it 'does not cherry pick the commit' do
- post api("/projects/#{project.id}/repository/commits/#{master_pickable_commit.id}/cherry_pick"), branch: 'master'
+ context 'when authenticated', 'as a developer' do
+ let(:current_user) { guest }
+
+ before do
+ project.add_developer(guest)
+ end
+
+ context 'when branch is protected' do
+ before do
+ create(:protected_branch, project: project, name: 'feature')
+ end
+
+ it 'returns 400 if you are not allowed to push to the target branch' do
+ post api(route, current_user), branch: 'feature'
- expect(response).to have_http_status(401)
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']).to eq('You are not allowed to push into this branch')
+ end
end
end
end
- describe 'Post comment to commit' do
- context 'authorized user' do
- it 'returns comment' do
- post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment'
- expect(response).to have_http_status(201)
- expect(json_response['note']).to eq('My comment')
- expect(json_response['path']).to be_nil
- expect(json_response['line']).to be_nil
- expect(json_response['line_type']).to be_nil
+ describe 'POST /projects/:id/repository/commits/:sha/comments' do
+ let(:commit) { project.repository.commit }
+ let(:commit_id) { commit.id }
+ let(:note) { 'My comment' }
+ let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/comments" }
+
+ shared_examples_for 'ref new comment' do
+ context 'when ref exists' do
+ it 'creates the comment' do
+ post api(route, current_user), note: note
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(response).to match_response_schema('public_api/v4/commit_note')
+ expect(json_response['note']).to eq('My comment')
+ expect(json_response['path']).to be_nil
+ expect(json_response['line']).to be_nil
+ expect(json_response['line_type']).to be_nil
+ end
end
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { post api(route, current_user), note: 'My comment' }
+ end
+ end
+ end
+
+ context 'when unauthenticated', 'and project is public' do
+ let(:project) { create(:project, :public, :repository) }
+
+ it_behaves_like '400 response' do
+ let(:request) { post api(route), note: 'My comment' }
+ end
+ end
+
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { post api(route), note: 'My comment' }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
+
+ context 'when authenticated', 'as an owner' do
+ let(:current_user) { user }
+
+ it_behaves_like 'ref new comment'
+
it 'returns the inline comment' do
- post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 1, line_type: 'new'
+ post api(route, current_user), note: 'My comment', path: project.repository.commit.raw_diffs.first.new_path, line: 1, line_type: 'new'
- expect(response).to have_http_status(201)
+ expect(response).to have_gitlab_http_status(201)
+ expect(response).to match_response_schema('public_api/v4/commit_note')
expect(json_response['note']).to eq('My comment')
expect(json_response['path']).to eq(project.repository.commit.raw_diffs.first.new_path)
expect(json_response['line']).to eq(1)
expect(json_response['line_type']).to eq('new')
end
+ context 'when ref does not exist' do
+ let(:commit_id) { 'unknown' }
+
+ it_behaves_like '404 response' do
+ let(:request) { post api(route, current_user), note: 'My comment' }
+ let(:message) { '404 Commit Not Found' }
+ end
+ end
+
it 'returns 400 if note is missing' do
- post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
- expect(response).to have_http_status(400)
+ post api(route, current_user)
+
+ expect(response).to have_gitlab_http_status(400)
end
- it 'returns 404 if note is attached to non existent commit' do
- post api("/projects/#{project.id}/repository/commits/1234ab/comments", user), note: 'My comment'
- expect(response).to have_http_status(404)
+ context 'when ref contains a dot' do
+ let(:commit_id) { branch_with_dot.name }
+
+ it_behaves_like 'ref new comment'
end
- end
- context 'unauthorized user' do
- it 'does not return the diff of the selected commit' do
- post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments")
- expect(response).to have_http_status(401)
+ context 'when ref contains a slash' do
+ let(:commit_id) { branch_with_slash.name }
+
+ it_behaves_like '404 response' do
+ let(:request) { post api(route, current_user), note: 'My comment' }
+ end
+ end
+
+ context 'when ref contains an escaped slash' do
+ let(:commit_id) { CGI.escape(branch_with_slash.name) }
+
+ it_behaves_like 'ref new comment'
+ end
+
+ context 'requesting with the escaped project full path' do
+ let(:project_id) { CGI.escape(project.full_path) }
+
+ it_behaves_like 'ref new comment'
+
+ context 'when ref contains a dot' do
+ let(:commit_id) { branch_with_dot.name }
+
+ it_behaves_like 'ref new comment'
+ end
end
end
end
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 4c5ded7a492..87716c6fe3a 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -13,7 +13,14 @@ describe API::Environments do
describe 'GET /projects/:id/environments' do
context 'as member of the project' do
it 'returns project environments' do
- project_data_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace)
+ project_data_keys = %w(
+ id description default_branch tag_list
+ ssh_url_to_repo http_url_to_repo web_url
+ name name_with_namespace
+ path path_with_namespace
+ star_count forks_count
+ created_at last_activity_at
+ )
get api("/projects/#{project.id}/environments", user)
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
index 7a847442469..48db964d782 100644
--- a/spec/requests/api/events_spec.rb
+++ b/spec/requests/api/events_spec.rb
@@ -138,5 +138,40 @@ describe API::Events do
expect(response).to have_http_status(404)
end
end
+
+ context 'when exists some events' do
+ before do
+ create_event(note1)
+ create_event(note2)
+ create_event(merge_request1)
+ end
+
+ let(:note1) { create(:note_on_merge_request, project: private_project, author: user) }
+ let(:note2) { create(:note_on_issue, project: private_project, author: user) }
+ let(:merge_request1) { create(:merge_request, state: 'closed', author: user, assignee: user, source_project: private_project, title: 'Test') }
+ let(:merge_request2) { create(:merge_request, state: 'closed', author: user, assignee: user, source_project: private_project, title: 'Test') }
+
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new do
+ get api("/projects/#{private_project.id}/events", user)
+ end.count
+
+ create_event(merge_request2)
+
+ expect do
+ get api("/projects/#{private_project.id}/events", user)
+ end.not_to exceed_query_limit(control_count)
+
+ expect(response).to have_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response[0]).to include('target_type' => 'MergeRequest', 'target_id' => merge_request2.id)
+ expect(json_response[1]).to include('target_type' => 'MergeRequest', 'target_id' => merge_request1.id)
+ end
+
+ def create_event(target)
+ create(:event, project: private_project, author: user, target: target)
+ end
+ end
end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index b9ebf6c4c16..9baac12821f 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -186,7 +186,14 @@ describe API::Projects do
context 'and with simple=true' do
it 'returns a simplified version of all the projects' do
- expected_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace)
+ expected_keys = %w(
+ id description default_branch tag_list
+ ssh_url_to_repo http_url_to_repo web_url
+ name name_with_namespace
+ path path_with_namespace
+ star_count forks_count
+ created_at last_activity_at
+ )
get api('/projects?simple=true', user)
@@ -689,6 +696,7 @@ describe API::Projects do
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(public_project.id)
expect(json_response['description']).to eq(public_project.description)
+ expect(json_response['default_branch']).to eq(public_project.default_branch)
expect(json_response.keys).not_to include('permissions')
end
end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index ef7d0c3ee41..9884c1ec206 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -1,66 +1,85 @@
require 'spec_helper'
-require 'mime/types'
describe API::Tags do
- include RepoHelpers
-
let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let!(:project) { create(:project, :repository, creator: user) }
- let!(:master) { create(:project_member, :master, user: user, project: project) }
- let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
+ let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
+ let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
+ let(:tag_name) { project.repository.find_tag('v1.1.0').name }
- describe "GET /projects/:id/repository/tags" do
- let(:tag_name) { project.repository.tag_names.sort.reverse.first }
- let(:description) { 'Awesome release!' }
+ let(:project_id) { project.id }
+ let(:current_user) { nil }
+
+ before do
+ project.add_master(user)
+ end
+
+ describe 'GET /projects/:id/repository/tags' do
+ let(:route) { "/projects/#{project_id}/repository/tags" }
shared_examples_for 'repository tags' do
it 'returns the repository tags' do
- get api("/projects/#{project.id}/repository/tags", current_user)
+ get api(route, current_user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/tags')
expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(tag_name)
end
- end
- context 'when unauthenticated' do
- it_behaves_like 'repository tags' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, current_user) }
+ end
end
end
- context 'when authenticated' do
- it_behaves_like 'repository tags' do
- let(:current_user) { user }
+ context 'when unauthenticated', 'and project is public' do
+ let(:project) { create(:project, :public, :repository) }
+
+ it_behaves_like 'repository tags'
+ end
+
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
end
end
- context 'without releases' do
- it "returns an array of project tags" do
- get api("/projects/#{project.id}/repository/tags", user)
+ context 'when authenticated', 'as a master' do
+ let(:current_user) { user }
- expect(response).to have_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.first['name']).to eq(tag_name)
+ it_behaves_like 'repository tags'
+
+ context 'requesting with the escaped project full path' do
+ let(:project_id) { CGI.escape(project.full_path) }
+
+ it_behaves_like 'repository tags'
+ end
+ end
+
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, guest) }
end
end
context 'with releases' do
+ let(:description) { 'Awesome release!' }
+
before do
release = project.releases.find_or_initialize_by(tag: tag_name)
release.update_attributes(description: description)
end
- it "returns an array of project tags with release info" do
- get api("/projects/#{project.id}/repository/tags", user)
+ it 'returns an array of project tags with release info' do
+ get api(route, user)
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/tags')
expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(tag_name)
expect(json_response.first['message']).to eq('Version 1.1.0')
expect(json_response.first['release']['description']).to eq(description)
@@ -69,210 +88,342 @@ describe API::Tags do
end
describe 'GET /projects/:id/repository/tags/:tag_name' do
- let(:tag_name) { project.repository.tag_names.sort.reverse.first }
+ let(:route) { "/projects/#{project_id}/repository/tags/#{tag_name}" }
shared_examples_for 'repository tag' do
- it 'returns the repository tag' do
- get api("/projects/#{project.id}/repository/tags/#{tag_name}", current_user)
-
- expect(response).to have_http_status(200)
+ it 'returns the repository branch' do
+ get api(route, current_user)
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/tag')
expect(json_response['name']).to eq(tag_name)
end
- it 'returns 404 for an invalid tag name' do
- get api("/projects/#{project.id}/repository/tags/foobar", current_user)
+ context 'when tag does not exist' do
+ let(:tag_name) { 'unknown' }
- expect(response).to have_http_status(404)
+ it_behaves_like '404 response' do
+ let(:request) { get api(route, current_user) }
+ let(:message) { '404 Tag Not Found' }
+ end
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, current_user) }
+ end
end
end
- context 'when unauthenticated' do
- it_behaves_like 'repository tag' do
- let(:project) { create(:project, :public, :repository) }
- let(:current_user) { nil }
+ context 'when unauthenticated', 'and project is public' do
+ let(:project) { create(:project, :public, :repository) }
+
+ it_behaves_like 'repository tag'
+ end
+
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { get api(route) }
+ let(:message) { '404 Project Not Found' }
end
end
- context 'when authenticated' do
- it_behaves_like 'repository tag' do
- let(:current_user) { user }
+ context 'when authenticated', 'as a master' do
+ let(:current_user) { user }
+
+ it_behaves_like 'repository tag'
+
+ context 'requesting with the escaped project full path' do
+ let(:project_id) { CGI.escape(project.full_path) }
+
+ it_behaves_like 'repository tag'
+ end
+ end
+
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { get api(route, guest) }
end
end
end
describe 'POST /projects/:id/repository/tags' do
- context 'lightweight tags' do
+ let(:tag_name) { 'new_tag' }
+ let(:route) { "/projects/#{project_id}/repository/tags" }
+
+ shared_examples_for 'repository new tag' do
it 'creates a new tag' do
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'v7.0.1',
- ref: 'master'
+ post api(route, current_user), tag_name: tag_name, ref: 'master'
- expect(response).to have_http_status(201)
- expect(json_response['name']).to eq('v7.0.1')
+ expect(response).to have_gitlab_http_status(201)
+ expect(response).to match_response_schema('public_api/v4/tag')
+ expect(json_response['name']).to eq(tag_name)
end
- end
- context 'lightweight tags with release notes' do
- it 'creates a new tag' do
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'v7.0.1',
- ref: 'master',
- release_description: 'Wow'
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
- expect(response).to have_http_status(201)
- expect(json_response['name']).to eq('v7.0.1')
- expect(json_response['release']['description']).to eq('Wow')
+ it_behaves_like '403 response' do
+ let(:request) { post api(route, current_user) }
+ end
end
end
- describe 'DELETE /projects/:id/repository/tags/:tag_name' do
- let(:tag_name) { project.repository.tag_names.sort.reverse.first }
+ context 'when unauthenticated', 'and project is private' do
+ it_behaves_like '404 response' do
+ let(:request) { post api(route) }
+ let(:message) { '404 Project Not Found' }
+ end
+ end
- before do
- allow_any_instance_of(Repository).to receive(:rm_tag).and_return(true)
+ context 'when authenticated', 'as a guest' do
+ it_behaves_like '403 response' do
+ let(:request) { post api(route, guest) }
end
+ end
+
+ context 'when authenticated', 'as a master' do
+ let(:current_user) { user }
+
+ context "when a protected branch doesn't already exist" do
+ it_behaves_like 'repository new tag'
- context 'delete tag' do
- it 'deletes an existing tag' do
- delete api("/projects/#{project.id}/repository/tags/#{tag_name}", user)
+ context 'when tag contains a dot' do
+ let(:tag_name) { 'v7.0.1' }
- expect(response).to have_http_status(204)
+ it_behaves_like 'repository new tag'
end
- it 'raises 404 if the tag does not exist' do
- delete api("/projects/#{project.id}/repository/tags/foobar", user)
- expect(response).to have_http_status(404)
+ context 'requesting with the escaped project full path' do
+ let(:project_id) { CGI.escape(project.full_path) }
+
+ it_behaves_like 'repository new tag'
+
+ context 'when tag contains a dot' do
+ let(:tag_name) { 'v7.0.1' }
+
+ it_behaves_like 'repository new tag'
+ end
end
end
- end
- context 'annotated tag' do
- it 'creates a new annotated tag' do
- # Identity must be set in .gitconfig to create annotated tag.
- repo_path = project.repository.path_to_repo
- system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.name #{user.name}))
- system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.email #{user.email}))
+ it 'returns 400 if tag name is invalid' do
+ post api(route, current_user), tag_name: 'new design', ref: 'master'
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']).to eq('Tag name invalid')
+ end
+
+ it 'returns 400 if tag already exists' do
+ post api(route, current_user), tag_name: 'new_design1', ref: 'master'
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'v7.1.0',
- ref: 'master',
- message: 'Release 7.1.0'
+ expect(response).to have_gitlab_http_status(201)
+ expect(response).to match_response_schema('public_api/v4/tag')
- expect(response).to have_http_status(201)
- expect(json_response['name']).to eq('v7.1.0')
- expect(json_response['message']).to eq('Release 7.1.0')
+ post api(route, current_user), tag_name: 'new_design1', ref: 'master'
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']).to eq('Tag new_design1 already exists')
end
- end
- it 'denies for user without push access' do
- post api("/projects/#{project.id}/repository/tags", user2),
- tag_name: 'v1.9.0',
- ref: '621491c677087aa243f165eab467bfdfbee00be1'
- expect(response).to have_http_status(403)
+ it 'returns 400 if ref name is invalid' do
+ post api(route, current_user), tag_name: 'new_design3', ref: 'foo'
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']).to eq('Target foo is invalid')
+ end
+
+ context 'lightweight tags with release notes' do
+ it 'creates a new tag' do
+ post api(route, current_user), tag_name: tag_name, ref: 'master', release_description: 'Wow'
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(response).to match_response_schema('public_api/v4/tag')
+ expect(json_response['name']).to eq(tag_name)
+ expect(json_response['release']['description']).to eq('Wow')
+ end
+ end
+
+ context 'annotated tag' do
+ it 'creates a new annotated tag' do
+ # Identity must be set in .gitconfig to create annotated tag.
+ repo_path = project.repository.path_to_repo
+ system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.name #{user.name}))
+ system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.email #{user.email}))
+
+ post api(route, current_user), tag_name: 'v7.1.0', ref: 'master', message: 'Release 7.1.0'
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(response).to match_response_schema('public_api/v4/tag')
+ expect(json_response['name']).to eq('v7.1.0')
+ expect(json_response['message']).to eq('Release 7.1.0')
+ end
+ end
end
+ end
+
+ describe 'DELETE /projects/:id/repository/tags/:tag_name' do
+ let(:route) { "/projects/#{project_id}/repository/tags/#{tag_name}" }
- it 'returns 400 if tag name is invalid' do
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'v 1.0.0',
- ref: 'master'
- expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('Tag name invalid')
+ before do
+ allow_any_instance_of(Repository).to receive(:rm_tag).and_return(true)
end
- it 'returns 400 if tag already exists' do
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'v8.0.0',
- ref: 'master'
- expect(response).to have_http_status(201)
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'v8.0.0',
- ref: 'master'
- expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('Tag v8.0.0 already exists')
+ shared_examples_for 'repository delete tag' do
+ it 'deletes a tag' do
+ delete api(route, current_user)
+
+ expect(response).to have_gitlab_http_status(204)
+ end
+
+ context 'when tag does not exist' do
+ let(:tag_name) { 'unknown' }
+
+ it_behaves_like '404 response' do
+ let(:request) { delete api(route, current_user) }
+ let(:message) { 'No such tag' }
+ end
+ end
+
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
+
+ it_behaves_like '403 response' do
+ let(:request) { delete api(route, current_user) }
+ end
+ end
end
- it 'returns 400 if ref name is invalid' do
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'mytag',
- ref: 'foo'
- expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('Target foo is invalid')
+ context 'when authenticated', 'as a master' do
+ let(:current_user) { user }
+
+ it_behaves_like 'repository delete tag'
+
+ context 'requesting with the escaped project full path' do
+ let(:project_id) { CGI.escape(project.full_path) }
+
+ it_behaves_like 'repository delete tag'
+ end
end
end
describe 'POST /projects/:id/repository/tags/:tag_name/release' do
- let(:tag_name) { project.repository.tag_names.first }
+ let(:route) { "/projects/#{project_id}/repository/tags/#{tag_name}/release" }
let(:description) { 'Awesome release!' }
- it 'creates description for existing git tag' do
- post api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
- description: description
+ shared_examples_for 'repository new release' do
+ it 'creates description for existing git tag' do
+ post api(route, user), description: description
- expect(response).to have_http_status(201)
- expect(json_response['tag_name']).to eq(tag_name)
- expect(json_response['description']).to eq(description)
- end
+ expect(response).to have_gitlab_http_status(201)
+ expect(response).to match_response_schema('public_api/v4/release')
+ expect(json_response['tag_name']).to eq(tag_name)
+ expect(json_response['description']).to eq(description)
+ end
+
+ context 'when tag does not exist' do
+ let(:tag_name) { 'unknown' }
+
+ it_behaves_like '404 response' do
+ let(:request) { post api(route, current_user), description: description }
+ let(:message) { 'Tag does not exist' }
+ end
+ end
- it 'returns 404 if the tag does not exist' do
- post api("/projects/#{project.id}/repository/tags/foobar/release", user),
- description: description
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
- expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('Tag does not exist')
+ it_behaves_like '403 response' do
+ let(:request) { post api(route, current_user), description: description }
+ end
+ end
end
- context 'on tag with existing release' do
- before do
- release = project.releases.find_or_initialize_by(tag: tag_name)
- release.update_attributes(description: description)
+ context 'when authenticated', 'as a master' do
+ let(:current_user) { user }
+
+ it_behaves_like 'repository new release'
+
+ context 'requesting with the escaped project full path' do
+ let(:project_id) { CGI.escape(project.full_path) }
+
+ it_behaves_like 'repository new release'
end
- it 'returns 409 if there is already a release' do
- post api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
- description: description
+ context 'on tag with existing release' do
+ before do
+ release = project.releases.find_or_initialize_by(tag: tag_name)
+ release.update_attributes(description: description)
+ end
+
+ it 'returns 409 if there is already a release' do
+ post api(route, user), description: description
- expect(response).to have_http_status(409)
- expect(json_response['message']).to eq('Release already exists')
+ expect(response).to have_gitlab_http_status(409)
+ expect(json_response['message']).to eq('Release already exists')
+ end
end
end
end
describe 'PUT id/repository/tags/:tag_name/release' do
- let(:tag_name) { project.repository.tag_names.first }
+ let(:route) { "/projects/#{project_id}/repository/tags/#{tag_name}/release" }
let(:description) { 'Awesome release!' }
let(:new_description) { 'The best release!' }
- context 'on tag with existing release' do
- before do
- release = project.releases.find_or_initialize_by(tag: tag_name)
- release.update_attributes(description: description)
+ shared_examples_for 'repository update release' 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)
+ end
+
+ it 'updates the release description' do
+ put api(route, current_user), description: new_description
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['tag_name']).to eq(tag_name)
+ expect(json_response['description']).to eq(new_description)
+ end
end
- it 'updates the release description' do
- put api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
- description: new_description
+ context 'when tag does not exist' do
+ let(:tag_name) { 'unknown' }
- expect(response).to have_http_status(200)
- expect(json_response['tag_name']).to eq(tag_name)
- expect(json_response['description']).to eq(new_description)
+ it_behaves_like '404 response' do
+ let(:request) { put api(route, current_user), description: new_description }
+ let(:message) { 'Tag does not exist' }
+ end
end
- end
- it 'returns 404 if the tag does not exist' do
- put api("/projects/#{project.id}/repository/tags/foobar/release", user),
- description: new_description
+ context 'when repository is disabled' do
+ include_context 'disabled repository'
- expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('Tag does not exist')
+ it_behaves_like '403 response' do
+ let(:request) { put api(route, current_user), description: new_description }
+ end
+ end
end
- it 'returns 404 if the release does not exist' do
- put api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
- description: new_description
+ context 'when authenticated', 'as a master' do
+ let(:current_user) { user }
+
+ it_behaves_like 'repository update release'
- expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('Release does not exist')
+ context 'requesting with the escaped project full path' do
+ let(:project_id) { CGI.escape(project.full_path) }
+
+ it_behaves_like 'repository update release'
+ end
+
+ context 'when release does not exist' do
+ it_behaves_like '404 response' do
+ let(:request) { put api(route, current_user), description: new_description }
+ let(:message) { 'Release does not exist' }
+ end
+ end
end
end
end
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
index c211cc20e53..fca5b5b5d82 100644
--- a/spec/requests/api/v3/projects_spec.rb
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -82,7 +82,14 @@ describe API::V3::Projects do
context 'GET /projects?simple=true' do
it 'returns a simplified version of all the projects' do
- expected_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace)
+ expected_keys = %w(
+ id description default_branch tag_list
+ ssh_url_to_repo http_url_to_repo web_url
+ name name_with_namespace
+ path path_with_namespace
+ star_count forks_count
+ created_at last_activity_at
+ )
get v3_api('/projects?simple=true', user)
@@ -644,6 +651,7 @@ describe API::V3::Projects do
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(public_project.id)
expect(json_response['description']).to eq(public_project.description)
+ expect(json_response['default_branch']).to eq(public_project.default_branch)
expect(json_response.keys).not_to include('permissions')
end
end
diff --git a/spec/serializers/merge_request_entity_spec.rb b/spec/serializers/merge_request_entity_spec.rb
index 18cd9e9c006..a2fd5b7daae 100644
--- a/spec/serializers/merge_request_entity_spec.rb
+++ b/spec/serializers/merge_request_entity_spec.rb
@@ -47,7 +47,7 @@ describe MergeRequestEntity do
:cancel_merge_when_pipeline_succeeds_path,
:create_issue_to_resolve_discussions_path,
:source_branch_path, :target_branch_commits_path,
- :target_branch_tree_path, :commits_count)
+ :target_branch_tree_path, :commits_count, :merge_ongoing)
end
it 'has email_patches_path' do
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index d23c09d6d1d..1c2d0b3e0dc 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -8,7 +8,7 @@ describe Auth::ContainerRegistryAuthenticationService do
let(:payload) { JWT.decode(subject[:token], rsa_key).first }
let(:authentication_abilities) do
- [:read_container_image, :create_container_image]
+ [:read_container_image, :create_container_image, :admin_container_image]
end
subject do
@@ -59,6 +59,12 @@ describe Auth::ContainerRegistryAuthenticationService do
it { expect(payload).to include('access' => []) }
end
+ shared_examples 'a deletable' do
+ it_behaves_like 'an accessible' do
+ let(:actions) { ['*'] }
+ end
+ end
+
shared_examples 'a pullable' do
it_behaves_like 'an accessible' do
let(:actions) { ['pull'] }
@@ -120,7 +126,7 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'allow developer to push images' do
before do
- project.team << [current_user, :developer]
+ project.add_developer(current_user)
end
let(:current_params) do
@@ -131,9 +137,22 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'container repository factory'
end
+ context 'disallow developer to delete images' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:*" }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
context 'allow reporter to pull images' do
before do
- project.team << [current_user, :reporter]
+ project.add_reporter(current_user)
end
context 'when pulling from root level repository' do
@@ -146,9 +165,22 @@ describe Auth::ContainerRegistryAuthenticationService do
end
end
+ context 'disallow reporter to delete images' do
+ before do
+ project.add_reporter(current_user)
+ end
+
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:*" }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
context 'return a least of privileges' do
before do
- project.team << [current_user, :reporter]
+ project.add_reporter(current_user)
end
let(:current_params) do
@@ -161,7 +193,7 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'disallow guest to pull or push images' do
before do
- project.team << [current_user, :guest]
+ project.add_guest(current_user)
end
let(:current_params) do
@@ -171,6 +203,19 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'an inaccessible'
it_behaves_like 'not a container repository factory'
end
+
+ context 'disallow guest to delete images' do
+ before do
+ project.add_guest(current_user)
+ end
+
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:*" }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
end
context 'for public project' do
@@ -194,6 +239,15 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'not a container repository factory'
end
+ context 'disallow anyone to delete images' do
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:*" }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
context 'when repository name is invalid' do
let(:current_params) do
{ scope: 'repository:invalid:push' }
@@ -225,16 +279,62 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'an inaccessible'
it_behaves_like 'not a container repository factory'
end
+
+ context 'disallow anyone to delete images' do
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:*" }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
end
context 'for external user' do
- let(:current_user) { create(:user, external: true) }
- let(:current_params) do
- { scope: "repository:#{project.full_path}:pull,push" }
+ context 'disallow anyone to pull or push images' do
+ let(:current_user) { create(:user, external: true) }
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:pull,push" }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
end
- it_behaves_like 'an inaccessible'
- it_behaves_like 'not a container repository factory'
+ context 'disallow anyone to delete images' do
+ let(:current_user) { create(:user, external: true) }
+ let(:current_params) do
+ { scope: "repository:#{project.path_with_namespace}:*" }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+ end
+ end
+ end
+
+ context 'delete authorized as master' do
+ let(:current_project) { create(:project) }
+ let(:current_user) { create(:user) }
+
+ let(:authentication_abilities) do
+ [:admin_container_image]
+ end
+
+ before do
+ current_project.add_master(current_user)
+ end
+
+ it_behaves_like 'a valid token'
+
+ context 'allow to delete images' do
+ let(:current_params) do
+ { scope: "repository:#{current_project.path_with_namespace}:*" }
+ end
+
+ it_behaves_like 'a deletable' do
+ let(:project) { current_project }
end
end
end
@@ -248,7 +348,7 @@ describe Auth::ContainerRegistryAuthenticationService do
end
before do
- current_project.team << [current_user, :developer]
+ current_project.add_developer(current_user)
end
it_behaves_like 'a valid token'
@@ -267,6 +367,16 @@ describe Auth::ContainerRegistryAuthenticationService do
end
end
+ context 'disallow to delete images' do
+ let(:current_params) do
+ { scope: "repository:#{current_project.path_with_namespace}:*" }
+ end
+
+ it_behaves_like 'an inaccessible' do
+ let(:project) { current_project }
+ end
+ end
+
context 'for other projects' do
context 'when pulling' do
let(:current_params) do
@@ -288,7 +398,7 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'when you are member' do
before do
- project.team << [current_user, :developer]
+ project.add_developer(current_user)
end
it_behaves_like 'a pullable'
@@ -318,7 +428,7 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'when you are member' do
before do
- project.team << [current_user, :developer]
+ project.add_developer(current_user)
end
it_behaves_like 'a pullable'
@@ -345,7 +455,7 @@ describe Auth::ContainerRegistryAuthenticationService do
let(:project) { create(:project, :public) }
before do
- project.team << [current_user, :developer]
+ project.add_developer(current_user)
end
it_behaves_like 'an inaccessible'
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
index c1f098530bf..426593be428 100644
--- a/spec/services/projects/autocomplete_service_spec.rb
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -88,4 +88,31 @@ describe Projects::AutocompleteService do
end
end
end
+
+ describe '#milestones' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+ let!(:group_milestone) { create(:milestone, group: group) }
+ let!(:project_milestone) { create(:milestone, project: project) }
+
+ let(:milestone_titles) { described_class.new(project, user).milestones.map(&:title) }
+
+ it 'includes project and group milestones' do
+ expect(milestone_titles).to eq([group_milestone.title, project_milestone.title])
+ end
+
+ it 'does not include closed milestones' do
+ group_milestone.close
+
+ expect(milestone_titles).to eq([project_milestone.title])
+ end
+
+ it 'does not include milestones from other projects in the group' do
+ other_project = create(:project, group: group)
+ project_milestone.update!(project: other_project)
+
+ expect(milestone_titles).to eq([group_milestone.title])
+ end
+ end
end
diff --git a/spec/services/projects/create_from_template_service_spec.rb b/spec/services/projects/create_from_template_service_spec.rb
new file mode 100644
index 00000000000..9919ec254c6
--- /dev/null
+++ b/spec/services/projects/create_from_template_service_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Projects::CreateFromTemplateService do
+ let(:user) { create(:user) }
+ let(:project_params) do
+ {
+ path: user.to_param,
+ template_name: 'rails'
+ }
+ end
+
+ subject { described_class.new(user, project_params) }
+
+ it 'calls the importer service' do
+ expect_any_instance_of(Projects::GitlabProjectsImportService).to receive(:execute)
+
+ subject.execute
+ end
+
+ it 'returns the project thats created' do
+ project = subject.execute
+
+ expect(project).to be_saved
+ expect(project.scheduled?).to be(true)
+ end
+end
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index c0ab1ea704d..034065aab00 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -38,8 +38,7 @@ describe Projects::ImportService do
context 'with a Github repository' do
it 'succeeds if repository import is successfully' do
- expect_any_instance_of(Repository).to receive(:fetch_remote).and_return(true)
- expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(true)
+ expect_any_instance_of(Github::Import).to receive(:execute).and_return(true)
result = subject.execute
@@ -52,16 +51,7 @@ describe Projects::ImportService do
result = subject.execute
expect(result[:status]).to eq :error
- expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.full_path} - Failed to import the repository"
- end
-
- it 'does not remove the GitHub remote' do
- expect_any_instance_of(Repository).to receive(:fetch_remote).and_return(true)
- expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(true)
-
- subject.execute
-
- expect(project.repository.raw_repository.remote_names).to include('github')
+ expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - The remote data could not be imported."
end
end
@@ -102,8 +92,7 @@ describe Projects::ImportService do
end
it 'succeeds if importer succeeds' do
- allow_any_instance_of(Repository).to receive(:fetch_remote).and_return(true)
- allow_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(true)
+ allow_any_instance_of(Github::Import).to receive(:execute).and_return(true)
result = subject.execute
@@ -111,10 +100,7 @@ describe Projects::ImportService do
end
it 'flushes various caches' do
- allow_any_instance_of(Repository).to receive(:fetch_remote)
- .and_return(true)
-
- allow_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute)
+ allow_any_instance_of(Github::Import).to receive(:execute)
.and_return(true)
expect_any_instance_of(Repository).to receive(:expire_content_cache)
@@ -123,8 +109,7 @@ describe Projects::ImportService do
end
it 'fails if importer fails' do
- allow_any_instance_of(Repository).to receive(:fetch_remote).and_return(true)
- allow_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(false)
+ allow_any_instance_of(Github::Import).to receive(:execute).and_return(false)
result = subject.execute
@@ -133,8 +118,7 @@ describe Projects::ImportService do
end
it 'fails if importer raise an error' do
- allow_any_instance_of(Gitlab::Shell).to receive(:fetch_remote).and_return(true)
- allow_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_raise(Projects::ImportService::Error.new('Github: failed to connect API'))
+ allow_any_instance_of(Github::Import).to receive(:execute).and_raise(Projects::ImportService::Error.new('Github: failed to connect API'))
result = subject.execute
@@ -143,9 +127,9 @@ describe Projects::ImportService do
end
it 'expires content cache after error' do
- allow_any_instance_of(Project).to receive(:repository_exists?).and_return(false, true)
+ allow_any_instance_of(Project).to receive(:repository_exists?).and_return(false)
- expect_any_instance_of(Gitlab::Shell).to receive(:fetch_remote).and_raise(Gitlab::Shell::Error.new('Failed to import the repository'))
+ expect_any_instance_of(Repository).to receive(:fetch_remote).and_raise(Gitlab::Shell::Error.new)
expect_any_instance_of(Repository).to receive(:expire_content_cache)
subject.execute
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index b78ecfb61c4..30fa0ee6873 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -424,6 +424,26 @@ describe QuickActions::InterpretService do
end
end
+ context 'assign command with me alias' do
+ let(:content) { "/assign me" }
+
+ context 'Issue' do
+ it 'fetches assignee and populates assignee_ids if content contains /assign' do
+ _, updates = service.execute(content, issue)
+
+ expect(updates).to eq(assignee_ids: [developer.id])
+ end
+ end
+
+ context 'Merge Request' do
+ it 'fetches assignee and populates assignee_ids if content contains /assign' do
+ _, updates = service.execute(content, merge_request)
+
+ expect(updates).to eq(assignee_ids: [developer.id])
+ end
+ end
+ end
+
it_behaves_like 'empty command' do
let(:content) { '/assign @abcd1234' }
let(:issuable) { issue }
diff --git a/spec/services/submit_usage_ping_service_spec.rb b/spec/services/submit_usage_ping_service_spec.rb
index 817fa4262d5..c8a6fc1a99b 100644
--- a/spec/services/submit_usage_ping_service_spec.rb
+++ b/spec/services/submit_usage_ping_service_spec.rb
@@ -46,6 +46,8 @@ describe SubmitUsagePingService do
.by(1)
expect(ConversationalDevelopmentIndex::Metric.last.leader_issues).to eq 10.2
+ expect(ConversationalDevelopmentIndex::Metric.last.instance_issues).to eq 3.2
+ expect(ConversationalDevelopmentIndex::Metric.last.percentage_issues).to eq 31.37
end
end
@@ -60,6 +62,7 @@ describe SubmitUsagePingService do
conv_index: {
leader_issues: 10.2,
instance_issues: 3.2,
+ percentage_issues: 31.37,
leader_notes: 25.3,
instance_notes: 23.2,
@@ -86,7 +89,9 @@ describe SubmitUsagePingService do
instance_projects_prometheus_active: 0.30,
leader_service_desk_issues: 15.8,
- instance_service_desk_issues: 15.1
+ instance_service_desk_issues: 15.1,
+
+ non_existing_column: 'value'
}
}
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index e3805160b04..8f1eb4863d9 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -3,7 +3,8 @@ require 'spec_helper'
describe SystemNoteService do
include Gitlab::Routing
- let(:project) { create(:project) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
let(:author) { create(:user) }
let(:noteable) { create(:issue, project: project) }
let(:issue) { noteable }
@@ -242,25 +243,51 @@ describe SystemNoteService do
end
describe '.change_milestone' do
- subject { described_class.change_milestone(noteable, project, author, milestone) }
+ context 'for a project milestone' do
+ subject { described_class.change_milestone(noteable, project, author, milestone) }
- let(:milestone) { create(:milestone, project: project) }
+ let(:milestone) { create(:milestone, project: project) }
- it_behaves_like 'a system note' do
- let(:action) { 'milestone' }
- end
+ it_behaves_like 'a system note' do
+ let(:action) { 'milestone' }
+ end
- context 'when milestone added' do
- it 'sets the note text' do
- expect(subject.note).to eq "changed milestone to #{milestone.to_reference}"
+ context 'when milestone added' do
+ it 'sets the note text' do
+ expect(subject.note).to eq "changed milestone to #{milestone.to_reference}"
+ end
+ end
+
+ context 'when milestone removed' do
+ let(:milestone) { nil }
+
+ it 'sets the note text' do
+ expect(subject.note).to eq 'removed milestone'
+ end
end
end
- context 'when milestone removed' do
- let(:milestone) { nil }
+ context 'for a group milestone' do
+ subject { described_class.change_milestone(noteable, project, author, milestone) }
- it 'sets the note text' do
- expect(subject.note).to eq 'removed milestone'
+ let(:milestone) { create(:milestone, group: group) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'milestone' }
+ end
+
+ context 'when milestone added' do
+ it 'sets the note text to use the milestone name' do
+ expect(subject.note).to eq "changed milestone to #{milestone.to_reference(format: :name)}"
+ end
+ end
+
+ context 'when milestone removed' do
+ let(:milestone) { nil }
+
+ it 'sets the note text' do
+ expect(subject.note).to eq 'removed milestone'
+ end
end
end
end
diff --git a/spec/services/wiki_pages/update_service_spec.rb b/spec/services/wiki_pages/update_service_spec.rb
index a242bf5a5cc..2399db7d3d4 100644
--- a/spec/services/wiki_pages/update_service_spec.rb
+++ b/spec/services/wiki_pages/update_service_spec.rb
@@ -9,7 +9,8 @@ describe WikiPages::UpdateService do
{
content: 'New content for wiki page',
format: 'markdown',
- message: 'New wiki message'
+ message: 'New wiki message',
+ title: 'New Title'
}
end
@@ -27,6 +28,7 @@ describe WikiPages::UpdateService do
expect(updated_page.message).to eq(opts[:message])
expect(updated_page.content).to eq(opts[:content])
expect(updated_page.format).to eq(opts[:format].to_sym)
+ expect(updated_page.title).to eq(opts[:title])
end
it 'executes webhooks' do
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 06769b241ad..0ba6ed56314 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -134,13 +134,13 @@ RSpec.configure do |config|
ActiveRecord::Migrator
.migrate(migrations_paths, previous_migration.version)
- ActiveRecord::Base.descendants.each(&:reset_column_information)
+ reset_column_in_migration_models
end
config.after(:example, :migration) do
ActiveRecord::Migrator.migrate(migrations_paths)
- ActiveRecord::Base.descendants.each(&:reset_column_information)
+ reset_column_in_migration_models
end
config.around(:each, :nested_groups) do |example|
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
index c0a5491a430..30911e7fa86 100644
--- a/spec/support/cycle_analytics_helpers.rb
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -41,7 +41,9 @@ module CycleAnalyticsHelpers
target_branch: 'master'
}
- MergeRequests::CreateService.new(project, user, opts).execute
+ mr = MergeRequests::CreateService.new(project, user, opts).execute
+ NewMergeRequestWorker.new.perform(mr, user)
+ mr
end
def merge_merge_requests_closing_issue(issue)
diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index b0d513026d6..8282ba7e536 100644
--- a/spec/support/features/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -277,6 +277,17 @@ shared_examples 'issuable record that supports quick actions in its description
expect(issuable.subscribed?(master, project)).to be_falsy
end
end
+
+ context "with a note assigning the #{issuable_type} to the current user" do
+ it "assigns the #{issuable_type} to the current user" do
+ write_note("/assign me")
+
+ expect(page).not_to have_content '/assign me'
+ expect(page).to have_content 'Commands applied'
+
+ expect(issuable.reload.assignees).to eq [master]
+ end
+ end
end
describe "preview of note on #{issuable_type}", js: true do
diff --git a/spec/support/issuable_shared_examples.rb b/spec/support/issuable_shared_examples.rb
index 970fe10db2b..42f3b4db23c 100644
--- a/spec/support/issuable_shared_examples.rb
+++ b/spec/support/issuable_shared_examples.rb
@@ -21,15 +21,15 @@ shared_examples 'system notes for milestones' do
create(:group_member, group: group, user: user)
end
- it 'does not create system note' do
+ it 'creates a system note' do
expect do
update_issuable(milestone: group_milestone)
- end.not_to change { Note.system.count }
+ end.to change { Note.system.count }.by(1)
end
end
context 'project milestones' do
- it 'creates system note' do
+ it 'creates a system note' do
expect do
update_issuable(milestone: create(:milestone))
end.to change { Note.system.count }.by(1)
diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb
index 21a054af4e1..c90359d7cfa 100644
--- a/spec/support/markdown_feature.rb
+++ b/spec/support/markdown_feature.rb
@@ -23,7 +23,7 @@ class MarkdownFeature
# Direct references ----------------------------------------------------------
def project
- @project ||= create(:project, :repository).tap do |project|
+ @project ||= create(:project, :repository, group: group).tap do |project|
project.team << [user, :master]
end
end
@@ -75,6 +75,10 @@ class MarkdownFeature
@milestone ||= create(:milestone, name: 'next goal', project: project)
end
+ def group_milestone
+ @group_milestone ||= create(:milestone, name: 'group-milestone', group: group)
+ end
+
# Cross-references -----------------------------------------------------------
def xproject
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 7afa57fb76b..d12b2757427 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -155,7 +155,7 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('a.gfm.gfm-milestone', count: 6)
+ expect(actual).to have_selector('a.gfm.gfm-milestone', count: 8)
end
end
diff --git a/spec/support/migrations_helpers.rb b/spec/support/migrations_helpers.rb
index 91fbb4eaf48..aabdad13047 100644
--- a/spec/support/migrations_helpers.rb
+++ b/spec/support/migrations_helpers.rb
@@ -15,6 +15,16 @@ module MigrationsHelpers
ActiveRecord::Migrator.migrations(migrations_paths)
end
+ def reset_column_in_migration_models
+ described_class.constants.sort.each do |name|
+ const = described_class.const_get(name)
+
+ if const.is_a?(Class) && const < ActiveRecord::Base
+ const.reset_column_information
+ end
+ end
+ end
+
def previous_migration
migrations.each_cons(2) do |previous, migration|
break previous if migration.name == described_class.name
diff --git a/spec/support/stored_repositories.rb b/spec/support/stored_repositories.rb
index df18926d58c..f3deae0f455 100644
--- a/spec/support/stored_repositories.rb
+++ b/spec/support/stored_repositories.rb
@@ -2,4 +2,16 @@ RSpec.configure do |config|
config.before(:each, :repository) do
TestEnv.clean_test_path
end
+
+ config.before(:all, :broken_storage) do
+ FileUtils.rm_rf Gitlab.config.repositories.storages.broken['path']
+ end
+
+ config.before(:each, :broken_storage) do
+ allow(Gitlab::GitalyClient).to receive(:call) do
+ raise GRPC::Unavailable.new('Gitaly broken in this spec')
+ end
+
+ Gitlab::Git::Storage::CircuitBreaker.reset_all!
+ end
end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index c1298ed9cae..1e39f80699c 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -63,8 +63,6 @@ module TestEnv
# See gitlab.yml.example test section for paths
#
def init(opts = {})
- Rake.application.rake_require 'tasks/gitlab/helpers'
- Rake::Task.define_task :environment
# Disable mailer for spinach tests
disable_mailer if opts[:mailer] == false
@@ -124,41 +122,50 @@ module TestEnv
end
def setup_gitlab_shell
- gitlab_shell_dir = File.dirname(Gitlab.config.gitlab_shell.path)
- gitlab_shell_needs_update = component_needs_update?(gitlab_shell_dir,
+ puts "\n==> Setting up Gitlab Shell..."
+ start = Time.now
+ gitlab_shell_dir = Gitlab.config.gitlab_shell.path
+ shell_needs_update = component_needs_update?(gitlab_shell_dir,
Gitlab::Shell.version_required)
- Rake.application.rake_require 'tasks/gitlab/shell'
- unless !gitlab_shell_needs_update || Rake.application.invoke_task('gitlab:shell:install')
+ unless !shell_needs_update || system('rake', 'gitlab:shell:install')
+ puts "\nGitLab Shell failed to install, cleaning up #{gitlab_shell_dir}!\n"
FileUtils.rm_rf(gitlab_shell_dir)
- raise "Can't install gitlab-shell"
+ exit 1
end
+
+ puts " GitLab Shell setup in #{Time.now - start} seconds...\n"
end
def setup_gitaly
+ puts "\n==> Setting up Gitaly..."
+ start = Time.now
socket_path = Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '')
gitaly_dir = File.dirname(socket_path)
if gitaly_dir_stale?(gitaly_dir)
- puts "rm -rf #{gitaly_dir}"
- FileUtils.rm_rf(gitaly_dir)
+ puts " Gitaly is outdated, cleaning up #{gitaly_dir}!"
+ FileUtils.rm_rf(gitaly_dir)
end
gitaly_needs_update = component_needs_update?(gitaly_dir,
Gitlab::GitalyClient.expected_server_version)
- Rake.application.rake_require 'tasks/gitlab/gitaly'
- unless !gitaly_needs_update || Rake.application.invoke_task("gitlab:gitaly:install[#{gitaly_dir}]")
+ unless !gitaly_needs_update || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]")
+ puts "\nGitaly failed to install, cleaning up #{gitaly_dir}!\n"
FileUtils.rm_rf(gitaly_dir)
- raise "Can't install gitaly"
+ exit 1
end
start_gitaly(gitaly_dir)
+ puts " Gitaly setup in #{Time.now - start} seconds...\n"
end
def gitaly_dir_stale?(dir)
gitaly_executable = File.join(dir, 'gitaly')
- !File.exist?(gitaly_executable) || (File.mtime(gitaly_executable) < File.mtime(Rails.root.join('GITALY_SERVER_VERSION')))
+ return false unless File.exist?(gitaly_executable)
+
+ File.mtime(gitaly_executable) < File.mtime(Rails.root.join('GITALY_SERVER_VERSION'))
end
def start_gitaly(gitaly_dir)
@@ -243,6 +250,14 @@ module TestEnv
"#{forked_repo_path}_bare"
end
+ def with_empty_bare_repository(name = nil)
+ path = Rails.root.join('tmp/tests', name || 'empty-bare-repository').to_s
+
+ yield(Rugged::Repository.init_at(path, :bare))
+ ensure
+ FileUtils.rm_rf(path)
+ end
+
private
def factory_repo_path
diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb
index a2f4ec39d89..cc932a4ec4c 100644
--- a/spec/tasks/gitlab/gitaly_rake_spec.rb
+++ b/spec/tasks/gitlab/gitaly_rake_spec.rb
@@ -41,6 +41,8 @@ describe 'gitlab:gitaly namespace rake task' do
end
describe 'gmake/make' do
+ let(:command_preamble) { %w[/usr/bin/env -u RUBYOPT] }
+
before(:all) do
@old_env_ci = ENV.delete('CI')
end
@@ -57,12 +59,12 @@ describe 'gitlab:gitaly namespace rake task' do
context 'gmake is available' do
before do
expect_any_instance_of(Object).to receive(:checkout_or_clone_version)
- allow_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true)
+ allow_any_instance_of(Object).to receive(:run_command!).with(command_preamble + ['gmake']).and_return(true)
end
it 'calls gmake in the gitaly directory' do
expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['/usr/bin/gmake', 0])
- expect_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true)
+ expect_any_instance_of(Object).to receive(:run_command!).with(command_preamble + ['gmake']).and_return(true)
run_rake_task('gitlab:gitaly:install', clone_path)
end
@@ -71,12 +73,12 @@ describe 'gitlab:gitaly namespace rake task' do
context 'gmake is not available' do
before do
expect_any_instance_of(Object).to receive(:checkout_or_clone_version)
- allow_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true)
+ allow_any_instance_of(Object).to receive(:run_command!).with(command_preamble + ['make']).and_return(true)
end
it 'calls make in the gitaly directory' do
expect(Gitlab::Popen).to receive(:popen).with(%w[which gmake]).and_return(['', 42])
- expect_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true)
+ expect_any_instance_of(Object).to receive(:run_command!).with(command_preamble + ['make']).and_return(true)
run_rake_task('gitlab:gitaly:install', clone_path)
end
@@ -105,6 +107,8 @@ describe 'gitlab:gitaly namespace rake task' do
# Gitaly storage configuration generated from #{Gitlab.config.source} on #{Time.current.to_s(:long)}
# This is in TOML format suitable for use in Gitaly's config.toml file.
socket_path = "/path/to/my.socket"
+ [gitlab-shell]
+ dir = "#{Gitlab.config.gitlab_shell.path}"
[[storage]]
name = "default"
path = "/path/to/default"
diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb
index 303193bab9b..ee51000161a 100644
--- a/spec/workers/merge_worker_spec.rb
+++ b/spec/workers/merge_worker_spec.rb
@@ -27,4 +27,15 @@ describe MergeWorker do
expect(source_project.repository.branch_names).not_to include('markdown')
end
end
+
+ it 'persists merge_jid' do
+ merge_request = create(:merge_request, merge_jid: nil)
+ user = create(:user)
+ worker = described_class.new
+
+ allow(worker).to receive(:jid) { '999' }
+
+ expect { worker.perform(merge_request.id, user.id, {}) }
+ .to change { merge_request.reload.merge_jid }.from(nil).to('999')
+ end
end
diff --git a/spec/workers/new_issue_worker_spec.rb b/spec/workers/new_issue_worker_spec.rb
new file mode 100644
index 00000000000..ed49ce57c0b
--- /dev/null
+++ b/spec/workers/new_issue_worker_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe NewIssueWorker do
+ describe '#perform' do
+ let(:worker) { described_class.new }
+
+ context 'when an issue not found' do
+ it 'does not call Services' do
+ expect(EventCreateService).not_to receive(:new)
+ expect(NotificationService).not_to receive(:new)
+
+ worker.perform(99, create(:user).id)
+ end
+
+ it 'logs an error' do
+ expect(Rails.logger).to receive(:error).with('NewIssueWorker: couldn\'t find Issue with ID=99, skipping job')
+
+ worker.perform(99, create(:user).id)
+ end
+ end
+
+ context 'when a user not found' do
+ it 'does not call Services' do
+ expect(EventCreateService).not_to receive(:new)
+ expect(NotificationService).not_to receive(:new)
+
+ worker.perform(create(:issue).id, 99)
+ end
+
+ it 'logs an error' do
+ expect(Rails.logger).to receive(:error).with('NewIssueWorker: couldn\'t find User with ID=99, skipping job')
+
+ worker.perform(create(:issue).id, 99)
+ end
+ end
+
+ context 'when everything is ok' do
+ let(:project) { create(:project, :public) }
+ let(:mentioned) { create(:user) }
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, project: project, description: "issue for #{mentioned.to_reference}") }
+
+ it 'creates a new event record' do
+ expect{ worker.perform(issue.id, user.id) }.to change { Event.count }.from(0).to(1)
+ end
+
+ it 'creates a notification for the assignee' do
+ expect(Notify).to receive(:new_issue_email).with(mentioned.id, issue.id).and_return(double(deliver_later: true))
+
+ worker.perform(issue.id, user.id)
+ end
+ end
+ end
+end
diff --git a/spec/workers/new_merge_request_worker_spec.rb b/spec/workers/new_merge_request_worker_spec.rb
new file mode 100644
index 00000000000..85af6184d39
--- /dev/null
+++ b/spec/workers/new_merge_request_worker_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe NewMergeRequestWorker do
+ describe '#perform' do
+ let(:worker) { described_class.new }
+
+ context 'when a merge request not found' do
+ it 'does not call Services' do
+ expect(EventCreateService).not_to receive(:new)
+ expect(NotificationService).not_to receive(:new)
+
+ worker.perform(99, create(:user).id)
+ end
+
+ it 'logs an error' do
+ expect(Rails.logger).to receive(:error).with('NewMergeRequestWorker: couldn\'t find MergeRequest with ID=99, skipping job')
+
+ worker.perform(99, create(:user).id)
+ end
+ end
+
+ context 'when a user not found' do
+ it 'does not call Services' do
+ expect(EventCreateService).not_to receive(:new)
+ expect(NotificationService).not_to receive(:new)
+
+ worker.perform(create(:merge_request).id, 99)
+ end
+
+ it 'logs an error' do
+ expect(Rails.logger).to receive(:error).with('NewMergeRequestWorker: couldn\'t find User with ID=99, skipping job')
+
+ worker.perform(create(:merge_request).id, 99)
+ end
+ end
+
+ context 'when everything is ok' do
+ let(:project) { create(:project, :public) }
+ let(:mentioned) { create(:user) }
+ let(:user) { create(:user) }
+ let(:merge_request) do
+ create(:merge_request, source_project: project, description: "mr for #{mentioned.to_reference}")
+ end
+
+ it 'creates a new event record' do
+ expect{ worker.perform(merge_request.id, user.id) }.to change { Event.count }.from(0).to(1)
+ end
+
+ it 'creates a notification for the assignee' do
+ expect(Notify).to receive(:new_merge_request_email).with(mentioned.id, merge_request.id).and_return(double(deliver_later: true))
+
+ worker.perform(merge_request.id, user.id)
+ end
+ end
+ end
+end
diff --git a/spec/workers/stuck_merge_jobs_worker_spec.rb b/spec/workers/stuck_merge_jobs_worker_spec.rb
new file mode 100644
index 00000000000..a5ad78393c9
--- /dev/null
+++ b/spec/workers/stuck_merge_jobs_worker_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe StuckMergeJobsWorker do
+ describe 'perform' do
+ let(:worker) { described_class.new }
+
+ context 'merge job identified as completed' do
+ it 'updates merge request to merged when locked but has merge_commit_sha' do
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(%w(123 456))
+ mr_with_sha = create(:merge_request, :locked, merge_jid: '123', state: :locked, merge_commit_sha: 'foo-bar-baz')
+ mr_without_sha = create(:merge_request, :locked, merge_jid: '123', state: :locked, merge_commit_sha: nil)
+
+ worker.perform
+
+ expect(mr_with_sha.reload).to be_merged
+ expect(mr_without_sha.reload).to be_opened
+ end
+
+ it 'updates merge request to opened when locked but has not been merged' do
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(%w(123))
+ merge_request = create(:merge_request, :locked, merge_jid: '123', state: :locked)
+
+ worker.perform
+
+ expect(merge_request.reload).to be_opened
+ end
+
+ it 'logs updated stuck merge job ids' do
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(%w(123 456))
+
+ create(:merge_request, :locked, merge_jid: '123')
+ create(:merge_request, :locked, merge_jid: '456')
+
+ expect(Rails).to receive_message_chain(:logger, :info).with('Updated state of locked merge jobs. JIDs: 123, 456')
+
+ worker.perform
+ end
+ end
+
+ context 'merge job not identified as completed' do
+ it 'does not change merge request state when job is not completed yet' do
+ allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([])
+
+ merge_request = create(:merge_request, :locked, merge_jid: '123')
+
+ expect { worker.perform }.not_to change { merge_request.reload.state }.from('locked')
+ end
+ end
+ end
+end
diff --git a/vendor/assets/javascripts/jquery.nicescroll.js b/vendor/assets/javascripts/jquery.nicescroll.js
deleted file mode 100644
index 7653f25df4b..00000000000
--- a/vendor/assets/javascripts/jquery.nicescroll.js
+++ /dev/null
@@ -1,3634 +0,0 @@
-/* jquery.nicescroll
--- version 3.6.0
--- copyright 2014-11-21 InuYaksa*2014
--- licensed under the MIT
---
--- http://nicescroll.areaaperta.com/
--- https://github.com/inuyaksa/jquery.nicescroll
---
-*/
-
-(function(factory) {
- if (typeof define === 'function' && define.amd) {
- // AMD. Register as anonymous module.
- define(['jquery'], factory);
- } else {
- // Browser globals.
- factory(jQuery);
- }
-}(function(jQuery) {
- "use strict";
-
- // globals
- var domfocus = false;
- var mousefocus = false;
- var tabindexcounter = 0;
- var ascrailcounter = 2000;
- var globalmaxzindex = 0;
-
- var $ = jQuery; // sandbox
-
- // http://stackoverflow.com/questions/2161159/get-script-path
- function getScriptPath() {
- var scripts = document.getElementsByTagName('script');
- var path = scripts[scripts.length - 1].src.split('?')[0];
- return (path.split('/').length > 0) ? path.split('/').slice(0, -1).join('/') + '/' : '';
- }
-
- var vendors = ['webkit','ms','moz','o'];
-
- var setAnimationFrame = window.requestAnimationFrame || false;
- var clearAnimationFrame = window.cancelAnimationFrame || false;
-
- if (!setAnimationFrame) { // legacy detection
- for (var vx in vendors) {
- var v = vendors[vx];
- if (!setAnimationFrame) setAnimationFrame = window[v + 'RequestAnimationFrame'];
- if (!clearAnimationFrame) clearAnimationFrame = window[v + 'CancelAnimationFrame'] || window[v + 'CancelRequestAnimationFrame'];
- }
- }
-
- var ClsMutationObserver = window.MutationObserver || window.WebKitMutationObserver || false;
-
- var _globaloptions = {
- zindex: "auto",
- cursoropacitymin: 0,
- cursoropacitymax: 1,
- cursorcolor: "#424242",
- cursorwidth: "5px",
- cursorborder: "1px solid #fff",
- cursorborderradius: "5px",
- scrollspeed: 60,
- mousescrollstep: 8 * 3,
- touchbehavior: false,
- hwacceleration: true,
- usetransition: true,
- boxzoom: false,
- dblclickzoom: true,
- gesturezoom: true,
- grabcursorenabled: true,
- autohidemode: true,
- background: "",
- iframeautoresize: true,
- cursorminheight: 32,
- preservenativescrolling: true,
- railoffset: false,
- railhoffset: false,
- bouncescroll: true,
- spacebarenabled: true,
- railpadding: {
- top: 0,
- right: 0,
- left: 0,
- bottom: 0
- },
- disableoutline: true,
- horizrailenabled: true,
- railalign: "right",
- railvalign: "bottom",
- enabletranslate3d: true,
- enablemousewheel: true,
- enablekeyboard: true,
- smoothscroll: true,
- sensitiverail: true,
- enablemouselockapi: true,
- // cursormaxheight:false,
- cursorfixedheight: false,
- directionlockdeadzone: 6,
- hidecursordelay: 400,
- nativeparentscrolling: true,
- enablescrollonselection: true,
- overflowx: true,
- overflowy: true,
- cursordragspeed: 0.3,
- rtlmode: "auto",
- cursordragontouch: false,
- oneaxismousemode: "auto",
- scriptpath: getScriptPath(),
- preventmultitouchscrolling: true
- };
-
- var browserdetected = false;
-
- var getBrowserDetection = function() {
-
- if (browserdetected) return browserdetected;
-
- var _el = document.createElement('DIV'),
- _style = _el.style,
- _agent = navigator.userAgent,
- _platform = navigator.platform,
- d = {};
-
- d.haspointerlock = "pointerLockElement" in document || "webkitPointerLockElement" in document || "mozPointerLockElement" in document;
-
- d.isopera = ("opera" in window); // 12-
- d.isopera12 = (d.isopera && ("getUserMedia" in navigator));
- d.isoperamini = (Object.prototype.toString.call(window.operamini) === "[object OperaMini]");
-
- d.isie = (("all" in document) && ("attachEvent" in _el) && !d.isopera); //IE10-
- d.isieold = (d.isie && !("msInterpolationMode" in _style)); // IE6 and older
- d.isie7 = d.isie && !d.isieold && (!("documentMode" in document) || (document.documentMode == 7));
- d.isie8 = d.isie && ("documentMode" in document) && (document.documentMode == 8);
- d.isie9 = d.isie && ("performance" in window) && (document.documentMode >= 9);
- d.isie10 = d.isie && ("performance" in window) && (document.documentMode == 10);
- d.isie11 = ("msRequestFullscreen" in _el) && (document.documentMode >= 11); // IE11+
-
- d.isie9mobile = /iemobile.9/i.test(_agent); //wp 7.1 mango
- if (d.isie9mobile) d.isie9 = false;
- d.isie7mobile = (!d.isie9mobile && d.isie7) && /iemobile/i.test(_agent); //wp 7.0
-
- d.ismozilla = ("MozAppearance" in _style);
-
- d.iswebkit = ("WebkitAppearance" in _style);
-
- d.ischrome = ("chrome" in window);
- d.ischrome22 = (d.ischrome && d.haspointerlock);
- d.ischrome26 = (d.ischrome && ("transition" in _style)); // issue with transform detection (maintain prefix)
-
- d.cantouch = ("ontouchstart" in document.documentElement) || ("ontouchstart" in window); // detection for Chrome Touch Emulation
- d.hasmstouch = (window.MSPointerEvent || false); // IE10 pointer events
- d.hasw3ctouch = (window.PointerEvent || false); //IE11 pointer events, following W3C Pointer Events spec
-
- d.ismac = /^mac$/i.test(_platform);
-
- d.isios = (d.cantouch && /iphone|ipad|ipod/i.test(_platform));
- d.isios4 = ((d.isios) && !("seal" in Object));
- d.isios7 = ((d.isios)&&("webkitHidden" in document)); //iOS 7+
-
- d.isandroid = (/android/i.test(_agent));
-
- d.haseventlistener = ("addEventListener" in _el);
-
- d.trstyle = false;
- d.hastransform = false;
- d.hastranslate3d = false;
- d.transitionstyle = false;
- d.hastransition = false;
- d.transitionend = false;
-
- var a;
- var check = ['transform', 'msTransform', 'webkitTransform', 'MozTransform', 'OTransform'];
- for (a = 0; a < check.length; a++) {
- if (typeof _style[check[a]] != "undefined") {
- d.trstyle = check[a];
- break;
- }
- }
- d.hastransform = (!!d.trstyle);
- if (d.hastransform) {
- _style[d.trstyle] = "translate3d(1px,2px,3px)";
- d.hastranslate3d = /translate3d/.test(_style[d.trstyle]);
- }
-
- d.transitionstyle = false;
- d.prefixstyle = '';
- d.transitionend = false;
- check = ['transition', 'webkitTransition', 'msTransition', 'MozTransition', 'OTransition', 'OTransition', 'KhtmlTransition'];
- var prefix = ['', '-webkit-', '-ms-', '-moz-', '-o-', '-o', '-khtml-'];
- var evs = ['transitionend', 'webkitTransitionEnd', 'msTransitionEnd', 'transitionend', 'otransitionend', 'oTransitionEnd', 'KhtmlTransitionEnd'];
- for (a = 0; a < check.length; a++) {
- if (check[a] in _style) {
- d.transitionstyle = check[a];
- d.prefixstyle = prefix[a];
- d.transitionend = evs[a];
- break;
- }
- }
- if (d.ischrome26) { // always use prefix
- d.prefixstyle = prefix[1];
- }
-
- d.hastransition = (d.transitionstyle);
-
- function detectCursorGrab() {
- var lst = ['-webkit-grab', '-moz-grab', 'grab'];
- if ((d.ischrome && !d.ischrome22) || d.isie) lst = []; // force setting for IE returns false positive and chrome cursor bug
- for (var a = 0; a < lst.length; a++) {
- var p = lst[a];
- _style.cursor = p;
- if (_style.cursor == p) return p;
- }
- return 'url(//mail.google.com/mail/images/2/openhand.cur),n-resize'; // thank you google for custom cursor!
- }
- d.cursorgrabvalue = detectCursorGrab();
-
- d.hasmousecapture = ("setCapture" in _el);
-
- d.hasMutationObserver = (ClsMutationObserver !== false);
-
- _el = null; //memory released
-
- browserdetected = d;
-
- return d;
- };
-
- var NiceScrollClass = function(myopt, me) {
-
- var self = this;
-
- this.version = '3.6.0';
- this.name = 'nicescroll';
-
- this.me = me;
-
- this.opt = {
- doc: $("body"),
- win: false
- };
-
- $.extend(this.opt, _globaloptions); // clone opts
-
- // Options for internal use
- this.opt.snapbackspeed = 80;
-
- if (myopt || false) {
- for (var a in self.opt) {
- if (typeof myopt[a] != "undefined") self.opt[a] = myopt[a];
- }
- }
-
- this.doc = self.opt.doc;
- this.iddoc = (this.doc && this.doc[0]) ? this.doc[0].id || '' : '';
- this.ispage = /^BODY|HTML/.test((self.opt.win) ? self.opt.win[0].nodeName : this.doc[0].nodeName);
- this.haswrapper = (self.opt.win !== false);
- this.win = self.opt.win || (this.ispage ? $(window) : this.doc);
- this.docscroll = (this.ispage && !this.haswrapper) ? $(window) : this.win;
- this.body = $("body");
- this.viewport = false;
-
- this.isfixed = false;
-
- this.iframe = false;
- this.isiframe = ((this.doc[0].nodeName == 'IFRAME') && (this.win[0].nodeName == 'IFRAME'));
-
- this.istextarea = (this.win[0].nodeName == 'TEXTAREA');
-
- this.forcescreen = false; //force to use screen position on events
-
- this.canshowonmouseevent = (self.opt.autohidemode != "scroll");
-
- // Events jump table
- this.onmousedown = false;
- this.onmouseup = false;
- this.onmousemove = false;
- this.onmousewheel = false;
- this.onkeypress = false;
- this.ongesturezoom = false;
- this.onclick = false;
-
- // Nicescroll custom events
- this.onscrollstart = false;
- this.onscrollend = false;
- this.onscrollcancel = false;
-
- this.onzoomin = false;
- this.onzoomout = false;
-
- // Let's start!
- this.view = false;
- this.page = false;
-
- this.scroll = {
- x: 0,
- y: 0
- };
- this.scrollratio = {
- x: 0,
- y: 0
- };
- this.cursorheight = 20;
- this.scrollvaluemax = 0;
-
- this.isrtlmode = (this.opt.rtlmode == "auto") ? ((this.win[0] == window ? this.body : this.win).css("direction") == "rtl") : (this.opt.rtlmode === true);
- // this.checkrtlmode = false;
-
- this.scrollrunning = false;
-
- this.scrollmom = false;
-
- this.observer = false; // observer div changes
- this.observerremover = false; // observer on parent for remove detection
- this.observerbody = false; // observer on body for position change
-
- do {
- this.id = "ascrail" + (ascrailcounter++);
- } while (document.getElementById(this.id));
-
- this.rail = false;
- this.cursor = false;
- this.cursorfreezed = false;
- this.selectiondrag = false;
-
- this.zoom = false;
- this.zoomactive = false;
-
- this.hasfocus = false;
- this.hasmousefocus = false;
-
- this.visibility = true;
- this.railslocked = false; // locked by resize
- this.locked = false; // prevent lost of locked status sets by user
- this.hidden = false; // rails always hidden
- this.cursoractive = true; // user can interact with cursors
-
- this.wheelprevented = false; //prevent mousewheel event
-
- this.overflowx = self.opt.overflowx;
- this.overflowy = self.opt.overflowy;
-
- this.nativescrollingarea = false;
- this.checkarea = 0;
-
- this.events = []; // event list for unbind
-
- this.saved = {}; // style saved
-
- this.delaylist = {};
- this.synclist = {};
-
- this.lastdeltax = 0;
- this.lastdeltay = 0;
-
- this.detected = getBrowserDetection();
-
- var cap = $.extend({}, this.detected);
-
- this.canhwscroll = (cap.hastransform && self.opt.hwacceleration);
- this.ishwscroll = (this.canhwscroll && self.haswrapper);
-
- this.hasreversehr = (this.isrtlmode&&!cap.iswebkit); //RTL mode with reverse horizontal axis
-
- this.istouchcapable = false; // desktop devices with touch screen support
-
- //## Check WebKit-based desktop with touch support
- //## + Firefox 18 nightly build (desktop) false positive (or desktop with touch support)
- if (cap.cantouch && !cap.isios && !cap.isandroid && (cap.iswebkit || cap.ismozilla)) {
- this.istouchcapable = true;
- cap.cantouch = false; // parse normal desktop events
- }
-
- //## disable MouseLock API on user request
- if (!self.opt.enablemouselockapi) {
- cap.hasmousecapture = false;
- cap.haspointerlock = false;
- }
-
-/* deprecated
- this.delayed = function(name, fn, tm, lazy) {
- };
-*/
-
- this.debounced = function(name, fn, tm) {
- var dd = self.delaylist[name];
- self.delaylist[name] = fn;
- if (!dd) {
- setTimeout(function() {
- var fn = self.delaylist[name];
- self.delaylist[name] = false;
- fn.call(self);
- }, tm);
- }
- };
-
- var _onsync = false;
-
- this.synched = function(name, fn) {
-
- function requestSync() {
- if (_onsync) return;
- setAnimationFrame(function() {
- _onsync = false;
- for (var nn in self.synclist) {
- var fn = self.synclist[nn];
- if (fn) fn.call(self);
- self.synclist[nn] = false;
- }
- });
- _onsync = true;
- }
-
- self.synclist[name] = fn;
- requestSync();
- return name;
- };
-
- this.unsynched = function(name) {
- if (self.synclist[name]) self.synclist[name] = false;
- };
-
- this.css = function(el, pars) { // save & set
- for (var n in pars) {
- self.saved.css.push([el, n, el.css(n)]);
- el.css(n, pars[n]);
- }
- };
-
- this.scrollTop = function(val) {
- return (typeof val == "undefined") ? self.getScrollTop() : self.setScrollTop(val);
- };
-
- this.scrollLeft = function(val) {
- return (typeof val == "undefined") ? self.getScrollLeft() : self.setScrollLeft(val);
- };
-
- // derived by by Dan Pupius www.pupius.net
- var BezierClass = function(st, ed, spd, p1, p2, p3, p4) {
-
- this.st = st;
- this.ed = ed;
- this.spd = spd;
-
- this.p1 = p1 || 0;
- this.p2 = p2 || 1;
- this.p3 = p3 || 0;
- this.p4 = p4 || 1;
-
- this.ts = (new Date()).getTime();
- this.df = this.ed - this.st;
- };
- BezierClass.prototype = {
- B2: function(t) {
- return 3 * t * t * (1 - t);
- },
- B3: function(t) {
- return 3 * t * (1 - t) * (1 - t);
- },
- B4: function(t) {
- return (1 - t) * (1 - t) * (1 - t);
- },
- getNow: function() {
- var nw = (new Date()).getTime();
- var pc = 1 - ((nw - this.ts) / this.spd);
- var bz = this.B2(pc) + this.B3(pc) + this.B4(pc);
- return (pc < 0) ? this.ed : this.st + Math.round(this.df * bz);
- },
- update: function(ed, spd) {
- this.st = this.getNow();
- this.ed = ed;
- this.spd = spd;
- this.ts = (new Date()).getTime();
- this.df = this.ed - this.st;
- return this;
- }
- };
-
- //derived from http://stackoverflow.com/questions/11236090/
- function getMatrixValues() {
- var tr = self.doc.css(cap.trstyle);
- if (tr && (tr.substr(0, 6) == "matrix")) {
- return tr.replace(/^.*\((.*)\)$/g, "$1").replace(/px/g, '').split(/, +/);
- }
- return false;
- }
-
- if (this.ishwscroll) {
- // hw accelerated scroll
- this.doc.translate = {
- x: 0,
- y: 0,
- tx: "0px",
- ty: "0px"
- };
-
- //this one can help to enable hw accel on ios6 http://indiegamr.com/ios6-html-hardware-acceleration-changes-and-how-to-fix-them/
- if (cap.hastranslate3d && cap.isios) this.doc.css("-webkit-backface-visibility", "hidden"); // prevent flickering http://stackoverflow.com/questions/3461441/
-
- this.getScrollTop = function(last) {
- if (!last) {
- var mtx = getMatrixValues();
- if (mtx) return (mtx.length == 16) ? -mtx[13] : -mtx[5]; //matrix3d 16 on IE10
- if (self.timerscroll && self.timerscroll.bz) return self.timerscroll.bz.getNow();
- }
- return self.doc.translate.y;
- };
-
- this.getScrollLeft = function(last) {
- if (!last) {
- var mtx = getMatrixValues();
- if (mtx) return (mtx.length == 16) ? -mtx[12] : -mtx[4]; //matrix3d 16 on IE10
- if (self.timerscroll && self.timerscroll.bh) return self.timerscroll.bh.getNow();
- }
- return self.doc.translate.x;
- };
-
- this.notifyScrollEvent = function(el) {
- var e = document.createEvent("UIEvents");
- e.initUIEvent("scroll", false, true, window, 1);
- e.niceevent = true;
- el.dispatchEvent(e);
- };
-
- var cxscrollleft = (this.isrtlmode) ? 1 : -1;
-
- if (cap.hastranslate3d && self.opt.enabletranslate3d) {
- this.setScrollTop = function(val, silent) {
- self.doc.translate.y = val;
- self.doc.translate.ty = (val * -1) + "px";
- self.doc.css(cap.trstyle, "translate3d(" + self.doc.translate.tx + "," + self.doc.translate.ty + ",0px)");
- if (!silent) self.notifyScrollEvent(self.win[0]);
- };
- this.setScrollLeft = function(val, silent) {
- self.doc.translate.x = val;
- self.doc.translate.tx = (val * cxscrollleft) + "px";
- self.doc.css(cap.trstyle, "translate3d(" + self.doc.translate.tx + "," + self.doc.translate.ty + ",0px)");
- if (!silent) self.notifyScrollEvent(self.win[0]);
- };
- } else {
- this.setScrollTop = function(val, silent) {
- self.doc.translate.y = val;
- self.doc.translate.ty = (val * -1) + "px";
- self.doc.css(cap.trstyle, "translate(" + self.doc.translate.tx + "," + self.doc.translate.ty + ")");
- if (!silent) self.notifyScrollEvent(self.win[0]);
- };
- this.setScrollLeft = function(val, silent) {
- self.doc.translate.x = val;
- self.doc.translate.tx = (val * cxscrollleft) + "px";
- self.doc.css(cap.trstyle, "translate(" + self.doc.translate.tx + "," + self.doc.translate.ty + ")");
- if (!silent) self.notifyScrollEvent(self.win[0]);
- };
- }
- } else {
- // native scroll
- this.getScrollTop = function() {
- return self.docscroll.scrollTop();
- };
- this.setScrollTop = function(val) {
- return self.docscroll.scrollTop(val);
- };
- this.getScrollLeft = function() {
- if (self.detected.ismozilla && self.isrtlmode)
- return Math.abs(self.docscroll.scrollLeft());
- return self.docscroll.scrollLeft();
- };
- this.setScrollLeft = function(val) {
- return self.docscroll.scrollLeft((self.detected.ismozilla && self.isrtlmode) ? -val : val);
- };
- }
-
- this.getTarget = function(e) {
- if (!e) return false;
- if (e.target) return e.target;
- if (e.srcElement) return e.srcElement;
- return false;
- };
-
- this.hasParent = function(e, id) {
- if (!e) return false;
- var el = e.target || e.srcElement || e || false;
- while (el && el.id != id) {
- el = el.parentNode || false;
- }
- return (el !== false);
- };
-
- function getZIndex() {
- var dom = self.win;
- if ("zIndex" in dom) return dom.zIndex(); // use jQuery UI method when available
- while (dom.length > 0) {
- if (dom[0].nodeType == 9) return false;
- var zi = dom.css('zIndex');
- if (!isNaN(zi) && zi != 0) return parseInt(zi);
- dom = dom.parent();
- }
- return false;
- }
-
- //inspired by http://forum.jquery.com/topic/width-includes-border-width-when-set-to-thin-medium-thick-in-ie
- var _convertBorderWidth = {
- "thin": 1,
- "medium": 3,
- "thick": 5
- };
-
- function getWidthToPixel(dom, prop, chkheight) {
- var wd = dom.css(prop);
- var px = parseFloat(wd);
- if (isNaN(px)) {
- px = _convertBorderWidth[wd] || 0;
- var brd = (px == 3) ? ((chkheight) ? (self.win.outerHeight() - self.win.innerHeight()) : (self.win.outerWidth() - self.win.innerWidth())) : 1; //DON'T TRUST CSS
- if (self.isie8 && px) px += 1;
- return (brd) ? px : 0;
- }
- return px;
- }
-
- this.getDocumentScrollOffset = function() {
- return {top:window.pageYOffset||document.documentElement.scrollTop,
- left:window.pageXOffset||document.documentElement.scrollLeft};
- }
-
- this.getOffset = function() {
- if (self.isfixed) {
- var ofs = self.win.offset(); // fix Chrome auto issue (when right/bottom props only)
- var scrl = self.getDocumentScrollOffset();
- ofs.top-=scrl.top;
- ofs.left-=scrl.left;
- return ofs;
- }
- var ww = self.win.offset();
- if (!self.viewport) return ww;
- var vp = self.viewport.offset();
- return {
- top: ww.top - vp.top,// + self.viewport.scrollTop(),
- left: ww.left - vp.left // + self.viewport.scrollLeft()
- };
- };
-
- this.updateScrollBar = function(len) {
- if (self.ishwscroll) {
- self.rail.css({ //**
- height: self.win.innerHeight() - (self.opt.railpadding.top + self.opt.railpadding.bottom)
- });
- if (self.railh) self.railh.css({ //**
- width: self.win.innerWidth() - (self.opt.railpadding.left + self.opt.railpadding.right)
- });
-
- } else {
- var wpos = self.getOffset();
- var pos = {
- top: wpos.top,
- left: wpos.left - (self.opt.railpadding.left + self.opt.railpadding.right)
- };
- pos.top += getWidthToPixel(self.win, 'border-top-width', true);
- pos.left += (self.rail.align) ? self.win.outerWidth() - getWidthToPixel(self.win, 'border-right-width') - self.rail.width : getWidthToPixel(self.win, 'border-left-width');
-
- var off = self.opt.railoffset;
- if (off) {
- if (off.top) pos.top += off.top;
- if (self.rail.align && off.left) pos.left += off.left;
- }
-
- if (!self.railslocked) self.rail.css({
- top: pos.top,
- left: pos.left,
- height: ((len) ? len.h : self.win.innerHeight()) - (self.opt.railpadding.top + self.opt.railpadding.bottom)
- });
-
- if (self.zoom) {
- self.zoom.css({
- top: pos.top + 1,
- left: (self.rail.align == 1) ? pos.left - 20 : pos.left + self.rail.width + 4
- });
- }
-
- if (self.railh && !self.railslocked) {
- var pos = {
- top: wpos.top,
- left: wpos.left
- };
- var off = self.opt.railhoffset;
- if (!!off) {
- if (!!off.top) pos.top += off.top;
- if (!!off.left) pos.left += off.left;
- }
- var y = (self.railh.align) ? pos.top + getWidthToPixel(self.win, 'border-top-width', true) + self.win.innerHeight() - self.railh.height : pos.top + getWidthToPixel(self.win, 'border-top-width', true);
- var x = pos.left + getWidthToPixel(self.win, 'border-left-width');
- self.railh.css({
- top: y - (self.opt.railpadding.top + self.opt.railpadding.bottom),
- left: x,
- width: self.railh.width
- });
- }
-
-
- }
- };
-
- this.doRailClick = function(e, dbl, hr) {
- var fn, pg, cur, pos;
-
- if (self.railslocked) return;
- self.cancelEvent(e);
-
- if (dbl) {
- fn = (hr) ? self.doScrollLeft : self.doScrollTop;
- cur = (hr) ? ((e.pageX - self.railh.offset().left - (self.cursorwidth / 2)) * self.scrollratio.x) : ((e.pageY - self.rail.offset().top - (self.cursorheight / 2)) * self.scrollratio.y);
- fn(cur);
- } else {
- fn = (hr) ? self.doScrollLeftBy : self.doScrollBy;
- cur = (hr) ? self.scroll.x : self.scroll.y;
- pos = (hr) ? e.pageX - self.railh.offset().left : e.pageY - self.rail.offset().top;
- pg = (hr) ? self.view.w : self.view.h;
- fn((cur >= pos) ? pg: -pg);// (cur >= pos) ? fn(pg): fn(-pg);
- }
-
- };
-
- self.hasanimationframe = (setAnimationFrame);
- self.hascancelanimationframe = (clearAnimationFrame);
-
- if (!self.hasanimationframe) {
- setAnimationFrame = function(fn) {
- return setTimeout(fn, 15 - Math.floor((+new Date()) / 1000) % 16);
- }; // 1000/60)};
- clearAnimationFrame = clearInterval;
- } else if (!self.hascancelanimationframe) clearAnimationFrame = function() {
- self.cancelAnimationFrame = true;
- };
-
- this.init = function() {
-
- self.saved.css = [];
-
- if (cap.isie7mobile) return true; // SORRY, DO NOT WORK!
- if (cap.isoperamini) return true; // SORRY, DO NOT WORK!
-
- if (cap.hasmstouch) self.css((self.ispage) ? $("html") : self.win, {
- '-ms-touch-action': 'none'
- });
-
- self.zindex = "auto";
- if (!self.ispage && self.opt.zindex == "auto") {
- self.zindex = getZIndex() || "auto";
- } else {
- self.zindex = self.opt.zindex;
- }
-
- if (!self.ispage && self.zindex != "auto") {
- if (self.zindex > globalmaxzindex) globalmaxzindex = self.zindex;
- }
-
- if (self.isie && self.zindex == 0 && self.opt.zindex == "auto") { // fix IE auto == 0
- self.zindex = "auto";
- }
-
- if (!self.ispage || (!cap.cantouch && !cap.isieold && !cap.isie9mobile)) {
-
- var cont = self.docscroll;
- if (self.ispage) cont = (self.haswrapper) ? self.win : self.doc;
-
- if (!cap.isie9mobile) self.css(cont, {
- 'overflow-y': 'hidden'
- });
-
- if (self.ispage && cap.isie7) {
- if (self.doc[0].nodeName == 'BODY') self.css($("html"), {
- 'overflow-y': 'hidden'
- }); //IE7 double scrollbar issue
- else if (self.doc[0].nodeName == 'HTML') self.css($("body"), {
- 'overflow-y': 'hidden'
- }); //IE7 double scrollbar issue
- }
-
- if (cap.isios && !self.ispage && !self.haswrapper) self.css($("body"), {
- "-webkit-overflow-scrolling": "touch"
- }); //force hw acceleration
-
- var cursor = $(document.createElement('div'));
- cursor.css({
- position: "relative",
- top: 0,
- "float": "right",
- width: self.opt.cursorwidth,
- height: "0px",
- 'background-color': self.opt.cursorcolor,
- border: self.opt.cursorborder,
- 'background-clip': 'padding-box',
- '-webkit-border-radius': self.opt.cursorborderradius,
- '-moz-border-radius': self.opt.cursorborderradius,
- 'border-radius': self.opt.cursorborderradius
- });
-
- cursor.hborder = parseFloat(cursor.outerHeight() - cursor.innerHeight());
-
- cursor.addClass('nicescroll-cursors');
-
- self.cursor = cursor;
-
- var rail = $(document.createElement('div'));
- rail.attr('id', self.id);
- rail.addClass('nicescroll-rails nicescroll-rails-vr');
-
- var v, a, kp = ["left","right","top","bottom"]; //**
- for (var n in kp) {
- a = kp[n];
- v = self.opt.railpadding[a];
- (v) ? rail.css("padding-"+a,v+"px") : self.opt.railpadding[a] = 0;
- }
-
- rail.append(cursor);
-
- rail.width = Math.max(parseFloat(self.opt.cursorwidth), cursor.outerWidth());
- rail.css({
- width: rail.width + "px",
- 'zIndex': self.zindex,
- "background": self.opt.background,
- cursor: "default"
- });
-
- rail.visibility = true;
- rail.scrollable = true;
-
- rail.align = (self.opt.railalign == "left") ? 0 : 1;
-
- self.rail = rail;
-
- self.rail.drag = false;
-
- var zoom = false;
- if (self.opt.boxzoom && !self.ispage && !cap.isieold) {
- zoom = document.createElement('div');
-
- self.bind(zoom, "click", self.doZoom);
- self.bind(zoom, "mouseenter", function() {
- self.zoom.css('opacity', self.opt.cursoropacitymax);
- });
- self.bind(zoom, "mouseleave", function() {
- self.zoom.css('opacity', self.opt.cursoropacitymin);
- });
-
- self.zoom = $(zoom);
- self.zoom.css({
- "cursor": "pointer",
- 'z-index': self.zindex,
- 'backgroundImage': 'url(' + self.opt.scriptpath + 'zoomico.png)',
- 'height': 18,
- 'width': 18,
- 'backgroundPosition': '0px 0px'
- });
- if (self.opt.dblclickzoom) self.bind(self.win, "dblclick", self.doZoom);
- if (cap.cantouch && self.opt.gesturezoom) {
- self.ongesturezoom = function(e) {
- if (e.scale > 1.5) self.doZoomIn(e);
- if (e.scale < 0.8) self.doZoomOut(e);
- return self.cancelEvent(e);
- };
- self.bind(self.win, "gestureend", self.ongesturezoom);
- }
- }
-
- // init HORIZ
-
- self.railh = false;
- var railh;
-
- if (self.opt.horizrailenabled) {
-
- self.css(cont, {
- 'overflow-x': 'hidden'
- });
-
- var cursor = $(document.createElement('div'));
- cursor.css({
- position: "absolute",
- top: 0,
- height: self.opt.cursorwidth,
- width: "0px",
- 'background-color': self.opt.cursorcolor,
- border: self.opt.cursorborder,
- 'background-clip': 'padding-box',
- '-webkit-border-radius': self.opt.cursorborderradius,
- '-moz-border-radius': self.opt.cursorborderradius,
- 'border-radius': self.opt.cursorborderradius
- });
-
- if (cap.isieold) cursor.css({'overflow':'hidden'}); //IE6 horiz scrollbar issue
-
- cursor.wborder = parseFloat(cursor.outerWidth() - cursor.innerWidth());
-
- cursor.addClass('nicescroll-cursors');
-
- self.cursorh = cursor;
-
- railh = $(document.createElement('div'));
- railh.attr('id', self.id + '-hr');
- railh.addClass('nicescroll-rails nicescroll-rails-hr');
- railh.height = Math.max(parseFloat(self.opt.cursorwidth), cursor.outerHeight());
- railh.css({
- height: railh.height + "px",
- 'zIndex': self.zindex,
- "background": self.opt.background
- });
-
- railh.append(cursor);
-
- railh.visibility = true;
- railh.scrollable = true;
-
- railh.align = (self.opt.railvalign == "top") ? 0 : 1;
-
- self.railh = railh;
-
- self.railh.drag = false;
-
- }
-
- //
-
- if (self.ispage) {
- rail.css({
- position: "fixed",
- top: "0px",
- height: "100%"
- });
- (rail.align) ? rail.css({
- right: "0px"
- }): rail.css({
- left: "0px"
- });
- self.body.append(rail);
- if (self.railh) {
- railh.css({
- position: "fixed",
- left: "0px",
- width: "100%"
- });
- (railh.align) ? railh.css({
- bottom: "0px"
- }): railh.css({
- top: "0px"
- });
- self.body.append(railh);
- }
- } else {
- if (self.ishwscroll) {
- if (self.win.css('position') == 'static') self.css(self.win, {
- 'position': 'relative'
- });
- var bd = (self.win[0].nodeName == 'HTML') ? self.body : self.win;
- $(bd).scrollTop(0).scrollLeft(0); // fix rail position if content already scrolled
- if (self.zoom) {
- self.zoom.css({
- position: "absolute",
- top: 1,
- right: 0,
- "margin-right": rail.width + 4
- });
- bd.append(self.zoom);
- }
- rail.css({
- position: "absolute",
- top: 0
- });
- (rail.align) ? rail.css({
- right: 0
- }): rail.css({
- left: 0
- });
- bd.append(rail);
- if (railh) {
- railh.css({
- position: "absolute",
- left: 0,
- bottom: 0
- });
- (railh.align) ? railh.css({
- bottom: 0
- }): railh.css({
- top: 0
- });
- bd.append(railh);
- }
- } else {
- self.isfixed = (self.win.css("position") == "fixed");
- var rlpos = (self.isfixed) ? "fixed" : "absolute";
-
- if (!self.isfixed) self.viewport = self.getViewport(self.win[0]);
- if (self.viewport) {
- self.body = self.viewport;
- if ((/fixed|absolute/.test(self.viewport.css("position"))) == false) self.css(self.viewport, {
- "position": "relative"
- });
- }
-
- rail.css({
- position: rlpos
- });
- if (self.zoom) self.zoom.css({
- position: rlpos
- });
- self.updateScrollBar();
- self.body.append(rail);
- if (self.zoom) self.body.append(self.zoom);
- if (self.railh) {
- railh.css({
- position: rlpos
- });
- self.body.append(railh);
- }
- }
-
- if (cap.isios) self.css(self.win, {
- '-webkit-tap-highlight-color': 'rgba(0,0,0,0)',
- '-webkit-touch-callout': 'none'
- }); // prevent grey layer on click
-
- if (cap.isie && self.opt.disableoutline) self.win.attr("hideFocus", "true"); // IE, prevent dotted rectangle on focused div
- if (cap.iswebkit && self.opt.disableoutline) self.win.css({"outline": "none"}); // Webkit outline
- //if (cap.isopera&&self.opt.disableoutline) self.win.css({"outline":"0"}); // Opera 12- to test [TODO]
-
- }
-
- if (self.opt.autohidemode === false) {
- self.autohidedom = false;
- self.rail.css({
- opacity: self.opt.cursoropacitymax
- });
- if (self.railh) self.railh.css({
- opacity: self.opt.cursoropacitymax
- });
- } else if ((self.opt.autohidemode === true) || (self.opt.autohidemode === "leave")) {
- self.autohidedom = $().add(self.rail);
- if (cap.isie8) self.autohidedom = self.autohidedom.add(self.cursor);
- if (self.railh) self.autohidedom = self.autohidedom.add(self.railh);
- if (self.railh && cap.isie8) self.autohidedom = self.autohidedom.add(self.cursorh);
- } else if (self.opt.autohidemode == "scroll") {
- self.autohidedom = $().add(self.rail);
- if (self.railh) self.autohidedom = self.autohidedom.add(self.railh);
- } else if (self.opt.autohidemode == "cursor") {
- self.autohidedom = $().add(self.cursor);
- if (self.railh) self.autohidedom = self.autohidedom.add(self.cursorh);
- } else if (self.opt.autohidemode == "hidden") {
- self.autohidedom = false;
- self.hide();
- self.railslocked = false;
- }
-
- if (cap.isie9mobile) {
-
- self.scrollmom = new ScrollMomentumClass2D(self);
-
- self.onmangotouch = function() {
- var py = self.getScrollTop();
- var px = self.getScrollLeft();
-
- if ((py == self.scrollmom.lastscrolly) && (px == self.scrollmom.lastscrollx)) return true;
-
- var dfy = py - self.mangotouch.sy;
- var dfx = px - self.mangotouch.sx;
- var df = Math.round(Math.sqrt(Math.pow(dfx, 2) + Math.pow(dfy, 2)));
- if (df == 0) return;
-
- var dry = (dfy < 0) ? -1 : 1;
- var drx = (dfx < 0) ? -1 : 1;
-
- var tm = +new Date();
- if (self.mangotouch.lazy) clearTimeout(self.mangotouch.lazy);
-
- if (((tm - self.mangotouch.tm) > 80) || (self.mangotouch.dry != dry) || (self.mangotouch.drx != drx)) {
- self.scrollmom.stop();
- self.scrollmom.reset(px, py);
- self.mangotouch.sy = py;
- self.mangotouch.ly = py;
- self.mangotouch.sx = px;
- self.mangotouch.lx = px;
- self.mangotouch.dry = dry;
- self.mangotouch.drx = drx;
- self.mangotouch.tm = tm;
- } else {
-
- self.scrollmom.stop();
- self.scrollmom.update(self.mangotouch.sx - dfx, self.mangotouch.sy - dfy);
- self.mangotouch.tm = tm;
-
- var ds = Math.max(Math.abs(self.mangotouch.ly - py), Math.abs(self.mangotouch.lx - px));
- self.mangotouch.ly = py;
- self.mangotouch.lx = px;
-
- if (ds > 2) {
- self.mangotouch.lazy = setTimeout(function() {
- self.mangotouch.lazy = false;
- self.mangotouch.dry = 0;
- self.mangotouch.drx = 0;
- self.mangotouch.tm = 0;
- self.scrollmom.doMomentum(30);
- }, 100);
- }
- }
- };
-
- var top = self.getScrollTop();
- var lef = self.getScrollLeft();
- self.mangotouch = {
- sy: top,
- ly: top,
- dry: 0,
- sx: lef,
- lx: lef,
- drx: 0,
- lazy: false,
- tm: 0
- };
-
- self.bind(self.docscroll, "scroll", self.onmangotouch);
-
- } else {
-
- if (cap.cantouch || self.istouchcapable || self.opt.touchbehavior || cap.hasmstouch) {
-
- self.scrollmom = new ScrollMomentumClass2D(self);
-
- self.ontouchstart = function(e) {
- if (e.pointerType && e.pointerType != 2 && e.pointerType != "touch") return false;
-
- self.hasmoving = false;
-
- if (!self.railslocked) {
-
- var tg;
- if (cap.hasmstouch) {
- tg = (e.target) ? e.target : false;
- while (tg) {
- var nc = $(tg).getNiceScroll();
- if ((nc.length > 0) && (nc[0].me == self.me)) break;
- if (nc.length > 0) return false;
- if ((tg.nodeName == 'DIV') && (tg.id == self.id)) break;
- tg = (tg.parentNode) ? tg.parentNode : false;
- }
- }
-
- self.cancelScroll();
-
- tg = self.getTarget(e);
-
- if (tg) {
- var skp = (/INPUT/i.test(tg.nodeName)) && (/range/i.test(tg.type));
- if (skp) return self.stopPropagation(e);
- }
-
- if (!("clientX" in e) && ("changedTouches" in e)) {
- e.clientX = e.changedTouches[0].clientX;
- e.clientY = e.changedTouches[0].clientY;
- }
-
- if (self.forcescreen) {
- var le = e;
- e = {
- "original": (e.original) ? e.original : e
- };
- e.clientX = le.screenX;
- e.clientY = le.screenY;
- }
-
- self.rail.drag = {
- x: e.clientX,
- y: e.clientY,
- sx: self.scroll.x,
- sy: self.scroll.y,
- st: self.getScrollTop(),
- sl: self.getScrollLeft(),
- pt: 2,
- dl: false
- };
-
- if (self.ispage || !self.opt.directionlockdeadzone) {
- self.rail.drag.dl = "f";
- } else {
-
- var view = {
- w: $(window).width(),
- h: $(window).height()
- };
-
- var page = {
- w: Math.max(document.body.scrollWidth, document.documentElement.scrollWidth),
- h: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)
- };
-
- var maxh = Math.max(0, page.h - view.h);
- var maxw = Math.max(0, page.w - view.w);
-
- if (!self.rail.scrollable && self.railh.scrollable) self.rail.drag.ck = (maxh > 0) ? "v" : false;
- else if (self.rail.scrollable && !self.railh.scrollable) self.rail.drag.ck = (maxw > 0) ? "h" : false;
- else self.rail.drag.ck = false;
- if (!self.rail.drag.ck) self.rail.drag.dl = "f";
- }
-
- if (self.opt.touchbehavior && self.isiframe && cap.isie) {
- var wp = self.win.position();
- self.rail.drag.x += wp.left;
- self.rail.drag.y += wp.top;
- }
-
- self.hasmoving = false;
- self.lastmouseup = false;
- self.scrollmom.reset(e.clientX, e.clientY);
-
- if (!cap.cantouch && !this.istouchcapable && !e.pointerType) {
-
- var ip = (tg) ? /INPUT|SELECT|TEXTAREA/i.test(tg.nodeName) : false;
- if (!ip) {
- if (!self.ispage && cap.hasmousecapture) tg.setCapture();
- if (self.opt.touchbehavior) {
- if (tg.onclick && !(tg._onclick || false)) { // intercept DOM0 onclick event
- tg._onclick = tg.onclick;
- tg.onclick = function(e) {
- if (self.hasmoving) return false;
- tg._onclick.call(this, e);
- };
- }
- return self.cancelEvent(e);
- }
- return self.stopPropagation(e);
- }
-
- if (/SUBMIT|CANCEL|BUTTON/i.test($(tg).attr('type'))) {
- pc = {
- "tg": tg,
- "click": false
- };
- self.preventclick = pc;
- }
-
- }
- }
-
- };
-
- self.ontouchend = function(e) {
- if (!self.rail.drag) return true;
- if (self.rail.drag.pt == 2) {
- if (e.pointerType && e.pointerType != 2 && e.pointerType != "touch") return false;
- self.scrollmom.doMomentum();
- self.rail.drag = false;
- if (self.hasmoving) {
- self.lastmouseup = true;
- self.hideCursor();
- if (cap.hasmousecapture) document.releaseCapture();
- if (!cap.cantouch) return self.cancelEvent(e);
- }
- }
- else if (self.rail.drag.pt == 1) {
- return self.onmouseup(e);
- }
-
- };
-
- var moveneedoffset = (self.opt.touchbehavior && self.isiframe && !cap.hasmousecapture);
-
- self.ontouchmove = function(e, byiframe) {
-
- if (!self.rail.drag) return false;
-
- if (e.targetTouches && self.opt.preventmultitouchscrolling) {
- if (e.targetTouches.length > 1) return false; // multitouch
- }
-
- if (e.pointerType && e.pointerType != 2 && e.pointerType != "touch") return false;
-
- if (self.rail.drag.pt == 2) {
- if (cap.cantouch && (cap.isios) && (typeof e.original == "undefined")) return true; // prevent ios "ghost" events by clickable elements
-
- self.hasmoving = true;
-
- if (self.preventclick && !self.preventclick.click) {
- self.preventclick.click = self.preventclick.tg.onclick || false;
- self.preventclick.tg.onclick = self.onpreventclick;
- }
-
- var ev = $.extend({
- "original": e
- }, e);
- e = ev;
-
- if (("changedTouches" in e)) {
- e.clientX = e.changedTouches[0].clientX;
- e.clientY = e.changedTouches[0].clientY;
- }
-
- if (self.forcescreen) {
- var le = e;
- e = {
- "original": (e.original) ? e.original : e
- };
- e.clientX = le.screenX;
- e.clientY = le.screenY;
- }
-
- var ofy,ofx;
- ofx = ofy = 0;
-
- if (moveneedoffset && !byiframe) {
- var wp = self.win.position();
- ofx = -wp.left;
- ofy = -wp.top;
- }
-
- var fy = e.clientY + ofy;
- var my = (fy - self.rail.drag.y);
- var fx = e.clientX + ofx;
- var mx = (fx - self.rail.drag.x);
-
- var ny = self.rail.drag.st - my;
-
- if (self.ishwscroll && self.opt.bouncescroll) {
- if (ny < 0) {
- ny = Math.round(ny / 2);
- // fy = 0;
- } else if (ny > self.page.maxh) {
- ny = self.page.maxh + Math.round((ny - self.page.maxh) / 2);
- // fy = 0;
- }
- } else {
- if (ny < 0) {
- ny = 0;
- fy = 0;
- }
- if (ny > self.page.maxh) {
- ny = self.page.maxh;
- fy = 0;
- }
- }
-
- var nx;
- if (self.railh && self.railh.scrollable) {
- nx = (self.isrtlmode) ? mx - self.rail.drag.sl : self.rail.drag.sl - mx;
-
- if (self.ishwscroll && self.opt.bouncescroll) {
- if (nx < 0) {
- nx = Math.round(nx / 2);
- // fx = 0;
- } else if (nx > self.page.maxw) {
- nx = self.page.maxw + Math.round((nx - self.page.maxw) / 2);
- // fx = 0;
- }
- } else {
- if (nx < 0) {
- nx = 0;
- fx = 0;
- }
- if (nx > self.page.maxw) {
- nx = self.page.maxw;
- fx = 0;
- }
- }
-
- }
-
- var grabbed = false;
- if (self.rail.drag.dl) {
- grabbed = true;
- if (self.rail.drag.dl == "v") nx = self.rail.drag.sl;
- else if (self.rail.drag.dl == "h") ny = self.rail.drag.st;
- } else {
- var ay = Math.abs(my);
- var ax = Math.abs(mx);
- var dz = self.opt.directionlockdeadzone;
- if (self.rail.drag.ck == "v") {
- if (ay > dz && (ax <= (ay * 0.3))) {
- self.rail.drag = false;
- return true;
- } else if (ax > dz) {
- self.rail.drag.dl = "f";
- $("body").scrollTop($("body").scrollTop()); // stop iOS native scrolling (when active javascript has blocked)
- }
- } else if (self.rail.drag.ck == "h") {
- if (ax > dz && (ay <= (ax * 0.3))) {
- self.rail.drag = false;
- return true;
- } else if (ay > dz) {
- self.rail.drag.dl = "f";
- $("body").scrollLeft($("body").scrollLeft()); // stop iOS native scrolling (when active javascript has blocked)
- }
- }
- }
-
- self.synched("touchmove", function() {
- if (self.rail.drag && (self.rail.drag.pt == 2)) {
- if (self.prepareTransition) self.prepareTransition(0);
- if (self.rail.scrollable) self.setScrollTop(ny);
- self.scrollmom.update(fx, fy);
- if (self.railh && self.railh.scrollable) {
- self.setScrollLeft(nx);
- self.showCursor(ny, nx);
- } else {
- self.showCursor(ny);
- }
- if (cap.isie10) document.selection.clear();
- }
- });
-
- if (cap.ischrome && self.istouchcapable) grabbed = false; //chrome touch emulation doesn't like!
- if (grabbed) return self.cancelEvent(e);
- }
- else if (self.rail.drag.pt == 1) { // drag on cursor
- return self.onmousemove(e);
- }
-
- };
-
- }
-
- self.onmousedown = function(e, hronly) {
- if (self.rail.drag && self.rail.drag.pt != 1) return;
- if (self.railslocked) return self.cancelEvent(e);
- self.cancelScroll();
- self.rail.drag = {
- x: e.clientX,
- y: e.clientY,
- sx: self.scroll.x,
- sy: self.scroll.y,
- pt: 1,
- hr: (!!hronly)
- };
- var tg = self.getTarget(e);
- if (!self.ispage && cap.hasmousecapture) tg.setCapture();
- if (self.isiframe && !cap.hasmousecapture) {
- self.saved.csspointerevents = self.doc.css("pointer-events");
- self.css(self.doc, {
- "pointer-events": "none"
- });
- }
- self.hasmoving = false;
- return self.cancelEvent(e);
- };
-
- self.onmouseup = function(e) {
- if (self.rail.drag) {
- if (self.rail.drag.pt != 1) return true;
- if (cap.hasmousecapture) document.releaseCapture();
- if (self.isiframe && !cap.hasmousecapture) self.doc.css("pointer-events", self.saved.csspointerevents);
- self.rail.drag = false;
- //if (!self.rail.active) self.hideCursor();
- if (self.hasmoving) self.triggerScrollEnd(); // TODO - check &&!self.scrollrunning
- return self.cancelEvent(e);
- }
- };
-
- self.onmousemove = function(e) {
- if (self.rail.drag) {
- if (self.rail.drag.pt != 1) return;
-
- if (cap.ischrome && e.which == 0) return self.onmouseup(e);
-
- self.cursorfreezed = true;
- self.hasmoving = true;
-
- if (self.rail.drag.hr) {
- self.scroll.x = self.rail.drag.sx + (e.clientX - self.rail.drag.x);
- if (self.scroll.x < 0) self.scroll.x = 0;
- var mw = self.scrollvaluemaxw;
- if (self.scroll.x > mw) self.scroll.x = mw;
- } else {
- self.scroll.y = self.rail.drag.sy + (e.clientY - self.rail.drag.y);
- if (self.scroll.y < 0) self.scroll.y = 0;
- var my = self.scrollvaluemax;
- if (self.scroll.y > my) self.scroll.y = my;
- }
-
- self.synched('mousemove', function() {
- if (self.rail.drag && (self.rail.drag.pt == 1)) {
- self.showCursor();
- if (self.rail.drag.hr) {
- if (self.hasreversehr) {
- self.doScrollLeft(self.scrollvaluemaxw-Math.round(self.scroll.x * self.scrollratio.x), self.opt.cursordragspeed);
- } else {
- self.doScrollLeft(Math.round(self.scroll.x * self.scrollratio.x), self.opt.cursordragspeed);
- }
- }
- else self.doScrollTop(Math.round(self.scroll.y * self.scrollratio.y), self.opt.cursordragspeed);
- }
- });
-
- return self.cancelEvent(e);
- }
- /*
- else {
- self.checkarea = true;
- }
-*/
- };
-
- if (cap.cantouch || self.opt.touchbehavior) {
-
- self.onpreventclick = function(e) {
- if (self.preventclick) {
- self.preventclick.tg.onclick = self.preventclick.click;
- self.preventclick = false;
- return self.cancelEvent(e);
- }
- }
-
- self.bind(self.win, "mousedown", self.ontouchstart); // control content dragging
-
- self.onclick = (cap.isios) ? false : function(e) {
- if (self.lastmouseup) {
- self.lastmouseup = false;
- return self.cancelEvent(e);
- } else {
- return true;
- }
- };
-
- if (self.opt.grabcursorenabled && cap.cursorgrabvalue) {
- self.css((self.ispage) ? self.doc : self.win, {
- 'cursor': cap.cursorgrabvalue
- });
- self.css(self.rail, {
- 'cursor': cap.cursorgrabvalue
- });
- }
-
- } else {
-
- var checkSelectionScroll = function(e) {
- if (!self.selectiondrag) return;
-
- if (e) {
- var ww = self.win.outerHeight();
- var df = (e.pageY - self.selectiondrag.top);
- if (df > 0 && df < ww) df = 0;
- if (df >= ww) df -= ww;
- self.selectiondrag.df = df;
- }
- if (self.selectiondrag.df == 0) return;
-
- var rt = -Math.floor(self.selectiondrag.df / 6) * 2;
- self.doScrollBy(rt);
-
- self.debounced("doselectionscroll", function() {
- checkSelectionScroll()
- }, 50);
- };
-
- if ("getSelection" in document) { // A grade - Major browsers
- self.hasTextSelected = function() {
- return (document.getSelection().rangeCount > 0);
- };
- } else if ("selection" in document) { //IE9-
- self.hasTextSelected = function() {
- return (document.selection.type != "None");
- };
- } else {
- self.hasTextSelected = function() { // no support
- return false;
- };
- }
-
- self.onselectionstart = function(e) {
-/* More testing - severe chrome issues
- if (!self.haswrapper&&(e.which&&e.which==2)) { // fool browser to manage middle button scrolling
- self.win.css({'overflow':'auto'});
- setTimeout(function(){
- self.win.css({'overflow':''});
- },10);
- return true;
- }
-*/
- if (self.ispage) return;
- self.selectiondrag = self.win.offset();
- };
-
- self.onselectionend = function(e) {
- self.selectiondrag = false;
- };
- self.onselectiondrag = function(e) {
- if (!self.selectiondrag) return;
- if (self.hasTextSelected()) self.debounced("selectionscroll", function() {
- checkSelectionScroll(e)
- }, 250);
- };
-
-
- }
-
- if (cap.hasw3ctouch) { //IE11+
- self.css(self.rail, {
- 'touch-action': 'none'
- });
- self.css(self.cursor, {
- 'touch-action': 'none'
- });
- self.bind(self.win, "pointerdown", self.ontouchstart);
- self.bind(document, "pointerup", self.ontouchend);
- self.bind(document, "pointermove", self.ontouchmove);
- } else if (cap.hasmstouch) { //IE10
- self.css(self.rail, {
- '-ms-touch-action': 'none'
- });
- self.css(self.cursor, {
- '-ms-touch-action': 'none'
- });
- self.bind(self.win, "MSPointerDown", self.ontouchstart);
- self.bind(document, "MSPointerUp", self.ontouchend);
- self.bind(document, "MSPointerMove", self.ontouchmove);
- self.bind(self.cursor, "MSGestureHold", function(e) {
- e.preventDefault()
- });
- self.bind(self.cursor, "contextmenu", function(e) {
- e.preventDefault()
- });
- } else if (this.istouchcapable) { //desktop with screen touch enabled
- self.bind(self.win, "touchstart", self.ontouchstart);
- self.bind(document, "touchend", self.ontouchend);
- self.bind(document, "touchcancel", self.ontouchend);
- self.bind(document, "touchmove", self.ontouchmove);
- }
-
-
- if (self.opt.cursordragontouch || (!cap.cantouch && !self.opt.touchbehavior)) {
-
- self.rail.css({
- "cursor": "default"
- });
- self.railh && self.railh.css({
- "cursor": "default"
- });
-
- self.jqbind(self.rail, "mouseenter", function() {
- if (!self.ispage && !self.win.is(":visible")) return false;
- if (self.canshowonmouseevent) self.showCursor();
- self.rail.active = true;
- });
- self.jqbind(self.rail, "mouseleave", function() {
- self.rail.active = false;
- if (!self.rail.drag) self.hideCursor();
- });
-
- if (self.opt.sensitiverail) {
- self.bind(self.rail, "click", function(e) {
- self.doRailClick(e, false, false)
- });
- self.bind(self.rail, "dblclick", function(e) {
- self.doRailClick(e, true, false)
- });
- self.bind(self.cursor, "click", function(e) {
- self.cancelEvent(e)
- });
- self.bind(self.cursor, "dblclick", function(e) {
- self.cancelEvent(e)
- });
- }
-
- if (self.railh) {
- self.jqbind(self.railh, "mouseenter", function() {
- if (!self.ispage && !self.win.is(":visible")) return false;
- if (self.canshowonmouseevent) self.showCursor();
- self.rail.active = true;
- });
- self.jqbind(self.railh, "mouseleave", function() {
- self.rail.active = false;
- if (!self.rail.drag) self.hideCursor();
- });
-
- if (self.opt.sensitiverail) {
- self.bind(self.railh, "click", function(e) {
- self.doRailClick(e, false, true)
- });
- self.bind(self.railh, "dblclick", function(e) {
- self.doRailClick(e, true, true)
- });
- self.bind(self.cursorh, "click", function(e) {
- self.cancelEvent(e)
- });
- self.bind(self.cursorh, "dblclick", function(e) {
- self.cancelEvent(e)
- });
- }
-
- }
-
- }
-
- if (!cap.cantouch && !self.opt.touchbehavior) {
-
- self.bind((cap.hasmousecapture) ? self.win : document, "mouseup", self.onmouseup);
- self.bind(document, "mousemove", self.onmousemove);
- if (self.onclick) self.bind(document, "click", self.onclick);
-
- self.bind(self.cursor, "mousedown", self.onmousedown);
- self.bind(self.cursor, "mouseup", self.onmouseup);
-
- if (self.railh) {
- self.bind(self.cursorh, "mousedown", function(e) {
- self.onmousedown(e, true)
- });
- self.bind(self.cursorh, "mouseup", self.onmouseup);
- }
-
- if (!self.ispage && self.opt.enablescrollonselection) {
- self.bind(self.win[0], "mousedown", self.onselectionstart);
- self.bind(document, "mouseup", self.onselectionend);
- self.bind(self.cursor, "mouseup", self.onselectionend);
- if (self.cursorh) self.bind(self.cursorh, "mouseup", self.onselectionend);
- self.bind(document, "mousemove", self.onselectiondrag);
- }
-
- if (self.zoom) {
- self.jqbind(self.zoom, "mouseenter", function() {
- if (self.canshowonmouseevent) self.showCursor();
- self.rail.active = true;
- });
- self.jqbind(self.zoom, "mouseleave", function() {
- self.rail.active = false;
- if (!self.rail.drag) self.hideCursor();
- });
- }
-
- } else {
-
- self.bind((cap.hasmousecapture) ? self.win : document, "mouseup", self.ontouchend);
- self.bind(document, "mousemove", self.ontouchmove);
- if (self.onclick) self.bind(document, "click", self.onclick);
-
- if (self.opt.cursordragontouch) {
- self.bind(self.cursor, "mousedown", self.onmousedown);
- self.bind(self.cursor, "mouseup", self.onmouseup);
- //self.bind(self.cursor, "mousemove", self.onmousemove);
- self.cursorh && self.bind(self.cursorh, "mousedown", function(e) {
- self.onmousedown(e, true)
- });
- //self.cursorh && self.bind(self.cursorh, "mousemove", self.onmousemove);
- self.cursorh && self.bind(self.cursorh, "mouseup", self.onmouseup);
- }
-
- }
-
- if (self.opt.enablemousewheel) {
- if (!self.isiframe) self.bind((cap.isie && self.ispage) ? document : self.win /*self.docscroll*/ , "mousewheel", self.onmousewheel);
- self.bind(self.rail, "mousewheel", self.onmousewheel);
- if (self.railh) self.bind(self.railh, "mousewheel", self.onmousewheelhr);
- }
-
- if (!self.ispage && !cap.cantouch && !(/HTML|^BODY/.test(self.win[0].nodeName))) {
- if (!self.win.attr("tabindex")) self.win.attr({
- "tabindex": tabindexcounter++
- });
-
- self.jqbind(self.win, "focus", function(e) {
- domfocus = (self.getTarget(e)).id || true;
- self.hasfocus = true;
- if (self.canshowonmouseevent) self.noticeCursor();
- });
- self.jqbind(self.win, "blur", function(e) {
- domfocus = false;
- self.hasfocus = false;
- });
-
- self.jqbind(self.win, "mouseenter", function(e) {
- mousefocus = (self.getTarget(e)).id || true;
- self.hasmousefocus = true;
- if (self.canshowonmouseevent) self.noticeCursor();
- });
- self.jqbind(self.win, "mouseleave", function() {
- mousefocus = false;
- self.hasmousefocus = false;
- if (!self.rail.drag) self.hideCursor();
- });
-
- }
-
- } // !ie9mobile
-
- //Thanks to http://www.quirksmode.org !!
- self.onkeypress = function(e) {
- if (self.railslocked && self.page.maxh == 0) return true;
-
- e = (e) ? e : window.e;
- var tg = self.getTarget(e);
- if (tg && /INPUT|TEXTAREA|SELECT|OPTION/.test(tg.nodeName)) {
- var tp = tg.getAttribute('type') || tg.type || false;
- if ((!tp) || !(/submit|button|cancel/i.tp)) return true;
- }
-
- if ($(tg).attr('contenteditable')) return true;
-
- if (self.hasfocus || (self.hasmousefocus && !domfocus) || (self.ispage && !domfocus && !mousefocus)) {
- var key = e.keyCode;
-
- if (self.railslocked && key != 27) return self.cancelEvent(e);
-
- var ctrl = e.ctrlKey || false;
- var shift = e.shiftKey || false;
-
- var ret = false;
- switch (key) {
- case 38:
- case 63233: //safari
- self.doScrollBy(24 * 3);
- ret = true;
- break;
- case 40:
- case 63235: //safari
- self.doScrollBy(-24 * 3);
- ret = true;
- break;
- case 37:
- case 63232: //safari
- if (self.railh) {
- (ctrl) ? self.doScrollLeft(0): self.doScrollLeftBy(24 * 3);
- ret = true;
- }
- break;
- case 39:
- case 63234: //safari
- if (self.railh) {
- (ctrl) ? self.doScrollLeft(self.page.maxw): self.doScrollLeftBy(-24 * 3);
- ret = true;
- }
- break;
- case 33:
- case 63276: // safari
- self.doScrollBy(self.view.h);
- ret = true;
- break;
- case 34:
- case 63277: // safari
- self.doScrollBy(-self.view.h);
- ret = true;
- break;
- case 36:
- case 63273: // safari
- (self.railh && ctrl) ? self.doScrollPos(0, 0): self.doScrollTo(0);
- ret = true;
- break;
- case 35:
- case 63275: // safari
- (self.railh && ctrl) ? self.doScrollPos(self.page.maxw, self.page.maxh): self.doScrollTo(self.page.maxh);
- ret = true;
- break;
- case 32:
- if (self.opt.spacebarenabled) {
- (shift) ? self.doScrollBy(self.view.h): self.doScrollBy(-self.view.h);
- ret = true;
- }
- break;
- case 27: // ESC
- if (self.zoomactive) {
- self.doZoom();
- ret = true;
- }
- break;
- }
- if (ret) return self.cancelEvent(e);
- }
- };
-
- if (self.opt.enablekeyboard) self.bind(document, (cap.isopera && !cap.isopera12) ? "keypress" : "keydown", self.onkeypress);
-
- self.bind(document, "keydown", function(e) {
- var ctrl = e.ctrlKey || false;
- if (ctrl) self.wheelprevented = true;
- });
- self.bind(document, "keyup", function(e) {
- var ctrl = e.ctrlKey || false;
- if (!ctrl) self.wheelprevented = false;
- });
- self.bind(window,"blur",function(e){
- self.wheelprevented = false;
- });
-
- self.bind(window, 'resize', self.lazyResize);
- self.bind(window, 'orientationchange', self.lazyResize);
-
- self.bind(window, "load", self.lazyResize);
-
- if (cap.ischrome && !self.ispage && !self.haswrapper) { //chrome void scrollbar bug - it persists in version 26
- var tmp = self.win.attr("style");
- var ww = parseFloat(self.win.css("width")) + 1;
- self.win.css('width', ww);
- self.synched("chromefix", function() {
- self.win.attr("style", tmp)
- });
- }
-
-
- // Trying a cross-browser implementation - good luck!
-
- self.onAttributeChange = function(e) {
- self.lazyResize(self.isieold ? 250 : 30);
- };
-
- if (ClsMutationObserver !== false) {
- self.observerbody = new ClsMutationObserver(function(mutations) {
- mutations.forEach(function(mut){
- if (mut.type=="attributes") {
- return ($("body").hasClass("modal-open")) ? self.hide() : self.show(); // Support for Bootstrap modal
- }
- });
- if (document.body.scrollHeight!=self.page.maxh) return self.lazyResize(30);
- });
- self.observerbody.observe(document.body, {
- childList: true,
- subtree: true,
- characterData: false,
- attributes: true,
- attributeFilter: ['class']
- });
- }
-
- if (!self.ispage && !self.haswrapper) {
- // redesigned MutationObserver for Chrome18+/Firefox14+/iOS6+ with support for: remove div, add/remove content
- if (ClsMutationObserver !== false) {
- self.observer = new ClsMutationObserver(function(mutations) {
- mutations.forEach(self.onAttributeChange);
- });
- self.observer.observe(self.win[0], {
- childList: true,
- characterData: false,
- attributes: true,
- subtree: false
- });
- self.observerremover = new ClsMutationObserver(function(mutations) {
- mutations.forEach(function(mo) {
- if (mo.removedNodes.length > 0) {
- for (var dd in mo.removedNodes) {
- if (!!self && (mo.removedNodes[dd] == self.win[0])) return self.remove();
- }
- }
- });
- });
- self.observerremover.observe(self.win[0].parentNode, {
- childList: true,
- characterData: false,
- attributes: false,
- subtree: false
- });
- } else {
- self.bind(self.win, (cap.isie && !cap.isie9) ? "propertychange" : "DOMAttrModified", self.onAttributeChange);
- if (cap.isie9) self.win[0].attachEvent("onpropertychange", self.onAttributeChange); //IE9 DOMAttrModified bug
- self.bind(self.win, "DOMNodeRemoved", function(e) {
- if (e.target == self.win[0]) self.remove();
- });
- }
- }
-
- //
-
- if (!self.ispage && self.opt.boxzoom) self.bind(window, "resize", self.resizeZoom);
- if (self.istextarea) self.bind(self.win, "mouseup", self.lazyResize);
-
- // self.checkrtlmode = true;
- self.lazyResize(30);
-
- }
-
- if (this.doc[0].nodeName == 'IFRAME') {
- var oniframeload = function() {
- self.iframexd = false;
- var doc;
- try {
- doc = 'contentDocument' in this ? this.contentDocument : this.contentWindow.document;
- var a = doc.domain;
- } catch (e) {
- self.iframexd = true;
- doc = false
- }
-
- if (self.iframexd) {
- if ("console" in window) console.log('NiceScroll error: policy restriced iframe');
- return true; //cross-domain - I can't manage this
- }
-
- self.forcescreen = true;
-
- if (self.isiframe) {
- self.iframe = {
- "doc": $(doc),
- "html": self.doc.contents().find('html')[0],
- "body": self.doc.contents().find('body')[0]
- };
- self.getContentSize = function() {
- return {
- w: Math.max(self.iframe.html.scrollWidth, self.iframe.body.scrollWidth),
- h: Math.max(self.iframe.html.scrollHeight, self.iframe.body.scrollHeight)
- };
- };
- self.docscroll = $(self.iframe.body); //$(this.contentWindow);
- }
-
- if (!cap.isios && self.opt.iframeautoresize && !self.isiframe) {
- self.win.scrollTop(0); // reset position
- self.doc.height(""); //reset height to fix browser bug
- var hh = Math.max(doc.getElementsByTagName('html')[0].scrollHeight, doc.body.scrollHeight);
- self.doc.height(hh);
- }
- self.lazyResize(30);
-
- if (cap.isie7) self.css($(self.iframe.html), {
- 'overflow-y': 'hidden'
- });
- self.css($(self.iframe.body), {
- 'overflow-y': 'hidden'
- });
-
- if (cap.isios && self.haswrapper) {
- self.css($(doc.body), {
- '-webkit-transform': 'translate3d(0,0,0)'
- }); // avoid iFrame content clipping - thanks to http://blog.derraab.com/2012/04/02/avoid-iframe-content-clipping-with-css-transform-on-ios/
- }
-
- if ('contentWindow' in this) {
- self.bind(this.contentWindow, "scroll", self.onscroll); //IE8 & minor
- } else {
- self.bind(doc, "scroll", self.onscroll);
- }
-
- if (self.opt.enablemousewheel) {
- self.bind(doc, "mousewheel", self.onmousewheel);
- }
-
- if (self.opt.enablekeyboard) self.bind(doc, (cap.isopera) ? "keypress" : "keydown", self.onkeypress);
-
- if (cap.cantouch || self.opt.touchbehavior) {
- self.bind(doc, "mousedown", self.ontouchstart);
- self.bind(doc, "mousemove", function(e) {
- return self.ontouchmove(e, true)
- });
- if (self.opt.grabcursorenabled && cap.cursorgrabvalue) self.css($(doc.body), {
- 'cursor': cap.cursorgrabvalue
- });
- }
-
- self.bind(doc, "mouseup", self.ontouchend);
-
- if (self.zoom) {
- if (self.opt.dblclickzoom) self.bind(doc, 'dblclick', self.doZoom);
- if (self.ongesturezoom) self.bind(doc, "gestureend", self.ongesturezoom);
- }
- };
-
- if (this.doc[0].readyState && this.doc[0].readyState == "complete") {
- setTimeout(function() {
- oniframeload.call(self.doc[0], false)
- }, 500);
- }
- self.bind(this.doc, "load", oniframeload);
-
- }
-
- };
-
- this.showCursor = function(py, px) {
- if (self.cursortimeout) {
- clearTimeout(self.cursortimeout);
- self.cursortimeout = 0;
- }
- if (!self.rail) return;
- if (self.autohidedom) {
- self.autohidedom.stop().css({
- opacity: self.opt.cursoropacitymax
- });
- self.cursoractive = true;
- }
-
- if (!self.rail.drag || self.rail.drag.pt != 1) {
- if ((typeof py != "undefined") && (py !== false)) {
- self.scroll.y = Math.round(py * 1 / self.scrollratio.y);
- }
- if (typeof px != "undefined") {
- self.scroll.x = Math.round(px * 1 / self.scrollratio.x);
- }
- }
-
- self.cursor.css({
- height: self.cursorheight,
- top: self.scroll.y
- });
- if (self.cursorh) {
- var lx = (self.hasreversehr) ? self.scrollvaluemaxw-self.scroll.x : self.scroll.x;
- (!self.rail.align && self.rail.visibility) ? self.cursorh.css({
- width: self.cursorwidth,
- left: lx + self.rail.width
- }): self.cursorh.css({
- width: self.cursorwidth,
- left: lx
- });
- self.cursoractive = true;
- }
-
- if (self.zoom) self.zoom.stop().css({
- opacity: self.opt.cursoropacitymax
- });
- };
-
- this.hideCursor = function(tm) {
- if (self.cursortimeout) return;
- if (!self.rail) return;
- if (!self.autohidedom) return;
- if (self.hasmousefocus && self.opt.autohidemode == "leave") return;
- self.cursortimeout = setTimeout(function() {
- if (!self.rail.active || !self.showonmouseevent) {
- self.autohidedom.stop().animate({
- opacity: self.opt.cursoropacitymin
- });
- if (self.zoom) self.zoom.stop().animate({
- opacity: self.opt.cursoropacitymin
- });
- self.cursoractive = false;
- }
- self.cursortimeout = 0;
- }, tm || self.opt.hidecursordelay);
- };
-
- this.noticeCursor = function(tm, py, px) {
- self.showCursor(py, px);
- if (!self.rail.active) self.hideCursor(tm);
- };
-
- this.getContentSize =
- (self.ispage) ?
- function() {
- return {
- w: Math.max(document.body.scrollWidth, document.documentElement.scrollWidth),
- h: Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)
- }
- } : (self.haswrapper) ?
- function() {
- return {
- w: self.doc.outerWidth() + parseInt(self.win.css('paddingLeft')) + parseInt(self.win.css('paddingRight')),
- h: self.doc.outerHeight() + parseInt(self.win.css('paddingTop')) + parseInt(self.win.css('paddingBottom'))
- }
- } : function() {
- return {
- w: self.docscroll[0].scrollWidth,
- h: self.docscroll[0].scrollHeight
- }
- };
-
- this.onResize = function(e, page) {
-
- if (!self || !self.win) return false;
-
- if (!self.haswrapper && !self.ispage) {
- if (self.win.css('display') == 'none') {
- if (self.visibility) self.hideRail().hideRailHr();
- return false;
- } else {
- if (!self.hidden && !self.visibility) self.showRail().showRailHr();
- }
- }
-
- var premaxh = self.page.maxh;
- var premaxw = self.page.maxw;
-
- var preview = {
- h: self.view.h,
- w: self.view.w
- };
-
- self.view = {
- w: (self.ispage) ? self.win.width() : parseInt(self.win[0].clientWidth),
- h: (self.ispage) ? self.win.height() : parseInt(self.win[0].clientHeight)
- };
-
- self.page = (page) ? page : self.getContentSize();
-
- self.page.maxh = Math.max(0, self.page.h - self.view.h);
- self.page.maxw = Math.max(0, self.page.w - self.view.w);
-
- if ((self.page.maxh == premaxh) && (self.page.maxw == premaxw) && (self.view.w == preview.w) && (self.view.h == preview.h)) {
- // test position
- if (!self.ispage) {
- var pos = self.win.offset();
- if (self.lastposition) {
- var lst = self.lastposition;
- if ((lst.top == pos.top) && (lst.left == pos.left)) return self; //nothing to do
- }
- self.lastposition = pos;
- } else {
- return self; //nothing to do
- }
- }
-
- if (self.page.maxh == 0) {
- self.hideRail();
- self.scrollvaluemax = 0;
- self.scroll.y = 0;
- self.scrollratio.y = 0;
- self.cursorheight = 0;
- self.setScrollTop(0);
- self.rail.scrollable = false;
- } else {
- self.page.maxh -= (self.opt.railpadding.top + self.opt.railpadding.bottom); //**
- self.rail.scrollable = true;
- }
-
- if (self.page.maxw == 0) {
- self.hideRailHr();
- self.scrollvaluemaxw = 0;
- self.scroll.x = 0;
- self.scrollratio.x = 0;
- self.cursorwidth = 0;
- self.setScrollLeft(0);
- self.railh.scrollable = false;
- } else {
- self.page.maxw -= (self.opt.railpadding.left + self.opt.railpadding.right); //**
- self.railh.scrollable = true;
- }
-
- self.railslocked = (self.locked) || ((self.page.maxh == 0) && (self.page.maxw == 0));
- if (self.railslocked) {
- if (!self.ispage) self.updateScrollBar(self.view);
- return false;
- }
-
- if (!self.hidden && !self.visibility) {
- self.showRail().showRailHr();
- }
- else if (!self.hidden && !self.railh.visibility) self.showRailHr();
-
- if (self.istextarea && self.win.css('resize') && self.win.css('resize') != 'none') self.view.h -= 20;
-
- self.cursorheight = Math.min(self.view.h, Math.round(self.view.h * (self.view.h / self.page.h)));
- self.cursorheight = (self.opt.cursorfixedheight) ? self.opt.cursorfixedheight : Math.max(self.opt.cursorminheight, self.cursorheight);
-
- self.cursorwidth = Math.min(self.view.w, Math.round(self.view.w * (self.view.w / self.page.w)));
- self.cursorwidth = (self.opt.cursorfixedheight) ? self.opt.cursorfixedheight : Math.max(self.opt.cursorminheight, self.cursorwidth);
-
- self.scrollvaluemax = self.view.h - self.cursorheight - self.cursor.hborder - (self.opt.railpadding.top + self.opt.railpadding.bottom); //**
-
- if (self.railh) {
- self.railh.width = (self.page.maxh > 0) ? (self.view.w - self.rail.width) : self.view.w;
- self.scrollvaluemaxw = self.railh.width - self.cursorwidth - self.cursorh.wborder - (self.opt.railpadding.left + self.opt.railpadding.right); //**
- }
-
- /*
- if (self.checkrtlmode&&self.railh) {
- self.checkrtlmode = false;
- if (self.opt.rtlmode&&self.scroll.x==0) self.setScrollLeft(self.page.maxw);
- }
-*/
-
- if (!self.ispage) self.updateScrollBar(self.view);
-
- self.scrollratio = {
- x: (self.page.maxw / self.scrollvaluemaxw),
- y: (self.page.maxh / self.scrollvaluemax)
- };
-
- var sy = self.getScrollTop();
- if (sy > self.page.maxh) {
- self.doScrollTop(self.page.maxh);
- } else {
- self.scroll.y = Math.round(self.getScrollTop() * (1 / self.scrollratio.y));
- self.scroll.x = Math.round(self.getScrollLeft() * (1 / self.scrollratio.x));
- if (self.cursoractive) self.noticeCursor();
- }
-
- if (self.scroll.y && (self.getScrollTop() == 0)) self.doScrollTo(Math.floor(self.scroll.y * self.scrollratio.y));
-
- return self;
- };
-
- this.resize = self.onResize;
-
- this.lazyResize = function(tm) { // event debounce
- tm = (isNaN(tm)) ? 30 : tm;
- self.debounced('resize', self.resize, tm);
- return self;
- };
-
- // modified by MDN https://developer.mozilla.org/en-US/docs/DOM/Mozilla_event_reference/wheel
- function _modernWheelEvent(dom, name, fn, bubble) {
- self._bind(dom, name, function(e) {
- var e = (e) ? e : window.event;
- var event = {
- original: e,
- target: e.target || e.srcElement,
- type: "wheel",
- deltaMode: e.type == "MozMousePixelScroll" ? 0 : 1,
- deltaX: 0,
- deltaZ: 0,
- preventDefault: function() {
- e.preventDefault ? e.preventDefault() : e.returnValue = false;
- return false;
- },
- stopImmediatePropagation: function() {
- (e.stopImmediatePropagation) ? e.stopImmediatePropagation(): e.cancelBubble = true;
- }
- };
-
- if (name == "mousewheel") {
- event.deltaY = -1 / 40 * e.wheelDelta;
- e.wheelDeltaX && (event.deltaX = -1 / 40 * e.wheelDeltaX);
- } else {
- event.deltaY = e.detail;
- }
-
- return fn.call(dom, event);
- }, bubble);
- };
-
-
-
- this.jqbind = function(dom, name, fn) { // use jquery bind for non-native events (mouseenter/mouseleave)
- self.events.push({
- e: dom,
- n: name,
- f: fn,
- q: true
- });
- $(dom).bind(name, fn);
- };
-
- this.bind = function(dom, name, fn, bubble) { // touch-oriented & fixing jquery bind
- var el = ("jquery" in dom) ? dom[0] : dom;
-
- if (name == 'mousewheel') {
- if (window.addEventListener||'onwheel' in document) { // modern brosers & IE9 detection fix
- self._bind(el, "wheel", fn, bubble || false);
- } else {
- var wname = (typeof document.onmousewheel != "undefined") ? "mousewheel" : "DOMMouseScroll"; // older IE/Firefox
- _modernWheelEvent(el, wname, fn, bubble || false);
- if (wname == "DOMMouseScroll") _modernWheelEvent(el, "MozMousePixelScroll", fn, bubble || false); // Firefox legacy
- }
- } else if (el.addEventListener) {
- if (cap.cantouch && /mouseup|mousedown|mousemove/.test(name)) { // touch device support
- var tt = (name == 'mousedown') ? 'touchstart' : (name == 'mouseup') ? 'touchend' : 'touchmove';
- self._bind(el, tt, function(e) {
- if (e.touches) {
- if (e.touches.length < 2) {
- var ev = (e.touches.length) ? e.touches[0] : e;
- ev.original = e;
- fn.call(this, ev);
- }
- } else if (e.changedTouches) {
- var ev = e.changedTouches[0];
- ev.original = e;
- fn.call(this, ev);
- } //blackberry
- }, bubble || false);
- }
- self._bind(el, name, fn, bubble || false);
- if (cap.cantouch && name == "mouseup") self._bind(el, "touchcancel", fn, bubble || false);
- } else {
- self._bind(el, name, function(e) {
- e = e || window.event || false;
- if (e) {
- if (e.srcElement) e.target = e.srcElement;
- }
- if (!("pageY" in e)) {
- e.pageX = e.clientX + document.documentElement.scrollLeft;
- e.pageY = e.clientY + document.documentElement.scrollTop;
- }
- return ((fn.call(el, e) === false) || bubble === false) ? self.cancelEvent(e) : true;
- });
- }
- };
-
- if (cap.haseventlistener) { // W3C standard model
- this._bind = function(el, name, fn, bubble) { // primitive bind
- self.events.push({
- e: el,
- n: name,
- f: fn,
- b: bubble,
- q: false
- });
- el.addEventListener(name, fn, bubble || false);
- };
- this.cancelEvent = function(e) {
- if (!e) return false;
- var e = (e.original) ? e.original : e;
- e.preventDefault();
- e.stopPropagation();
- if (e.preventManipulation) e.preventManipulation(); //IE10
- return false;
- };
- this.stopPropagation = function(e) {
- if (!e) return false;
- var e = (e.original) ? e.original : e;
- e.stopPropagation();
- return false;
- };
- this._unbind = function(el, name, fn, bub) { // primitive unbind
- el.removeEventListener(name, fn, bub);
- };
- } else { // old IE model
- this._bind = function(el, name, fn, bubble) { // primitive bind
- self.events.push({
- e: el,
- n: name,
- f: fn,
- b: bubble,
- q: false
- });
- if (el.attachEvent) {
- el.attachEvent("on" + name, fn);
- } else {
- el["on" + name] = fn;
- }
- };
- // Thanks to http://www.switchonthecode.com !!
- this.cancelEvent = function(e) {
- var e = window.event || false;
- if (!e) return false;
- e.cancelBubble = true;
- e.cancel = true;
- e.returnValue = false;
- return false;
- };
- this.stopPropagation = function(e) {
- var e = window.event || false;
- if (!e) return false;
- e.cancelBubble = true;
- return false;
- };
- this._unbind = function(el, name, fn, bub) { // primitive unbind IE old
- if (el.detachEvent) {
- el.detachEvent('on' + name, fn);
- } else {
- el['on' + name] = false;
- }
- };
- }
-
- this.unbindAll = function() {
- for (var a = 0; a < self.events.length; a++) {
- var r = self.events[a];
- (r.q) ? r.e.unbind(r.n, r.f): self._unbind(r.e, r.n, r.f, r.b);
- }
- };
-
- this.showRail = function() {
- if ((self.page.maxh != 0) && (self.ispage || self.win.css('display') != 'none')) {
- self.visibility = true;
- self.rail.visibility = true;
- self.rail.css('display', 'block');
- }
- return self;
- };
-
- this.showRailHr = function() {
- if (!self.railh) return self;
- if ((self.page.maxw != 0) && (self.ispage || self.win.css('display') != 'none')) {
- self.railh.visibility = true;
- self.railh.css('display', 'block');
- }
- return self;
- };
-
- this.hideRail = function() {
- self.visibility = false;
- self.rail.visibility = false;
- self.rail.css('display', 'none');
- return self;
- };
-
- this.hideRailHr = function() {
- if (!self.railh) return self;
- self.railh.visibility = false;
- self.railh.css('display', 'none');
- return self;
- };
-
- this.show = function() {
- self.hidden = false;
- self.railslocked = false;
- return self.showRail().showRailHr();
- };
-
- this.hide = function() {
- self.hidden = true;
- self.railslocked = true;
- return self.hideRail().hideRailHr();
- };
-
- this.toggle = function() {
- return (self.hidden) ? self.show() : self.hide();
- };
-
- this.remove = function() {
- self.stop();
- if (self.cursortimeout) clearTimeout(self.cursortimeout);
- self.doZoomOut();
- self.unbindAll();
-
- if (cap.isie9) self.win[0].detachEvent("onpropertychange", self.onAttributeChange); //IE9 DOMAttrModified bug
-
- if (self.observer !== false) self.observer.disconnect();
- if (self.observerremover !== false) self.observerremover.disconnect();
- if (self.observerbody !== false) self.observerbody.disconnect();
-
- self.events = null;
-
- if (self.cursor) {
- self.cursor.remove();
- }
- if (self.cursorh) {
- self.cursorh.remove();
- }
- if (self.rail) {
- self.rail.remove();
- }
- if (self.railh) {
- self.railh.remove();
- }
- if (self.zoom) {
- self.zoom.remove();
- }
- for (var a = 0; a < self.saved.css.length; a++) {
- var d = self.saved.css[a];
- d[0].css(d[1], (typeof d[2] == "undefined") ? '' : d[2]);
- }
- self.saved = false;
- self.me.data('__nicescroll', ''); //erase all traces
-
- // memory leak fixed by GianlucaGuarini - thanks a lot!
- // remove the current nicescroll from the $.nicescroll array & normalize array
- var lst = $.nicescroll;
- lst.each(function(i) {
- if (!this) return;
- if (this.id === self.id) {
- delete lst[i];
- for (var b = ++i; b < lst.length; b++, i++) lst[i] = lst[b];
- lst.length--;
- if (lst.length) delete lst[lst.length];
- }
- });
-
- for (var i in self) {
- self[i] = null;
- delete self[i];
- }
-
- self = null;
-
- };
-
- this.scrollstart = function(fn) {
- this.onscrollstart = fn;
- return self;
- };
- this.scrollend = function(fn) {
- this.onscrollend = fn;
- return self;
- };
- this.scrollcancel = function(fn) {
- this.onscrollcancel = fn;
- return self;
- };
-
- this.zoomin = function(fn) {
- this.onzoomin = fn;
- return self;
- };
- this.zoomout = function(fn) {
- this.onzoomout = fn;
- return self;
- };
-
- this.isScrollable = function(e) {
- var dom = (e.target) ? e.target : e;
- if (dom.nodeName == 'OPTION') return true;
- while (dom && (dom.nodeType == 1) && !(/^BODY|HTML/.test(dom.nodeName))) {
- var dd = $(dom);
- var ov = dd.css('overflowY') || dd.css('overflowX') || dd.css('overflow') || '';
- if (/scroll|auto/.test(ov)) return (dom.clientHeight != dom.scrollHeight);
- dom = (dom.parentNode) ? dom.parentNode : false;
- }
- return false;
- };
-
- this.getViewport = function(me) {
- var dom = (me && me.parentNode) ? me.parentNode : false;
- while (dom && (dom.nodeType == 1) && !(/^BODY|HTML/.test(dom.nodeName))) {
- var dd = $(dom);
- if (/fixed|absolute/.test(dd.css("position"))) return dd;
- var ov = dd.css('overflowY') || dd.css('overflowX') || dd.css('overflow') || '';
- if ((/scroll|auto/.test(ov)) && (dom.clientHeight != dom.scrollHeight)) return dd;
- if (dd.getNiceScroll().length > 0) return dd;
- dom = (dom.parentNode) ? dom.parentNode : false;
- }
- return false; //(dom) ? $(dom) : false;
- };
-
- this.triggerScrollEnd = function() {
- if (!self.onscrollend) return;
-
- var px = self.getScrollLeft();
- var py = self.getScrollTop();
-
- var info = {
- "type": "scrollend",
- "current": {
- "x": px,
- "y": py
- },
- "end": {
- "x": px,
- "y": py
- }
- };
- self.onscrollend.call(self, info);
- }
-
- function execScrollWheel(e, hr, chkscroll) {
- var px, py;
-
- if (e.deltaMode == 0) { // PIXEL
- px = -Math.floor(e.deltaX * (self.opt.mousescrollstep / (18 * 3)));
- py = -Math.floor(e.deltaY * (self.opt.mousescrollstep / (18 * 3)));
- } else if (e.deltaMode == 1) { // LINE
- px = -Math.floor(e.deltaX * self.opt.mousescrollstep);
- py = -Math.floor(e.deltaY * self.opt.mousescrollstep);
- }
-
- if (hr && self.opt.oneaxismousemode && (px == 0) && py) { // classic vertical-only mousewheel + browser with x/y support
- px = py;
- py = 0;
-
- if (chkscroll) {
- var hrend = (px < 0) ? (self.getScrollLeft() >= self.page.maxw) : (self.getScrollLeft() <= 0);
- if (hrend) { // preserve vertical scrolling
- py = px;
- px = 0;
- }
- }
-
- }
-
- if (px) {
- if (self.scrollmom) {
- self.scrollmom.stop()
- }
- self.lastdeltax += px;
- self.debounced("mousewheelx", function() {
- var dt = self.lastdeltax;
- self.lastdeltax = 0;
- if (!self.rail.drag) {
- self.doScrollLeftBy(dt)
- }
- }, 15);
- }
- if (py) {
- if (self.opt.nativeparentscrolling && chkscroll && !self.ispage && !self.zoomactive) {
- if (py < 0) {
- if (self.getScrollTop() >= self.page.maxh) return true;
- } else {
- if (self.getScrollTop() <= 0) return true;
- }
- }
- if (self.scrollmom) {
- self.scrollmom.stop()
- }
- self.lastdeltay += py;
- self.debounced("mousewheely", function() {
- var dt = self.lastdeltay;
- self.lastdeltay = 0;
- if (!self.rail.drag) {
- self.doScrollBy(dt)
- }
- }, 15);
- }
-
- e.stopImmediatePropagation();
- return e.preventDefault();
- };
-
- this.onmousewheel = function(e) {
- if (self.wheelprevented) return;
- if (self.railslocked) {
- self.debounced("checkunlock", self.resize, 250);
- return true;
- }
- if (self.rail.drag) return self.cancelEvent(e);
-
- if (self.opt.oneaxismousemode == "auto" && e.deltaX != 0) self.opt.oneaxismousemode = false; // check two-axis mouse support (not very elegant)
-
- if (self.opt.oneaxismousemode && e.deltaX == 0) {
- if (!self.rail.scrollable) {
- if (self.railh && self.railh.scrollable) {
- return self.onmousewheelhr(e);
- } else {
- return true;
- }
- }
- }
-
- var nw = +(new Date());
- var chk = false;
- if (self.opt.preservenativescrolling && ((self.checkarea + 600) < nw)) {
- self.nativescrollingarea = self.isScrollable(e);
- chk = true;
- }
- self.checkarea = nw;
- if (self.nativescrollingarea) return true; // this isn't my business
- var ret = execScrollWheel(e, false, chk);
- if (ret) self.checkarea = 0;
- return ret;
- };
-
- this.onmousewheelhr = function(e) {
- if (self.wheelprevented) return;
- if (self.railslocked || !self.railh.scrollable) return true;
- if (self.rail.drag) return self.cancelEvent(e);
-
- var nw = +(new Date());
- var chk = false;
- if (self.opt.preservenativescrolling && ((self.checkarea + 600) < nw)) {
- self.nativescrollingarea = self.isScrollable(e);
- chk = true;
- }
- self.checkarea = nw;
- if (self.nativescrollingarea) return true; // this isn't my business
- if (self.railslocked) return self.cancelEvent(e);
-
- return execScrollWheel(e, true, chk);
- };
-
- this.stop = function() {
- self.cancelScroll();
- if (self.scrollmon) self.scrollmon.stop();
- self.cursorfreezed = false;
- self.scroll.y = Math.round(self.getScrollTop() * (1 / self.scrollratio.y));
- self.noticeCursor();
- return self;
- };
-
- this.getTransitionSpeed = function(dif) {
- var sp = Math.round(self.opt.scrollspeed * 10);
- var ex = Math.min(sp, Math.round((dif / 20) * self.opt.scrollspeed));
- return (ex > 20) ? ex : 0;
- };
-
- if (!self.opt.smoothscroll) {
- this.doScrollLeft = function(x, spd) { //direct
- var y = self.getScrollTop();
- self.doScrollPos(x, y, spd);
- };
- this.doScrollTop = function(y, spd) { //direct
- var x = self.getScrollLeft();
- self.doScrollPos(x, y, spd);
- };
- this.doScrollPos = function(x, y, spd) { //direct
- var nx = (x > self.page.maxw) ? self.page.maxw : x;
- if (nx < 0) nx = 0;
- var ny = (y > self.page.maxh) ? self.page.maxh : y;
- if (ny < 0) ny = 0;
- self.synched('scroll', function() {
- self.setScrollTop(ny);
- self.setScrollLeft(nx);
- });
- };
- this.cancelScroll = function() {}; // direct
- } else if (self.ishwscroll && cap.hastransition && self.opt.usetransition && !!self.opt.smoothscroll) {
- this.prepareTransition = function(dif, istime) {
- var ex = (istime) ? ((dif > 20) ? dif : 0) : self.getTransitionSpeed(dif);
- var trans = (ex) ? cap.prefixstyle + 'transform ' + ex + 'ms ease-out' : '';
- if (!self.lasttransitionstyle || self.lasttransitionstyle != trans) {
- self.lasttransitionstyle = trans;
- self.doc.css(cap.transitionstyle, trans);
- }
- return ex;
- };
-
- this.doScrollLeft = function(x, spd) { //trans
- var y = (self.scrollrunning) ? self.newscrolly : self.getScrollTop();
- self.doScrollPos(x, y, spd);
- };
-
- this.doScrollTop = function(y, spd) { //trans
- var x = (self.scrollrunning) ? self.newscrollx : self.getScrollLeft();
- self.doScrollPos(x, y, spd);
- };
-
- this.doScrollPos = function(x, y, spd) { //trans
-
- var py = self.getScrollTop();
- var px = self.getScrollLeft();
-
- if (((self.newscrolly - py) * (y - py) < 0) || ((self.newscrollx - px) * (x - px) < 0)) self.cancelScroll(); //inverted movement detection
-
- if (self.opt.bouncescroll == false) {
- if (y < 0) y = 0;
- else if (y > self.page.maxh) y = self.page.maxh;
- if (x < 0) x = 0;
- else if (x > self.page.maxw) x = self.page.maxw;
- }
-
- if (self.scrollrunning && x == self.newscrollx && y == self.newscrolly) return false;
-
- self.newscrolly = y;
- self.newscrollx = x;
-
- self.newscrollspeed = spd || false;
-
- if (self.timer) return false;
-
- self.timer = setTimeout(function() {
-
- var top = self.getScrollTop();
- var lft = self.getScrollLeft();
-
- var dst = {};
- dst.x = x - lft;
- dst.y = y - top;
- dst.px = lft;
- dst.py = top;
-
- var dd = Math.round(Math.sqrt(Math.pow(dst.x, 2) + Math.pow(dst.y, 2)));
- var ms = (self.newscrollspeed && self.newscrollspeed > 1) ? self.newscrollspeed : self.getTransitionSpeed(dd);
- if (self.newscrollspeed && self.newscrollspeed <= 1) ms *= self.newscrollspeed;
-
- self.prepareTransition(ms, true);
-
- if (self.timerscroll && self.timerscroll.tm) clearInterval(self.timerscroll.tm);
-
- if (ms > 0) {
-
- if (!self.scrollrunning && self.onscrollstart) {
- var info = {
- "type": "scrollstart",
- "current": {
- "x": lft,
- "y": top
- },
- "request": {
- "x": x,
- "y": y
- },
- "end": {
- "x": self.newscrollx,
- "y": self.newscrolly
- },
- "speed": ms
- };
- self.onscrollstart.call(self, info);
- }
-
- if (cap.transitionend) {
- if (!self.scrollendtrapped) {
- self.scrollendtrapped = true;
- self.bind(self.doc, cap.transitionend, self.onScrollTransitionEnd, false); //I have got to do something usefull!!
- }
- } else {
- if (self.scrollendtrapped) clearTimeout(self.scrollendtrapped);
- self.scrollendtrapped = setTimeout(self.onScrollTransitionEnd, ms); // simulate transitionend event
- }
-
- var py = top;
- var px = lft;
- self.timerscroll = {
- bz: new BezierClass(py, self.newscrolly, ms, 0, 0, 0.58, 1),
- bh: new BezierClass(px, self.newscrollx, ms, 0, 0, 0.58, 1)
- };
- if (!self.cursorfreezed) self.timerscroll.tm = setInterval(function() {
- self.showCursor(self.getScrollTop(), self.getScrollLeft())
- }, 60);
-
- }
-
- self.synched("doScroll-set", function() {
- self.timer = 0;
- if (self.scrollendtrapped) self.scrollrunning = true;
- self.setScrollTop(self.newscrolly);
- self.setScrollLeft(self.newscrollx);
- if (!self.scrollendtrapped) self.onScrollTransitionEnd();
- });
-
-
- }, 50);
-
- };
-
- this.cancelScroll = function() {
- if (!self.scrollendtrapped) return true;
- var py = self.getScrollTop();
- var px = self.getScrollLeft();
- self.scrollrunning = false;
- if (!cap.transitionend) clearTimeout(cap.transitionend);
- self.scrollendtrapped = false;
- self._unbind(self.doc[0], cap.transitionend, self.onScrollTransitionEnd);
- self.prepareTransition(0);
- self.setScrollTop(py); // fire event onscroll
- if (self.railh) self.setScrollLeft(px);
- if (self.timerscroll && self.timerscroll.tm) clearInterval(self.timerscroll.tm);
- self.timerscroll = false;
-
- self.cursorfreezed = false;
-
- self.showCursor(py, px);
- return self;
- };
- this.onScrollTransitionEnd = function() {
- if (self.scrollendtrapped) self._unbind(self.doc[0], cap.transitionend, self.onScrollTransitionEnd);
- self.scrollendtrapped = false;
- self.prepareTransition(0);
- if (self.timerscroll && self.timerscroll.tm) clearInterval(self.timerscroll.tm);
- self.timerscroll = false;
- var py = self.getScrollTop();
- var px = self.getScrollLeft();
- self.setScrollTop(py); // fire event onscroll
- if (self.railh) self.setScrollLeft(px); // fire event onscroll left
-
- self.noticeCursor(false, py, px);
-
- self.cursorfreezed = false;
-
- if (py < 0) py = 0
- else if (py > self.page.maxh) py = self.page.maxh;
- if (px < 0) px = 0
- else if (px > self.page.maxw) px = self.page.maxw;
- if ((py != self.newscrolly) || (px != self.newscrollx)) return self.doScrollPos(px, py, self.opt.snapbackspeed);
-
- if (self.onscrollend && self.scrollrunning) {
- self.triggerScrollEnd();
- }
- self.scrollrunning = false;
-
- };
-
- } else {
-
- this.doScrollLeft = function(x, spd) { //no-trans
- var y = (self.scrollrunning) ? self.newscrolly : self.getScrollTop();
- self.doScrollPos(x, y, spd);
- };
-
- this.doScrollTop = function(y, spd) { //no-trans
- var x = (self.scrollrunning) ? self.newscrollx : self.getScrollLeft();
- self.doScrollPos(x, y, spd);
- };
-
- this.doScrollPos = function(x, y, spd) { //no-trans
- var y = ((typeof y == "undefined") || (y === false)) ? self.getScrollTop(true) : y;
-
- if ((self.timer) && (self.newscrolly == y) && (self.newscrollx == x)) return true;
-
- if (self.timer) clearAnimationFrame(self.timer);
- self.timer = 0;
-
- var py = self.getScrollTop();
- var px = self.getScrollLeft();
-
- if (((self.newscrolly - py) * (y - py) < 0) || ((self.newscrollx - px) * (x - px) < 0)) self.cancelScroll(); //inverted movement detection
-
- self.newscrolly = y;
- self.newscrollx = x;
-
- if (!self.bouncescroll || !self.rail.visibility) {
- if (self.newscrolly < 0) {
- self.newscrolly = 0;
- } else if (self.newscrolly > self.page.maxh) {
- self.newscrolly = self.page.maxh;
- }
- }
- if (!self.bouncescroll || !self.railh.visibility) {
- if (self.newscrollx < 0) {
- self.newscrollx = 0;
- } else if (self.newscrollx > self.page.maxw) {
- self.newscrollx = self.page.maxw;
- }
- }
-
- self.dst = {};
- self.dst.x = x - px;
- self.dst.y = y - py;
- self.dst.px = px;
- self.dst.py = py;
-
- var dst = Math.round(Math.sqrt(Math.pow(self.dst.x, 2) + Math.pow(self.dst.y, 2)));
-
- self.dst.ax = self.dst.x / dst;
- self.dst.ay = self.dst.y / dst;
-
- var pa = 0;
- var pe = dst;
-
- if (self.dst.x == 0) {
- pa = py;
- pe = y;
- self.dst.ay = 1;
- self.dst.py = 0;
- } else if (self.dst.y == 0) {
- pa = px;
- pe = x;
- self.dst.ax = 1;
- self.dst.px = 0;
- }
-
- var ms = self.getTransitionSpeed(dst);
- if (spd && spd <= 1) ms *= spd;
- if (ms > 0) {
- self.bzscroll = (self.bzscroll) ? self.bzscroll.update(pe, ms) : new BezierClass(pa, pe, ms, 0, 1, 0, 1);
- } else {
- self.bzscroll = false;
- }
-
- if (self.timer) return;
-
- if ((py == self.page.maxh && y >= self.page.maxh) || (px == self.page.maxw && x >= self.page.maxw)) self.checkContentSize();
-
- var sync = 1;
-
- function scrolling() {
- if (self.cancelAnimationFrame) return true;
-
- self.scrollrunning = true;
-
- sync = 1 - sync;
- if (sync) return (self.timer = setAnimationFrame(scrolling) || 1);
-
- var done = 0;
- var sx, sy;
-
- var sc = sy = self.getScrollTop();
- if (self.dst.ay) {
- sc = (self.bzscroll) ? self.dst.py + (self.bzscroll.getNow() * self.dst.ay) : self.newscrolly;
- var dr = sc - sy;
- if ((dr < 0 && sc < self.newscrolly) || (dr > 0 && sc > self.newscrolly)) sc = self.newscrolly;
- self.setScrollTop(sc);
- if (sc == self.newscrolly) done = 1;
- } else {
- done = 1;
- }
-
- var scx = sx = self.getScrollLeft();
- if (self.dst.ax) {
- scx = (self.bzscroll) ? self.dst.px + (self.bzscroll.getNow() * self.dst.ax) : self.newscrollx;
- var dr = scx - sx;
- if ((dr < 0 && scx < self.newscrollx) || (dr > 0 && scx > self.newscrollx)) scx = self.newscrollx;
- self.setScrollLeft(scx);
- if (scx == self.newscrollx) done += 1;
- } else {
- done += 1;
- }
-
- if (done == 2) {
- self.timer = 0;
- self.cursorfreezed = false;
- self.bzscroll = false;
- self.scrollrunning = false;
- if (sc < 0) sc = 0;
- else if (sc > self.page.maxh) sc = self.page.maxh;
- if (scx < 0) scx = 0;
- else if (scx > self.page.maxw) scx = self.page.maxw;
- if ((scx != self.newscrollx) || (sc != self.newscrolly)) self.doScrollPos(scx, sc);
- else {
- if (self.onscrollend) {
- self.triggerScrollEnd();
- }
- }
- } else {
- self.timer = setAnimationFrame(scrolling) || 1;
- }
- };
- self.cancelAnimationFrame = false;
- self.timer = 1;
-
- if (self.onscrollstart && !self.scrollrunning) {
- var info = {
- "type": "scrollstart",
- "current": {
- "x": px,
- "y": py
- },
- "request": {
- "x": x,
- "y": y
- },
- "end": {
- "x": self.newscrollx,
- "y": self.newscrolly
- },
- "speed": ms
- };
- self.onscrollstart.call(self, info);
- }
-
- scrolling();
-
- if ((py == self.page.maxh && y >= py) || (px == self.page.maxw && x >= px)) self.checkContentSize();
-
- self.noticeCursor();
- };
-
- this.cancelScroll = function() {
- if (self.timer) clearAnimationFrame(self.timer);
- self.timer = 0;
- self.bzscroll = false;
- self.scrollrunning = false;
- return self;
- };
-
- }
-
- this.doScrollBy = function(stp, relative) {
- var ny = 0;
- if (relative) {
- ny = Math.floor((self.scroll.y - stp) * self.scrollratio.y)
- } else {
- var sy = (self.timer) ? self.newscrolly : self.getScrollTop(true);
- ny = sy - stp;
- }
- if (self.bouncescroll) {
- var haf = Math.round(self.view.h / 2);
- if (ny < -haf) ny = -haf
- else if (ny > (self.page.maxh + haf)) ny = (self.page.maxh + haf);
- }
- self.cursorfreezed = false;
-
- var py = self.getScrollTop(true);
- if (ny < 0 && py <= 0) return self.noticeCursor();
- else if (ny > self.page.maxh && py >= self.page.maxh) {
- self.checkContentSize();
- return self.noticeCursor();
- }
-
- self.doScrollTop(ny);
- };
-
- this.doScrollLeftBy = function(stp, relative) {
- var nx = 0;
- if (relative) {
- nx = Math.floor((self.scroll.x - stp) * self.scrollratio.x)
- } else {
- var sx = (self.timer) ? self.newscrollx : self.getScrollLeft(true);
- nx = sx - stp;
- }
- if (self.bouncescroll) {
- var haf = Math.round(self.view.w / 2);
- if (nx < -haf) nx = -haf;
- else if (nx > (self.page.maxw + haf)) nx = (self.page.maxw + haf);
- }
- self.cursorfreezed = false;
-
- var px = self.getScrollLeft(true);
- if (nx < 0 && px <= 0) return self.noticeCursor();
- else if (nx > self.page.maxw && px >= self.page.maxw) return self.noticeCursor();
-
- self.doScrollLeft(nx);
- };
-
- this.doScrollTo = function(pos, relative) {
- var ny = (relative) ? Math.round(pos * self.scrollratio.y) : pos;
- if (ny < 0) ny = 0;
- else if (ny > self.page.maxh) ny = self.page.maxh;
- self.cursorfreezed = false;
- self.doScrollTop(pos);
- };
-
- this.checkContentSize = function() {
- var pg = self.getContentSize();
- if ((pg.h != self.page.h) || (pg.w != self.page.w)) self.resize(false, pg);
- };
-
- self.onscroll = function(e) {
- if (self.rail.drag) return;
- if (!self.cursorfreezed) {
- self.synched('scroll', function() {
- self.scroll.y = Math.round(self.getScrollTop() * (1 / self.scrollratio.y));
- if (self.railh) self.scroll.x = Math.round(self.getScrollLeft() * (1 / self.scrollratio.x));
- self.noticeCursor();
- });
- }
- };
- self.bind(self.docscroll, "scroll", self.onscroll);
-
- this.doZoomIn = function(e) {
- if (self.zoomactive) return;
- self.zoomactive = true;
-
- self.zoomrestore = {
- style: {}
- };
- var lst = ['position', 'top', 'left', 'zIndex', 'backgroundColor', 'marginTop', 'marginBottom', 'marginLeft', 'marginRight'];
- var win = self.win[0].style;
- for (var a in lst) {
- var pp = lst[a];
- self.zoomrestore.style[pp] = (typeof win[pp] != "undefined") ? win[pp] : '';
- }
-
- self.zoomrestore.style.width = self.win.css('width');
- self.zoomrestore.style.height = self.win.css('height');
-
- self.zoomrestore.padding = {
- w: self.win.outerWidth() - self.win.width(),
- h: self.win.outerHeight() - self.win.height()
- };
-
- if (cap.isios4) {
- self.zoomrestore.scrollTop = $(window).scrollTop();
- $(window).scrollTop(0);
- }
-
- self.win.css({
- "position": (cap.isios4) ? "absolute" : "fixed",
- "top": 0,
- "left": 0,
- "z-index": globalmaxzindex + 100,
- "margin": "0px"
- });
- var bkg = self.win.css("backgroundColor");
- if (bkg == "" || /transparent|rgba\(0, 0, 0, 0\)|rgba\(0,0,0,0\)/.test(bkg)) self.win.css("backgroundColor", "#fff");
- self.rail.css({
- "z-index": globalmaxzindex + 101
- });
- self.zoom.css({
- "z-index": globalmaxzindex + 102
- });
- self.zoom.css('backgroundPosition', '0px -18px');
- self.resizeZoom();
-
- if (self.onzoomin) self.onzoomin.call(self);
-
- return self.cancelEvent(e);
- };
-
- this.doZoomOut = function(e) {
- if (!self.zoomactive) return;
- self.zoomactive = false;
-
- self.win.css("margin", "");
- self.win.css(self.zoomrestore.style);
-
- if (cap.isios4) {
- $(window).scrollTop(self.zoomrestore.scrollTop);
- }
-
- self.rail.css({
- "z-index": self.zindex
- });
- self.zoom.css({
- "z-index": self.zindex
- });
- self.zoomrestore = false;
- self.zoom.css('backgroundPosition', '0px 0px');
- self.onResize();
-
- if (self.onzoomout) self.onzoomout.call(self);
-
- return self.cancelEvent(e);
- };
-
- this.doZoom = function(e) {
- return (self.zoomactive) ? self.doZoomOut(e) : self.doZoomIn(e);
- };
-
- this.resizeZoom = function() {
- if (!self.zoomactive) return;
-
- var py = self.getScrollTop(); //preserve scrolling position
- self.win.css({
- width: $(window).width() - self.zoomrestore.padding.w + "px",
- height: $(window).height() - self.zoomrestore.padding.h + "px"
- });
- self.onResize();
-
- self.setScrollTop(Math.min(self.page.maxh, py));
- };
-
- this.init();
-
- $.nicescroll.push(this);
-
- };
-
- // Inspired by the work of Kin Blas
- // http://webpro.host.adobe.com/people/jblas/momentum/includes/jquery.momentum.0.7.js
-
-
- var ScrollMomentumClass2D = function(nc) {
- var self = this;
- this.nc = nc;
-
- this.lastx = 0;
- this.lasty = 0;
- this.speedx = 0;
- this.speedy = 0;
- this.lasttime = 0;
- this.steptime = 0;
- this.snapx = false;
- this.snapy = false;
- this.demulx = 0;
- this.demuly = 0;
-
- this.lastscrollx = -1;
- this.lastscrolly = -1;
-
- this.chkx = 0;
- this.chky = 0;
-
- this.timer = 0;
-
- this.time = function() {
- return +new Date(); //beautifull hack
- };
-
- this.reset = function(px, py) {
- self.stop();
- var now = self.time();
- self.steptime = 0;
- self.lasttime = now;
- self.speedx = 0;
- self.speedy = 0;
- self.lastx = px;
- self.lasty = py;
- self.lastscrollx = -1;
- self.lastscrolly = -1;
- };
-
- this.update = function(px, py) {
- var now = self.time();
- self.steptime = now - self.lasttime;
- self.lasttime = now;
- var dy = py - self.lasty;
- var dx = px - self.lastx;
- var sy = self.nc.getScrollTop();
- var sx = self.nc.getScrollLeft();
- var newy = sy + dy;
- var newx = sx + dx;
- self.snapx = (newx < 0) || (newx > self.nc.page.maxw);
- self.snapy = (newy < 0) || (newy > self.nc.page.maxh);
- self.speedx = dx;
- self.speedy = dy;
- self.lastx = px;
- self.lasty = py;
- };
-
- this.stop = function() {
- self.nc.unsynched("domomentum2d");
- if (self.timer) clearTimeout(self.timer);
- self.timer = 0;
- self.lastscrollx = -1;
- self.lastscrolly = -1;
- };
-
- this.doSnapy = function(nx, ny) {
- var snap = false;
-
- if (ny < 0) {
- ny = 0;
- snap = true;
- } else if (ny > self.nc.page.maxh) {
- ny = self.nc.page.maxh;
- snap = true;
- }
-
- if (nx < 0) {
- nx = 0;
- snap = true;
- } else if (nx > self.nc.page.maxw) {
- nx = self.nc.page.maxw;
- snap = true;
- }
-
- (snap) ? self.nc.doScrollPos(nx, ny, self.nc.opt.snapbackspeed): self.nc.triggerScrollEnd();
- };
-
- this.doMomentum = function(gp) {
- var t = self.time();
- var l = (gp) ? t + gp : self.lasttime;
-
- var sl = self.nc.getScrollLeft();
- var st = self.nc.getScrollTop();
-
- var pageh = self.nc.page.maxh;
- var pagew = self.nc.page.maxw;
-
- self.speedx = (pagew > 0) ? Math.min(60, self.speedx) : 0;
- self.speedy = (pageh > 0) ? Math.min(60, self.speedy) : 0;
-
- var chk = l && (t - l) <= 60;
-
- if ((st < 0) || (st > pageh) || (sl < 0) || (sl > pagew)) chk = false;
-
- var sy = (self.speedy && chk) ? self.speedy : false;
- var sx = (self.speedx && chk) ? self.speedx : false;
-
- if (sy || sx) {
- var tm = Math.max(16, self.steptime); //timeout granularity
-
- if (tm > 50) { // do smooth
- var xm = tm / 50;
- self.speedx *= xm;
- self.speedy *= xm;
- tm = 50;
- }
-
- self.demulxy = 0;
-
- self.lastscrollx = self.nc.getScrollLeft();
- self.chkx = self.lastscrollx;
- self.lastscrolly = self.nc.getScrollTop();
- self.chky = self.lastscrolly;
-
- var nx = self.lastscrollx;
- var ny = self.lastscrolly;
-
- var onscroll = function() {
- var df = ((self.time() - t) > 600) ? 0.04 : 0.02;
-
- if (self.speedx) {
- nx = Math.floor(self.lastscrollx - (self.speedx * (1 - self.demulxy)));
- self.lastscrollx = nx;
- if ((nx < 0) || (nx > pagew)) df = 0.10;
- }
-
- if (self.speedy) {
- ny = Math.floor(self.lastscrolly - (self.speedy * (1 - self.demulxy)));
- self.lastscrolly = ny;
- if ((ny < 0) || (ny > pageh)) df = 0.10;
- }
-
- self.demulxy = Math.min(1, self.demulxy + df);
-
- self.nc.synched("domomentum2d", function() {
-
- if (self.speedx) {
- var scx = self.nc.getScrollLeft();
- if (scx != self.chkx) self.stop();
- self.chkx = nx;
- self.nc.setScrollLeft(nx);
- }
-
- if (self.speedy) {
- var scy = self.nc.getScrollTop();
- if (scy != self.chky) self.stop();
- self.chky = ny;
- self.nc.setScrollTop(ny);
- }
-
- if (!self.timer) {
- self.nc.hideCursor();
- self.doSnapy(nx, ny);
- }
-
- });
-
- if (self.demulxy < 1) {
- self.timer = setTimeout(onscroll, tm);
- } else {
- self.stop();
- self.nc.hideCursor();
- self.doSnapy(nx, ny);
- }
- };
-
- onscroll();
-
- } else {
- self.doSnapy(self.nc.getScrollLeft(), self.nc.getScrollTop());
- }
-
- }
-
- };
-
-
- // override jQuery scrollTop
-
- var _scrollTop = jQuery.fn.scrollTop; // preserve original function
-
- jQuery.cssHooks["pageYOffset"] = {
- get: function(elem, computed, extra) {
- var nice = $.data(elem, '__nicescroll') || false;
- return (nice && nice.ishwscroll) ? nice.getScrollTop() : _scrollTop.call(elem);
- },
- set: function(elem, value) {
- var nice = $.data(elem, '__nicescroll') || false;
- (nice && nice.ishwscroll) ? nice.setScrollTop(parseInt(value)): _scrollTop.call(elem, value);
- return this;
- }
- };
-
- /*
- $.fx.step["scrollTop"] = function(fx){
- $.cssHooks["scrollTop"].set( fx.elem, fx.now + fx.unit );
- };
-*/
-
- jQuery.fn.scrollTop = function(value) {
- if (typeof value == "undefined") {
- var nice = (this[0]) ? $.data(this[0], '__nicescroll') || false : false;
- return (nice && nice.ishwscroll) ? nice.getScrollTop() : _scrollTop.call(this);
- } else {
- return this.each(function() {
- var nice = $.data(this, '__nicescroll') || false;
- (nice && nice.ishwscroll) ? nice.setScrollTop(parseInt(value)): _scrollTop.call($(this), value);
- });
- }
- };
-
- // override jQuery scrollLeft
-
- var _scrollLeft = jQuery.fn.scrollLeft; // preserve original function
-
- $.cssHooks.pageXOffset = {
- get: function(elem, computed, extra) {
- var nice = $.data(elem, '__nicescroll') || false;
- return (nice && nice.ishwscroll) ? nice.getScrollLeft() : _scrollLeft.call(elem);
- },
- set: function(elem, value) {
- var nice = $.data(elem, '__nicescroll') || false;
- (nice && nice.ishwscroll) ? nice.setScrollLeft(parseInt(value)): _scrollLeft.call(elem, value);
- return this;
- }
- };
-
- /*
- $.fx.step["scrollLeft"] = function(fx){
- $.cssHooks["scrollLeft"].set( fx.elem, fx.now + fx.unit );
- };
-*/
-
- jQuery.fn.scrollLeft = function(value) {
- if (typeof value == "undefined") {
- var nice = (this[0]) ? $.data(this[0], '__nicescroll') || false : false;
- return (nice && nice.ishwscroll) ? nice.getScrollLeft() : _scrollLeft.call(this);
- } else {
- return this.each(function() {
- var nice = $.data(this, '__nicescroll') || false;
- (nice && nice.ishwscroll) ? nice.setScrollLeft(parseInt(value)): _scrollLeft.call($(this), value);
- });
- }
- };
-
- var NiceScrollArray = function(doms) {
- var self = this;
- this.length = 0;
- this.name = "nicescrollarray";
-
- this.each = function(fn) {
- for (var a = 0, i = 0; a < self.length; a++) fn.call(self[a], i++);
- return self;
- };
-
- this.push = function(nice) {
- self[self.length] = nice;
- self.length++;
- };
-
- this.eq = function(idx) {
- return self[idx];
- };
-
- if (doms) {
- for (var a = 0; a < doms.length; a++) {
- var nice = $.data(doms[a], '__nicescroll') || false;
- if (nice) {
- this[this.length] = nice;
- this.length++;
- }
- };
- }
-
- return this;
- };
-
- function mplex(el, lst, fn) {
- for (var a = 0; a < lst.length; a++) fn(el, lst[a]);
- };
- mplex(
- NiceScrollArray.prototype, ['show', 'hide', 'toggle', 'onResize', 'resize', 'remove', 'stop', 'doScrollPos'],
- function(e, n) {
- e[n] = function() {
- var args = arguments;
- return this.each(function() {
- this[n].apply(this, args);
- });
- };
- }
- );
-
- jQuery.fn.getNiceScroll = function(index) {
- if (typeof index == "undefined") {
- return new NiceScrollArray(this);
- } else {
- var nice = this[index] && $.data(this[index], '__nicescroll') || false;
- return nice;
- }
- };
-
- jQuery.extend(jQuery.expr[':'], {
- nicescroll: function(a) {
- return ($.data(a, '__nicescroll')) ? true : false;
- }
- });
-
- $.fn.niceScroll = function(wrapper, opt) {
- if (typeof opt == "undefined") {
- if ((typeof wrapper == "object") && !("jquery" in wrapper)) {
- opt = wrapper;
- wrapper = false;
- }
- }
- opt = $.extend({},opt); // cloning
- var ret = new NiceScrollArray();
- if (typeof opt == "undefined") opt = {};
-
- if (wrapper || false) {
- opt.doc = $(wrapper);
- opt.win = $(this);
- }
- var docundef = !("doc" in opt);
- if (!docundef && !("win" in opt)) opt.win = $(this);
-
- this.each(function() {
- var nice = $(this).data('__nicescroll') || false;
- if (!nice) {
- opt.doc = (docundef) ? $(this) : opt.doc;
- nice = new NiceScrollClass(opt, $(this));
- $(this).data('__nicescroll', nice);
- }
- ret.push(nice);
- });
- return (ret.length == 1) ? ret[0] : ret;
- };
-
- window.NiceScroll = {
- getjQuery: function() {
- return jQuery
- }
- };
-
- if (!$.nicescroll) {
- $.nicescroll = new NiceScrollArray();
- $.nicescroll.options = _globaloptions;
- }
-
-})); \ No newline at end of file
diff --git a/vendor/project_templates/rails.tar.gz b/vendor/project_templates/rails.tar.gz
new file mode 100644
index 00000000000..b54cae3143a
--- /dev/null
+++ b/vendor/project_templates/rails.tar.gz
Binary files differ
diff --git a/yarn.lock b/yarn.lock
index 504448265b6..64c932b7ff5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -41,10 +41,14 @@ after@0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
-ajv-keywords@^1.0.0, ajv-keywords@^1.1.1:
+ajv-keywords@^1.0.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c"
+ajv-keywords@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0"
+
ajv@^4.7.0:
version "4.11.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.2.tgz#f166c3c11cbc6cb9dcc102a5bcfe5b72c95287e6"
@@ -52,6 +56,15 @@ ajv@^4.7.0:
co "^4.6.0"
json-stable-stringify "^1.0.1"
+ajv@^5.1.5:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.0.tgz#c1735024c5da2ef75cc190713073d44f098bf486"
+ dependencies:
+ co "^4.6.0"
+ fast-deep-equal "^0.1.0"
+ json-schema-traverse "^0.3.0"
+ json-stable-stringify "^1.0.1"
+
align-text@^0.1.1, align-text@^0.1.3:
version "0.1.4"
resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
@@ -128,6 +141,10 @@ arr-flatten@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.0.1.tgz#e5ffe54d45e19f32f216e91eb99c8ce892bb604b"
+array-find-index@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
+
array-find@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/array-find/-/array-find-1.0.0.tgz#6c8e286d11ed768327f8e62ecee87353ca3e78b8"
@@ -136,6 +153,10 @@ array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
+array-flatten@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.1.tgz#426bb9da84090c1838d812c8150af20a8331e296"
+
array-slice@^0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5"
@@ -233,6 +254,13 @@ aws4@^1.2.1:
version "1.6.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
+axios@^0.16.2:
+ version "0.16.2"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-0.16.2.tgz#ba4f92f17167dfbab40983785454b9ac149c3c6d"
+ dependencies:
+ follow-redirects "^1.2.3"
+ is-buffer "^1.1.5"
+
babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0:
version "6.22.0"
resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4"
@@ -405,14 +433,13 @@ babel-helpers@^6.23.0:
babel-runtime "^6.22.0"
babel-template "^6.23.0"
-babel-loader@^6.2.10:
- version "6.2.10"
- resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-6.2.10.tgz#adefc2b242320cd5d15e65b31cea0e8b1b02d4b0"
+babel-loader@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.1.tgz#b87134c8b12e3e4c2a94e0546085bc680a2b8488"
dependencies:
- find-cache-dir "^0.1.1"
- loader-utils "^0.2.11"
+ find-cache-dir "^1.0.0"
+ loader-utils "^1.0.2"
mkdirp "^0.5.1"
- object-assign "^4.0.1"
babel-messages@^6.23.0:
version "6.23.0"
@@ -825,11 +852,7 @@ babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.22.0, babel-types@^6.23
lodash "^4.2.0"
to-fast-properties "^1.0.1"
-babylon@^6.11.0:
- version "6.15.0"
- resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e"
-
-babylon@^6.13.0, babylon@^6.15.0, babylon@^6.16.1:
+babylon@^6.11.0, babylon@^6.13.0, babylon@^6.15.0, babylon@^6.16.1:
version "6.16.1"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.16.1.tgz#30c5a22f481978a9e7f8cdfdf496b11d94b404d3"
@@ -887,6 +910,10 @@ block-stream@*:
dependencies:
inherits "~2.0.0"
+bluebird@^2.10.2:
+ version "2.11.0"
+ resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1"
+
bluebird@^3.0.5, bluebird@^3.1.1, bluebird@^3.3.0:
version "3.4.7"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
@@ -910,6 +937,17 @@ body-parser@^1.16.1:
raw-body "~2.2.0"
type-is "~1.6.15"
+bonjour@^3.5.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5"
+ dependencies:
+ array-flatten "^2.1.0"
+ deep-equal "^1.0.1"
+ dns-equal "^1.0.0"
+ dns-txt "^2.0.2"
+ multicast-dns "^6.0.1"
+ multicast-dns-service-types "^1.1.0"
+
boom@2.x.x:
version "2.10.1"
resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
@@ -1003,6 +1041,10 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
caniuse-db "^1.0.30000639"
electron-to-chromium "^1.2.7"
+buffer-indexof@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.0.tgz#f54f647c4f4e25228baa656a2e57e43d5f270982"
+
buffer-shims@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-shims/-/buffer-shims-1.0.0.tgz#9978ce317388c649ad8793028c3477ef044a8b51"
@@ -1049,14 +1091,29 @@ callsites@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca"
+camelcase-keys@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7"
+ dependencies:
+ camelcase "^2.0.0"
+ map-obj "^1.0.0"
+
camelcase@^1.0.2:
version "1.2.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
+camelcase@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
+
camelcase@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a"
+camelcase@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
+
caniuse-api@^1.5.2:
version "1.6.1"
resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c"
@@ -1106,6 +1163,21 @@ chokidar@^1.4.1, chokidar@^1.4.3, chokidar@^1.6.0:
optionalDependencies:
fsevents "^1.0.0"
+chokidar@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468"
+ dependencies:
+ anymatch "^1.3.0"
+ async-each "^1.0.0"
+ glob-parent "^2.0.0"
+ inherits "^2.0.1"
+ is-binary-path "^1.0.0"
+ is-glob "^2.0.0"
+ path-is-absolute "^1.0.0"
+ readdirp "^2.0.0"
+ optionalDependencies:
+ fsevents "^1.0.0"
+
cipher-base@^1.0.0, cipher-base@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.3.tgz#eeabf194419ce900da3018c207d212f2a6df0a07"
@@ -1363,6 +1435,19 @@ cookie@0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
+copy-webpack-plugin@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.0.1.tgz#9728e383b94316050d0c7463958f2b85c0aa8200"
+ dependencies:
+ bluebird "^2.10.2"
+ fs-extra "^0.26.4"
+ glob "^6.0.4"
+ is-glob "^3.1.0"
+ loader-utils "^0.2.15"
+ lodash "^4.3.0"
+ minimatch "^3.0.0"
+ node-dir "^0.1.10"
+
core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.4.1.tgz#4de911e667b0eae9124e34254b53aea6fc618d3e"
@@ -1415,6 +1500,14 @@ cropper@^2.3.0:
dependencies:
jquery ">= 1.9.1"
+cross-spawn@^5.0.1:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
+ dependencies:
+ lru-cache "^4.0.1"
+ shebang-command "^1.2.0"
+ which "^1.2.9"
+
cryptiles@2.x.x:
version "2.0.5"
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8"
@@ -1521,6 +1614,12 @@ csso@~2.3.1:
clap "^1.0.9"
source-map "^0.5.3"
+currently-unhandled@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
+ dependencies:
+ array-find-index "^1.0.1"
+
custom-event@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
@@ -1529,7 +1628,13 @@ d3@^3.5.11:
version "3.5.11"
resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.11.tgz#d130750eed0554db70e8432102f920a12407b69c"
-d@^0.1.1, d@~0.1.1:
+d@1:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
+ dependencies:
+ es5-ext "^0.10.9"
+
+d@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309"
dependencies:
@@ -1567,11 +1672,11 @@ debug@2.6.7:
dependencies:
ms "2.0.0"
-debug@^2.1.0, debug@^2.1.1, debug@^2.2.0:
- version "2.6.0"
- resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b"
+debug@^2.1.0, debug@^2.1.1, debug@^2.2.0, debug@^2.4.5, debug@^2.6.6, debug@^2.6.8:
+ version "2.6.8"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc"
dependencies:
- ms "0.7.2"
+ ms "2.0.0"
decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2:
version "1.2.0"
@@ -1587,6 +1692,10 @@ decompress-response@^3.2.0:
dependencies:
mimic-response "^1.0.0"
+deep-equal@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
+
deep-extend@~0.4.0:
version "0.4.1"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.1.tgz#efe4113d08085f4e6f9687759810f807469e2253"
@@ -1623,6 +1732,17 @@ del@^2.0.2:
pinkie-promise "^2.0.0"
rimraf "^2.2.8"
+del@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/del/-/del-3.0.0.tgz#53ecf699ffcbcb39637691ab13baf160819766e5"
+ dependencies:
+ globby "^6.1.0"
+ is-path-cwd "^1.0.0"
+ is-path-in-cwd "^1.0.0"
+ p-map "^1.1.1"
+ pify "^3.0.0"
+ rimraf "^2.2.8"
+
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
@@ -1668,6 +1788,23 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"
+dns-equal@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d"
+
+dns-packet@^1.0.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.1.1.tgz#2369d45038af045f3898e6fa56862aed3f40296c"
+ dependencies:
+ ip "^1.1.0"
+ safe-buffer "^5.0.1"
+
+dns-txt@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6"
+ dependencies:
+ buffer-indexof "^1.0.0"
+
doctrine@1.5.0, doctrine@^1.2.2:
version "1.5.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
@@ -1834,14 +1971,14 @@ engine.io@1.8.3:
engine.io-parser "1.3.2"
ws "1.1.2"
-enhanced-resolve@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.1.0.tgz#9f4b626f577245edcf4b2ad83d86e17f4f421dec"
+enhanced-resolve@^3.4.0:
+ version "3.4.1"
+ resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e"
dependencies:
graceful-fs "^4.1.2"
memory-fs "^0.4.0"
object-assign "^4.0.1"
- tapable "^0.2.5"
+ tapable "^0.2.7"
enhanced-resolve@~0.9.0:
version "0.9.1"
@@ -1871,52 +2008,52 @@ error-ex@^1.2.0:
dependencies:
is-arrayish "^0.2.1"
-es5-ext@^0.10.7, es5-ext@^0.10.8, es5-ext@~0.10.11, es5-ext@~0.10.2, es5-ext@~0.10.7:
- version "0.10.12"
- resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.12.tgz#aa84641d4db76b62abba5e45fd805ecbab140047"
+es5-ext@^0.10.14, es5-ext@^0.10.8, es5-ext@^0.10.9, es5-ext@~0.10.14, es5-ext@~0.10.2:
+ version "0.10.24"
+ resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.24.tgz#a55877c9924bc0c8d9bd3c2cbe17495ac1709b14"
dependencies:
es6-iterator "2"
es6-symbol "~3.1"
-es6-iterator@2:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.0.tgz#bd968567d61635e33c0b80727613c9cb4b096bac"
+es6-iterator@2, es6-iterator@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.1.tgz#8e319c9f0453bf575d374940a655920e59ca5512"
dependencies:
- d "^0.1.1"
- es5-ext "^0.10.7"
- es6-symbol "3"
+ d "1"
+ es5-ext "^0.10.14"
+ es6-symbol "^3.1"
es6-map@^0.1.3:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.4.tgz#a34b147be224773a4d7da8072794cefa3632b897"
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/es6-map/-/es6-map-0.1.5.tgz#9136e0503dcc06a301690f0bb14ff4e364e949f0"
dependencies:
- d "~0.1.1"
- es5-ext "~0.10.11"
- es6-iterator "2"
- es6-set "~0.1.3"
- es6-symbol "~3.1.0"
- event-emitter "~0.3.4"
+ d "1"
+ es5-ext "~0.10.14"
+ es6-iterator "~2.0.1"
+ es6-set "~0.1.5"
+ es6-symbol "~3.1.1"
+ event-emitter "~0.3.5"
es6-promise@^3.0.2, es6-promise@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6"
-es6-set@~0.1.3:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.4.tgz#9516b6761c2964b92ff479456233a247dc707ce8"
+es6-set@~0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/es6-set/-/es6-set-0.1.5.tgz#d2b3ec5d4d800ced818db538d28974db0a73ccb1"
dependencies:
- d "~0.1.1"
- es5-ext "~0.10.11"
- es6-iterator "2"
- es6-symbol "3"
- event-emitter "~0.3.4"
+ d "1"
+ es5-ext "~0.10.14"
+ es6-iterator "~2.0.1"
+ es6-symbol "3.1.1"
+ event-emitter "~0.3.5"
-es6-symbol@3, es6-symbol@~3.1, es6-symbol@~3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.0.tgz#94481c655e7a7cad82eba832d97d5433496d7ffa"
+es6-symbol@3, es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@~3.1, es6-symbol@~3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
dependencies:
- d "~0.1.1"
- es5-ext "~0.10.11"
+ d "1"
+ es5-ext "~0.10.14"
es6-weak-map@^2.0.1:
version "2.0.1"
@@ -1967,12 +2104,12 @@ eslint-import-resolver-node@^0.2.0:
object-assign "^4.0.1"
resolve "^1.1.6"
-eslint-import-resolver-webpack@^0.8.1:
- version "0.8.1"
- resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.8.1.tgz#c7f8b4d5bd3c5b489457e5728c5db1c4ffbac9aa"
+eslint-import-resolver-webpack@^0.8.3:
+ version "0.8.3"
+ resolved "https://registry.yarnpkg.com/eslint-import-resolver-webpack/-/eslint-import-resolver-webpack-0.8.3.tgz#ad61e28df378a474459d953f246fd43f92675385"
dependencies:
array-find "^1.0.0"
- debug "^2.2.0"
+ debug "^2.6.8"
enhanced-resolve "~0.9.0"
find-root "^0.1.1"
has "^1.0.1"
@@ -2112,12 +2249,12 @@ eve-raphael@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/eve-raphael/-/eve-raphael-0.5.0.tgz#17c754b792beef3fa6684d79cf5a47c63c4cda30"
-event-emitter@~0.3.4:
- version "0.3.4"
- resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.4.tgz#8d63ddfb4cfe1fae3b32ca265c4c720222080bb5"
+event-emitter@~0.3.5:
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
dependencies:
- d "~0.1.1"
- es5-ext "~0.10.7"
+ d "1"
+ es5-ext "~0.10.14"
event-stream@~3.3.0:
version "3.3.4"
@@ -2151,6 +2288,18 @@ evp_bytestokey@^1.0.0:
dependencies:
create-hash "^1.1.1"
+execa@^0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
+ dependencies:
+ cross-spawn "^5.0.1"
+ get-stream "^3.0.0"
+ is-stream "^1.1.0"
+ npm-run-path "^2.0.0"
+ p-finally "^1.0.0"
+ signal-exit "^3.0.0"
+ strip-eof "^1.0.0"
+
exit-hook@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
@@ -2236,6 +2385,10 @@ extsprintf@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.0.2.tgz#e1080e0658e300b06294990cc70e1502235fd550"
+fast-deep-equal@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-0.1.0.tgz#5c6f4599aba6b333ee3342e2ed978672f1001f8d"
+
fast-levenshtein@~2.0.4:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
@@ -2323,13 +2476,13 @@ finalhandler@1.0.3, finalhandler@~1.0.3:
statuses "~1.3.1"
unpipe "~1.0.0"
-find-cache-dir@^0.1.1:
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-0.1.1.tgz#c8defae57c8a52a8a784f9e31c57c742e993a0b9"
+find-cache-dir@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f"
dependencies:
commondir "^1.0.1"
- mkdirp "^0.5.1"
- pkg-dir "^1.0.0"
+ make-dir "^1.0.0"
+ pkg-dir "^2.0.0"
find-root@^0.1.1:
version "0.1.2"
@@ -2342,7 +2495,7 @@ find-up@^1.0.0:
path-exists "^2.0.0"
pinkie-promise "^2.0.0"
-find-up@^2.1.0:
+find-up@^2.0.0, find-up@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
dependencies:
@@ -2361,6 +2514,12 @@ flatten@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
+follow-redirects@^1.2.3:
+ version "1.2.3"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.3.tgz#01abaeca85e3609837d9fcda3167a7e42fdaca21"
+ dependencies:
+ debug "^2.4.5"
+
for-in@^0.1.5:
version "0.1.6"
resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.6.tgz#c9f96e89bfad18a545af5ec3ed352a1d9e5b4dc8"
@@ -2401,6 +2560,16 @@ fs-access@^1.0.0:
dependencies:
null-check "^1.0.0"
+fs-extra@^0.26.4:
+ version "0.26.7"
+ resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.26.7.tgz#9ae1fdd94897798edab76d0918cf42d0c3184fa9"
+ dependencies:
+ graceful-fs "^4.1.2"
+ jsonfile "^2.1.0"
+ klaw "^1.0.0"
+ path-is-absolute "^1.0.0"
+ rimraf "^2.2.8"
+
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -2461,6 +2630,10 @@ get-caller-file@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
+get-stdin@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
+
get-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
@@ -2494,6 +2667,16 @@ glob@^5.0.15:
once "^1.3.0"
path-is-absolute "^1.0.0"
+glob@^6.0.4:
+ version "6.0.4"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
+ dependencies:
+ inflight "^1.0.4"
+ inherits "2"
+ minimatch "2 || 3"
+ once "^1.3.0"
+ path-is-absolute "^1.0.0"
+
glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
@@ -2520,6 +2703,16 @@ globby@^5.0.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
+globby@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c"
+ dependencies:
+ array-union "^1.0.1"
+ glob "^7.0.3"
+ object-assign "^4.0.1"
+ pify "^2.0.0"
+ pinkie-promise "^2.0.0"
+
good-listener@^1.2.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
@@ -2560,7 +2753,7 @@ got@^7.0.0:
url-parse-lax "^1.0.0"
url-to-options "^1.0.1"
-graceful-fs@^4.1.11, graceful-fs@^4.1.2:
+graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
version "4.1.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
@@ -2617,6 +2810,10 @@ has-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
+has-flag@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
+
has-symbol-support-x@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/has-symbol-support-x/-/has-symbol-support-x-1.3.0.tgz#588bd6927eaa0e296afae24160659167fc2be4f8"
@@ -2776,10 +2973,23 @@ immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
+imports-loader@^0.7.1:
+ version "0.7.1"
+ resolved "https://registry.yarnpkg.com/imports-loader/-/imports-loader-0.7.1.tgz#f204b5f34702a32c1db7d48d89d5e867a0441253"
+ dependencies:
+ loader-utils "^1.0.2"
+ source-map "^0.5.6"
+
imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
+indent-string@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80"
+ dependencies:
+ repeating "^2.0.0"
+
indexes-of@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
@@ -2829,6 +3039,12 @@ inquirer@^0.12.0:
strip-ansi "^3.0.0"
through "^2.3.6"
+internal-ip@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c"
+ dependencies:
+ meow "^3.3.0"
+
interpret@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c"
@@ -2843,6 +3059,10 @@ invert-kv@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
+ip@^1.1.0:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
+
ipaddr.js@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.3.0.tgz#1e03a52fdad83a8bbb2b25cbf4998b4cffcd3dec"
@@ -2868,9 +3088,9 @@ is-binary-path@^1.0.0:
dependencies:
binary-extensions "^1.0.0"
-is-buffer@^1.0.2:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.4.tgz#cfc86ccd5dc5a52fa80489111c6920c457e2d98b"
+is-buffer@^1.0.2, is-buffer@^1.1.5:
+ version "1.1.5"
+ resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc"
is-builtin-module@^1.0.0:
version "1.0.0"
@@ -3007,7 +3227,7 @@ is-retry-allowed@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34"
-is-stream@^1.0.0:
+is-stream@^1.0.0, is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
@@ -3230,6 +3450,10 @@ json-loader@^0.5.4:
version "0.5.4"
resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.4.tgz#8baa1365a632f58a3c46d20175fc6002c96e37de"
+json-schema-traverse@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.0.tgz#0016c0b1ca1efe46d44d37541bcdfc19dcfae0db"
+
json-schema@0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
@@ -3252,6 +3476,12 @@ json5@^0.5.0, json5@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
+jsonfile@^2.1.0:
+ version "2.4.0"
+ resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8"
+ optionalDependencies:
+ graceful-fs "^4.1.6"
+
jsonify@~0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
@@ -3311,9 +3541,9 @@ karma-sourcemap-loader@^0.3.7:
dependencies:
graceful-fs "^4.1.2"
-karma-webpack@^2.0.2:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-2.0.2.tgz#bd38350af5645c9644090770939ebe7ce726f864"
+karma-webpack@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-2.0.4.tgz#3e2d4f48ba94a878e1c66bb8e1ae6128987a175b"
dependencies:
async "~0.9.0"
loader-utils "^0.2.5"
@@ -3359,6 +3589,12 @@ kind-of@^3.0.2:
dependencies:
is-buffer "^1.0.2"
+klaw@^1.0.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439"
+ optionalDependencies:
+ graceful-fs "^4.1.9"
+
latest-version@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-1.0.1.tgz#72cfc46e3e8d1be651e1ebb54ea9f6ea96f374bb"
@@ -3398,11 +3634,20 @@ load-json-file@^1.0.0:
pinkie-promise "^2.0.0"
strip-bom "^2.0.0"
+load-json-file@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8"
+ dependencies:
+ graceful-fs "^4.1.2"
+ parse-json "^2.2.0"
+ pify "^2.0.0"
+ strip-bom "^3.0.0"
+
loader-runner@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
-loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.2.5:
+loader-utils@^0.2.15, loader-utils@^0.2.5:
version "0.2.16"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d"
dependencies:
@@ -3578,6 +3823,10 @@ log4js@^0.6.31:
readable-stream "~1.0.2"
semver "~4.3.3"
+loglevel@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.4.1.tgz#95b383f91a3c2756fd4ab093667e4309161f2bcd"
+
longest@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
@@ -3588,6 +3837,13 @@ loose-envify@^1.0.0:
dependencies:
js-tokens "^3.0.0"
+loud-rejection@^1.0.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
+ dependencies:
+ currently-unhandled "^0.4.1"
+ signal-exit "^3.0.0"
+
lowercase-keys@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306"
@@ -3613,6 +3869,16 @@ macaddress@^0.2.8:
version "0.2.8"
resolved "https://registry.yarnpkg.com/macaddress/-/macaddress-0.2.8.tgz#5904dc537c39ec6dbefeae902327135fa8511f12"
+make-dir@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978"
+ dependencies:
+ pify "^2.3.0"
+
+map-obj@^1.0.0, map-obj@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
+
map-stream@~0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
@@ -3629,6 +3895,12 @@ media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
+mem@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76"
+ dependencies:
+ mimic-fn "^1.0.0"
+
memory-fs@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290"
@@ -3640,6 +3912,21 @@ memory-fs@^0.4.0, memory-fs@~0.4.1:
errno "^0.1.3"
readable-stream "^2.0.1"
+meow@^3.3.0:
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
+ dependencies:
+ camelcase-keys "^2.0.0"
+ decamelize "^1.1.2"
+ loud-rejection "^1.0.0"
+ map-obj "^1.0.1"
+ minimist "^1.1.3"
+ normalize-package-data "^2.3.4"
+ object-assign "^4.0.1"
+ read-pkg-up "^1.0.1"
+ redent "^1.0.0"
+ trim-newlines "^1.0.0"
+
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
@@ -3673,30 +3960,24 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
-"mime-db@>= 1.24.0 < 2", mime-db@~1.26.0:
- version "1.26.0"
- resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.26.0.tgz#eaffcd0e4fc6935cf8134da246e2e6c35305adff"
-
-mime-db@~1.27.0:
+"mime-db@>= 1.24.0 < 2", mime-db@~1.27.0:
version "1.27.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1"
-mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.7:
+mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7:
version "2.1.15"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed"
dependencies:
mime-db "~1.27.0"
-mime-types@~2.1.11:
- version "2.1.14"
- resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.14.tgz#f7ef7d97583fcaf3b7d282b6f8b5679dab1e94ee"
- dependencies:
- mime-db "~1.26.0"
-
mime@1.3.4, mime@1.3.x, mime@^1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
+mimic-fn@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
+
mimic-response@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.0.tgz#df3d3652a73fded6b9b0b24146e6fd052353458e"
@@ -3715,7 +3996,7 @@ minimist@0.0.8, minimist@~0.0.1:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
-minimist@^1.2.0:
+minimist@^1.1.3, minimist@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
@@ -3729,6 +4010,10 @@ moment@2.x:
version "2.17.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82"
+monaco-editor@0.8.3:
+ version "0.8.3"
+ resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.8.3.tgz#523bdf2d1524db2c2dfc3cae0a7b6edc48d6dea6"
+
mousetrap@^1.4.6:
version "1.4.6"
resolved "https://registry.yarnpkg.com/mousetrap/-/mousetrap-1.4.6.tgz#eaca72e22e56d5b769b7555873b688c3332e390a"
@@ -3745,6 +4030,17 @@ ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+multicast-dns-service-types@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901"
+
+multicast-dns@^6.0.1:
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.1.1.tgz#6e7de86a570872ab17058adea7160bbeca814dde"
+ dependencies:
+ dns-packet "^1.0.1"
+ thunky "^0.1.0"
+
mute-stream@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"
@@ -3771,6 +4067,16 @@ nested-error-stacks@^1.0.0:
dependencies:
inherits "~2.0.1"
+node-dir@^0.1.10:
+ version "0.1.17"
+ resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5"
+ dependencies:
+ minimatch "^3.0.2"
+
+node-forge@0.6.33:
+ version "0.6.33"
+ resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.33.tgz#463811879f573d45155ad6a9f43dc296e8e85ebc"
+
node-libs-browser@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-1.1.1.tgz#2a38243abedd7dffcd07a97c9aca5668975a6fea"
@@ -3877,7 +4183,7 @@ nopt@~1.0.10:
dependencies:
abbrev "1"
-normalize-package-data@^2.3.2:
+normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
version "2.3.5"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.3.5.tgz#8d924f142960e1777e7ffe170543631cc7cb02df"
dependencies:
@@ -3903,6 +4209,12 @@ normalize-url@^1.4.0:
query-string "^4.1.0"
sort-keys "^1.0.0"
+npm-run-path@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
+ dependencies:
+ path-key "^2.0.0"
+
npmlog@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.0.2.tgz#d03950e0e78ce1527ba26d2a7592e9348ac3e75f"
@@ -4034,6 +4346,14 @@ os-locale@^1.4.0:
dependencies:
lcid "^1.0.0"
+os-locale@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2"
+ dependencies:
+ execa "^0.7.0"
+ lcid "^1.0.0"
+ mem "^1.1.0"
+
os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
@@ -4063,6 +4383,10 @@ p-locate@^2.0.0:
dependencies:
p-limit "^1.1.0"
+p-map@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.1.1.tgz#05f5e4ae97a068371bc2a5cc86bfbdbc19c4ae7a"
+
p-timeout@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-1.2.0.tgz#9820f99434c5817868b4f34809ee5291660d5b6c"
@@ -4153,6 +4477,10 @@ path-is-inside@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
+path-key@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
+
path-parse@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
@@ -4169,6 +4497,12 @@ path-type@^1.0.0:
pify "^2.0.0"
pinkie-promise "^2.0.0"
+path-type@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73"
+ dependencies:
+ pify "^2.0.0"
+
pause-stream@0.0.11:
version "0.0.11"
resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445"
@@ -4181,10 +4515,14 @@ pbkdf2@^3.0.3:
dependencies:
create-hmac "^1.1.2"
-pify@^2.0.0:
+pify@^2.0.0, pify@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
+pify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
+
pikaday@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/pikaday/-/pikaday-1.5.1.tgz#0a48549bc1a14ea1d08c44074d761bc2f2bfcfd3"
@@ -4207,6 +4545,12 @@ pkg-dir@^1.0.0:
dependencies:
find-up "^1.0.0"
+pkg-dir@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
+ dependencies:
+ find-up "^2.1.0"
+
pkg-up@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-1.0.0.tgz#3e08fb461525c4421624a33b9f7e6d0af5b05a26"
@@ -4598,6 +4942,10 @@ querystringify@0.0.x:
version "0.0.4"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-0.0.4.tgz#0cf7f84f9463ff0ae51c4c4b142d95be37724d9c"
+querystringify@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-1.0.0.tgz#6286242112c5b712fa654e526652bf6a13ff05cb"
+
randomatic@^1.1.3:
version "1.1.6"
resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb"
@@ -4675,6 +5023,13 @@ read-pkg-up@^1.0.1:
find-up "^1.0.0"
read-pkg "^1.0.0"
+read-pkg-up@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be"
+ dependencies:
+ find-up "^2.0.0"
+ read-pkg "^2.0.0"
+
read-pkg@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28"
@@ -4683,6 +5038,14 @@ read-pkg@^1.0.0:
normalize-package-data "^2.3.2"
path-type "^1.0.0"
+read-pkg@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8"
+ dependencies:
+ load-json-file "^2.0.0"
+ normalize-package-data "^2.3.2"
+ path-type "^2.0.0"
+
readable-stream@^2.0.0, "readable-stream@^2.0.0 || ^1.1.13", readable-stream@^2.1.0, readable-stream@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.2.2.tgz#a9e6fec3c7dda85f8bb1b3ba7028604556fc825e"
@@ -4756,6 +5119,13 @@ recursive-readdir@2.1.1:
dependencies:
minimatch "3.0.3"
+redent@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde"
+ dependencies:
+ indent-string "^2.1.0"
+ strip-indent "^1.0.1"
+
reduce-css-calc@^1.2.6:
version "1.3.0"
resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716"
@@ -4968,6 +5338,12 @@ select@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
+selfsigned@^1.9.1:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.9.1.tgz#cdda4492d70d486570f87c65546023558e1dfa5a"
+ dependencies:
+ node-forge "0.6.33"
+
semver-diff@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"
@@ -5047,6 +5423,16 @@ sha.js@^2.3.6:
dependencies:
inherits "^2.0.1"
+shebang-command@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
+ dependencies:
+ shebang-regex "^1.0.0"
+
+shebang-regex@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
+
shelljs@^0.7.5:
version "0.7.6"
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.6.tgz#379cccfb56b91c8601e4793356eb5382924de9ad"
@@ -5136,16 +5522,16 @@ sockjs-client@1.0.1:
json3 "^3.3.2"
url-parse "^1.0.1"
-sockjs-client@1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.2.tgz#f0212a8550e4c9468c8cceaeefd2e3493c033ad5"
+sockjs-client@1.1.4:
+ version "1.1.4"
+ resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.1.4.tgz#5babe386b775e4cf14e7520911452654016c8b12"
dependencies:
- debug "^2.2.0"
+ debug "^2.6.6"
eventsource "0.1.6"
faye-websocket "~0.11.0"
inherits "^2.0.1"
json3 "^3.3.2"
- url-parse "^1.1.1"
+ url-parse "^1.1.8"
sockjs@0.3.18:
version "0.3.18"
@@ -5164,9 +5550,9 @@ source-list-map@^0.1.7, source-list-map@~0.1.7:
version "0.1.8"
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106"
-source-list-map@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-1.1.1.tgz#1a33ac210ca144d1e561f906ebccab5669ff4cb4"
+source-list-map@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
source-map-support@^0.4.2:
version "0.4.11"
@@ -5259,10 +5645,6 @@ sshpk@^1.7.0:
jsbn "~0.1.0"
tweetnacl "~0.14.0"
-stats-webpack-plugin@^0.4.3:
- version "0.4.3"
- resolved "https://registry.yarnpkg.com/stats-webpack-plugin/-/stats-webpack-plugin-0.4.3.tgz#b2f618202f28dd04ab47d7ecf54ab846137b7aea"
-
"statuses@>= 1.3.1 < 2", statuses@~1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
@@ -5343,6 +5725,16 @@ strip-bom@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
+strip-eof@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
+
+strip-indent@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2"
+ dependencies:
+ get-stdin "^4.0.1"
+
strip-json-comments@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
@@ -5365,6 +5757,12 @@ supports-color@^3.1.0, supports-color@^3.1.1, supports-color@^3.1.2, supports-co
dependencies:
has-flag "^1.0.0"
+supports-color@^4.2.1:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.1.tgz#65a4bb2631e90e02420dba5554c375a4754bb836"
+ dependencies:
+ has-flag "^2.0.0"
+
svgo@^0.7.0:
version "0.7.2"
resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5"
@@ -5392,9 +5790,9 @@ tapable@^0.1.8:
version "0.1.10"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4"
-tapable@^0.2.5, tapable@~0.2.5:
- version "0.2.6"
- resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.6.tgz#206be8e188860b514425375e6f1ae89bfb01fd8d"
+tapable@^0.2.7:
+ version "0.2.7"
+ resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.7.tgz#e46c0daacbb2b8a98b9b0cea0f4052105817ed5c"
tar-pack@~3.3.0:
version "3.3.0"
@@ -5447,6 +5845,10 @@ through@2, through@^2.3.6, through@~2.3, through@~2.3.1:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
+thunky@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/thunky/-/thunky-0.1.0.tgz#bf30146824e2b6e67b0f2d7a4ac8beb26908684e"
+
timeago.js@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/timeago.js/-/timeago.js-2.0.5.tgz#730c74fbdb0b0917a553675a4460e3a7f80db86c"
@@ -5509,6 +5911,10 @@ traverse@0.6.6:
version "0.6.6"
resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137"
+trim-newlines@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
+
trim-right@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
@@ -5546,9 +5952,9 @@ typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
-uglify-js@^2.6, uglify-js@^2.8.27:
- version "2.8.27"
- resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.27.tgz#47787f912b0f242e5b984343be8e35e95f694c9c"
+uglify-js@^2.6, uglify-js@^2.8.29:
+ version "2.8.29"
+ resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
dependencies:
source-map "~0.5.1"
yargs "~3.10.0"
@@ -5559,6 +5965,14 @@ uglify-to-browserify@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
+uglifyjs-webpack-plugin@^0.4.6:
+ version "0.4.6"
+ resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309"
+ dependencies:
+ source-map "^0.5.6"
+ uglify-js "^2.8.29"
+ webpack-sources "^1.0.1"
+
uid-number@~0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81"
@@ -5633,13 +6047,20 @@ url-parse@1.0.x:
querystringify "0.0.x"
requires-port "1.0.x"
-url-parse@^1.0.1, url-parse@^1.1.1:
+url-parse@^1.0.1:
version "1.1.7"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.7.tgz#025cff999653a459ab34232147d89514cc87d74a"
dependencies:
querystringify "0.0.x"
requires-port "1.0.x"
+url-parse@^1.1.8:
+ version "1.1.9"
+ resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.9.tgz#c67f1d775d51f0a18911dd7b3ffad27bb9e5bd19"
+ dependencies:
+ querystringify "~1.0.0"
+ requires-port "1.0.x"
+
url-to-options@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/url-to-options/-/url-to-options-1.0.1.tgz#1505a03a289a48cbd7a434efbaeec5055f5633a9"
@@ -5774,12 +6195,12 @@ vuex@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/vuex/-/vuex-2.3.1.tgz#cde8e997c1f9957719bc7dea154f9aa691d981a6"
-watchpack@^1.3.1:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.3.1.tgz#7d8693907b28ce6013e7f3610aa2a1acf07dad87"
+watchpack@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac"
dependencies:
async "^2.1.2"
- chokidar "^1.4.3"
+ chokidar "^1.7.0"
graceful-fs "^4.1.2"
wbuf@^1.1.0, wbuf@^1.4.0:
@@ -5804,35 +6225,40 @@ webpack-bundle-analyzer@^2.8.2:
opener "^1.4.3"
ws "^2.3.1"
-webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.9.0:
- version "1.10.0"
- resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.10.0.tgz#7d5be2651e692fddfafd8aaed177c16ff51f0eb8"
+webpack-dev-middleware@^1.0.11, webpack-dev-middleware@^1.11.0:
+ version "1.11.0"
+ resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-1.11.0.tgz#09691d0973a30ad1f82ac73a12e2087f0a4754f9"
dependencies:
memory-fs "~0.4.1"
mime "^1.3.4"
path-is-absolute "^1.0.0"
range-parser "^1.0.3"
-webpack-dev-server@^2.4.2:
- version "2.4.2"
- resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.4.2.tgz#cf595d6b40878452b6d2ad7229056b686f8a16be"
+webpack-dev-server@^2.6.1:
+ version "2.6.1"
+ resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.6.1.tgz#0b292a9da96daf80a65988f69f87b4166e5defe7"
dependencies:
ansi-html "0.0.7"
+ bonjour "^3.5.0"
chokidar "^1.6.0"
compression "^1.5.2"
connect-history-api-fallback "^1.3.0"
+ del "^3.0.0"
express "^4.13.3"
html-entities "^1.2.0"
http-proxy-middleware "~0.17.4"
+ internal-ip "^1.2.0"
+ loglevel "^1.4.1"
opn "4.0.2"
portfinder "^1.0.9"
+ selfsigned "^1.9.1"
serve-index "^1.7.2"
sockjs "0.3.18"
- sockjs-client "1.1.2"
+ sockjs-client "1.1.4"
spdy "^3.4.1"
strip-ansi "^3.0.0"
supports-color "^3.1.1"
- webpack-dev-middleware "^1.9.0"
+ webpack-dev-middleware "^1.11.0"
yargs "^6.0.0"
webpack-sources@^0.1.0:
@@ -5842,38 +6268,43 @@ webpack-sources@^0.1.0:
source-list-map "~0.1.7"
source-map "~0.5.3"
-webpack-sources@^0.2.3:
- version "0.2.3"
- resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-0.2.3.tgz#17c62bfaf13c707f9d02c479e0dcdde8380697fb"
+webpack-sources@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf"
dependencies:
- source-list-map "^1.1.1"
+ source-list-map "^2.0.0"
source-map "~0.5.3"
-webpack@^2.6.1:
- version "2.6.1"
- resolved "https://registry.yarnpkg.com/webpack/-/webpack-2.6.1.tgz#2e0457f0abb1ac5df3ab106c69c672f236785f07"
+webpack-stats-plugin@^0.1.5:
+ version "0.1.5"
+ resolved "https://registry.yarnpkg.com/webpack-stats-plugin/-/webpack-stats-plugin-0.1.5.tgz#29e5f12ebfd53158d31d656a113ac1f7b86179d9"
+
+webpack@^3.4.0:
+ version "3.4.0"
+ resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.4.0.tgz#e9465b660ad79dd2d33874d968b31746ea9a8e63"
dependencies:
acorn "^5.0.0"
acorn-dynamic-import "^2.0.0"
- ajv "^4.7.0"
- ajv-keywords "^1.1.1"
+ ajv "^5.1.5"
+ ajv-keywords "^2.0.0"
async "^2.1.2"
- enhanced-resolve "^3.0.0"
+ enhanced-resolve "^3.4.0"
+ escope "^3.6.0"
interpret "^1.0.0"
json-loader "^0.5.4"
json5 "^0.5.1"
loader-runner "^2.3.0"
- loader-utils "^0.2.16"
+ loader-utils "^1.1.0"
memory-fs "~0.4.1"
mkdirp "~0.5.0"
node-libs-browser "^2.0.0"
source-map "^0.5.3"
- supports-color "^3.1.0"
- tapable "~0.2.5"
- uglify-js "^2.8.27"
- watchpack "^1.3.1"
- webpack-sources "^0.2.3"
- yargs "^6.0.0"
+ supports-color "^4.2.1"
+ tapable "^0.2.7"
+ uglifyjs-webpack-plugin "^0.4.6"
+ watchpack "^1.4.0"
+ webpack-sources "^1.0.1"
+ yargs "^8.0.2"
websocket-driver@>=0.3.6, websocket-driver@>=0.5.1:
version "0.6.5"
@@ -5893,7 +6324,11 @@ which-module@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f"
-which@^1.1.1, which@^1.2.1:
+which-module@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
+
+which@^1.1.1, which@^1.2.1, which@^1.2.9:
version "1.2.12"
resolved "https://registry.yarnpkg.com/which/-/which-1.2.12.tgz#de67b5e450269f194909ef23ece4ebe416fa1192"
dependencies:
@@ -5992,6 +6427,12 @@ yargs-parser@^4.2.0:
dependencies:
camelcase "^3.0.0"
+yargs-parser@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9"
+ dependencies:
+ camelcase "^4.1.0"
+
yargs@^6.0.0:
version "6.6.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208"
@@ -6010,6 +6451,24 @@ yargs@^6.0.0:
y18n "^3.2.1"
yargs-parser "^4.2.0"
+yargs@^8.0.2:
+ version "8.0.2"
+ resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360"
+ dependencies:
+ camelcase "^4.1.0"
+ cliui "^3.2.0"
+ decamelize "^1.1.1"
+ get-caller-file "^1.0.1"
+ os-locale "^2.0.0"
+ read-pkg-up "^2.0.0"
+ require-directory "^2.1.1"
+ require-main-filename "^1.0.1"
+ set-blocking "^2.0.0"
+ string-width "^2.0.0"
+ which-module "^2.0.0"
+ y18n "^3.2.1"
+ yargs-parser "^7.0.0"
+
yargs@~3.10.0:
version "3.10.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"